refactor: enhance routing and state management in CoopBuilder, add migration checks on startup, and update Tailwind configuration for improved component styling

This commit is contained in:
Jennie Robinson Faber 2025-08-23 18:24:31 +01:00
parent 848386e3dd
commit 4cea1f71fe
55 changed files with 4053 additions and 1486 deletions

View file

@ -1,4 +1,5 @@
import { defineStore } from "pinia";
import { allocatePayroll } from "~/types/members";
export const useBudgetStore = defineStore(
"budget",
@ -69,8 +70,26 @@ export const useBudgetStore = defineStore(
"Other Expenses",
]);
// Define budget item type
interface BudgetItem {
id: string;
name: string;
mainCategory: string;
subcategory: string;
source: string;
monthlyValues: Record<string, number>;
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({
const budgetWorksheet = ref<{
revenue: BudgetItem[];
expenses: BudgetItem[];
}>({
revenue: [],
expenses: [],
});
@ -271,6 +290,30 @@ export const useBudgetStore = defineStore(
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
}
}
// Initialize worksheet from wizard data
async function initializeFromWizardData() {
if (isInitialized.value && budgetWorksheet.value.revenue.length > 0) {
@ -280,340 +323,232 @@ export const useBudgetStore = defineStore(
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");
try {
// Use the new coopBuilder store instead of the old stores
const coopStore = useCoopBuilderStore();
const streamsStore = useStreamsStore();
const membersStore = useMembersStore();
const policiesStore = usePoliciesStore();
console.log("Streams:", coopStore.streams.length, "streams");
console.log("Members:", coopStore.members.length, "members");
console.log("Equal wage:", coopStore.equalHourlyWage || "No wage set");
console.log("Overhead costs:", coopStore.overheadCosts.length, "costs");
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 = [];
budgetWorksheet.value.expenses = [];
// Clear existing data
budgetWorksheet.value.revenue = [];
budgetWorksheet.value.expenses = [];
// 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
// Add revenue streams from wizard
if (streamsStore.streams.length === 0) {
console.log("No wizard streams found, adding sample data");
// Initialize with minimal demo if no wizard data exists
await streamsStore.initializeWithFixtures();
}
coopStore.streams.forEach((stream) => {
const monthlyAmount = stream.monthly || 0;
console.log(
"Adding stream:",
stream.label,
"category:",
stream.category,
"amount:",
monthlyAmount
);
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);
// Use the helper function for category mapping
const mappedCategory = mapStreamToBudgetCategory(stream.category);
// 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";
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")}`;
monthlyValues[monthKey] = monthlyAmount;
}
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",
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
const totalHours = membersStore.capacityTotals.targetHours || 0;
const hourlyWage = policiesStore.equalHourlyWage || 0;
const oncostPct = policiesStore.payrollOncostPct || 0;
if (totalHours > 0 && hourlyWage > 0) {
const monthlyPayroll = totalHours * hourlyWage * (1 + oncostPct / 100);
// Create monthly values for payroll
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] = monthlyPayroll;
}
budgetWorksheet.value.expenses.push({
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,
},
},
});
}
// Add overhead costs from wizard
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";
// Create monthly values for overhead costs
const monthlyValues = {};
// Create monthly values - split the annual target evenly across 12 months
const monthlyValues: Record<string, number> = {};
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] = cost.amount;
monthlyValues[monthKey] = monthlyAmount;
}
console.log(
"Created monthly values for",
stream.label,
":",
monthlyValues
);
budgetWorksheet.value.expenses.push({
id: `expense-${cost.id}`,
name: cost.name,
mainCategory: expenseCategory,
subcategory: cost.name, // Use the cost name as subcategory
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: annualAmount,
worst: annualAmount * 0.8,
mostLikely: annualAmount,
best: monthlyAmount * 12,
worst: monthlyAmount * 6,
mostLikely: monthlyAmount * 10,
},
year2: {
best: annualAmount * 1.1,
worst: annualAmount * 0.9,
mostLikely: annualAmount * 1.05,
best: monthlyAmount * 15,
worst: monthlyAmount * 8,
mostLikely: monthlyAmount * 12,
},
year3: {
best: annualAmount * 1.2,
worst: annualAmount,
mostLikely: annualAmount * 1.1,
best: monthlyAmount * 18,
worst: monthlyAmount * 10,
mostLikely: monthlyAmount * 15,
},
},
});
}
});
});
// Add production costs from wizard
productionCosts.value.forEach((cost) => {
if (cost.amount > 0) {
const annualAmount = cost.amount * 12;
// Create monthly values for production costs
const monthlyValues = {};
// Add payroll from wizard data using the allocatePayroll function
const totalHours = coopStore.members.reduce((sum, m) => sum + (m.hoursPerMonth || 0), 0);
const hourlyWage = coopStore.equalHourlyWage || 0;
const oncostPct = coopStore.payrollOncostPct || 0;
// Calculate total payroll budget (before oncosts)
const basePayrollBudget = totalHours * hourlyWage;
if (basePayrollBudget > 0 && coopStore.members.length > 0) {
// Calculate total with oncosts
const monthlyPayroll = basePayrollBudget * (1 + oncostPct / 100);
// Create monthly values for payroll
const monthlyValues: Record<string, number> = {};
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] = cost.amount;
monthlyValues[monthKey] = monthlyPayroll;
}
budgetWorksheet.value.expenses.push({
id: `expense-${cost.id}`,
name: cost.name,
mainCategory: "Development Costs",
subcategory: cost.name, // Use the cost name as subcategory
id: "expense-payroll",
name: "Payroll",
mainCategory: "Salaries & Benefits",
subcategory: "Base wages and benefits",
source: "wizard",
monthlyValues,
values: {
year1: {
best: annualAmount,
worst: annualAmount * 0.7,
mostLikely: annualAmount * 0.9,
best: monthlyPayroll * 12,
worst: monthlyPayroll * 8,
mostLikely: monthlyPayroll * 12,
},
year2: {
best: annualAmount * 1.2,
worst: annualAmount * 0.8,
mostLikely: annualAmount,
best: monthlyPayroll * 14,
worst: monthlyPayroll * 10,
mostLikely: monthlyPayroll * 13,
},
year3: {
best: annualAmount * 1.3,
worst: annualAmount * 0.9,
mostLikely: annualAmount * 1.1,
best: monthlyPayroll * 16,
worst: monthlyPayroll * 12,
mostLikely: monthlyPayroll * 15,
},
},
});
}
});
// If still no data after initialization, add a sample row
if (budgetWorksheet.value.revenue.length === 0) {
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")}`;
monthlyValues[monthKey] = 667; // ~8000/12
}
// 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";
budgetWorksheet.value.revenue.push({
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 },
},
// Create monthly values for overhead costs
const monthlyValues: Record<string, number> = {};
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] = 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,
},
},
});
}
});
}
if (budgetWorksheet.value.expenses.length === 0) {
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")}`;
monthlyValues[monthKey] = 67; // ~800/12
}
// Production costs are handled within overhead costs in the new architecture
budgetWorksheet.value.expenses.push({
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 },
},
// 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})`
);
});
}
// 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})`
);
});
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
// 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") {
// Set appropriate subcategory based on the main category and item name
if (item.category === "Games & Products") {
const gameSubcategories = [
"Direct sales",
"Platform revenue share",
@ -688,14 +623,22 @@ export const useBudgetStore = defineStore(
}
});
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;
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