refactor: update routing paths in app.vue, enhance AnnualBudget component layout, and streamline dashboard and budget pages for improved user experience

This commit is contained in:
Jennie Robinson Faber 2025-09-08 09:39:30 +01:00
parent 09d8794d72
commit 864a81065c
23 changed files with 3211 additions and 1978 deletions

View file

@ -1,8 +1,28 @@
import { defineStore } from "pinia";
import type { OneOffEvent } from '~/types/cash'
export interface CashEvent {
id: string
date: string
week: number
month: number
type: 'Influx' | 'Outflow'
amount: number
sourceRef: string
policyTag: string
category: string
name: string
certainty?: 'Confirmed' | 'Likely' | 'Potential'
isRecurring: boolean
notes?: string
}
export const useCashStore = defineStore("cash", () => {
// 13-week calendar events
const cashEvents = ref([]);
const cashEvents = ref<CashEvent[]>([]);
// One-off events for longer-term planning (12+ months)
const oneOffEvents = ref<OneOffEvent[]>([]);
// Payment queue - staged payments within policy
const paymentQueue = ref([]);
@ -18,33 +38,123 @@ export const useCashStore = defineStore("cash", () => {
const weeklyProjections = computed(() => {
const weeks = [];
let runningBalance = currentCash.value;
const budgetStore = useBudgetStore();
// Get budget data for the next 3+ months to cover 13 weeks
const today = new Date();
let totalMonthlyRevenue = 0;
let totalMonthlyExpenses = 0;
// Average across current and next 2 months
for (let monthOffset = 0; monthOffset < 3; monthOffset++) {
const targetMonth = new Date(today.getFullYear(), today.getMonth() + monthOffset, 1);
const monthKey = `${targetMonth.getFullYear()}-${String(targetMonth.getMonth() + 1).padStart(2, '0')}`;
totalMonthlyRevenue += budgetStore.monthlyTotals?.[monthKey]?.revenue || 0;
totalMonthlyExpenses += budgetStore.monthlyTotals?.[monthKey]?.expenses || 0;
}
// Convert to weekly averages (3 months = 13 weeks)
const weeklyRevenue = totalMonthlyRevenue / 13;
const weeklyExpenses = totalMonthlyExpenses / 13;
for (let week = 1; week <= 13; week++) {
// Start with budget-based weekly flows
let weekInflow = weeklyRevenue;
let weekOutflow = weeklyExpenses;
// Add any specific cash events for this week
const weekEvents = cashEvents.value.filter((e) => e.week === week);
const weekInflow = weekEvents
weekInflow += weekEvents
.filter((e) => e.type === "Influx")
.reduce((sum, e) => sum + e.amount, 0);
const weekOutflow = weekEvents
weekOutflow += weekEvents
.filter((e) => e.type === "Outflow")
.reduce((sum, e) => sum + e.amount, 0);
// Add one-off transactions that fall in this week
const weekStart = new Date(today.getTime() + (week - 1) * 7 * 24 * 60 * 60 * 1000);
const weekEnd = new Date(weekStart.getTime() + 6 * 24 * 60 * 60 * 1000);
const weekOneOffs = oneOffEvents.value.filter(event => {
if (!event.dateExpected) return false;
const eventDate = new Date(event.dateExpected);
return eventDate >= weekStart && eventDate <= weekEnd;
});
weekOneOffs.forEach(event => {
if (event.type === 'income') {
weekInflow += event.amount;
} else {
weekOutflow += event.amount;
}
});
const net = weekInflow - weekOutflow;
runningBalance += net;
weeks.push({
number: week,
inflow: weekInflow,
outflow: weekOutflow,
net,
balance: runningBalance,
cushion: runningBalance, // Will be calculated properly later
breachesCushion: false, // Will be calculated properly later
inflow: Math.round(weekInflow),
outflow: Math.round(weekOutflow),
net: Math.round(net),
balance: Math.round(runningBalance),
cushion: Math.round(runningBalance),
breachesCushion: runningBalance < 0,
weekStart: weekStart.toISOString().split('T')[0],
weekEnd: weekEnd.toISOString().split('T')[0],
oneOffEvents: weekOneOffs
});
}
return weeks;
});
// Computed monthly projections including one-off events
const monthlyProjections = computed(() => {
const months = [];
let runningBalance = 0; // Always start with $0 for new cooperatives
const budgetStore = useBudgetStore();
for (let month = 0; month < 12; month++) {
let monthlyRevenue = 0;
let monthlyExpenses = 0;
// Get regular revenue/expenses from budget
if (budgetStore.monthlyTotals) {
const today = new Date();
const targetMonth = new Date(today.getFullYear(), today.getMonth() + month, 1);
const monthKey = `${targetMonth.getFullYear()}-${String(targetMonth.getMonth() + 1).padStart(2, '0')}`;
monthlyRevenue = budgetStore.monthlyTotals[monthKey]?.revenue || 0;
monthlyExpenses = budgetStore.monthlyTotals[monthKey]?.expenses || 0;
}
// Add one-off events for this month
const monthOneOffs = oneOffEvents.value.filter(event => event.month === month);
monthOneOffs.forEach(event => {
if (event.type === 'income') {
monthlyRevenue += event.amount;
} else {
monthlyExpenses += event.amount;
}
});
const netCashFlow = monthlyRevenue - monthlyExpenses;
runningBalance += netCashFlow;
months.push({
month,
monthName: new Date(2024, month).toLocaleString('en', { month: 'short' }),
revenue: monthlyRevenue,
expenses: monthlyExpenses,
netCashFlow,
runningBalance,
oneOffEvents: monthOneOffs
});
}
return months;
});
// Actions
function addCashEvent(event) {
cashEvents.value.push({
@ -97,19 +207,79 @@ export const useCashStore = defineStore("cash", () => {
currentSavings.value = savings;
}
// One-off events management
function addOneOffEvent(event: Omit<OneOffEvent, 'id'>): string {
const id = `oneoff-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Calculate month from dateExpected
const eventDate = new Date(event.dateExpected);
const month = eventDate.getMonth();
const newEvent: OneOffEvent = {
id,
month,
...event
};
oneOffEvents.value.push(newEvent);
return id;
}
function updateOneOffEvent(eventId: string, updates: Partial<OneOffEvent>) {
const event = oneOffEvents.value.find(e => e.id === eventId);
if (event) {
Object.assign(event, updates);
}
}
function removeOneOffEvent(eventId: string) {
const index = oneOffEvents.value.findIndex(e => e.id === eventId);
if (index > -1) {
oneOffEvents.value.splice(index, 1);
}
}
function getEventsByMonth(month: number) {
return oneOffEvents.value.filter(event => event.month === month);
}
function addCashEventFromOneOff(oneOffEvent: OneOffEvent, weekNumber: number) {
// Convert a one-off event to a weekly cash event
const cashEvent: Partial<CashEvent> = {
date: oneOffEvent.dateExpected,
week: weekNumber,
month: oneOffEvent.month,
type: oneOffEvent.type === 'income' ? 'Influx' : 'Outflow',
amount: oneOffEvent.amount,
sourceRef: oneOffEvent.id,
policyTag: oneOffEvent.category,
category: oneOffEvent.category,
name: oneOffEvent.name,
isRecurring: false
};
addCashEvent(cashEvent);
}
return {
cashEvents: readonly(cashEvents),
oneOffEvents: readonly(oneOffEvents),
paymentQueue: readonly(paymentQueue),
firstBreachWeek: readonly(firstBreachWeek),
currentCash: readonly(currentCash),
currentSavings: readonly(currentSavings),
weeklyProjections,
monthlyProjections,
addCashEvent,
updateCashEvent,
removeCashEvent,
addToPaymentQueue,
stagePayment,
updateCurrentBalances,
// One-off events
addOneOffEvent,
updateOneOffEvent,
removeOneOffEvent,
getEventsByMonth,
addCashEventFromOneOff
};
}, {
persist: {
@ -118,7 +288,8 @@ export const useCashStore = defineStore("cash", () => {
"currentCash",
"currentSavings",
"cashEvents",
"paymentQueue"
"paymentQueue",
"oneOffEvents"
],
},
});