feat: add initial application structure with configuration, UI components, and state management

This commit is contained in:
Jennie Robinson Faber 2025-08-09 18:13:16 +01:00
parent fadf94002c
commit 0af6b17792
56 changed files with 6137 additions and 129 deletions

289
pages/index.vue Normal file
View file

@ -0,0 +1,289 @@
<template>
<section class="py-8 space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold">Dashboard</h2>
<div class="flex gap-2">
<UButton
icon="i-heroicons-arrow-down-tray"
color="gray"
@click="onExport"
>Export JSON</UButton
>
<UButton icon="i-heroicons-arrow-up-tray" color="gray" @click="onImport"
>Import JSON</UButton
>
</div>
</div>
<!-- Key Metrics Row -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<RunwayMeter
:months="metrics.runway"
:description="`You have ${$format.number(metrics.runway)} months of runway with current spending.`"
/>
<CoverageMeter
:funded-paid-hours="Math.round(metrics.totalTargetHours * 0.65)"
:target-hours="metrics.totalTargetHours"
description="Funded hours vs target capacity across all members."
/>
<ReserveMeter
:current-savings="metrics.finances.currentBalances.savings"
:savings-target-months="metrics.finances.policies.savingsTargetMonths"
:monthly-burn="metrics.monthlyBurn"
description="Build savings to your target before increasing paid hours."
/>
<UCard>
<div class="text-center space-y-3">
<div class="text-3xl font-bold text-red-600">65%</div>
<div class="text-sm text-gray-600">
<GlossaryTooltip
term="Concentration"
term-id="concentration"
definition="Dependence on few revenue sources. UI shows top source percentage."
/>
</div>
<ConcentrationChip
status="red"
:top-source-pct="65"
:show-percentage="false"
variant="soft"
/>
<p class="text-xs text-gray-500 mt-2">
Most of your money comes from one place. Add another stream to reduce risk.
</p>
</div>
</UCard>
</div>
<!-- Alerts Section -->
<UCard>
<template #header>
<h3 class="text-lg font-medium">Alerts</h3>
</template>
<div class="space-y-3">
<UAlert
color="red"
variant="subtle"
icon="i-heroicons-exclamation-triangle"
title="Revenue Concentration Risk"
description="Most of your money comes from one place. Add another stream to reduce risk."
:actions="[{ label: 'Plan Mix', click: () => navigateTo('/mix') }]"
/>
<UAlert
color="orange"
variant="subtle"
icon="i-heroicons-calendar"
title="Cash Cushion Breach Forecast"
description="Week 7 would drop below your minimum cushion."
:actions="[{ label: 'View Calendar', click: () => navigateTo('/cash') }]"
/>
<UAlert
color="yellow"
variant="subtle"
icon="i-heroicons-banknotes"
title="Savings Below Target"
description="Build savings to your target before increasing paid hours."
:actions="[{ label: 'View Progress', click: () => navigateTo('/budget') }]"
/>
<UAlert
color="amber"
variant="subtle"
icon="i-heroicons-clock"
title="Over-Deferred Member"
description="Alex has reached 85% of quarterly deferred cap."
/>
</div>
</UCard>
<!-- Scenario Snapshots -->
<UCard>
<template #header>
<h3 class="text-lg font-medium">Scenario Snapshots</h3>
</template>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="p-4 border border-gray-200 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium text-sm">Operate Current</h4>
<UBadge color="green" variant="subtle" size="xs">Active</UBadge>
</div>
<div class="text-2xl font-bold text-orange-600 mb-1">2.8 months</div>
<p class="text-xs text-gray-600">Continue existing plan</p>
</div>
<div class="p-4 border border-gray-200 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium text-sm">Quit Day Jobs</h4>
<UBadge color="gray" variant="subtle" size="xs">Scenario</UBadge>
</div>
<div class="text-2xl font-bold text-red-600 mb-1">1.4 months</div>
<p class="text-xs text-gray-600">Full-time co-op work</p>
</div>
<div class="p-4 border border-gray-200 rounded-lg">
<div class="flex items-center justify-between mb-2">
<h4 class="font-medium text-sm">Start Production</h4>
<UBadge color="gray" variant="subtle" size="xs">Scenario</UBadge>
</div>
<div class="text-2xl font-bold text-yellow-600 mb-1">2.1 months</div>
<p class="text-xs text-gray-600">Launch development</p>
</div>
</div>
<div class="mt-4">
<UButton variant="outline" @click="navigateTo('/scenarios')">
Compare All Scenarios
</UButton>
</div>
</UCard>
<!-- Next Value Accounting Session -->
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium">Next Value Accounting Session</h3>
<UBadge color="blue" variant="subtle">January 2024</UBadge>
</div>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 class="font-medium mb-3">Session Preparation</h4>
<div class="space-y-2">
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
<span class="text-sm">Month closed & reviewed</span>
</div>
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-check-circle" class="text-green-500" />
<span class="text-sm">Contributions logged</span>
</div>
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-x-circle" class="text-gray-400" />
<span class="text-sm text-gray-600">Surplus calculated</span>
</div>
<div class="flex items-center gap-3">
<UIcon name="i-heroicons-x-circle" class="text-gray-400" />
<span class="text-sm text-gray-600">Member needs reviewed</span>
</div>
</div>
<div class="mt-4">
<UProgress value="50" :max="100" color="blue" />
<p class="text-xs text-gray-600 mt-1">2 of 4 items complete</p>
</div>
</div>
<div>
<h4 class="font-medium mb-3">Available for Distribution</h4>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Surplus</span>
<span class="font-medium text-green-600">{{ $format.currency(1200) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Deferred owed</span>
<span class="font-medium text-orange-600">{{ $format.currency(metrics.finances.deferredLiabilities.totalDeferred) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Savings gap</span>
<span class="font-medium text-blue-600">{{ $format.currency(2000) }}</span>
</div>
</div>
<div class="mt-4">
<UButton color="primary" @click="navigateTo('/session')">
Start Session
</UButton>
</div>
</div>
</div>
</UCard>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/mix')"
>
<div class="text-left">
<div class="font-medium">Revenue Mix</div>
<div class="text-xs text-gray-500">Plan revenue streams</div>
</div>
</UButton>
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/cash')"
>
<div class="text-left">
<div class="font-medium">Cash Calendar</div>
<div class="text-xs text-gray-500">13-week cash flow</div>
</div>
</UButton>
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/scenarios')"
>
<div class="text-left">
<div class="font-medium">Scenarios</div>
<div class="text-xs text-gray-500">What-if analysis</div>
</div>
</UButton>
<UButton
block
color="primary"
class="justify-start h-auto p-4"
@click="navigateTo('/session')"
>
<div class="text-left">
<div class="font-medium">Next Session</div>
<div class="text-xs">Value Accounting</div>
</div>
</UButton>
</div>
</section>
</template>
<script setup lang="ts">
// Dashboard page
const { $format } = useNuxtApp()
const { calculateMetrics } = useFixtures()
// Load fixture data and calculate metrics
const metrics = await calculateMetrics()
const onExport = () => {
const data = exportAll();
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "urgent-tools.json";
a.click();
URL.revokeObjectURL(url);
};
const onImport = async () => {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
const text = await file.text();
importAll(JSON.parse(text));
};
input.click();
};
const { exportAll, importAll } = useFixtureIO();
</script>