199 lines
5.3 KiB
Vue
199 lines
5.3 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">
|
|
<UFormField label="Label">
|
|
<UInput
|
|
v-model="newMilestone.label"
|
|
placeholder="e.g. Product launch" />
|
|
</UFormField>
|
|
<UFormField label="Date">
|
|
<UInput v-model="newMilestone.date" type="date" />
|
|
</UFormField>
|
|
</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>
|