186 lines
No EOL
5.2 KiB
Vue
186 lines
No EOL
5.2 KiB
Vue
<template>
|
|
<div class="hidden" data-ui="advanced_accordion_v1" />
|
|
<UAccordion :items="accordionItems" :multiple="false" class="shadow-sm rounded-xl">
|
|
<template #advanced>
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
<!-- Scenarios Panel -->
|
|
<div class="space-y-6">
|
|
<h4 class="font-semibold">Scenarios</h4>
|
|
<USelect
|
|
v-model="scenario"
|
|
:options="scenarioOptions"
|
|
placeholder="Select scenario"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Stress Test Panel -->
|
|
<div class="space-y-6">
|
|
<h4 class="font-semibold">Stress Test</h4>
|
|
<div class="space-y-2">
|
|
<div>
|
|
<label class="text-xs text-gray-600">Revenue Delay (months)</label>
|
|
<URange
|
|
v-model="stress.revenueDelay"
|
|
:min="0"
|
|
:max="6"
|
|
:step="1"
|
|
class="mt-1"
|
|
/>
|
|
<div class="text-xs text-gray-500">{{ stress.revenueDelay }} months</div>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-600">Cost Shock (%)</label>
|
|
<URange
|
|
v-model="stress.costShockPct"
|
|
:min="0"
|
|
:max="30"
|
|
:step="1"
|
|
class="mt-1"
|
|
/>
|
|
<div class="text-xs text-gray-500">{{ stress.costShockPct }}%</div>
|
|
</div>
|
|
<UCheckbox
|
|
v-model="stress.grantLost"
|
|
label="Grant lost"
|
|
/>
|
|
<div class="text-sm text-gray-600 pt-2 border-t">
|
|
Projected runway: {{ projectedRunway }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Milestones Panel -->
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="font-semibold">Milestones</h4>
|
|
<UButton
|
|
size="xs"
|
|
variant="outline"
|
|
@click="showMilestoneModal = true"
|
|
>
|
|
+ Add
|
|
</UButton>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="milestone in milestoneStatus()"
|
|
:key="milestone.id"
|
|
class="flex items-center justify-between text-sm"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<span>{{ milestone.willReach ? '✅' : '⚠️' }}</span>
|
|
<span>{{ milestone.label }}</span>
|
|
</div>
|
|
<span class="text-xs text-gray-600">{{ formatDate(milestone.date) }}</span>
|
|
</div>
|
|
<div v-if="milestones.length === 0" class="text-sm text-gray-600 italic">
|
|
No milestones yet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
</UAccordion>
|
|
|
|
<!-- Milestone Modal -->
|
|
<UModal v-model="showMilestoneModal">
|
|
<UCard>
|
|
<template #header>
|
|
<h3 class="text-lg font-medium">Add Milestone</h3>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<UFormGroup label="Label">
|
|
<UInput v-model="newMilestone.label" placeholder="e.g. Product launch" />
|
|
</UFormGroup>
|
|
<UFormGroup label="Date">
|
|
<UInput v-model="newMilestone.date" type="date" />
|
|
</UFormGroup>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<UButton variant="ghost" @click="showMilestoneModal = false">
|
|
Cancel
|
|
</UButton>
|
|
<UButton @click="addNewMilestone" :disabled="!newMilestone.label || !newMilestone.date">
|
|
Add Milestone
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</UModal>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const {
|
|
scenario,
|
|
setScenario,
|
|
stress,
|
|
updateStress,
|
|
milestones,
|
|
milestoneStatus,
|
|
addMilestone,
|
|
runwayMonths
|
|
} = useCoopBuilder()
|
|
|
|
// Accordion setup
|
|
const accordionItems = [{
|
|
label: 'Advanced Planning',
|
|
icon: 'i-heroicons-wrench-screwdriver',
|
|
slot: 'advanced',
|
|
defaultOpen: false
|
|
}]
|
|
|
|
// Scenarios
|
|
const scenarioOptions = [
|
|
{ label: 'Current', value: 'current' },
|
|
{ label: 'Quit Day Jobs', value: 'quit-jobs' },
|
|
{ label: 'Start Production', value: 'start-production' },
|
|
{ label: 'Custom', value: 'custom' }
|
|
]
|
|
|
|
// Stress test with live preview
|
|
const projectedRunway = computed(() => {
|
|
const months = runwayMonths(undefined, { useStress: true })
|
|
if (!isFinite(months)) return '∞'
|
|
if (months < 1) return '<1m'
|
|
return `${Math.round(months)}m`
|
|
})
|
|
|
|
// Milestones modal
|
|
const showMilestoneModal = ref(false)
|
|
const newMilestone = reactive({
|
|
label: '',
|
|
date: ''
|
|
})
|
|
|
|
function addNewMilestone() {
|
|
if (!newMilestone.label || !newMilestone.date) return
|
|
|
|
addMilestone(newMilestone.label, newMilestone.date)
|
|
newMilestone.label = ''
|
|
newMilestone.date = ''
|
|
showMilestoneModal.value = false
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})
|
|
}
|
|
|
|
// Watch scenario changes
|
|
watch(scenario, (newValue) => {
|
|
setScenario(newValue)
|
|
})
|
|
|
|
// Watch stress changes
|
|
watch(stress, (newValue) => {
|
|
updateStress(newValue)
|
|
}, { deep: true })
|
|
</script> |