refactor: enhance routing and state management in CoopBuilder, add migration checks on startup, and update Tailwind configuration for improved component styling
This commit is contained in:
parent
848386e3dd
commit
4cea1f71fe
55 changed files with 4053 additions and 1486 deletions
98
components/advanced/MilestonesPanel.vue
Normal file
98
components/advanced/MilestonesPanel.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium">Milestones</h4>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-plus"
|
||||
@click="showAddForm = true"
|
||||
>
|
||||
Add
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div v-if="milestoneStatuses.length === 0" class="text-sm text-gray-500 italic py-2">
|
||||
No milestones set. Add key dates to track progress.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="milestone in milestoneStatuses"
|
||||
:key="milestone.id"
|
||||
class="flex items-center justify-between p-2 border border-gray-200 rounded"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
:name="milestone.willReach ? 'i-heroicons-check-circle' : 'i-heroicons-exclamation-triangle'"
|
||||
:class="milestone.willReach ? 'text-green-500' : 'text-amber-500'"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">{{ milestone.label }}</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ formatDate(milestone.date) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
color="red"
|
||||
icon="i-heroicons-trash"
|
||||
@click="removeMilestone(milestone.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add milestone modal -->
|
||||
<UModal v-model="showAddForm">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold mb-4">Add Milestone</h3>
|
||||
<div class="space-y-3">
|
||||
<UInput
|
||||
v-model="newMilestone.label"
|
||||
placeholder="Milestone name (e.g., 'Product launch')"
|
||||
/>
|
||||
<UInput
|
||||
v-model="newMilestone.date"
|
||||
type="date"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<UButton @click="saveMilestone">Save</UButton>
|
||||
<UButton variant="ghost" @click="cancelAdd">Cancel</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UModal>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { milestoneStatus, addMilestone, removeMilestone } = useCoopBuilder()
|
||||
|
||||
const showAddForm = ref(false)
|
||||
const newMilestone = ref({ label: '', date: '' })
|
||||
|
||||
const milestoneStatuses = computed(() => milestoneStatus())
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
|
||||
}
|
||||
|
||||
function saveMilestone() {
|
||||
if (newMilestone.value.label && newMilestone.value.date) {
|
||||
addMilestone(newMilestone.value.label, newMilestone.value.date)
|
||||
newMilestone.value = { label: '', date: '' }
|
||||
showAddForm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
newMilestone.value = { label: '', date: '' }
|
||||
showAddForm.value = false
|
||||
}
|
||||
</script>
|
||||
49
components/advanced/ScenariosPanel.vue
Normal file
49
components/advanced/ScenariosPanel.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h4 class="font-medium">Scenarios</h4>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<USelect
|
||||
:model-value="scenario"
|
||||
:options="scenarioOptions"
|
||||
@update:model-value="setScenario"
|
||||
/>
|
||||
|
||||
<div v-if="scenario !== 'current'" class="p-3 bg-blue-50 border border-blue-200 rounded text-sm">
|
||||
<div class="flex items-center gap-2 text-blue-800">
|
||||
<UIcon name="i-heroicons-information-circle" class="w-4 h-4" />
|
||||
<span class="font-medium">Scenario Active</span>
|
||||
</div>
|
||||
<p class="text-blue-700 mt-1">
|
||||
{{ getScenarioDescription(scenario) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { scenario, setScenario } = useCoopBuilder()
|
||||
|
||||
const scenarioOptions = [
|
||||
{ label: 'Current', value: 'current' },
|
||||
{ label: 'Quit Day Jobs', value: 'quit-jobs' },
|
||||
{ label: 'Start Production', value: 'start-production' },
|
||||
{ label: 'Custom', value: 'custom', disabled: true }
|
||||
]
|
||||
|
||||
function getScenarioDescription(scenario: string): string {
|
||||
switch (scenario) {
|
||||
case 'quit-jobs':
|
||||
return 'All external income removed. Shows runway if everyone works full-time for the co-op.'
|
||||
case 'start-production':
|
||||
return 'Service revenue reduced by 30%. Models transition from services to product development.'
|
||||
case 'custom':
|
||||
return 'Custom scenario configuration coming soon.'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
93
components/advanced/StressTestPanel.vue
Normal file
93
components/advanced/StressTestPanel.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h4 class="font-medium">Stress Test</h4>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Revenue Delay: {{ stress.revenueDelay }} months
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
:value="stress.revenueDelay"
|
||||
min="0"
|
||||
max="6"
|
||||
step="1"
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||
@input="(e) => updateStress({ revenueDelay: Number(e.target.value) })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Cost Shock: +{{ stress.costShockPct }}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
:value="stress.costShockPct"
|
||||
min="0"
|
||||
max="30"
|
||||
step="5"
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||
@input="(e) => updateStress({ costShockPct: Number(e.target.value) })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UCheckbox
|
||||
:model-value="stress.grantLost"
|
||||
label="Major Grant Lost"
|
||||
@update:model-value="(val) => updateStress({ grantLost: val })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isStressActive" class="p-3 bg-orange-50 border border-orange-200 rounded">
|
||||
<div class="text-sm">
|
||||
<div class="flex items-center gap-2 text-orange-800 mb-1">
|
||||
<UIcon name="i-heroicons-exclamation-triangle" class="w-4 h-4" />
|
||||
<span class="font-medium">Stress Test Active</span>
|
||||
</div>
|
||||
<div class="text-orange-700">
|
||||
Projected runway: <span class="font-semibold">{{ displayStressedRunway }}</span>
|
||||
<span v-if="runwayChange !== 0" class="ml-2">
|
||||
({{ runwayChange > 0 ? '+' : '' }}{{ runwayChange }} months)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { stress, updateStress, runwayMonths } = useCoopBuilder()
|
||||
|
||||
const isStressActive = computed(() =>
|
||||
stress.value.revenueDelay > 0 ||
|
||||
stress.value.costShockPct > 0 ||
|
||||
stress.value.grantLost
|
||||
)
|
||||
|
||||
const stressedRunway = computed(() => runwayMonths(undefined, { useStress: true }))
|
||||
const normalRunway = computed(() => runwayMonths())
|
||||
|
||||
const displayStressedRunway = computed(() => {
|
||||
const months = stressedRunway.value
|
||||
if (!isFinite(months)) return '∞'
|
||||
if (months < 1) return '<1 month'
|
||||
return `${Math.round(months)} months`
|
||||
})
|
||||
|
||||
const runwayChange = computed(() => {
|
||||
const normal = normalRunway.value
|
||||
const stressed = stressedRunway.value
|
||||
|
||||
if (!isFinite(normal) && !isFinite(stressed)) return 0
|
||||
if (!isFinite(normal)) return -99 // Very large negative change
|
||||
if (!isFinite(stressed)) return 0
|
||||
|
||||
return Math.round(stressed - normal)
|
||||
})
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue