Init commit!
This commit is contained in:
commit
086d682592
34 changed files with 19249 additions and 0 deletions
287
app/pages/cashflow.vue
Normal file
287
app/pages/cashflow.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue