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

@ -431,13 +431,16 @@ export const useBudgetStore = defineStore(
console.log("Current revenue items:", budgetWorksheet.value.revenue.length);
console.log("Current expense items:", budgetWorksheet.value.expenses.length);
// Check if we have actual budget data (not just initialized flag)
if (isInitialized.value && (budgetWorksheet.value.revenue.length > 0 || budgetWorksheet.value.expenses.length > 0)) {
console.log("Already initialized with data, skipping...");
// Check if we have actual budget data - prioritize preserving user data
const hasUserData = budgetWorksheet.value.revenue.length > 0 || budgetWorksheet.value.expenses.length > 0;
if (hasUserData) {
console.log("Budget data already exists with user changes, preserving...");
isInitialized.value = true; // Mark as initialized to prevent future overwrites
return;
}
console.log("Initializing budget from wizard data...");
console.log("No existing budget data found, initializing from wizard data...");
try {
// Use the new coopBuilder store instead of the old stores
@ -449,7 +452,7 @@ export const useBudgetStore = defineStore(
console.log("- Equal wage:", coopStore.equalHourlyWage || "No wage set");
console.log("- Overhead costs:", coopStore.overheadCosts.length, coopStore.overheadCosts);
// Clear existing data
// Only clear data if we're truly initializing from scratch
budgetWorksheet.value.revenue = [];
budgetWorksheet.value.expenses = [];
@ -851,13 +854,22 @@ export const useBudgetStore = defineStore(
}
function updateMonthlyValue(category, itemId, monthKey, value) {
console.log('updateMonthlyValue called:', { category, itemId, monthKey, value });
const items = budgetWorksheet.value[category];
const item = items.find((i) => i.id === itemId);
if (item) {
if (!item.monthlyValues) {
item.monthlyValues = {};
}
item.monthlyValues[monthKey] = Number(value) || 0;
const numericValue = Number(value) || 0;
// Update directly - Pinia's reactivity will handle persistence
item.monthlyValues[monthKey] = numericValue;
console.log('Updated item.monthlyValues:', item.monthlyValues);
console.log('Item updated:', item.name);
} else {
console.error('Item not found:', { category, itemId, availableItems: items.map(i => ({id: i.id, name: i.name})) });
}
}

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"
],
},
});

View file

@ -141,6 +141,16 @@ export const useMembersStore = defineStore(
const member = members.value.find((m) => m.id === memberId);
if (member) {
member.capacity = { ...member.capacity, ...capacity };
// Recalculate monthly pay based on new capacity
recalculateMemberPay(memberId);
}
}
function recalculateMemberPay(memberId, equalWage = 25) {
const member = members.value.find((m) => m.id === memberId);
if (member) {
const targetHours = member.capacity?.targetHours || 0;
member.monthlyPayPlanned = targetHours * equalWage;
}
}
@ -197,18 +207,47 @@ export const useMembersStore = defineStore(
}
// Coverage calculations for individual members
function getMemberCoverage(memberId) {
function getMemberCoverage(memberId, equalWage = 25) {
const member = members.value.find((m) => m.id === memberId);
if (!member) return { coveragePct: undefined };
if (!member) return { minPct: 0, targetPct: 0 };
return coverage(
member.minMonthlyNeeds || 0,
member.monthlyPayPlanned || 0
);
// Calculate what they're getting paid based on their hours
const minHours = member.capacity?.minHours || 0;
const targetHours = member.capacity?.targetHours || 0;
// Current monthly pay planned
const monthlyPay = member.monthlyPayPlanned || 0;
// Calculate coverage percentages
const minMonthlyPay = minHours * equalWage;
const targetMonthlyPay = targetHours * equalWage;
const minPct = minMonthlyPay > 0 ? Math.min(100, (monthlyPay / minMonthlyPay) * 100) : 0;
const targetPct = targetMonthlyPay > 0 ? Math.min(100, (monthlyPay / targetMonthlyPay) * 100) : 0;
return { minPct, targetPct };
}
// Team-wide coverage statistics
const teamStats = computed(() => teamCoverageStats(members.value));
// Team-wide coverage statistics - accepts equalWage parameter
function getTeamStats(equalWage = 25) {
const coverageValues = members.value.map(m => {
const coverage = getMemberCoverage(m.id, equalWage);
return coverage.minPct;
}).filter(v => v !== undefined);
if (coverageValues.length === 0) {
return { under100: 0, median: 0 };
}
const sorted = [...coverageValues].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
const under100 = coverageValues.filter(v => v < 100).length;
return { under100, median };
}
// Computed team stats (using default wage)
const teamStats = computed(() => getTeamStats());
// Pay policy configuration
const payPolicy = ref({
@ -258,6 +297,8 @@ export const useMembersStore = defineStore(
setMonthlyNeeds,
setPlannedPay,
getMemberCoverage,
getTeamStats,
recalculateMemberPay,
// Legacy actions
addMember,
updateMember,