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([]); // One-off events for longer-term planning (12+ months) const oneOffEvents = ref([]); // Payment queue - staged payments within policy const paymentQueue = ref([]); // Week that first breaches minimum cushion const firstBreachWeek = ref(null); // Current cash and savings balances - start empty const currentCash = ref(0); const currentSavings = ref(0); // Computed weekly projections 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); weekInflow += weekEvents .filter((e) => e.type === "Influx") .reduce((sum, e) => sum + e.amount, 0); 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: 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({ id: Date.now().toString(), date: event.date, week: event.week, type: event.type, // Influx|Outflow amount: event.amount, sourceRef: event.sourceRef, policyTag: event.policyTag, // Payroll|Tax|Vendor|SavingsSweep ...event, }); } function updateCashEvent(id, updates) { const event = cashEvents.value.find((e) => e.id === id); if (event) { Object.assign(event, updates); } } function removeCashEvent(id) { const index = cashEvents.value.findIndex((e) => e.id === id); if (index > -1) { cashEvents.value.splice(index, 1); } } function addToPaymentQueue(payment) { paymentQueue.value.push({ id: Date.now().toString(), amount: payment.amount, recipient: payment.recipient, scheduledWeek: payment.scheduledWeek, priority: payment.priority, canStage: payment.canStage !== false, ...payment, }); } function stagePayment(paymentId, newWeek) { const payment = paymentQueue.value.find((p) => p.id === paymentId); if (payment && payment.canStage) { payment.scheduledWeek = newWeek; } } function updateCurrentBalances(cash, savings) { currentCash.value = cash; currentSavings.value = savings; } // One-off events management function addOneOffEvent(event: Omit): 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) { 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 = { 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: { key: "urgent-tools-cash", paths: [ "currentCash", "currentSavings", "cashEvents", "paymentQueue", "oneOffEvents" ], }, });