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:
parent
09d8794d72
commit
864a81065c
23 changed files with 3211 additions and 1978 deletions
191
stores/cash.ts
191
stores/cash.ts
|
|
@ -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"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue