- Fixed brutalist styling across all components - Removed notes field from transaction list display - Added whitespace-nowrap to prevent description wrapping - Updated modals with consistent border styling - Improved form layouts and button styling
798 lines
27 KiB
Vue
798 lines
27 KiB
Vue
<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-4 border-black p-6">
|
|
<div class="text-center">
|
|
<p class="text-sm font-bold uppercase text-black">
|
|
AVAILABLE BALANCE
|
|
</p>
|
|
<p class="text-2xl font-bold text-black">
|
|
{{ formatCurrency(cashFlow?.startingBalance?.value || 0) }}
|
|
</p>
|
|
<button
|
|
@click="openBalanceModal"
|
|
class="mt-2 text-xs text-black hover:bg-black hover:text-white px-2 py-1 border-2 border-black font-bold uppercase">
|
|
UPDATE
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Runway Card -->
|
|
<div class="bg-white border-4 border-black p-6">
|
|
<div class="text-center">
|
|
<p class="text-sm font-bold uppercase text-black">RUNWAY</p>
|
|
<p class="text-2xl font-bold text-black">
|
|
{{ runwayText }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Core Expenses Card -->
|
|
<div class="bg-white border-4 border-black p-6">
|
|
<div class="text-center">
|
|
<p class="text-sm font-bold uppercase text-black">
|
|
CORE EXPENSES
|
|
</p>
|
|
<p class="text-2xl font-bold text-black">
|
|
{{ formatCurrency(monthlyCoreExpenses) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Income Card -->
|
|
<div class="bg-white border-4 border-black p-6">
|
|
<div class="text-center">
|
|
<p class="text-sm font-bold uppercase text-black">MONTHLY INCOME</p>
|
|
<p class="text-2xl font-bold text-black">
|
|
{{ formatCurrency(monthlyIncome) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scenario Control Panel -->
|
|
<div class="bg-white border-4 border-black 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-bold uppercase text-black">
|
|
FINANCIAL SCENARIO
|
|
</h3>
|
|
<p class="text-sm text-black">
|
|
<span
|
|
class="font-bold uppercase">
|
|
{{
|
|
scenario === "interest"
|
|
? "CURRENT (INTEREST)"
|
|
: "BANKRUPTCY (SURPLUS)"
|
|
}}
|
|
</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-bold uppercase text-black">SCENARIO:</label>
|
|
<select
|
|
v-model="scenario"
|
|
@change="updateScenario"
|
|
class="px-3 py-2 border-2 border-black bg-white focus:outline-none focus:bg-black focus:text-white font-bold">
|
|
<option value="interest">INTEREST</option>
|
|
<option value="bankruptcy">BANKRUPTCY</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-bold uppercase text-black">
|
|
SURPLUS: {{ formatCurrency(calculatedSurplusPayment) }}/MO
|
|
</div>
|
|
<div class="text-xs text-black uppercase">
|
|
INC: {{ formatCurrency(householdNetIncome) }} | THR:
|
|
{{ formatCurrency(surplusThreshold) }} | AVL:
|
|
{{
|
|
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-black bg-opacity-75 overflow-y-auto h-full w-full z-50">
|
|
<div
|
|
class="relative top-20 mx-auto p-5 border-4 border-black w-96 bg-white">
|
|
<div class="mt-3">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-bold uppercase text-black">
|
|
CONFIRM DELETE
|
|
</h3>
|
|
<button @click="cancelDelete" class="text-black hover:bg-black hover:text-white p-1">
|
|
<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-black">
|
|
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-white border-2 border-black">
|
|
<p class="text-sm font-bold">{{ transactionToDelete.description }}</p>
|
|
<p class="text-sm text-black">
|
|
{{ 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-bold text-black bg-white border-2 border-black hover:bg-black hover:text-white focus:outline-none uppercase">
|
|
CANCEL
|
|
</button>
|
|
<button
|
|
@click="confirmDelete"
|
|
class="px-4 py-2 text-sm font-bold text-white bg-black border-2 border-black hover:bg-white hover:text-black focus:outline-none uppercase">
|
|
DELETE
|
|
</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 border-4 border-black overflow-hidden">
|
|
<div
|
|
class="px-6 py-4 border-b-4 border-black flex justify-between items-center">
|
|
<h3 class="text-lg font-bold uppercase">
|
|
ALL TRANSACTIONS - NEXT 13 WEEKS
|
|
</h3>
|
|
<button
|
|
@click="handleAddTransaction"
|
|
class="bg-black text-white hover:bg-white hover:text-black font-bold py-2 px-4 border-2 border-black flex items-center uppercase">
|
|
<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-bold uppercase tracking-wider">
|
|
DATE
|
|
</th>
|
|
<th
|
|
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider">
|
|
DESCRIPTION
|
|
</th>
|
|
<th
|
|
class="px-6 py-3 text-left text-xs font-bold uppercase tracking-wider">
|
|
CATEGORY
|
|
</th>
|
|
<th
|
|
class="px-6 py-3 text-right text-xs font-bold uppercase tracking-wider">
|
|
AMOUNT
|
|
</th>
|
|
<th
|
|
class="px-6 py-3 text-right text-xs font-bold uppercase tracking-wider">
|
|
BALANCE
|
|
</th>
|
|
<th
|
|
class="px-6 py-3 text-center text-xs font-bold uppercase tracking-wider">
|
|
ACTIONS
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y-2 divide-black">
|
|
<tr
|
|
v-for="transaction in projectedTransactions"
|
|
:key="`${transaction.id}-${transaction.date}`"
|
|
class="hover:bg-gray-100">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-black font-mono">
|
|
{{ formatDate(transaction.date) }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div>
|
|
<p class="text-sm font-bold text-black">
|
|
{{ transaction.description }}
|
|
</p>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="text-sm text-black font-mono">{{
|
|
transaction.category
|
|
}}</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<div class="text-black">
|
|
<p class="text-sm font-bold font-mono">
|
|
{{ formatCurrency(transaction.amount) }}
|
|
</p>
|
|
<!-- Show business split info if applicable -->
|
|
<div v-if="transaction.businessPercentage > 0" class="text-xs text-black space-y-0.5">
|
|
<div>PER: {{ formatCurrency(transaction.amount) }}</div>
|
|
<div>BUS: {{ formatCurrency(transaction.businessAmount || 0) }} ({{ transaction.businessPercentage }}%)</div>
|
|
<div class="font-bold">TTL: {{ 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-black">
|
|
({{
|
|
formatCurrency(
|
|
transaction.originalAmount,
|
|
transaction.originalCurrency
|
|
)
|
|
}})
|
|
</p>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<p
|
|
class="text-sm font-bold font-mono"
|
|
:class="
|
|
transaction.runningBalance < 0
|
|
? 'bg-black text-white px-1'
|
|
: 'text-black'
|
|
">
|
|
{{ formatCurrency(transaction.runningBalance) }}
|
|
</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-black text-sm font-bold px-2 py-1 border-2 border-black hover:bg-black hover:text-white uppercase">
|
|
EDIT
|
|
</button>
|
|
<button
|
|
@click="handleDeleteTransaction(transaction.id)"
|
|
class="text-black text-sm font-bold px-2 py-1 border-2 border-black hover:bg-black hover:text-white uppercase">
|
|
DEL
|
|
</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-black"
|
|
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-bold uppercase text-black mb-2">
|
|
NO TRANSACTIONS
|
|
</h3>
|
|
<p class="text-black 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>
|