Init commit!

This commit is contained in:
Jennie Robinson Faber 2025-08-22 18:36:16 +01:00
commit 086d682592
34 changed files with 19249 additions and 0 deletions

287
app/pages/cashflow.vue Normal file
View file

@ -0,0 +1,287 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Cash Flow Analysis</h1>
<p class="text-gray-600 mt-2">Detailed cash flow projections and scenario analysis</p>
</div>
<!-- Navigation Tabs -->
<div class="mb-8">
<UTabs :items="tabs" v-model="activeTab">
<template #projections>
<div class="space-y-6">
<!-- Chart Controls -->
<div class="flex justify-between items-center">
<div class="flex space-x-4">
<UButton
@click="projectionType = 'weekly'"
:variant="projectionType === 'weekly' ? 'solid' : 'outline'"
size="sm">
Weekly View
</UButton>
<UButton
@click="projectionType = 'monthly'"
:variant="projectionType === 'monthly' ? 'solid' : 'outline'"
size="sm">
Monthly View
</UButton>
</div>
<div class="flex space-x-2">
<UButton @click="exportData" variant="outline" size="sm">
Export Data
</UButton>
</div>
</div>
<!-- Cash Flow Chart -->
<UCard>
<CashFlowChart
:data="projectionData"
:critical-balance="criticalBalance"
@weeks-changed="updateProjectionPeriod" />
</UCard>
<!-- Summary Table -->
<UCard>
<template #header>
<h3 class="text-lg font-semibold">Projection Summary</h3>
</template>
<UTable
:rows="projectionSummary"
:columns="summaryColumns" />
</UCard>
</div>
</template>
<template #scenarios>
<div class="space-y-6">
<!-- Scenario Comparison -->
<UCard>
<template #header>
<h3 class="text-lg font-semibold">Runway Scenarios</h3>
</template>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div
v-for="(scenario, key) in runwayScenarios"
:key="key"
class="text-center p-4 border rounded-lg">
<h4 class="font-semibold capitalize mb-2">{{ key }}</h4>
<p class="text-2xl font-bold" :class="getScenarioColor(scenario)">
{{ formatRunwayDays(scenario) }}
</p>
<p class="text-xs text-gray-500 mt-1">{{ getScenarioDescription(key) }}</p>
</div>
</div>
</UCard>
<!-- Risk Assessment -->
<UCard>
<template #header>
<h3 class="text-lg font-semibold">Risk Assessment</h3>
</template>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<h4 class="font-medium">Current Risk Level</h4>
<p class="text-sm text-gray-600">Based on realistic scenario</p>
</div>
<UBadge
:color="getRiskBadgeColor(cashFlow.riskLevel.value)"
variant="subtle"
size="lg">
{{ cashFlow.riskLevel.value.toUpperCase() }}
</UBadge>
</div>
<div class="space-y-2">
<h5 class="font-medium">Recommendations:</h5>
<ul class="list-disc list-inside space-y-1 text-sm text-gray-600">
<li v-for="rec in riskRecommendations" :key="rec">{{ rec }}</li>
</ul>
</div>
</div>
</UCard>
</div>
</template>
<template #transactions>
<RecurringTransactionManager
:transactions="cashFlow.transactions.value"
@add-transaction="handleAddTransaction"
@edit-transaction="handleEditTransaction" />
</template>
</UTabs>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Transaction } from "~/types/cashflow";
// SEO
useSeoMeta({
title: 'Cash Flow Analysis - Faber Finances',
description: 'Detailed cash flow projections and scenario analysis'
});
// Initialize cash flow composable
const cashFlow = useCashFlow();
// Reactive state
const activeTab = ref(0);
const projectionType = ref<'weekly' | 'monthly'>('weekly');
const projectionPeriod = ref(13);
const criticalBalance = ref(50000);
// Tab configuration
const tabs = [
{ slot: 'projections', label: 'Projections' },
{ slot: 'scenarios', label: 'Scenarios' },
{ slot: 'transactions', label: 'Transactions' }
];
// Table columns
const summaryColumns = [
{ key: 'period', label: 'Period' },
{ key: 'inflow', label: 'Inflow' },
{ key: 'outflow', label: 'Outflow' },
{ key: 'netFlow', label: 'Net Flow' },
{ key: 'balance', label: 'Ending Balance' }
];
// Computed properties
const projectionData = computed(() => {
if (projectionType.value === 'weekly') {
return cashFlow.generateWeeklyProjections(projectionPeriod.value);
} else {
return cashFlow.generateProjections(Math.ceil(projectionPeriod.value / 4.33));
}
});
const projectionSummary = computed(() => {
return projectionData.value.map((projection, index) => ({
period: projectionType.value === 'weekly'
? `Week ${index + 1}`
: projection.date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
inflow: formatCurrency(projection.inflow),
outflow: formatCurrency(projection.outflow),
netFlow: formatCurrency(projection.netFlow),
balance: formatCurrency(projection.projectedBalance)
}));
});
const runwayScenarios = computed(() => {
return cashFlow.calculateRunwayScenarios();
});
const riskRecommendations = computed(() => {
const risk = cashFlow.riskLevel.value;
const recommendations = [];
if (risk === 'critical') {
recommendations.push('Immediate cash injection required');
recommendations.push('Delay all non-essential expenses');
recommendations.push('Accelerate revenue collection');
recommendations.push('Consider emergency funding options');
} else if (risk === 'high') {
recommendations.push('Monitor cash flow weekly');
recommendations.push('Reduce discretionary spending');
recommendations.push('Focus on converting opportunities');
recommendations.push('Prepare contingency plans');
} else if (risk === 'medium') {
recommendations.push('Review monthly expenses for optimization');
recommendations.push('Build stronger revenue pipeline');
recommendations.push('Consider increasing cash reserves');
} else {
recommendations.push('Maintain current financial discipline');
recommendations.push('Consider strategic investments');
recommendations.push('Build reserves for future opportunities');
}
return recommendations;
});
// Methods
const updateProjectionPeriod = (weeks: number) => {
projectionPeriod.value = weeks;
};
const handleAddTransaction = () => {
console.log("Add transaction clicked");
// TODO: Implement transaction modal
};
const handleEditTransaction = (transaction: Transaction) => {
console.log("Edit transaction:", transaction);
// TODO: Implement transaction modal
};
const exportData = () => {
const data = {
projections: projectionData.value,
scenarios: runwayScenarios.value,
transactions: cashFlow.transactions.value,
currentBalance: cashFlow.currentBalance.value,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `cashflow-export-${new Date().toISOString().split('T')[0]}.json`;
link.click();
URL.revokeObjectURL(url);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
const formatRunwayDays = (days: number) => {
if (days === Infinity) return "∞";
if (days > 365) return `${Math.round(days / 365)} years`;
if (days > 30) return `${Math.round(days / 30)} months`;
return `${Math.round(days)} days`;
};
const getScenarioColor = (days: number) => {
if (days < 30) return "text-red-600";
if (days < 90) return "text-orange-600";
return "text-green-600";
};
const getScenarioDescription = (scenario: string) => {
switch (scenario) {
case 'conservative': return 'Only confirmed transactions';
case 'realistic': return 'Probability-weighted';
case 'optimistic': return 'All transactions at full value';
case 'cashOnly': return 'Current balance only';
default: return '';
}
};
const getRiskBadgeColor = (riskLevel: string) => {
switch (riskLevel) {
case "critical": return "red";
case "high": return "orange";
case "medium": return "yellow";
default: return "green";
}
};
// Initialize with some default balance if none exists
onMounted(() => {
if (cashFlow.currentBalance.value === 0) {
cashFlow.setCurrentBalance(50000);
}
});
</script>