refactor budget worksheet layout and functionality, streamline revenue and expense management, and improve modal interactions for better user experience
This commit is contained in:
parent
f67b138d95
commit
848386e3dd
2 changed files with 1222 additions and 442 deletions
939
pages/budget.vue
939
pages/budget.vue
File diff suppressed because it is too large
Load diff
551
stores/budget.ts
551
stores/budget.ts
|
|
@ -8,40 +8,71 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
// 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'
|
||||
"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']
|
||||
"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'
|
||||
"Salaries & Benefits",
|
||||
"Development Costs",
|
||||
"Equipment & Technology",
|
||||
"Marketing & Outreach",
|
||||
"Office & Operations",
|
||||
"Legal & Professional",
|
||||
"Other Expenses",
|
||||
]);
|
||||
|
||||
// NEW: Budget worksheet structure (starts empty, populated from wizard data)
|
||||
const budgetWorksheet = ref({
|
||||
revenue: [],
|
||||
expenses: []
|
||||
expenses: [],
|
||||
});
|
||||
|
||||
// Track if worksheet has been initialized from wizard data
|
||||
|
|
@ -54,39 +85,49 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
// Computed grouped data for category headers
|
||||
const groupedRevenue = computed(() => {
|
||||
const groups = {};
|
||||
revenueCategories.value.forEach(category => {
|
||||
groups[category] = budgetWorksheet.value.revenue.filter(item => item.mainCategory === category);
|
||||
const groups: Record<string, any[]> = {};
|
||||
revenueCategories.value.forEach((category) => {
|
||||
groups[category] = budgetWorksheet.value.revenue.filter(
|
||||
(item) => item.mainCategory === category
|
||||
);
|
||||
});
|
||||
return groups;
|
||||
});
|
||||
|
||||
const groupedExpenses = computed(() => {
|
||||
const groups = {};
|
||||
expenseCategories.value.forEach(category => {
|
||||
groups[category] = budgetWorksheet.value.expenses.filter(item => item.mainCategory === category);
|
||||
const groups: Record<string, any[]> = {};
|
||||
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 years = ["year1", "year2", "year3"];
|
||||
const scenarios = ["best", "worst", "mostLikely"];
|
||||
const totals = {};
|
||||
|
||||
years.forEach(year => {
|
||||
years.forEach((year) => {
|
||||
totals[year] = {};
|
||||
scenarios.forEach(scenario => {
|
||||
scenarios.forEach((scenario) => {
|
||||
// Calculate revenue total
|
||||
const revenueTotal = budgetWorksheet.value.revenue.reduce((sum, item) => {
|
||||
const revenueTotal = budgetWorksheet.value.revenue.reduce(
|
||||
(sum, item) => {
|
||||
return sum + (item.values?.[year]?.[scenario] || 0);
|
||||
}, 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate expenses total
|
||||
const expensesTotal = budgetWorksheet.value.expenses.reduce((sum, item) => {
|
||||
const expensesTotal = budgetWorksheet.value.expenses.reduce(
|
||||
(sum, item) => {
|
||||
return sum + (item.values?.[year]?.[scenario] || 0);
|
||||
}, 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate net income
|
||||
const netIncome = revenueTotal - expensesTotal;
|
||||
|
|
@ -94,7 +135,7 @@ export const useBudgetStore = defineStore(
|
|||
totals[year][scenario] = {
|
||||
revenue: revenueTotal,
|
||||
expenses: expensesTotal,
|
||||
net: netIncome
|
||||
net: netIncome,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
@ -104,23 +145,34 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
// Monthly totals computation
|
||||
const monthlyTotals = computed(() => {
|
||||
const totals = {};
|
||||
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')}`;
|
||||
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) => {
|
||||
const revenueTotal = budgetWorksheet.value.revenue.reduce(
|
||||
(sum, item) => {
|
||||
return sum + (item.monthlyValues?.[monthKey] || 0);
|
||||
}, 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate expenses total for this month
|
||||
const expensesTotal = budgetWorksheet.value.expenses.reduce((sum, item) => {
|
||||
const expensesTotal = budgetWorksheet.value.expenses.reduce(
|
||||
(sum, item) => {
|
||||
return sum + (item.monthlyValues?.[monthKey] || 0);
|
||||
}, 0);
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate net income
|
||||
const netIncome = revenueTotal - expensesTotal;
|
||||
|
|
@ -128,7 +180,7 @@ export const useBudgetStore = defineStore(
|
|||
totals[monthKey] = {
|
||||
revenue: revenueTotal,
|
||||
expenses: expensesTotal,
|
||||
net: netIncome
|
||||
net: netIncome,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +190,7 @@ export const useBudgetStore = defineStore(
|
|||
// LEGACY: Keep for backward compatibility
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = String(currentDate.getMonth() + 1).padStart(2, '0');
|
||||
const currentMonth = String(currentDate.getMonth() + 1).padStart(2, "0");
|
||||
const currentPeriod = ref(`${currentYear}-${currentMonth}`);
|
||||
|
||||
const currentBudget = computed(() => {
|
||||
|
|
@ -222,24 +274,24 @@ export const useBudgetStore = defineStore(
|
|||
// Initialize worksheet from wizard data
|
||||
async function initializeFromWizardData() {
|
||||
if (isInitialized.value && budgetWorksheet.value.revenue.length > 0) {
|
||||
console.log('Already initialized with data, skipping...');
|
||||
console.log("Already initialized with data, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing budget from wizard data...');
|
||||
console.log("Initializing budget from wizard data...");
|
||||
|
||||
// Import stores dynamically to avoid circular deps
|
||||
const { useStreamsStore } = await import('./streams');
|
||||
const { useMembersStore } = await import('./members');
|
||||
const { usePoliciesStore } = await import('./policies');
|
||||
const { useStreamsStore } = await import("./streams");
|
||||
const { useMembersStore } = await import("./members");
|
||||
const { usePoliciesStore } = await import("./policies");
|
||||
|
||||
const streamsStore = useStreamsStore();
|
||||
const membersStore = useMembersStore();
|
||||
const policiesStore = usePoliciesStore();
|
||||
|
||||
console.log('Streams:', streamsStore.streams.length, 'streams');
|
||||
console.log('Members capacity:', membersStore.capacityTotals);
|
||||
console.log('Policies wage:', policiesStore.equalHourlyWage);
|
||||
console.log("Streams:", streamsStore.streams.length, "streams");
|
||||
console.log("Members capacity:", membersStore.capacityTotals);
|
||||
console.log("Policies wage:", policiesStore.equalHourlyWage);
|
||||
|
||||
// Clear existing data
|
||||
budgetWorksheet.value.revenue = [];
|
||||
|
|
@ -247,50 +299,92 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
// Add revenue streams from wizard
|
||||
if (streamsStore.streams.length === 0) {
|
||||
console.log('No wizard streams found, adding sample data');
|
||||
console.log("No wizard streams found, adding sample data");
|
||||
// Initialize with minimal demo if no wizard data exists
|
||||
await streamsStore.initializeWithFixtures();
|
||||
}
|
||||
|
||||
streamsStore.streams.forEach(stream => {
|
||||
streamsStore.streams.forEach((stream) => {
|
||||
const monthlyAmount = stream.targetMonthlyAmount || 0;
|
||||
console.log('Adding stream:', stream.name, 'category:', stream.category, 'subcategory:', stream.subcategory, 'amount:', monthlyAmount);
|
||||
console.log('Full stream object:', stream);
|
||||
console.log(
|
||||
"Adding stream:",
|
||||
stream.name,
|
||||
"category:",
|
||||
stream.category,
|
||||
"subcategory:",
|
||||
stream.subcategory,
|
||||
"amount:",
|
||||
monthlyAmount
|
||||
);
|
||||
console.log("Full stream object:", stream);
|
||||
|
||||
// Simple category mapping - just map the key categories we know exist
|
||||
let mappedCategory = 'Games & Products'; // Default
|
||||
const categoryLower = (stream.category || '').toLowerCase();
|
||||
if (categoryLower === 'games' || categoryLower === 'product') mappedCategory = 'Games & Products';
|
||||
else if (categoryLower === 'services' || categoryLower === 'service') mappedCategory = 'Services & Contracts';
|
||||
else if (categoryLower === 'grants' || categoryLower === 'grant') mappedCategory = 'Grants & Funding';
|
||||
else if (categoryLower === 'community') mappedCategory = 'Community Support';
|
||||
else if (categoryLower === 'partnerships' || categoryLower === 'partnership') mappedCategory = 'Partnerships';
|
||||
else if (categoryLower === 'investment') mappedCategory = 'Investment Income';
|
||||
let mappedCategory = "Games & Products"; // Default
|
||||
const categoryLower = (stream.category || "").toLowerCase();
|
||||
if (categoryLower === "games" || categoryLower === "product")
|
||||
mappedCategory = "Games & Products";
|
||||
else if (categoryLower === "services" || categoryLower === "service")
|
||||
mappedCategory = "Services & Contracts";
|
||||
else if (categoryLower === "grants" || categoryLower === "grant")
|
||||
mappedCategory = "Grants & Funding";
|
||||
else if (categoryLower === "community")
|
||||
mappedCategory = "Community Support";
|
||||
else if (
|
||||
categoryLower === "partnerships" ||
|
||||
categoryLower === "partnership"
|
||||
)
|
||||
mappedCategory = "Partnerships";
|
||||
else if (categoryLower === "investment")
|
||||
mappedCategory = "Investment Income";
|
||||
|
||||
console.log('Mapped category from', stream.category, 'to', mappedCategory);
|
||||
console.log(
|
||||
"Mapped category from",
|
||||
stream.category,
|
||||
"to",
|
||||
mappedCategory
|
||||
);
|
||||
|
||||
// Create monthly values - split the annual target evenly across 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')}`;
|
||||
const monthKey = `${date.getFullYear()}-${String(
|
||||
date.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
monthlyValues[monthKey] = monthlyAmount;
|
||||
}
|
||||
console.log('Created monthly values for', stream.name, ':', monthlyValues);
|
||||
console.log(
|
||||
"Created monthly values for",
|
||||
stream.name,
|
||||
":",
|
||||
monthlyValues
|
||||
);
|
||||
|
||||
budgetWorksheet.value.revenue.push({
|
||||
id: `revenue-${stream.id}`,
|
||||
name: stream.name,
|
||||
mainCategory: mappedCategory,
|
||||
subcategory: stream.subcategory || 'Direct sales', // Use actual subcategory from stream
|
||||
source: 'wizard',
|
||||
subcategory: stream.subcategory || "Direct sales", // Use actual subcategory from stream
|
||||
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 }
|
||||
}
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -306,42 +400,62 @@ export const useBudgetStore = defineStore(
|
|||
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')}`;
|
||||
const monthKey = `${date.getFullYear()}-${String(
|
||||
date.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
monthlyValues[monthKey] = monthlyPayroll;
|
||||
}
|
||||
|
||||
budgetWorksheet.value.expenses.push({
|
||||
id: 'expense-payroll',
|
||||
name: 'Payroll',
|
||||
mainCategory: 'Salaries & Benefits',
|
||||
subcategory: 'Base wages and benefits',
|
||||
source: 'wizard',
|
||||
id: "expense-payroll",
|
||||
name: "Payroll",
|
||||
mainCategory: "Salaries & Benefits",
|
||||
subcategory: "Base wages and benefits",
|
||||
source: "wizard",
|
||||
monthlyValues,
|
||||
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 }
|
||||
}
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add overhead costs from wizard
|
||||
overheadCosts.value.forEach(cost => {
|
||||
overheadCosts.value.forEach((cost) => {
|
||||
if (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 === 'Technology') expenseCategory = 'Equipment & Technology';
|
||||
else if (cost.category === 'Legal') expenseCategory = 'Legal & Professional';
|
||||
else if (cost.category === 'Marketing') expenseCategory = 'Marketing & Outreach';
|
||||
let expenseCategory = "Other Expenses"; // Default
|
||||
if (cost.category === "Operations")
|
||||
expenseCategory = "Office & Operations";
|
||||
else if (cost.category === "Technology")
|
||||
expenseCategory = "Equipment & Technology";
|
||||
else if (cost.category === "Legal")
|
||||
expenseCategory = "Legal & Professional";
|
||||
else if (cost.category === "Marketing")
|
||||
expenseCategory = "Marketing & Outreach";
|
||||
|
||||
// Create monthly values for overhead costs
|
||||
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')}`;
|
||||
const monthKey = `${date.getFullYear()}-${String(
|
||||
date.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
monthlyValues[monthKey] = cost.amount;
|
||||
}
|
||||
|
||||
|
|
@ -350,19 +464,31 @@ export const useBudgetStore = defineStore(
|
|||
name: cost.name,
|
||||
mainCategory: expenseCategory,
|
||||
subcategory: cost.name, // Use the cost name as subcategory
|
||||
source: 'wizard',
|
||||
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 }
|
||||
}
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add production costs from wizard
|
||||
productionCosts.value.forEach(cost => {
|
||||
productionCosts.value.forEach((cost) => {
|
||||
if (cost.amount > 0) {
|
||||
const annualAmount = cost.amount * 12;
|
||||
// Create monthly values for production costs
|
||||
|
|
@ -370,144 +496,204 @@ export const useBudgetStore = defineStore(
|
|||
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')}`;
|
||||
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: 'Development Costs',
|
||||
mainCategory: "Development Costs",
|
||||
subcategory: cost.name, // Use the cost name as subcategory
|
||||
source: 'wizard',
|
||||
source: "wizard",
|
||||
monthlyValues,
|
||||
values: {
|
||||
year1: { best: annualAmount, worst: annualAmount * 0.7, mostLikely: annualAmount * 0.9 },
|
||||
year2: { best: annualAmount * 1.2, worst: annualAmount * 0.8, mostLikely: annualAmount },
|
||||
year3: { best: annualAmount * 1.3, worst: annualAmount * 0.9, mostLikely: annualAmount * 1.1 }
|
||||
}
|
||||
year1: {
|
||||
best: annualAmount,
|
||||
worst: annualAmount * 0.7,
|
||||
mostLikely: annualAmount * 0.9,
|
||||
},
|
||||
year2: {
|
||||
best: annualAmount * 1.2,
|
||||
worst: annualAmount * 0.8,
|
||||
mostLikely: annualAmount,
|
||||
},
|
||||
year3: {
|
||||
best: annualAmount * 1.3,
|
||||
worst: annualAmount * 0.9,
|
||||
mostLikely: annualAmount * 1.1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If still no data after initialization, add a sample row
|
||||
if (budgetWorksheet.value.revenue.length === 0) {
|
||||
console.log('Adding sample revenue line');
|
||||
console.log("Adding sample revenue line");
|
||||
// Create monthly values for sample revenue
|
||||
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')}`;
|
||||
const monthKey = `${date.getFullYear()}-${String(
|
||||
date.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
monthlyValues[monthKey] = 667; // ~8000/12
|
||||
}
|
||||
|
||||
budgetWorksheet.value.revenue.push({
|
||||
id: 'revenue-sample',
|
||||
name: 'Sample Revenue',
|
||||
mainCategory: 'Games & Products',
|
||||
subcategory: 'Direct sales',
|
||||
source: 'user',
|
||||
id: "revenue-sample",
|
||||
name: "Sample Revenue",
|
||||
mainCategory: "Games & Products",
|
||||
subcategory: "Direct sales",
|
||||
source: "user",
|
||||
monthlyValues,
|
||||
values: {
|
||||
year1: { best: 10000, worst: 5000, mostLikely: 8000 },
|
||||
year2: { best: 12000, worst: 6000, mostLikely: 10000 },
|
||||
year3: { best: 15000, worst: 8000, mostLikely: 12000 }
|
||||
}
|
||||
year3: { best: 15000, worst: 8000, mostLikely: 12000 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (budgetWorksheet.value.expenses.length === 0) {
|
||||
console.log('Adding sample expense line');
|
||||
console.log("Adding sample expense line");
|
||||
// Create monthly values for sample expense
|
||||
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')}`;
|
||||
const monthKey = `${date.getFullYear()}-${String(
|
||||
date.getMonth() + 1
|
||||
).padStart(2, "0")}`;
|
||||
monthlyValues[monthKey] = 67; // ~800/12
|
||||
}
|
||||
|
||||
budgetWorksheet.value.expenses.push({
|
||||
id: 'expense-sample',
|
||||
name: 'Sample Expense',
|
||||
mainCategory: 'Other Expenses',
|
||||
subcategory: 'Miscellaneous',
|
||||
source: 'user',
|
||||
id: "expense-sample",
|
||||
name: "Sample Expense",
|
||||
mainCategory: "Other Expenses",
|
||||
subcategory: "Miscellaneous",
|
||||
source: "user",
|
||||
monthlyValues,
|
||||
values: {
|
||||
year1: { best: 1000, worst: 500, mostLikely: 800 },
|
||||
year2: { best: 1200, worst: 600, mostLikely: 1000 },
|
||||
year3: { best: 1500, worst: 800, mostLikely: 1200 }
|
||||
}
|
||||
year3: { best: 1500, worst: 800, mostLikely: 1200 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 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 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})`);
|
||||
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 => {
|
||||
[
|
||||
...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);
|
||||
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') {
|
||||
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';
|
||||
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';
|
||||
item.subcategory = "Office supplies";
|
||||
}
|
||||
} else {
|
||||
// For other categories, use appropriate default
|
||||
item.subcategory = 'Miscellaneous';
|
||||
item.subcategory = "Miscellaneous";
|
||||
}
|
||||
|
||||
delete item.category; // Remove old property
|
||||
}
|
||||
|
||||
if (!item.monthlyValues) {
|
||||
console.log('Migrating item to monthly values:', item.name);
|
||||
console.log("Migrating item to monthly values:", item.name);
|
||||
item.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')}`;
|
||||
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(
|
||||
"Added monthly values to",
|
||||
item.name,
|
||||
":",
|
||||
item.monthlyValues
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Initialization complete. Revenue items:', budgetWorksheet.value.revenue.length, 'Expense items:', budgetWorksheet.value.expenses.length);
|
||||
console.log(
|
||||
"Initialization complete. Revenue items:",
|
||||
budgetWorksheet.value.revenue.length,
|
||||
"Expense items:",
|
||||
budgetWorksheet.value.expenses.length
|
||||
);
|
||||
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
|
@ -515,7 +701,7 @@ export const useBudgetStore = defineStore(
|
|||
// NEW: Budget worksheet functions
|
||||
function updateBudgetValue(category, itemId, year, scenario, value) {
|
||||
const items = budgetWorksheet.value[category];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
const item = items.find((i) => i.id === itemId);
|
||||
if (item) {
|
||||
item.values[year][scenario] = Number(value) || 0;
|
||||
}
|
||||
|
|
@ -523,7 +709,7 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
function updateMonthlyValue(category, itemId, monthKey, value) {
|
||||
const items = budgetWorksheet.value[category];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
const item = items.find((i) => i.id === itemId);
|
||||
if (item) {
|
||||
if (!item.monthlyValues) {
|
||||
item.monthlyValues = {};
|
||||
|
|
@ -532,7 +718,7 @@ export const useBudgetStore = defineStore(
|
|||
}
|
||||
}
|
||||
|
||||
function addBudgetItem(category, name, selectedCategory = '') {
|
||||
function addBudgetItem(category, name, selectedCategory = "") {
|
||||
const id = `${category}-${Date.now()}`;
|
||||
|
||||
// Create empty monthly values for next 12 months
|
||||
|
|
@ -540,22 +726,26 @@ export const useBudgetStore = defineStore(
|
|||
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')}`;
|
||||
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: '', // Will be set by user via dropdown
|
||||
source: 'user',
|
||||
mainCategory:
|
||||
selectedCategory ||
|
||||
(category === "revenue" ? "Games & Products" : "Other Expenses"),
|
||||
subcategory: "", // Will be set by user via dropdown
|
||||
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 }
|
||||
}
|
||||
year3: { best: 0, worst: 0, mostLikely: 0 },
|
||||
},
|
||||
};
|
||||
budgetWorksheet.value[category].push(newItem);
|
||||
return id;
|
||||
|
|
@ -563,7 +753,7 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
function removeBudgetItem(category, itemId) {
|
||||
const items = budgetWorksheet.value[category];
|
||||
const index = items.findIndex(i => i.id === itemId);
|
||||
const index = items.findIndex((i) => i.id === itemId);
|
||||
if (index > -1) {
|
||||
items.splice(index, 1);
|
||||
}
|
||||
|
|
@ -571,7 +761,7 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
function renameBudgetItem(category, itemId, newName) {
|
||||
const items = budgetWorksheet.value[category];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
const item = items.find((i) => i.id === itemId);
|
||||
if (item) {
|
||||
item.name = newName;
|
||||
}
|
||||
|
|
@ -579,16 +769,22 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
function updateBudgetCategory(category, itemId, newCategory) {
|
||||
const items = budgetWorksheet.value[category];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
const item = items.find((i) => i.id === itemId);
|
||||
if (item) {
|
||||
item.category = newCategory;
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomCategory(type, categoryName) {
|
||||
if (type === 'revenue' && !revenueCategories.value.includes(categoryName)) {
|
||||
if (
|
||||
type === "revenue" &&
|
||||
!revenueCategories.value.includes(categoryName)
|
||||
) {
|
||||
revenueCategories.value.push(categoryName);
|
||||
} else if (type === 'expenses' && !expenseCategories.value.includes(categoryName)) {
|
||||
} else if (
|
||||
type === "expenses" &&
|
||||
!expenseCategories.value.includes(categoryName)
|
||||
) {
|
||||
expenseCategories.value.push(categoryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -601,9 +797,12 @@ export const useBudgetStore = defineStore(
|
|||
|
||||
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 => {
|
||||
[
|
||||
...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;
|
||||
});
|
||||
});
|
||||
|
|
@ -652,7 +851,15 @@ export const useBudgetStore = defineStore(
|
|||
{
|
||||
persist: {
|
||||
key: "urgent-tools-budget",
|
||||
paths: ["budgetWorksheet", "revenueCategories", "expenseCategories", "isInitialized", "overheadCosts", "productionCosts", "currentPeriod"],
|
||||
paths: [
|
||||
"budgetWorksheet",
|
||||
"revenueCategories",
|
||||
"expenseCategories",
|
||||
"isInitialized",
|
||||
"overheadCosts",
|
||||
"productionCosts",
|
||||
"currentPeriod",
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue