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; }); // 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; } // 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() { if (!isInitialized.value) 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; const basePayrollBudget = totalHours * hourlyWage; // Declare today once for the entire function const today = new Date(); if (basePayrollBudget > 0 && coopStore.members.length > 0) { // Use policy-driven 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 })); const allocatedMembers = allocatePayroll(membersForAllocation, payPolicy, basePayrollBudget); // Sum the allocated payroll amounts const totalAllocatedPayroll = allocatedMembers.reduce((sum, m) => { return sum + (m.monthlyPayPlanned || 0); }, 0); // Update monthly values for base payroll if (basePayrollIndex !== -1) { // Update base payroll entry 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")}`; budgetWorksheet.value.expenses[basePayrollIndex].monthlyValues[monthKey] = totalAllocatedPayroll; } // Update annual values for base payroll budgetWorksheet.value.expenses[basePayrollIndex].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 } }; } if (oncostIndex !== -1) { // Update oncost entry const oncostAmount = totalAllocatedPayroll * (oncostPct / 100); 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")}`; budgetWorksheet.value.expenses[oncostIndex].monthlyValues[monthKey] = oncostAmount; } // Update name with current percentage budgetWorksheet.value.expenses[oncostIndex].name = `Payroll Taxes & Benefits (${oncostPct}%)`; // Update annual values for oncosts budgetWorksheet.value.expenses[oncostIndex].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 } }; } // Handle legacy single payroll entry (update to combined amount for backwards compatibility) if (legacyIndex !== -1 && basePayrollIndex === -1) { const monthlyPayroll = totalAllocatedPayroll * (1 + oncostPct / 100); 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")}`; budgetWorksheet.value.expenses[legacyIndex].monthlyValues[monthKey] = monthlyPayroll; } budgetWorksheet.value.expenses[legacyIndex].values = { year1: { best: monthlyPayroll * 12, worst: monthlyPayroll * 8, mostLikely: monthlyPayroll * 12 }, year2: { best: monthlyPayroll * 14, worst: monthlyPayroll * 10, mostLikely: monthlyPayroll * 13 }, year3: { best: monthlyPayroll * 16, worst: monthlyPayroll * 12, mostLikely: monthlyPayroll * 15 } }; } } } // 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 (not just initialized flag) if (isInitialized.value && (budgetWorksheet.value.revenue.length > 0 || budgetWorksheet.value.expenses.length > 0)) { console.log("Already initialized with data, skipping..."); return; } console.log("Initializing budget 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); // Clear existing data budgetWorksheet.value.revenue = []; budgetWorksheet.value.expenses = []; // Declare today once for the entire function const today = 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(today.getFullYear(), today.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; console.log("=== PAYROLL CALCULATION DEBUG ==="); console.log("Total hours:", totalHours); console.log("Hourly wage:", hourlyWage); console.log("Oncost %:", oncostPct); console.log("Operating mode:", coopStore.operatingMode); console.log("Policy relationship:", coopStore.policy.relationship); // Calculate total payroll budget using policy allocation const basePayrollBudget = totalHours * hourlyWage; console.log("Base 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(today.getFullYear(), today.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 today variable from above 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")}`; 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(today.getFullYear(), today.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(today.getFullYear(), today.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; } 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) { 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; } } function addBudgetItem(category, name, selectedCategory = "", subcategory = "") { const id = `${category}-${Date.now()}`; // Create empty monthly values for next 12 months const monthlyValues = {}; 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")}`; 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, 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", ], }, } );