import { defineStore } from "pinia"; import { allocatePayroll } from "~/types/members"; export const useBudgetStore = defineStore( "budget", () => { // Schema version for persistence const schemaVersion = "2.0"; // Canonical categories from WizardRevenueStep - matches the wizard exactly const revenueCategories = ref([ "Games & Products", "Services & Contracts", "Grants & Funding", "Community Support", "Partnerships", "Investment Income", "In-Kind Contributions", ]); // Revenue subcategories by main category (for reference and grouping) const revenueSubcategories = ref({ "Games & Products": [ "Direct sales", "Platform revenue share", "DLC/expansions", "Merchandise", ], "Services & Contracts": [ "Contract development", "Consulting", "Workshops/teaching", "Technical services", ], "Grants & Funding": [ "Government funding", "Arts council grants", "Foundation support", "Research grants", ], "Community Support": [ "Patreon/subscriptions", "Crowdfunding", "Donations", "Mutual aid received", ], Partnerships: [ "Corporate partnerships", "Academic partnerships", "Sponsorships", ], "Investment Income": ["Impact investment", "Venture capital", "Loans"], "In-Kind Contributions": [ "Office space", "Equipment/hardware", "Software licenses", "Professional services", "Marketing/PR services", "Legal services", ], }); const expenseCategories = ref([ "Salaries & Benefits", "Development Costs", "Equipment & Technology", "Marketing & Outreach", "Office & Operations", "Legal & Professional", "Other Expenses", ]); // Define budget item type interface BudgetItem { id: string; name: string; mainCategory: string; subcategory: string; source: string; monthlyValues: Record; values: { year1: { best: number; worst: number; mostLikely: number }; year2: { best: number; worst: number; mostLikely: number }; year3: { best: number; worst: number; mostLikely: number }; }; } // NEW: Budget worksheet structure (starts empty, populated from wizard data) const budgetWorksheet = ref<{ revenue: BudgetItem[]; expenses: BudgetItem[]; }>({ revenue: [], expenses: [], }); // Track if worksheet has been initialized from wizard data const isInitialized = ref(false); // LEGACY: Keep for backward compatibility const budgetLines = ref({}); const overheadCosts = ref([]); const productionCosts = ref([]); // Computed grouped data for category headers const groupedRevenue = computed(() => { const groups: Record = {}; revenueCategories.value.forEach((category) => { groups[category] = budgetWorksheet.value.revenue.filter( (item) => item.mainCategory === category ); }); return groups; }); const groupedExpenses = computed(() => { const groups: Record = {}; expenseCategories.value.forEach((category) => { groups[category] = budgetWorksheet.value.expenses.filter( (item) => item.mainCategory === category ); }); return groups; }); // Computed totals for budget worksheet (legacy - keep for backward compatibility) const budgetTotals = computed(() => { const years = ["year1", "year2", "year3"]; const scenarios = ["best", "worst", "mostLikely"]; const totals = {}; years.forEach((year) => { totals[year] = {}; scenarios.forEach((scenario) => { // Calculate revenue total const revenueTotal = budgetWorksheet.value.revenue.reduce( (sum, item) => { return sum + (item.values?.[year]?.[scenario] || 0); }, 0 ); // Calculate expenses total const expensesTotal = budgetWorksheet.value.expenses.reduce( (sum, item) => { return sum + (item.values?.[year]?.[scenario] || 0); }, 0 ); // Calculate net income const netIncome = revenueTotal - expensesTotal; totals[year][scenario] = { revenue: revenueTotal, expenses: expensesTotal, net: netIncome, }; }); }); return totals; }); // Monthly totals computation const monthlyTotals = computed(() => { const totals: Record< string, { revenue: number; expenses: number; net: number } > = {}; // Generate month keys for next 12 months const today = new Date(); for (let i = 0; i < 12; i++) { const date = new Date(today.getFullYear(), today.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; // Calculate revenue total for this month const revenueTotal = budgetWorksheet.value.revenue.reduce( (sum, item) => { return sum + (item.monthlyValues?.[monthKey] || 0); }, 0 ); // Calculate expenses total for this month const expensesTotal = budgetWorksheet.value.expenses.reduce( (sum, item) => { return sum + (item.monthlyValues?.[monthKey] || 0); }, 0 ); // Calculate net income const netIncome = revenueTotal - expensesTotal; totals[monthKey] = { revenue: revenueTotal, expenses: expensesTotal, net: netIncome, }; } return totals; }); // Cumulative balance computation (running cash balance) const cumulativeBalances = computed(() => { const balances: Record = {}; let runningBalance = 0; // Assuming starting balance of 0 - could be configurable // Generate month keys for next 12 months in order const today = new Date(); for (let i = 0; i < 12; i++) { const date = new Date(today.getFullYear(), today.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; // Add this month's net income to running balance const monthlyNet = monthlyTotals.value[monthKey]?.net || 0; runningBalance += monthlyNet; balances[monthKey] = runningBalance; } return balances; }); // LEGACY: Keep for backward compatibility const currentDate = new Date(); const currentYear = currentDate.getFullYear(); const currentMonth = String(currentDate.getMonth() + 1).padStart(2, "0"); const currentPeriod = ref(`${currentYear}-${currentMonth}`); const currentBudget = computed(() => { return ( budgetLines.value[currentPeriod.value] || { period: currentPeriod.value, revenueByStream: {}, payrollCosts: { memberHours: [], oncostApplied: 0 }, overheadCosts: [], productionCosts: [], savingsChange: 0, net: 0, } ); }); // Actions function setBudgetLine(period, budgetData) { budgetLines.value[period] = { period, ...budgetData, }; } function updateRevenue(period, streamId, type, amount) { if (!budgetLines.value[period]) { budgetLines.value[period] = { period, revenueByStream: {} }; } if (!budgetLines.value[period].revenueByStream[streamId]) { budgetLines.value[period].revenueByStream[streamId] = {}; } budgetLines.value[period].revenueByStream[streamId][type] = amount; // Refresh payroll to account for revenue changes refreshPayrollInBudget(); } // Wizard-required actions function addOverheadLine(cost) { // Allow creating a blank line so the user can fill it out in the UI const safeName = cost?.name ?? ""; const safeAmountMonthly = typeof cost?.amountMonthly === "number" && !Number.isNaN(cost.amountMonthly) ? cost.amountMonthly : 0; overheadCosts.value.push({ id: Date.now().toString(), name: safeName, amount: safeAmountMonthly, category: cost?.category || "Operations", recurring: cost?.recurring ?? true, ...cost, }); } function removeOverheadLine(id) { const index = overheadCosts.value.findIndex((c) => c.id === id); if (index > -1) { overheadCosts.value.splice(index, 1); } } function addOverheadCost(cost) { addOverheadLine({ name: cost.name, amountMonthly: cost.amount, ...cost }); } function addProductionCost(cost) { productionCosts.value.push({ id: Date.now().toString(), name: cost.name, amount: cost.amount, category: cost.category || "Production", period: cost.period, ...cost, }); } function setCurrentPeriod(period) { currentPeriod.value = period; } // Helper function to map stream category to budget category function mapStreamToBudgetCategory(streamCategory) { const categoryLower = (streamCategory || "").toLowerCase(); // More comprehensive category mapping if (categoryLower.includes("game") || categoryLower.includes("product")) { return "Games & Products"; } else if (categoryLower.includes("service") || categoryLower.includes("consulting")) { return "Services & Contracts"; } else if (categoryLower.includes("grant") || categoryLower.includes("funding")) { return "Grants & Funding"; } else if (categoryLower.includes("community") || categoryLower.includes("donation")) { return "Community Support"; } else if (categoryLower.includes("partnership")) { return "Partnerships"; } else if (categoryLower.includes("investment")) { return "Investment Income"; } else if (categoryLower.includes("other")) { return "Services & Contracts"; // Map "Other" to services as fallback } else { return "Games & Products"; // Default fallback } } // Refresh payroll in budget when policy or operating mode changes function refreshPayrollInBudget() { console.log("=== REFRESH PAYROLL CALLED ==="); if (!isInitialized.value) { console.log("Not initialized, skipping payroll refresh"); return; } const coopStore = useCoopBuilderStore(); const basePayrollIndex = budgetWorksheet.value.expenses.findIndex(item => item.id === "expense-payroll-base"); const oncostIndex = budgetWorksheet.value.expenses.findIndex(item => item.id === "expense-payroll-oncosts"); // If no split payroll entries exist, look for legacy single entry const legacyIndex = budgetWorksheet.value.expenses.findIndex(item => item.id === "expense-payroll"); if (basePayrollIndex === -1 && legacyIndex === -1) return; // No existing payroll entries const totalHours = coopStore.members.reduce((sum, m) => sum + (m.hoursPerMonth || 0), 0); const hourlyWage = coopStore.equalHourlyWage || 0; const oncostPct = coopStore.payrollOncostPct || 0; // Keep theoretical maximum for reference const theoreticalMaxPayroll = totalHours * hourlyWage; // Policy for allocation const payPolicy = { relationship: coopStore.policy.relationship, roleBands: coopStore.policy.roleBands }; const membersForAllocation = coopStore.members.map(m => ({ ...m, displayName: m.name, monthlyPayPlanned: m.monthlyPayPlanned || 0, minMonthlyNeeds: m.minMonthlyNeeds || 0, hoursPerMonth: m.hoursPerMonth || 0 })); // Calculate payroll for each month individually using cumulative balance approach const refreshToday = new Date(); let totalAnnualPayroll = 0; let totalAnnualOncosts = 0; let runningBalance = 0; // Track cumulative cash balance for (let i = 0; i < 12; i++) { const date = new Date(refreshToday.getFullYear(), refreshToday.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; // Get revenue for this specific month const monthRevenue = budgetWorksheet.value.revenue.reduce((sum, item) => { return sum + (item.monthlyValues?.[monthKey] || 0); }, 0); // Get non-payroll expenses for this specific month const nonPayrollExpenses = budgetWorksheet.value.expenses.reduce((sum, item) => { // Exclude payroll items from the calculation if (item.id === "expense-payroll-base" || item.id === "expense-payroll-oncosts" || item.id === "expense-payroll") { return sum; } return sum + (item.monthlyValues?.[monthKey] || 0); }, 0); // Calculate normal payroll based on policy and wage (theoretical maximum) const theoreticalPayrollBudget = totalHours * hourlyWage; console.log(`Month ${monthKey}: Revenue=${monthRevenue}, NonPayrollExp=${nonPayrollExpenses}, TheoreticalPayroll=${theoreticalPayrollBudget}, RunningBalance=${runningBalance}`); // Only allocate payroll if members exist if (coopStore.members.length > 0) { // First, calculate payroll using normal policy allocation const allocatedMembers = allocatePayroll(membersForAllocation, payPolicy, theoreticalPayrollBudget); // Sum the allocated payroll amounts for this month let monthlyAllocatedPayroll = allocatedMembers.reduce((sum, m) => { return sum + (m.monthlyPayPlanned || 0); }, 0); let monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100); // Calculate projected balance after this month's expenses and payroll const totalExpensesWithPayroll = nonPayrollExpenses + monthlyAllocatedPayroll + monthlyOncostAmount; const monthlyNetIncome = monthRevenue - totalExpensesWithPayroll; const projectedBalance = runningBalance + monthlyNetIncome; // Check if this payroll would push cumulative balance below minimum threshold const minThreshold = coopStore.minCashThreshold || 0; if (projectedBalance < minThreshold) { // Calculate maximum sustainable payroll to maintain minimum cash threshold const targetNetIncome = minThreshold - runningBalance; const availableForExpenses = monthRevenue - targetNetIncome; const maxSustainablePayroll = Math.max(0, (availableForExpenses - nonPayrollExpenses) / (1 + oncostPct / 100)); console.log(`Month ${monthKey}: Reducing payroll from ${monthlyAllocatedPayroll} to ${maxSustainablePayroll} to maintain minimum cash threshold of ${minThreshold}`); // Proportionally reduce all member allocations if (monthlyAllocatedPayroll > 0) { const reductionRatio = maxSustainablePayroll / monthlyAllocatedPayroll; allocatedMembers.forEach(m => { m.monthlyPayPlanned = (m.monthlyPayPlanned || 0) * reductionRatio; }); } monthlyAllocatedPayroll = maxSustainablePayroll; monthlyOncostAmount = monthlyAllocatedPayroll * (oncostPct / 100); } // Update running balance with actual net income after payroll adjustments const actualNetIncome = monthRevenue - (nonPayrollExpenses + monthlyAllocatedPayroll + monthlyOncostAmount); runningBalance += actualNetIncome; // Update this specific month's payroll values if (basePayrollIndex !== -1) { budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = monthlyAllocatedPayroll; } if (oncostIndex !== -1) { budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = monthlyOncostAmount; } // Handle legacy single payroll entry if (legacyIndex !== -1 && basePayrollIndex === -1) { const combinedPayroll = monthlyAllocatedPayroll * (1 + oncostPct / 100); budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = combinedPayroll; } // Accumulate for annual totals totalAnnualPayroll += monthlyAllocatedPayroll; totalAnnualOncosts += monthlyOncostAmount; } else { // No members or theoretical payroll is 0 - set payroll to 0 if (basePayrollIndex !== -1) { budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = 0; } if (oncostIndex !== -1) { budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = 0; } if (legacyIndex !== -1 && basePayrollIndex === -1) { budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = 0; } // Update running balance with net income (revenue - non-payroll expenses) const actualNetIncome = monthRevenue - nonPayrollExpenses; runningBalance += actualNetIncome; } } // Update annual values based on actual totals if (basePayrollIndex !== -1) { budgetWorksheet.value.expenses[basePayrollIndex].values = { year1: { best: totalAnnualPayroll * 1.2, worst: totalAnnualPayroll * 0.8, mostLikely: totalAnnualPayroll }, year2: { best: totalAnnualPayroll * 1.3, worst: totalAnnualPayroll * 0.9, mostLikely: totalAnnualPayroll * 1.1 }, year3: { best: totalAnnualPayroll * 1.5, worst: totalAnnualPayroll, mostLikely: totalAnnualPayroll * 1.25 } }; } if (oncostIndex !== -1) { // Update name with current percentage budgetWorksheet.value.expenses[oncostIndex].name = `Payroll Taxes & Benefits (${oncostPct}%)`; budgetWorksheet.value.expenses[oncostIndex].values = { year1: { best: totalAnnualOncosts * 1.2, worst: totalAnnualOncosts * 0.8, mostLikely: totalAnnualOncosts }, year2: { best: totalAnnualOncosts * 1.3, worst: totalAnnualOncosts * 0.9, mostLikely: totalAnnualOncosts * 1.1 }, year3: { best: totalAnnualOncosts * 1.5, worst: totalAnnualOncosts, mostLikely: totalAnnualOncosts * 1.25 } }; } // Handle legacy single payroll entry annual values if (legacyIndex !== -1 && basePayrollIndex === -1) { const totalCombined = totalAnnualPayroll + totalAnnualOncosts; budgetWorksheet.value.expenses[legacyIndex].values = { year1: { best: totalCombined * 1.2, worst: totalCombined * 0.8, mostLikely: totalCombined }, year2: { best: totalCombined * 1.3, worst: totalCombined * 0.9, mostLikely: totalCombined * 1.1 }, year3: { best: totalCombined * 1.5, worst: totalCombined, mostLikely: totalCombined * 1.25 } }; } } // Force reinitialize - always reload from wizard data async function forceInitializeFromWizardData() { console.log("=== FORCE BUDGET INITIALIZATION ==="); isInitialized.value = false; budgetWorksheet.value.revenue = []; budgetWorksheet.value.expenses = []; await initializeFromWizardData(); } // Initialize worksheet from wizard data async function initializeFromWizardData() { console.log("=== BUDGET INITIALIZATION DEBUG ==="); console.log("Is already initialized:", isInitialized.value); 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 - 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("No existing budget data found, initializing from wizard data..."); try { // Use the new coopBuilder store instead of the old stores const coopStore = useCoopBuilderStore(); console.log("CoopStore Data:"); console.log("- Streams:", coopStore.streams.length, coopStore.streams); console.log("- Members:", coopStore.members.length, coopStore.members); console.log("- Equal wage:", coopStore.equalHourlyWage || "No wage set"); console.log("- Overhead costs:", coopStore.overheadCosts.length, coopStore.overheadCosts); // Only clear data if we're truly initializing from scratch budgetWorksheet.value.revenue = []; budgetWorksheet.value.expenses = []; // Declare date once for the entire function const initDate = new Date(); // Add revenue streams from wizard (but don't auto-load fixtures) // Note: We don't auto-load fixtures anymore, but wizard data should still work coopStore.streams.forEach((stream) => { const monthlyAmount = stream.monthly || 0; console.log( "Adding stream:", stream.label, "category:", stream.category, "amount:", monthlyAmount ); // Use the helper function for category mapping const mappedCategory = mapStreamToBudgetCategory(stream.category); console.log( "Mapped category from", stream.category, "to", mappedCategory ); // Create monthly values - split the annual target evenly across 12 months const monthlyValues: Record = {}; for (let i = 0; i < 12; i++) { const date = new Date(initDate.getFullYear(), initDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; monthlyValues[monthKey] = monthlyAmount; } console.log( "Created monthly values for", stream.label, ":", monthlyValues ); budgetWorksheet.value.revenue.push({ id: `revenue-${stream.id}`, name: stream.label, mainCategory: mappedCategory, subcategory: "Direct sales", // Default subcategory for coopStore streams source: "wizard", monthlyValues, values: { year1: { best: monthlyAmount * 12, worst: monthlyAmount * 6, mostLikely: monthlyAmount * 10, }, year2: { best: monthlyAmount * 15, worst: monthlyAmount * 8, mostLikely: monthlyAmount * 12, }, year3: { best: monthlyAmount * 18, worst: monthlyAmount * 10, mostLikely: monthlyAmount * 15, }, }, }); }); // Add payroll from wizard data using the policy-driven allocation system const totalHours = coopStore.members.reduce((sum, m) => sum + (m.hoursPerMonth || 0), 0); const hourlyWage = coopStore.equalHourlyWage || 0; const oncostPct = coopStore.payrollOncostPct || 0; // Use revenue-constrained payroll budget (same logic as useCoopBuilder) const totalRevenue = coopStore.streams.reduce((sum, s) => sum + (s.monthly || 0), 0); const overheadCosts = coopStore.overheadCosts.reduce((sum, c) => sum + (c.amount || 0), 0); const availableForPayroll = Math.max(0, totalRevenue - overheadCosts); // Keep theoretical maximum for reference const theoreticalMaxPayroll = totalHours * hourlyWage; console.log("=== PAYROLL CALCULATION DEBUG ==="); console.log("Total hours:", totalHours); console.log("Hourly wage:", hourlyWage); console.log("Oncost %:", oncostPct); console.log("Policy relationship:", coopStore.policy.relationship); console.log("Total revenue:", totalRevenue); console.log("Overhead costs:", overheadCosts); console.log("Available for payroll:", availableForPayroll); console.log("Theoretical max payroll:", theoreticalMaxPayroll); // Use revenue-constrained budget const basePayrollBudget = availableForPayroll; console.log("Using payroll budget:", basePayrollBudget); if (basePayrollBudget > 0 && coopStore.members.length > 0) { // Use policy-driven allocation to get actual member pay amounts const payPolicy = { relationship: coopStore.policy.relationship, roleBands: coopStore.policy.roleBands }; // Convert coopStore members to the format expected by allocatePayroll const membersForAllocation = coopStore.members.map(m => ({ ...m, displayName: m.name, // Ensure all required fields exist monthlyPayPlanned: m.monthlyPayPlanned || 0, minMonthlyNeeds: m.minMonthlyNeeds || 0, hoursPerMonth: m.hoursPerMonth || 0 })); // Allocate payroll based on policy const allocatedMembers = allocatePayroll(membersForAllocation, payPolicy, basePayrollBudget); console.log("Allocated members:", allocatedMembers.map(m => ({ name: m.name, planned: m.monthlyPayPlanned, needs: m.minMonthlyNeeds }))); // Sum the allocated amounts for total payroll const totalAllocatedPayroll = allocatedMembers.reduce((sum, m) => { const planned = m.monthlyPayPlanned || 0; console.log(`Member ${m.name}: planned ${planned}`); return sum + planned; }, 0); console.log("Total allocated payroll:", totalAllocatedPayroll); // Apply oncosts to the policy-allocated total const monthlyPayroll = totalAllocatedPayroll * (1 + oncostPct / 100); console.log("Monthly payroll with oncosts:", monthlyPayroll); // Create monthly values for payroll const monthlyValues: Record = {}; for (let i = 0; i < 12; i++) { const date = new Date(initDate.getFullYear(), initDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; monthlyValues[monthKey] = monthlyPayroll; } console.log("Creating split payroll budget items with monthly values:", Object.keys(monthlyValues).length, "months"); // Create base payroll monthly values (without oncosts) const baseMonthlyValues: Record = {}; const oncostMonthlyValues: Record = {}; // Reuse the initDate variable from above for (let i = 0; i < 12; i++) { const date = new Date(initDate.getFullYear(), initDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; baseMonthlyValues[monthKey] = totalAllocatedPayroll; oncostMonthlyValues[monthKey] = totalAllocatedPayroll * (oncostPct / 100); } // Add base payroll item budgetWorksheet.value.expenses.push({ id: "expense-payroll-base", name: "Payroll", mainCategory: "Salaries & Benefits", subcategory: "Base wages and benefits", source: "wizard", monthlyValues: baseMonthlyValues, values: { year1: { best: totalAllocatedPayroll * 12, worst: totalAllocatedPayroll * 8, mostLikely: totalAllocatedPayroll * 12, }, year2: { best: totalAllocatedPayroll * 14, worst: totalAllocatedPayroll * 10, mostLikely: totalAllocatedPayroll * 13, }, year3: { best: totalAllocatedPayroll * 16, worst: totalAllocatedPayroll * 12, mostLikely: totalAllocatedPayroll * 15, }, }, }); // Add payroll oncosts item (if oncost percentage > 0) if (oncostPct > 0) { const oncostAmount = totalAllocatedPayroll * (oncostPct / 100); budgetWorksheet.value.expenses.push({ id: "expense-payroll-oncosts", name: `Payroll Taxes & Benefits (${oncostPct}%)`, mainCategory: "Salaries & Benefits", subcategory: "Payroll taxes and benefits", source: "wizard", monthlyValues: oncostMonthlyValues, values: { year1: { best: oncostAmount * 12, worst: oncostAmount * 8, mostLikely: oncostAmount * 12, }, year2: { best: oncostAmount * 14, worst: oncostAmount * 10, mostLikely: oncostAmount * 13, }, year3: { best: oncostAmount * 16, worst: oncostAmount * 12, mostLikely: oncostAmount * 15, }, }, }); } } // Add overhead costs from wizard coopStore.overheadCosts.forEach((cost) => { if (cost.amount && cost.amount > 0) { const annualAmount = cost.amount * 12; // Map overhead cost categories to expense categories let expenseCategory = "Other Expenses"; // Default if (cost.category === "Operations") expenseCategory = "Office & Operations"; else if (cost.category === "Tools") expenseCategory = "Equipment & Technology"; else if (cost.category === "Professional") expenseCategory = "Legal & Professional"; else if (cost.category === "Marketing") expenseCategory = "Marketing & Outreach"; // Create monthly values for overhead costs const monthlyValues: Record = {}; for (let i = 0; i < 12; i++) { const date = new Date(initDate.getFullYear(), initDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; monthlyValues[monthKey] = cost.amount; } budgetWorksheet.value.expenses.push({ id: `expense-${cost.id}`, name: cost.name, mainCategory: expenseCategory, subcategory: cost.name, // Use the cost name as subcategory source: "wizard", monthlyValues, values: { year1: { best: annualAmount, worst: annualAmount * 0.8, mostLikely: annualAmount, }, year2: { best: annualAmount * 1.1, worst: annualAmount * 0.9, mostLikely: annualAmount * 1.05, }, year3: { best: annualAmount * 1.2, worst: annualAmount, mostLikely: annualAmount * 1.1, }, }, }); } }); // Production costs are handled within overhead costs in the new architecture // DISABLED: No sample data - budget should start empty // if (budgetWorksheet.value.revenue.length === 0) { // console.log("Adding sample revenue line"); // // ... sample revenue creation code removed // } // DISABLED: No sample data - expenses should start empty // if (budgetWorksheet.value.expenses.length === 0) { // console.log("Adding sample expense line"); // // ... sample expense creation code removed // } // Debug: Log all revenue items and their categories console.log("Final revenue items:"); budgetWorksheet.value.revenue.forEach((item) => { console.log( `- ${item.name}: ${item.mainCategory} > ${item.subcategory} (${item.values.year1.mostLikely})` ); }); console.log("Final expense items:"); budgetWorksheet.value.expenses.forEach((item) => { console.log( `- ${item.name}: ${item.mainCategory} > ${item.subcategory} (${item.values.year1.mostLikely})` ); }); // Ensure all items have monthlyValues and new structure - migrate existing items [ ...budgetWorksheet.value.revenue, ...budgetWorksheet.value.expenses, ].forEach((item) => { // Migrate to new structure if needed if (item.category && !item.mainCategory) { console.log("Migrating item structure for:", item.name); item.mainCategory = item.category; // Old category becomes mainCategory // Set appropriate subcategory based on the main category and item name if (item.category === "Games & Products") { const gameSubcategories = [ "Direct sales", "Platform revenue share", "DLC/expansions", "Merchandise", ]; item.subcategory = gameSubcategories.includes(item.name) ? item.name : "Direct sales"; } else if (item.category === "Services & Contracts") { const serviceSubcategories = [ "Contract development", "Consulting", "Workshops/teaching", "Technical services", ]; item.subcategory = serviceSubcategories.includes(item.name) ? item.name : "Contract development"; } else if (item.category === "Investment Income") { const investmentSubcategories = [ "Impact investment", "Venture capital", "Loans", ]; item.subcategory = investmentSubcategories.includes(item.name) ? item.name : "Impact investment"; } else if (item.category === "Salaries & Benefits") { item.subcategory = "Base wages and benefits"; } else if (item.category === "Office & Operations") { // Map specific office tools to appropriate subcategories if ( item.name.toLowerCase().includes("rent") || item.name.toLowerCase().includes("office") ) { item.subcategory = "Rent"; } else if (item.name.toLowerCase().includes("util")) { item.subcategory = "Utilities"; } else if (item.name.toLowerCase().includes("insurance")) { item.subcategory = "Insurance"; } else { item.subcategory = "Office supplies"; } } else { // For other categories, use appropriate default item.subcategory = "Miscellaneous"; } delete item.category; // Remove old property } if (!item.monthlyValues) { console.log("Migrating item to monthly values:", item.name); item.monthlyValues = {}; for (let i = 0; i < 12; i++) { const date = new Date(initDate.getFullYear(), initDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; // Try to use most likely value divided by 12, or default to 0 const yearlyValue = item.values?.year1?.mostLikely || 0; item.monthlyValues[monthKey] = Math.round(yearlyValue / 12); } console.log( "Added monthly values to", item.name, ":", item.monthlyValues ); } }); console.log( "Initialization complete. Revenue items:", budgetWorksheet.value.revenue.length, "Expense items:", budgetWorksheet.value.expenses.length ); isInitialized.value = true; // Trigger payroll refresh after initialization console.log("Triggering initial payroll refresh after initialization"); refreshPayrollInBudget(); } catch (error) { console.error("Error initializing budget from wizard data:", error); // DISABLED: No fallback sample data - budget should remain empty on errors // Budget initialization complete (without automatic fallback data) isInitialized.value = true; } } // NEW: Budget worksheet functions function updateBudgetValue(category, itemId, year, scenario, value) { const items = budgetWorksheet.value[category]; const item = items.find((i) => i.id === itemId); if (item) { item.values[year][scenario] = Number(value) || 0; } } 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 = {}; } 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); // Refresh payroll when any budget item changes (except payroll items themselves) if (!itemId.includes('payroll')) { console.log('Triggering payroll refresh for non-payroll item:', itemId); refreshPayrollInBudget(); } else { console.log('Skipping payroll refresh for payroll item:', itemId); } } else { console.error('Item not found:', { category, itemId, availableItems: items.map(i => ({id: i.id, name: i.name})) }); } } function addBudgetItem(category, name, selectedCategory = "", subcategory = "") { const id = `${category}-${Date.now()}`; // Create empty monthly values for next 12 months const monthlyValues = {}; const addDate = new Date(); for (let i = 0; i < 12; i++) { const date = new Date(addDate.getFullYear(), addDate.getMonth() + i, 1); const monthKey = `${date.getFullYear()}-${String( date.getMonth() + 1 ).padStart(2, "0")}`; monthlyValues[monthKey] = 0; } const newItem = { id, name, mainCategory: selectedCategory || (category === "revenue" ? "Games & Products" : "Other Expenses"), subcategory: subcategory || "", source: "user", monthlyValues, values: { year1: { best: 0, worst: 0, mostLikely: 0 }, year2: { best: 0, worst: 0, mostLikely: 0 }, year3: { best: 0, worst: 0, mostLikely: 0 }, }, }; budgetWorksheet.value[category].push(newItem); return id; } function removeBudgetItem(category, itemId) { const items = budgetWorksheet.value[category]; const index = items.findIndex((i) => i.id === itemId); if (index > -1) { items.splice(index, 1); } } function renameBudgetItem(category, itemId, newName) { const items = budgetWorksheet.value[category]; const item = items.find((i) => i.id === itemId); if (item) { item.name = newName; } } function updateBudgetCategory(category, itemId, newCategory) { const items = budgetWorksheet.value[category]; const item = items.find((i) => i.id === itemId); if (item) { item.category = newCategory; } } function addCustomCategory(type, categoryName) { if ( type === "revenue" && !revenueCategories.value.includes(categoryName) ) { revenueCategories.value.push(categoryName); } else if ( type === "expenses" && !expenseCategories.value.includes(categoryName) ) { expenseCategories.value.push(categoryName); } } // Reset function function resetBudgetOverhead() { overheadCosts.value = []; productionCosts.value = []; } function resetBudgetWorksheet() { // Reset all values to 0 but keep the structure [ ...budgetWorksheet.value.revenue, ...budgetWorksheet.value.expenses, ].forEach((item) => { Object.keys(item.values).forEach((year) => { Object.keys(item.values[year]).forEach((scenario) => { item.values[year][scenario] = 0; }); }); }); } return { // NEW: Budget worksheet budgetWorksheet, budgetTotals, monthlyTotals, cumulativeBalances, revenueCategories, expenseCategories, revenueSubcategories, groupedRevenue, groupedExpenses, isInitialized, initializeFromWizardData, forceInitializeFromWizardData, refreshPayrollInBudget, updateBudgetValue, updateMonthlyValue, addBudgetItem, removeBudgetItem, renameBudgetItem, updateBudgetCategory, addCustomCategory, resetBudgetWorksheet, // LEGACY: Keep for backward compatibility budgetLines, overheadCosts, productionCosts, currentPeriod: readonly(currentPeriod), currentBudget, schemaVersion, setBudgetLine, updateRevenue, // Wizard actions addOverheadLine, removeOverheadLine, resetBudgetOverhead, // Legacy actions addOverheadCost, addProductionCost, setCurrentPeriod, }; }, { persist: { key: "urgent-tools-budget", paths: [ "budgetWorksheet", "revenueCategories", "expenseCategories", "isInitialized", "overheadCosts", "productionCosts", "currentPeriod", ], }, } );