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

38
app/pages/basic.vue Normal file
View file

@ -0,0 +1,38 @@
<template>
<div class="p-8 bg-blue-100">
<h1 class="text-4xl font-bold text-blue-800 mb-4">Basic Styling Test</h1>
<p class="text-lg text-gray-700 mb-4">This tests if Tailwind CSS is working properly.</p>
<div class="bg-white p-6 rounded-lg shadow-lg mb-4">
<h2 class="text-2xl font-semibold text-gray-800 mb-2">White Card</h2>
<p class="text-gray-600">If you can see styling, Tailwind is working!</p>
</div>
<button
@click="counter++"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
Click Count: {{ counter }}
</button>
<div class="mt-4 p-4 bg-yellow-100 border-l-4 border-yellow-500">
<p class="text-yellow-700">
<strong>Test Results:</strong>
</p>
<ul class="list-disc list-inside text-yellow-600 mt-2">
<li>Background colors: {{ hasColors ? '✅ Working' : '❌ Not working' }}</li>
<li>Padding/margins: {{ hasSpacing ? '✅ Working' : '❌ Not working' }}</li>
<li>Typography: {{ hasTypography ? '✅ Working' : '❌ Not working' }}</li>
<li>Reactivity: {{ counter > 0 ? '✅ Working' : '❌ Click button to test' }}</li>
</ul>
</div>
</div>
</template>
<script setup>
const counter = ref(0);
// These would only be true if CSS is loading properly
const hasColors = ref(true); // We'll assume true for now
const hasSpacing = ref(true);
const hasTypography = ref(true);
</script>

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>

813
app/pages/index.vue Normal file
View file

@ -0,0 +1,813 @@
<template>
<div class="min-h-screen bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Key Metrics -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<!-- Current Balance Card -->
<div class="bg-white border-2 border-black shadow-lg p-6">
<div class="text-center">
<p class="text-sm font-medium text-black">
Available Balance
</p>
<p class="text-2xl font-bold text-blue-600">
{{ formatCurrency(cashFlow?.startingBalance?.value || 0) }}
</p>
<button
@click="openBalanceModal"
class="mt-2 text-xs text-blue-600 hover:text-blue-800 underline border-b border-blue-600">
Update Balances
</button>
</div>
</div>
<!-- Runway Card -->
<div class="bg-white border-2 border-black shadow-lg p-6">
<div class="text-center">
<p class="text-sm font-medium text-black">Runway</p>
<p class="text-2xl font-bold" :class="runwayColor">
{{ runwayText }}
</p>
</div>
</div>
<!-- Core Expenses Card -->
<div class="bg-white border-2 border-black shadow-lg p-6">
<div class="text-center">
<p class="text-sm font-medium text-black">
Core Expenses (Needs)
</p>
<p class="text-2xl font-bold text-red-600">
{{ formatCurrency(monthlyCoreExpenses) }}
</p>
</div>
</div>
<!-- Income Card -->
<div class="bg-white border-2 border-black shadow-lg p-6">
<div class="text-center">
<p class="text-sm font-medium text-black">Monthly Income</p>
<p class="text-2xl font-bold text-green-600">
{{ formatCurrency(monthlyIncome) }}
</p>
</div>
</div>
</div>
<!-- Scenario Control Panel -->
<div class="bg-white border-2 border-black shadow-lg p-6 mb-8">
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="mb-4 sm:mb-0">
<h3 class="text-lg font-semibold text-black">
Financial Scenario
</h3>
<p class="text-sm text-black">
<span
class="font-medium"
:class="
scenario === 'interest' ? 'text-red-600' : 'text-green-600'
">
{{
scenario === "interest"
? "🔴 Current (Interest Payments)"
: "🟢 Bankruptcy (Surplus Payments)"
}}
</span>
- {{ scenarioDescription }}
</p>
</div>
<div
class="flex flex-col sm:flex-row sm:items-center space-y-4 sm:space-y-0 sm:space-x-4">
<!-- Scenario Toggle -->
<div class="flex items-center space-x-3">
<label class="text-sm font-medium text-black">Scenario:</label>
<select
v-model="scenario"
@change="updateScenario"
class="px-3 py-2 border-2 border-black focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="interest">Interest Payments</option>
<option value="bankruptcy">Dynamic Surplus</option>
</select>
</div>
<!-- Dynamic Surplus Calculation Display (only show for bankruptcy scenario) -->
<div
v-if="scenario === 'bankruptcy'"
class="bg-white border-2 border-black px-3 py-2 text-sm">
<div class="font-medium text-black">
Surplus: {{ formatCurrency(calculatedSurplusPayment) }}/month
</div>
<div class="text-xs text-black">
Income: {{ formatCurrency(householdNetIncome) }} | Threshold:
{{ formatCurrency(surplusThreshold) }} | Available:
{{
formatCurrency(
Math.max(0, householdNetIncome - surplusThreshold)
)
}}
</div>
</div>
</div>
</div>
</div>
<!-- Transaction Modal -->
<TransactionModal
:is-open="isModalOpen"
:transaction="selectedTransaction"
@close="closeModal"
@save="saveTransaction"
@delete="handleDeleteTransactionFromModal" />
<!-- Delete Confirmation Modal -->
<div
v-if="showDeleteConfirmation"
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div
class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div class="mt-3">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">
Confirm Delete
</h3>
<button @click="cancelDelete" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="mb-4">
<p class="text-sm text-gray-600">
Are you sure you want to delete this transaction? This action cannot be undone.
</p>
<div v-if="transactionToDelete" class="mt-3 p-3 bg-gray-50 rounded">
<p class="text-sm font-medium">{{ transactionToDelete.description }}</p>
<p class="text-sm text-gray-500">
{{ formatCurrency(transactionToDelete.amount) }} - {{ transactionToDelete.category }}
</p>
</div>
</div>
<div class="flex justify-end space-x-3">
<button
@click="cancelDelete"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500">
Cancel
</button>
<button
@click="confirmDelete"
class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
Delete Transaction
</button>
</div>
</div>
</div>
</div>
<!-- Balance Modal -->
<BalanceModal
:is-open="isBalanceModalOpen"
:current-balance="cashFlow?.currentBalance?.value || 0"
@close="closeBalanceModal"
@update="updateBalance" />
<!-- Projected Transactions (Next 13 Weeks) -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div
class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold">
All Transactions - Next 13 Weeks
</h3>
<button
@click="handleAddTransaction"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 border-2 border-blue-800 flex items-center">
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Add Transaction
</button>
</div>
<div class="overflow-x-auto" v-if="projectedTransactions.length > 0">
<table class="w-full">
<thead class="bg-black text-white">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Date
</th>
<th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Description
</th>
<th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Category
</th>
<th
class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider">
Amount
</th>
<th
class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider">
Running Balance
</th>
<th
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">
Notes
</th>
<th
class="px-6 py-3 text-center text-xs font-medium uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-black">
<tr
v-for="transaction in projectedTransactions"
:key="`${transaction.id}-${transaction.date}`"
class="hover:bg-blue-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-black">
{{ formatDate(transaction.date) }}
</td>
<td class="px-6 py-4">
<div>
<p class="text-sm font-medium text-black">
{{ transaction.description }}
</p>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm text-black">{{
transaction.category
}}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div
:class="
transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
">
<p class="text-sm font-medium">
{{ formatCurrency(transaction.amount) }}
</p>
<!-- Show business split info if applicable -->
<div v-if="transaction.businessPercentage > 0" class="text-xs text-gray-500 space-y-0.5">
<div>Personal: {{ formatCurrency(transaction.amount) }}</div>
<div class="text-blue-600">Business: {{ formatCurrency(transaction.businessAmount || 0) }} ({{ transaction.businessPercentage }}%)</div>
<div class="font-medium">Total: {{ formatCurrency(transaction.totalAmount || transaction.amount) }}</div>
</div>
<!-- Show original currency info if different and no business split -->
<p
v-else-if="
transaction.originalCurrency &&
transaction.originalCurrency !== 'CAD'
"
class="text-xs text-gray-500">
({{
formatCurrency(
transaction.originalAmount,
transaction.originalCurrency
)
}})
</p>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<p
class="text-sm font-bold"
:class="
transaction.runningBalance < 0
? 'text-red-600'
: 'text-black'
">
{{ formatCurrency(transaction.runningBalance) }}
</p>
</td>
<td class="px-6 py-4 text-left">
<p class="text-sm text-black max-w-xs truncate" :title="transaction.notes">
{{ transaction.notes || '-' }}
</p>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<div class="flex justify-center space-x-2">
<button
@click="handleEditTransaction(transaction)"
class="text-blue-600 hover:text-blue-800 text-sm font-medium px-2 py-1 border border-blue-600 hover:bg-blue-50">
Edit
</button>
<button
@click="handleDeleteTransaction(transaction.id)"
class="text-red-600 hover:text-red-800 text-sm font-medium px-2 py-1 border border-red-600 hover:bg-red-50">
Delete
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty state when no transactions -->
<div v-else class="text-center py-12">
<svg
class="h-12 w-12 mx-auto mb-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">
No transactions yet
</h3>
<p class="text-gray-600 mb-4">
Add your first transaction to start tracking your cash flow
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
// SEO
useSeoMeta({
title: "Faber Finances",
description: "$$$",
});
// Initialize cash flow composable
const cashFlow = useCashFlow();
// Reactive state
const isModalOpen = ref(false);
const selectedTransaction = ref(null);
const isBalanceModalOpen = ref(false);
const newBalance = ref(0);
const showDeleteConfirmation = ref(false);
const transactionToDelete = ref(null);
// Scenario state
const scenario = ref("interest"); // 'interest' or 'bankruptcy'
const interestTransactionId = ref("transaction_1755673351011"); // Store Interest transaction ID
// Surplus calculation constants
const surplusThreshold = 3318; // CAD threshold for 2-person household surplus calculation
// Calculate household net income from transactions (Personal account only)
const householdNetIncome = computed(() => {
if (!cashFlow?.transactions?.value) return 0;
return cashFlow.transactions.value
.filter((t) => t.amount > 0) // Only income transactions (positive amounts)
.reduce((total, transaction) => {
if (transaction.type === "recurring") {
// Calculate monthly income from recurring transactions
const frequencyMultiplier = {
weekly: 4.33, // ~4.33 weeks per month
biweekly: 2.165, // ~2.165 biweeks per month
monthly: 1,
quarterly: 1 / 3, // 1 quarter = 1/3 month
custom: 1 / (transaction.customMonths || 1),
};
return (
total +
transaction.amount *
(frequencyMultiplier[transaction.frequency || "monthly"] || 1)
);
} else {
// For one-time transactions, don't include in monthly calculation
return total;
}
}, 0);
});
// Calculate dynamic surplus payment
const calculatedSurplusPayment = computed(() => {
if (householdNetIncome.value > surplusThreshold) {
const surplus = householdNetIncome.value - surplusThreshold;
return Math.round(surplus * 0.5); // 50% of surplus above threshold
}
return 0;
});
// Scenario descriptions
const scenarioDescription = computed(() => {
if (scenario.value === "interest") {
return "Paying $2,700/month in interest expenses";
} else {
return `Dynamic surplus based on income: ${formatCurrency(
calculatedSurplusPayment.value
)}/month`;
}
});
// Computed properties
const runwayText = computed(() => {
const days = cashFlow?.runwayDays?.value || 0;
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 runwayColor = computed(() => {
const days = cashFlow?.runwayDays?.value || 0;
if (days < 30) return "text-red-600";
if (days < 90) return "text-orange-600";
return "text-green-600";
});
// Core Expenses (Need) - Monthly recurring expenses categorized as "Expense: Need"
const monthlyCoreExpenses = computed(() => {
if (!cashFlow?.filteredTransactions?.value) return 0;
const transactions = cashFlow.filteredTransactions.value;
let totalCoreExpenses = 0;
transactions.forEach((transaction) => {
// Only include "Expense: Need" category expenses (negative amounts)
if (transaction.amount >= 0 || transaction.category !== "Expense: Need")
return;
if (transaction.type === "recurring") {
// Convert recurring transactions to monthly amounts
const frequencyMultiplier = {
weekly: 4.33, // ~4.33 weeks per month
biweekly: 2.165, // ~2.165 biweeks per month
monthly: 1,
quarterly: 1 / 3, // 1 quarter = 1/3 month
custom: 1 / (transaction.customMonths || 1),
};
const monthlyAmount =
Math.abs(transaction.amount) *
(frequencyMultiplier[transaction.frequency || "monthly"] || 1);
totalCoreExpenses += monthlyAmount * transaction.probability;
}
});
return totalCoreExpenses;
});
// Monthly Income - Monthly recurring income
const monthlyIncome = computed(() => {
if (!cashFlow?.filteredTransactions?.value) return 0;
const transactions = cashFlow.filteredTransactions.value;
let totalIncome = 0;
transactions.forEach((transaction) => {
// Only include income (positive amounts) - accept any income category
if (transaction.amount <= 0) return;
if (transaction.type === "recurring") {
// Convert recurring transactions to monthly amounts
const frequencyMultiplier = {
weekly: 4.33, // ~4.33 weeks per month
biweekly: 2.165, // ~2.165 biweeks per month
monthly: 1,
quarterly: 1 / 3, // 1 quarter = 1/3 month
custom: 1 / (transaction.customMonths || 1),
};
const monthlyAmount =
transaction.amount *
(frequencyMultiplier[transaction.frequency || "monthly"] || 1);
totalIncome += monthlyAmount * transaction.probability;
}
});
return totalIncome;
});
const projectedTransactions = computed(() => {
if (!cashFlow?.filteredTransactions?.value) return [];
const transactions = cashFlow.filteredTransactions.value;
const projectedTransactions = [];
const today = new Date();
const endDate = new Date(today.getTime() + 13 * 7 * 24 * 60 * 60 * 1000); // 13 weeks from now
transactions.forEach((transaction) => {
if (transaction.type === "recurring") {
// Generate recurring transactions for the next 13 weeks
const startDate = new Date(
Math.max(today.getTime(), new Date(transaction.date).getTime())
);
const transactionEndDate = transaction.endDate
? new Date(transaction.endDate)
: endDate;
let currentDate = new Date(startDate);
while (currentDate <= endDate && currentDate <= transactionEndDate) {
projectedTransactions.push({
...transaction,
date: new Date(currentDate),
isProjected: true,
});
// Calculate next occurrence based on frequency
switch (transaction.frequency) {
case "weekly":
currentDate = new Date(
currentDate.getTime() + 7 * 24 * 60 * 60 * 1000
);
break;
case "biweekly":
currentDate = new Date(
currentDate.getTime() + 14 * 24 * 60 * 60 * 1000
);
break;
case "monthly":
currentDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
currentDate.getDate()
);
break;
case "quarterly":
currentDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 3,
currentDate.getDate()
);
break;
case "custom":
const months = transaction.customMonths || 1;
currentDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + months,
currentDate.getDate()
);
break;
default:
// Default to monthly
currentDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
currentDate.getDate()
);
}
}
} else if (transaction.type === "one-time") {
// Include one-time transactions that fall within the next 13 weeks
const transactionDate = new Date(transaction.date);
if (transactionDate >= today && transactionDate <= endDate) {
projectedTransactions.push({
...transaction,
date: transactionDate,
isProjected: true,
});
}
}
});
// Sort by date (earliest first)
const sorted = projectedTransactions.sort(
(a, b) => new Date(a.date) - new Date(b.date)
);
// Calculate running balance for each transaction
// Start with account-specific starting balance (without projections)
let runningBalance = cashFlow?.startingBalance?.value || 0;
return sorted.map((transaction) => {
runningBalance += transaction.amount;
return {
...transaction,
runningBalance,
};
});
});
// Methods
const handleAddTransaction = () => {
selectedTransaction.value = null;
isModalOpen.value = true;
};
const handleEditTransaction = (transaction) => {
selectedTransaction.value = transaction;
isModalOpen.value = true;
};
const closeModal = () => {
isModalOpen.value = false;
selectedTransaction.value = null;
};
const saveTransaction = (transactionData) => {
if (!cashFlow) return;
const currentTransactions = cashFlow.transactions.value;
if (selectedTransaction.value) {
// Edit existing transaction
const index = currentTransactions.findIndex(
(t) => t.id === selectedTransaction.value.id
);
if (index !== -1) {
currentTransactions[index] = transactionData;
}
} else {
// Add new transaction
currentTransactions.push(transactionData);
}
cashFlow.updateTransactions(currentTransactions);
};
const openBalanceModal = () => {
newBalance.value = cashFlow?.currentBalance?.value || 0;
isBalanceModalOpen.value = true;
};
const closeBalanceModal = () => {
isBalanceModalOpen.value = false;
};
const updateBalance = (totalBalance) => {
if (cashFlow) {
cashFlow.setCurrentBalance(totalBalance);
}
closeBalanceModal();
};
// Delete transaction methods
const handleDeleteTransaction = (transactionId) => {
const transaction = cashFlow.transactions.value.find(t => t.id === transactionId);
if (transaction) {
transactionToDelete.value = transaction;
showDeleteConfirmation.value = true;
}
};
const handleDeleteTransactionFromModal = async (transactionId) => {
try {
await cashFlow.deleteTransaction(transactionId);
} catch (error) {
console.error('Failed to delete transaction:', error);
// You could add user notification here
}
};
const cancelDelete = () => {
showDeleteConfirmation.value = false;
transactionToDelete.value = null;
};
const confirmDelete = async () => {
if (transactionToDelete.value) {
try {
await cashFlow.deleteTransaction(transactionToDelete.value.id);
cancelDelete();
} catch (error) {
console.error('Failed to delete transaction:', error);
// You could add user notification here
}
}
};
const formatCurrency = (amount, currency = "CAD") => {
// For CAD amounts in the main dashboard, show no decimals for cleaner look
const decimals = currency === "CAD" ? 0 : 2;
return new Intl.NumberFormat("en-CA", {
style: "currency",
currency: currency,
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(amount || 0);
};
const formatDate = (date) => {
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
}).format(new Date(date));
};
// Scenario management methods (Personal account only)
const updateScenario = async () => {
if (!cashFlow?.transactions?.value) return;
// All transactions are now personal
const currentTransactions = [...cashFlow.transactions.value];
if (scenario.value === "bankruptcy") {
// Switch to bankruptcy scenario
// 1. Remove Interest transaction
const filteredTransactions = currentTransactions.filter(
(t) => !(t.description === "Interest" && t.amount === -2700)
);
// 2. Add dynamic Surplus Payment transaction (only if surplus > 0)
if (calculatedSurplusPayment.value > 0) {
const surplusTransaction = {
id: "bankruptcy_surplus_payment_transaction",
description: "Bankruptcy Surplus Payment",
amount: -calculatedSurplusPayment.value,
category: "Expense: Need",
type: "recurring",
frequency: "monthly",
date: new Date(),
probability: 1.0,
isConfirmed: true,
status: "committed",
notes: `Dynamic surplus: ${formatCurrency(
householdNetIncome.value
)} income - ${formatCurrency(
surplusThreshold
)} threshold = ${formatCurrency(
householdNetIncome.value - surplusThreshold
)} * 50%`,
};
filteredTransactions.push(surplusTransaction);
}
await cashFlow.updateTransactions(filteredTransactions);
} else {
// Switch to interest scenario
// 1. Remove Surplus Payment transaction
const filteredTransactions = currentTransactions.filter(
(t) => t.description !== "Bankruptcy Surplus Payment"
);
// 2. Add back Interest transaction
const interestTransaction = {
id: interestTransactionId.value,
description: "Interest",
amount: -2700,
category: "Expense: Need",
type: "recurring",
frequency: "monthly",
date: new Date("2025-08-29"),
probability: 1.0,
isConfirmed: true,
status: "committed",
notes: "",
};
filteredTransactions.push(interestTransaction);
await cashFlow.updateTransactions(filteredTransactions);
}
};
// Update surplus payment when income changes (watch for transaction changes, Personal only)
watch(
() => calculatedSurplusPayment.value,
async (newSurplus) => {
if (
scenario.value === "bankruptcy" &&
cashFlow?.transactions?.value
) {
const currentTransactions = [...cashFlow.transactions.value];
const surplusIndex = currentTransactions.findIndex(
(t) => t.description === "Bankruptcy Surplus Payment"
);
if (newSurplus > 0) {
if (surplusIndex !== -1) {
// Update existing surplus payment amount
currentTransactions[surplusIndex].amount = -newSurplus;
currentTransactions[
surplusIndex
].notes = `Dynamic surplus: ${formatCurrency(
householdNetIncome.value
)} income - ${formatCurrency(
surplusThreshold
)} threshold = ${formatCurrency(
householdNetIncome.value - surplusThreshold
)} * 50%`;
await cashFlow.updateTransactions(currentTransactions);
} else {
// Add new surplus payment if it doesn't exist
await updateScenario();
}
} else {
// Remove surplus payment if surplus becomes 0
if (surplusIndex !== -1) {
currentTransactions.splice(surplusIndex, 1);
await cashFlow.updateTransactions(currentTransactions);
}
}
}
}
);
// Initialize - balance will be loaded from localStorage by the composable
</script>

