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