287 lines
No EOL
9.6 KiB
Vue
287 lines
No EOL
9.6 KiB
Vue
<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> |