feat: add initial application structure with configuration, UI components, and state management
This commit is contained in:
parent
fadf94002c
commit
0af6b17792
56 changed files with 6137 additions and 129 deletions
367
pages/scenarios.vue
Normal file
367
pages/scenarios.vue
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
<template>
|
||||
<section class="py-8 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Scenarios & Runway</h2>
|
||||
</div>
|
||||
|
||||
<!-- 6-Month Preset Card -->
|
||||
<UCard class="bg-blue-50 border-blue-200">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-blue-900">
|
||||
6-Month Plan Analysis
|
||||
</h3>
|
||||
<UBadge color="info" variant="solid">Recommended</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl font-bold text-blue-600 mb-2">18.2 months</div>
|
||||
<div class="text-sm text-gray-600 mb-3">Extended runway</div>
|
||||
<UProgress value="91" color="info" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Key Changes</h4>
|
||||
<ul class="text-sm text-gray-600 space-y-1">
|
||||
<li>• Diversify revenue mix</li>
|
||||
<li>• Build 6-month savings buffer</li>
|
||||
<li>• Gradual capacity scaling</li>
|
||||
<li>• Risk mitigation focus</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Feasibility Gates</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm">Savings target achievable</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm">Cash floor maintained</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
name="i-heroicons-exclamation-triangle"
|
||||
class="text-yellow-500" />
|
||||
<span class="text-sm">Requires 2 new streams</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- Scenario Comparison -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<UCard class="border-green-200 bg-green-50">
|
||||
<div class="text-center space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium text-sm">Operate Current</h4>
|
||||
<UBadge color="success" variant="solid" size="xs">Active</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-orange-600">2.8 months</div>
|
||||
<div class="text-xs text-gray-600">Baseline scenario</div>
|
||||
<UButton size="xs" variant="ghost" @click="setScenario('current')">
|
||||
<UIcon name="i-heroicons-play" class="mr-1" />
|
||||
Continue
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-center space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium text-sm">Quit Day Jobs</h4>
|
||||
<UBadge color="error" variant="subtle" size="xs">High Risk</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-red-600">1.4 months</div>
|
||||
<div class="text-xs text-gray-600">Full-time co-op work</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
@click="setScenario('quitDayJobs')">
|
||||
<UIcon name="i-heroicons-briefcase" class="mr-1" />
|
||||
Analyze
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard>
|
||||
<div class="text-center space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium text-sm">Start Production</h4>
|
||||
<UBadge color="warning" variant="subtle" size="xs"
|
||||
>Medium Risk</UBadge
|
||||
>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-yellow-600">2.1 months</div>
|
||||
<div class="text-xs text-gray-600">Launch development</div>
|
||||
<UButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
@click="setScenario('startProduction')">
|
||||
<UIcon name="i-heroicons-rocket-launch" class="mr-1" />
|
||||
Analyze
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="border-blue-200">
|
||||
<div class="text-center space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium text-sm">6-Month Plan</h4>
|
||||
<UBadge color="info" variant="solid" size="xs">Planned</UBadge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-blue-600">18.2 months</div>
|
||||
<div class="text-xs text-gray-600">Extended planning</div>
|
||||
<UButton size="xs" color="primary" @click="setScenario('sixMonth')">
|
||||
<UIcon name="i-heroicons-calendar" class="mr-1" />
|
||||
Plan
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<!-- Feasibility Analysis -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<h3 class="text-lg font-medium">Feasibility Analysis</h3>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Gate Checks</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">Savings Target Reached</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-x-circle" class="text-red-500" />
|
||||
<span class="text-sm text-gray-600">€5,200 short</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">Cash Floor Maintained</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
|
||||
<span class="text-sm text-gray-600">Week 4+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">Revenue Diversification</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<UIcon
|
||||
name="i-heroicons-exclamation-triangle"
|
||||
class="text-yellow-500" />
|
||||
<span class="text-sm text-gray-600">Top: 65%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium mb-3">Key Dates</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Savings gate clear:</span>
|
||||
<span class="text-sm font-medium">March 2024</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">First cash breach:</span>
|
||||
<span class="text-sm font-medium text-red-600"
|
||||
>Week 7 (Feb 12)</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Deferred cap reset:</span>
|
||||
<span class="text-sm font-medium">April 1, 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<!-- What-If Sliders -->
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">What-If Analysis</h3>
|
||||
<UButton size="sm" variant="ghost" @click="resetSliders">
|
||||
Reset
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="revenue-slider" class="block text-sm font-medium mb-2">
|
||||
<GlossaryTooltip
|
||||
term="Monthly Revenue"
|
||||
term-id="revenue"
|
||||
definition="Total money earned from all streams in one month." />:
|
||||
{{ $format.currency(revenue) }}
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<URange
|
||||
id="revenue-slider"
|
||||
v-model="revenue"
|
||||
:min="5000"
|
||||
:max="20000"
|
||||
:step="500"
|
||||
:aria-label="`Monthly revenue: ${$format.currency(revenue)}`"
|
||||
class="flex-1" />
|
||||
<UInput
|
||||
v-model="revenue"
|
||||
type="number"
|
||||
:min="5000"
|
||||
:max="20000"
|
||||
:step="500"
|
||||
class="w-24"
|
||||
size="xs"
|
||||
aria-label="Monthly revenue input" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="hours-slider" class="block text-sm font-medium mb-2">
|
||||
Paid Hours: {{ paidHours }}h/month
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<URange
|
||||
id="hours-slider"
|
||||
v-model="paidHours"
|
||||
:min="100"
|
||||
:max="600"
|
||||
:step="20"
|
||||
:aria-label="`Paid hours: ${paidHours} per month`"
|
||||
class="flex-1" />
|
||||
<UInput
|
||||
v-model="paidHours"
|
||||
type="number"
|
||||
:min="100"
|
||||
:max="600"
|
||||
:step="20"
|
||||
class="w-20"
|
||||
size="xs"
|
||||
aria-label="Paid hours input" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="winrate-slider" class="block text-sm font-medium mb-2">
|
||||
Win Rate: {{ winRate }}%
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<URange
|
||||
id="winrate-slider"
|
||||
v-model="winRate"
|
||||
:min="40"
|
||||
:max="95"
|
||||
:step="5"
|
||||
:aria-label="`Win rate: ${winRate} percent`"
|
||||
class="flex-1" />
|
||||
<UInput
|
||||
v-model="winRate"
|
||||
type="number"
|
||||
:min="40"
|
||||
:max="95"
|
||||
:step="5"
|
||||
class="w-16"
|
||||
size="xs"
|
||||
aria-label="Win rate input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-sm mb-3">Impact on Runway</h4>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="text-2xl font-bold"
|
||||
:class="getRunwayColor(calculatedRunway)">
|
||||
{{ calculatedRunway }} months
|
||||
</div>
|
||||
<UProgress
|
||||
:value="Math.min(calculatedRunway * 10, 100)"
|
||||
:max="100"
|
||||
:color="getProgressColor(calculatedRunway)"
|
||||
class="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Monthly burn:</span>
|
||||
<span class="font-medium"
|
||||
>€{{ monthlyBurn.toLocaleString() }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-600">Coverage ratio:</span>
|
||||
<span class="font-medium"
|
||||
>{{ Math.round((paidHours / 400) * 100) }}%</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { $format } = useNuxtApp();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const scenariosStore = useScenariosStore();
|
||||
|
||||
const revenue = ref(12000);
|
||||
const paidHours = ref(320);
|
||||
const winRate = ref(70);
|
||||
|
||||
// Calculate dynamic metrics
|
||||
const monthlyBurn = computed(() => {
|
||||
const payroll = paidHours.value * 20 * 1.25; // €20/hr + 25% oncost
|
||||
const overhead = 1400;
|
||||
const production = 500;
|
||||
return payroll + overhead + production;
|
||||
});
|
||||
|
||||
const calculatedRunway = computed(() => {
|
||||
const totalCash = 13000; // cash + savings
|
||||
const adjustedRevenue = revenue.value * (winRate.value / 100);
|
||||
const netPerMonth = adjustedRevenue - monthlyBurn.value;
|
||||
|
||||
if (netPerMonth >= 0) return 999; // Infinite/sustainable
|
||||
return Math.max(0, totalCash / Math.abs(netPerMonth));
|
||||
});
|
||||
|
||||
function getRunwayColor(months: number) {
|
||||
if (months >= 6) return "text-green-600";
|
||||
if (months >= 3) return "text-blue-600";
|
||||
if (months >= 2) return "text-yellow-600";
|
||||
return "text-red-600";
|
||||
}
|
||||
|
||||
function getProgressColor(months: number) {
|
||||
if (months >= 6) return "success";
|
||||
if (months >= 3) return "info";
|
||||
if (months >= 2) return "warning";
|
||||
return "error";
|
||||
}
|
||||
|
||||
function setScenario(scenario: string) {
|
||||
scenariosStore.setActiveScenario(scenario);
|
||||
router.replace({ query: { ...route.query, scenario } });
|
||||
}
|
||||
|
||||
function resetSliders() {
|
||||
revenue.value = 12000;
|
||||
paidHours.value = 320;
|
||||
winRate.value = 70;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const q = route.query.scenario;
|
||||
if (typeof q === "string") {
|
||||
scenariosStore.setActiveScenario(q);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue