chore: update application configuration and UI components for improved styling and functionality

This commit is contained in:
Jennie Robinson Faber 2025-08-16 08:13:35 +01:00
parent 0af6b17792
commit 37ab8d7bab
54 changed files with 23293 additions and 1666 deletions

View file

@ -17,42 +17,42 @@
<!-- 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)"
<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
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."
/>
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 class="text-3xl font-bold" :class="concentrationColor">
{{ topSourcePct }}%
</div>
<ConcentrationChip
status="red"
:top-source-pct="65"
<div class="text-sm text-neutral-600">
<GlossaryTooltip
term="Concentration"
term-id="concentration"
definition="Dependence on few revenue sources. UI shows top source percentage." />
</div>
<ConcentrationChip
:status="concentrationStatus"
:top-source-pct="topSourcePct"
: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.
variant="soft" />
<p class="text-xs text-neutral-500 mt-2">
Most of your money comes from one place. Add another stream to
reduce risk.
</p>
</div>
</UCard>
@ -70,31 +70,31 @@
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') }]"
/>
: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') }]"
/>
:description="cashBreachDescription"
: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') }]"
/>
: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."
/>
description="Alex has reached 85% of quarterly deferred cap." />
</div>
</UCard>
@ -104,31 +104,37 @@
<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="p-4 border border-neutral-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 class="text-2xl font-bold text-orange-600 mb-1">
{{ scenarioMetrics.current.runway }} months
</div>
<p class="text-xs text-neutral-600">Continue existing plan</p>
</div>
<div class="p-4 border border-gray-200 rounded-lg">
<div class="p-4 border border-neutral-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 class="text-2xl font-bold text-red-600 mb-1">
{{ scenarioMetrics.quitJobs.runway }} months
</div>
<p class="text-xs text-neutral-600">Full-time co-op work</p>
</div>
<div class="p-4 border border-gray-200 rounded-lg">
<div class="p-4 border border-neutral-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 class="text-2xl font-bold text-yellow-600 mb-1">
{{ scenarioMetrics.startProduction.runway }} months
</div>
<p class="text-xs text-neutral-600">Launch development</p>
</div>
</div>
<div class="mt-4">
@ -159,34 +165,44 @@
<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>
<UIcon name="i-heroicons-x-circle" class="text-neutral-400" />
<span class="text-sm text-neutral-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>
<UIcon name="i-heroicons-x-circle" class="text-neutral-400" />
<span class="text-sm text-neutral-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>
<p class="text-xs text-neutral-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>
<span class="text-neutral-600">Surplus</span>
<span class="font-medium text-green-600">{{
$format.currency(metrics.finances.surplus || 0)
}}</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>
<span class="text-neutral-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>
<span class="text-neutral-600">Savings gap</span>
<span class="font-medium text-blue-600">{{
$format.currency(metrics.finances.savingsGap || 0)
}}</span>
</div>
</div>
<div class="mt-4">
@ -200,48 +216,44 @@
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<UButton
block
variant="ghost"
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/mix')"
>
@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 class="text-xs text-neutral-500">Plan revenue streams</div>
</div>
</UButton>
<UButton
block
variant="ghost"
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/cash')"
>
@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 class="text-xs text-neutral-500">13-week cash flow</div>
</div>
</UButton>
<UButton
block
variant="ghost"
<UButton
block
variant="ghost"
class="justify-start h-auto p-4"
@click="navigateTo('/scenarios')"
>
@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 class="text-xs text-neutral-500">What-if analysis</div>
</div>
</UButton>
<UButton
block
<UButton
block
color="primary"
class="justify-start h-auto p-4"
@click="navigateTo('/session')"
>
@click="navigateTo('/session')">
<div class="text-left">
<div class="font-medium">Next Session</div>
<div class="text-xs">Value Accounting</div>
@ -253,11 +265,126 @@
<script setup lang="ts">
// Dashboard page
const { $format } = useNuxtApp()
const { calculateMetrics } = useFixtures()
const { $format } = useNuxtApp();
// Load fixture data and calculate metrics
const metrics = await calculateMetrics()
// Use real store data instead of fixtures
const membersStore = useMembersStore();
const policiesStore = usePoliciesStore();
const streamsStore = useStreamsStore();
const budgetStore = useBudgetStore();
const cashStore = useCashStore();
// Calculate metrics from real store data
const metrics = computed(() => {
const totalTargetHours = membersStore.members.reduce(
(sum, member) => sum + (member.capacity?.targetHours || 0),
0
);
const totalTargetRevenue = streamsStore.streams.reduce(
(sum, stream) => sum + (stream.targetMonthlyAmount || 0),
0
);
const totalOverheadCosts = budgetStore.overheadCosts.reduce(
(sum, cost) => sum + (cost.amount || 0),
0
);
const monthlyPayroll =
totalTargetHours *
policiesStore.equalHourlyWage *
(1 + policiesStore.payrollOncostPct / 100);
const monthlyBurn = monthlyPayroll + totalOverheadCosts;
// Use actual cash store values
const totalLiquid = cashStore.currentCash + cashStore.currentSavings;
const runway = monthlyBurn > 0 ? totalLiquid / monthlyBurn : 0;
return {
totalTargetHours,
totalTargetRevenue,
monthlyPayroll,
monthlyBurn,
runway,
finances: {
currentBalances: {
cash: cashStore.currentCash,
savings: cashStore.currentSavings,
totalLiquid,
},
policies: {
equalHourlyWage: policiesStore.equalHourlyWage,
payrollOncostPct: policiesStore.payrollOncostPct,
savingsTargetMonths: policiesStore.savingsTargetMonths,
minCashCushionAmount: policiesStore.minCashCushionAmount,
},
deferredLiabilities: {
totalDeferred: membersStore.members.reduce(
(sum, m) =>
sum + (m.deferredHours || 0) * policiesStore.equalHourlyWage,
0
),
},
surplus: Math.max(0, totalTargetRevenue - monthlyBurn),
savingsGap: Math.max(
0,
policiesStore.savingsTargetMonths * monthlyBurn -
cashStore.currentSavings
),
},
};
});
// Calculate concentration metrics
const topSourcePct = computed(() => {
if (streamsStore.streams.length === 0) return 0;
const amounts = streamsStore.streams.map((s) => s.targetMonthlyAmount || 0);
const total = amounts.reduce((sum, amt) => sum + amt, 0);
return total > 0 ? Math.round((Math.max(...amounts) / total) * 100) : 0;
});
const concentrationStatus = computed(() => {
if (topSourcePct.value > 50) return "red";
if (topSourcePct.value > 35) return "yellow";
return "green";
});
const concentrationColor = computed(() => {
if (topSourcePct.value > 50) return "text-red-600";
if (topSourcePct.value > 35) return "text-yellow-600";
return "text-green-600";
});
// Calculate scenario metrics
const scenarioMetrics = computed(() => {
const baseRunway = metrics.value.runway;
return {
current: {
runway: Math.round(baseRunway * 100) / 100 || 0,
},
quitJobs: {
runway: Math.round(baseRunway * 0.7 * 100) / 100 || 0, // Shorter runway due to higher costs
},
startProduction: {
runway: Math.round(baseRunway * 0.8 * 100) / 100 || 0, // Moderate impact
},
};
});
// Cash breach description
const cashBreachDescription = computed(() => {
// Check cash store for first breach week from projections
const breachWeek = cashStore.weeklyProjections.find(
(week) => week.breachesCushion
);
if (breachWeek) {
return `Week ${breachWeek.number} would drop below your minimum cushion.`;
}
return "No cushion breach currently projected.";
});
const onExport = () => {
const data = exportAll();