285
app/pages/migrate.vue Normal file
View file

@ -0,0 +1,285 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Data Migration</h1>
<p class="text-gray-600 mt-2">Migrate your data from localStorage to MongoDB</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<!-- Status -->
<div v-if="status" class="mb-6 p-4 rounded" :class="statusClass">
{{ status }}
</div>
<!-- Migration Section -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">1. Auto-Migration from localStorage</h3>
<button
@click="migrateFromLocalStorage"
:disabled="loading"
class="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded">
{{ loading ? 'Migrating...' : 'Migrate from localStorage' }}
</button>
<p class="text-sm text-gray-600 mt-2">
This will automatically detect and migrate any existing localStorage data.
</p>
</div>
<!-- Manual Import Section -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">2. Manual Data Import</h3>
<!-- Current Balance -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
Current Balance (CAD $)
</label>
<input
v-model.number="manualData.balance"
type="number"
step="0.01"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0.00" />
</div>
<!-- Transactions JSON -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
Transactions JSON
</label>
<textarea
v-model="manualData.transactionsJson"
rows="10"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Paste your transactions JSON array here"></textarea>
<p class="text-sm text-gray-600 mt-1">
Paste the JSON array of your transactions (the data you provided earlier)
</p>
</div>
<button
@click="migrateManualData"
:disabled="loading"
class="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded">
{{ loading ? 'Importing...' : 'Import Manual Data' }}
</button>
</div>
<!-- Current Data Display -->
<div class="mb-8">
<h3 class="text-lg font-semibold mb-4">3. Current Data Status</h3>
<button
@click="loadCurrentData"
:disabled="loading"
class="bg-gray-500 hover:bg-gray-600 disabled:bg-gray-400 text-white font-bold py-2 px-4 rounded mb-4">
{{ loading ? 'Loading...' : 'Refresh Data Status' }}
</button>
<div class="bg-gray-50 p-4 rounded">
<p><strong>Current Balance:</strong> {{ formatCurrency(currentData.balance) }}</p>
<p><strong>Transactions:</strong> {{ currentData.transactions.length }} items</p>
<div v-if="currentData.transactions.length > 0" class="mt-2">
<p class="text-sm font-medium">Sample transactions:</p>
<ul class="text-sm text-gray-600 mt-1">
<li v-for="transaction in currentData.transactions.slice(0, 3)" :key="transaction.id">
{{ transaction.description }}: {{ formatCurrency(transaction.amount) }}
</li>
</ul>
</div>
</div>
</div>
<!-- Navigation -->
<div class="pt-4 border-t">
<NuxtLink
to="/"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
Back to Dashboard
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup>
// Page metadata
useSeoMeta({
title: 'Data Migration - Faber Finances',
description: 'Migrate your financial data'
});
// State
const loading = ref(false);
const status = ref('');
const statusClass = ref('');
const manualData = ref({
balance: 0,
transactionsJson: ''
});
const currentData = ref({
balance: 0,
transactions: []
});
// Load current data status
const loadCurrentData = async () => {
loading.value = true;
try {
// Load balance
const balanceResponse = await fetch('/api/balances');
if (balanceResponse.ok) {
const balanceData = await balanceResponse.json();
currentData.value.balance = balanceData.currentBalance || 0;
}
// Load transactions
const transactionsResponse = await fetch('/api/transactions');
if (transactionsResponse.ok) {
const transactionsData = await transactionsResponse.json();
currentData.value.transactions = transactionsData;
}
setStatus('Data loaded successfully', 'success');
} catch (error) {
console.error('Failed to load current data:', error);
setStatus('Failed to load current data: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Migrate from localStorage
const migrateFromLocalStorage = async () => {
loading.value = true;
try {
if (typeof window === 'undefined') {
throw new Error('Window not available');
}
const savedBalance = localStorage.getItem('cashflow_balance');
const savedTransactions = localStorage.getItem('cashflow_transactions');
if (!savedBalance && !savedTransactions) {
setStatus('No localStorage data found to migrate', 'warning');
return;
}
const migrationData = {
balance: savedBalance ? parseFloat(savedBalance) : 0,
transactions: savedTransactions ? JSON.parse(savedTransactions) : []
};
const response = await fetch('/api/migrate/localStorage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(migrationData)
});
if (!response.ok) {
throw new Error(`Migration failed: ${response.statusText}`);
}
const result = await response.json();
setStatus(
`Migration successful! Migrated ${result.transactionsCount} transactions and balance of ${formatCurrency(result.balance)}`,
'success'
);
// Clear localStorage after successful migration
localStorage.removeItem('cashflow_balance');
localStorage.removeItem('cashflow_transactions');
localStorage.removeItem('account_balances');
// Refresh current data
await loadCurrentData();
} catch (error) {
console.error('Migration error:', error);
setStatus('Migration failed: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Import manual data
const migrateManualData = async () => {
loading.value = true;
try {
let transactions = [];
if (manualData.value.transactionsJson.trim()) {
try {
transactions = JSON.parse(manualData.value.transactionsJson);
if (!Array.isArray(transactions)) {
throw new Error('Transactions must be an array');
}
} catch (error) {
throw new Error('Invalid JSON format: ' + error.message);
}
}
const migrationData = {
balance: manualData.value.balance || 0,
transactions: transactions
};
const response = await fetch('/api/migrate/localStorage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(migrationData)
});
if (!response.ok) {
throw new Error(`Import failed: ${response.statusText}`);
}
const result = await response.json();
setStatus(
`Import successful! Imported ${result.transactionsCount} transactions and balance of ${formatCurrency(result.balance)}`,
'success'
);
// Clear form
manualData.value = { balance: 0, transactionsJson: '' };
// Refresh current data
await loadCurrentData();
} catch (error) {
console.error('Import error:', error);
setStatus('Import failed: ' + error.message, 'error');
} finally {
loading.value = false;
}
};
// Utility functions
const setStatus = (message, type) => {
status.value = message;
statusClass.value = type === 'success' ? 'bg-green-100 text-green-800' :
type === 'warning' ? 'bg-yellow-100 text-yellow-800' :
type === 'error' ? 'bg-red-100 text-red-800' :
'bg-blue-100 text-blue-800';
// Clear status after 5 seconds
setTimeout(() => {
status.value = '';
}, 5000);
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: 'CAD'
}).format(amount || 0);
};
// Load current data on mount
onMounted(() => {
loadCurrentData();
});
</script>

24
app/pages/minimal.vue Normal file
View file

@ -0,0 +1,24 @@
<template>
<div class="p-8">
<h1 class="text-3xl font-bold text-blue-600">Minimal Test Page</h1>
<p class="mt-4">If you can see this, basic routing and Vue are working!</p>
<div class="mt-6 p-4 bg-green-50 border border-green-200 rounded">
<p> Vue template rendering: Working</p>
<p> Tailwind CSS: Working</p>
<p> Nuxt routing: Working</p>
</div>
<div class="mt-4">
<button
@click="counter++"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Counter: {{ counter }}
</button>
</div>
</div>
</template>
<script setup>
const counter = ref(0);
</script>

96
app/pages/simple.vue Normal file
View file

@ -0,0 +1,96 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Simple Test Page</h1>
<div class="space-y-4">
<p>This is a simple test page to verify basic functionality.</p>
<div class="p-4 bg-blue-50 border rounded">
<p>
Testing composable:
{{ composableWorking ? "Working" : "Not working" }}
</p>
<p>Current balance: {{ currentBalance }}</p>
<p>Transactions count: {{ transactionCount }}</p>
</div>
<div class="space-x-2">
<button
@click="setBalance"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Set Balance to $50,000
</button>
<button
@click="addSampleTransaction"
class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
Add Sample Transaction
</button>
</div>
<div
v-if="errorMessage"
class="p-4 bg-red-50 border border-red-200 rounded">
<p class="text-red-700">Error: {{ errorMessage }}</p>
</div>
</div>
</div>
</template>
<script setup>
const errorMessage = ref("");
const composableWorking = ref(false);
const currentBalance = ref(0);
const transactionCount = ref(0);
let cashFlow = null;
try {
cashFlow = useCashFlow();
composableWorking.value = true;
// Watch for changes
watchEffect(() => {
if (cashFlow) {
currentBalance.value = cashFlow.currentBalance.value;
transactionCount.value = cashFlow.transactions.value.length;
}
});
} catch (error) {
errorMessage.value = `Failed to initialize cashFlow: ${error.message}`;
console.error("CashFlow error:", error);
}
const setBalance = () => {
try {
if (cashFlow) {
cashFlow.setCurrentBalance(50000);
}
} catch (error) {
errorMessage.value = `Failed to set balance: ${error.message}`;
}
};
const addSampleTransaction = () => {
try {
if (cashFlow) {
const sampleTransaction = {
id: `sample-${Date.now()}`,
date: new Date(),
description: "Sample Transaction",
amount: 1000,
category: "Income",
type: "one-time",
probability: 1.0,
isConfirmed: true,
};
cashFlow.updateTransactions([
...cashFlow.transactions.value,
sampleTransaction,
]);
}
} catch (error) {
errorMessage.value = `Failed to add transaction: ${error.message}`;
}
};
</script>

206
app/pages/test.vue Normal file
View file

@ -0,0 +1,206 @@
<template>
<div class="min-h-screen bg-gray-50">
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">Cash Flow Test Page</h1>
<!-- Test Results -->
<UCard class="mb-6">
<template #header>
<h2 class="text-lg font-semibold">Component Test Results</h2>
</template>
<div class="space-y-4">
<div
v-for="test in testResults"
:key="test.name"
class="flex items-center justify-between">
<span>{{ test.name }}</span>
<UBadge :color="test.passed ? 'green' : 'red'">
{{ test.passed ? "PASS" : "FAIL" }}
</UBadge>
</div>
</div>
</UCard>
<!-- Cash Flow Summary -->
<UCard class="mb-6">
<template #header>
<h2 class="text-lg font-semibold">Cash Flow Summary</h2>
</template>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center">
<p class="text-sm text-gray-600">Current Balance</p>
<p class="text-xl font-bold">
{{ formatCurrency(cashFlow.currentBalance.value) }}
</p>
</div>
<div class="text-center">
<p class="text-sm text-gray-600">Transactions</p>
<p class="text-xl font-bold">
{{ cashFlow.transactions.value.length }}
</p>
</div>
<div class="text-center">
<p class="text-sm text-gray-600">Monthly Burn</p>
<p class="text-xl font-bold">
{{ formatCurrency(cashFlow.monthlyBurnRate.value) }}
</p>
</div>
<div class="text-center">
<p class="text-sm text-gray-600">Runway Days</p>
<p class="text-xl font-bold">
{{ Math.round(cashFlow.runwayDays.value) }}
</p>
</div>
</div>
</UCard>
<!-- Mini Chart Test -->
<UCard class="mb-6">
<template #header>
<h2 class="text-lg font-semibold">Chart Test (8 weeks)</h2>
</template>
<CashFlowChart :data="testProjections" :critical-balance="30000" />
</UCard>
<!-- Mini Transaction Manager Test -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold">Transaction Manager Test</h2>
</template>
<RecurringTransactionManager
:transactions="cashFlow.transactions.value"
@add-transaction="handleAddTransaction"
@edit-transaction="handleEditTransaction" />
</UCard>
</div>
</div>
</template>
<script setup lang="ts">
import type { Transaction } from "~/types/cashflow";
// SEO
useSeoMeta({
title: "Test Page",
description: "Testing cash flow functionality",
});
// Initialize cash flow
const cashFlow = useCashFlow();
// Test data
const testTransactions: Transaction[] = [
{
id: "test-1",
date: new Date("2024-01-01"),
description: "Test Salary",
amount: 10000,
category: "Income",
type: "recurring",
frequency: "monthly",
probability: 1.0,
isConfirmed: true,
status: "committed",
},
{
id: "test-2",
date: new Date("2024-01-01"),
description: "Test Rent",
amount: -2000,
category: "Expense: Need",
type: "recurring",
frequency: "monthly",
probability: 1.0,
isConfirmed: true,
status: "committed",
},
];
// Test results
const testResults = computed(() => {
const results = [];
// Test 1: Composable loads
try {
results.push({
name: "useCashFlow composable loads",
passed: !!cashFlow,
});
} catch {
results.push({
name: "useCashFlow composable loads",
passed: false,
});
}
// Test 2: Transaction updates work
try {
results.push({
name: "Transaction updates work",
passed: typeof cashFlow.updateTransactions === "function",
});
} catch {
results.push({
name: "Transaction updates work",
passed: false,
});
}
// Test 3: Projections generate
try {
const projections = cashFlow.generateWeeklyProjections(4);
results.push({
name: "Weekly projections generate",
passed: Array.isArray(projections) && projections.length > 0,
});
} catch {
results.push({
name: "Weekly projections generate",
passed: false,
});
}
// Test 4: Runway calculation works
try {
const runway = cashFlow.runwayDays.value;
results.push({
name: "Runway calculation works",
passed: typeof runway === "number",
});
} catch {
results.push({
name: "Runway calculation works",
passed: false,
});
}
return results;
});
const testProjections = computed(() => {
return cashFlow.generateWeeklyProjections(8);
});
const handleAddTransaction = () => {
console.log("Add transaction test");
};
const handleEditTransaction = (transaction: Transaction) => {
console.log("Edit transaction test:", transaction.description);
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount);
};
// Initialize test data
onMounted(() => {
cashFlow.setCurrentBalance(75000);
cashFlow.updateTransactions(testTransactions);
});
</script>