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:
parent
848386e3dd
commit
4cea1f71fe
55 changed files with 4053 additions and 1486 deletions
513
stores/budget.ts
513
stores/budget.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export const useCashStore = defineStore("cash", () => {
|
|||
// Week that first breaches minimum cushion
|
||||
const firstBreachWeek = ref(null);
|
||||
|
||||
// Current cash and savings balances - start with zeros
|
||||
// Current cash and savings balances - start empty
|
||||
const currentCash = ref(0);
|
||||
const currentSavings = ref(0);
|
||||
|
||||
|
|
@ -111,4 +111,14 @@ export const useCashStore = defineStore("cash", () => {
|
|||
stagePayment,
|
||||
updateCurrentBalances,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: "urgent-tools-cash",
|
||||
paths: [
|
||||
"currentCash",
|
||||
"currentSavings",
|
||||
"cashEvents",
|
||||
"paymentQueue"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCoopBuilderStore = defineStore(
|
||||
"coop-builder",
|
||||
() => {
|
||||
const currentStep = ref(1);
|
||||
|
||||
function setStep(step: number) {
|
||||
currentStep.value = Math.min(Math.max(1, step), 5);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentStep.value = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
setStep,
|
||||
reset,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: "urgent-tools-wizard",
|
||||
paths: ["currentStep"],
|
||||
},
|
||||
}
|
||||
);
|
||||
277
stores/coopBuilder.ts
Normal file
277
stores/coopBuilder.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import { defineStore } from "pinia";
|
||||
|
||||
export const useCoopBuilderStore = defineStore("coop", {
|
||||
state: () => ({
|
||||
operatingMode: "min" as "min" | "target",
|
||||
|
||||
// Flag to track if data was intentionally cleared
|
||||
_wasCleared: false,
|
||||
|
||||
members: [] as Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
role?: string;
|
||||
hoursPerMonth?: number;
|
||||
minMonthlyNeeds: number;
|
||||
targetMonthlyPay: number;
|
||||
externalMonthlyIncome: number;
|
||||
monthlyPayPlanned: number;
|
||||
}>,
|
||||
|
||||
streams: [] as Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
monthly: number;
|
||||
category?: string;
|
||||
certainty?: string;
|
||||
}>,
|
||||
|
||||
milestones: [] as Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
date: string;
|
||||
}>,
|
||||
|
||||
// Scenario and stress test state
|
||||
scenario: "current" as
|
||||
| "current"
|
||||
| "quit-jobs"
|
||||
| "start-production"
|
||||
| "custom",
|
||||
stress: {
|
||||
revenueDelay: 0,
|
||||
costShockPct: 0,
|
||||
grantLost: false,
|
||||
},
|
||||
|
||||
// Policy settings
|
||||
policy: {
|
||||
relationship: "equal-pay" as "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded",
|
||||
roleBands: {} as Record<string, number>,
|
||||
},
|
||||
equalHourlyWage: 50,
|
||||
payrollOncostPct: 25,
|
||||
savingsTargetMonths: 6,
|
||||
minCashCushion: 10000,
|
||||
|
||||
// Cash reserves
|
||||
currentCash: 50000,
|
||||
currentSavings: 15000,
|
||||
|
||||
// Overhead costs
|
||||
overheadCosts: [] as Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
category?: string;
|
||||
}>,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
totalRevenue: (state) => {
|
||||
return state.streams.reduce((sum, s) => sum + (s.monthly || 0), 0);
|
||||
},
|
||||
|
||||
totalOverhead: (state) => {
|
||||
return state.overheadCosts.reduce((sum, c) => sum + (c.amount || 0), 0);
|
||||
},
|
||||
|
||||
totalLiquid: (state) => {
|
||||
return state.currentCash + state.currentSavings;
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
// Member actions
|
||||
upsertMember(m: any) {
|
||||
const i = this.members.findIndex((x) => x.id === m.id);
|
||||
// Ensure all keys exist (prevents undefined stripping)
|
||||
const withDefaults = {
|
||||
id: m.id || Date.now().toString(),
|
||||
name: m.name || m.displayName || "",
|
||||
role: m.role ?? "",
|
||||
hoursPerMonth: m.hoursPerMonth ?? 0,
|
||||
minMonthlyNeeds: m.minMonthlyNeeds ?? 0,
|
||||
targetMonthlyPay: m.targetMonthlyPay ?? 0,
|
||||
externalMonthlyIncome: m.externalMonthlyIncome ?? 0,
|
||||
monthlyPayPlanned: m.monthlyPayPlanned ?? 0,
|
||||
};
|
||||
if (i === -1) {
|
||||
this.members.push(withDefaults);
|
||||
} else {
|
||||
this.members[i] = withDefaults;
|
||||
}
|
||||
},
|
||||
|
||||
removeMember(id: string) {
|
||||
this.members = this.members.filter((m) => m.id !== id);
|
||||
},
|
||||
|
||||
// Stream actions
|
||||
upsertStream(s: any) {
|
||||
const i = this.streams.findIndex((x) => x.id === s.id);
|
||||
const withDefaults = {
|
||||
id: s.id || Date.now().toString(),
|
||||
label: s.label || s.name || "",
|
||||
monthly: s.monthly || s.targetMonthlyAmount || 0,
|
||||
category: s.category ?? "",
|
||||
certainty: s.certainty ?? "Probable",
|
||||
};
|
||||
if (i === -1) {
|
||||
this.streams.push(withDefaults);
|
||||
} else {
|
||||
this.streams[i] = withDefaults;
|
||||
}
|
||||
},
|
||||
|
||||
removeStream(id: string) {
|
||||
this.streams = this.streams.filter((s) => s.id !== id);
|
||||
},
|
||||
|
||||
// Milestone actions
|
||||
addMilestone(label: string, date: string) {
|
||||
this.milestones.push({
|
||||
id: Date.now().toString(),
|
||||
label,
|
||||
date,
|
||||
});
|
||||
},
|
||||
|
||||
removeMilestone(id: string) {
|
||||
this.milestones = this.milestones.filter((m) => m.id !== id);
|
||||
},
|
||||
|
||||
// Operating mode
|
||||
setOperatingMode(mode: "min" | "target") {
|
||||
this.operatingMode = mode;
|
||||
},
|
||||
|
||||
// Scenario
|
||||
setScenario(
|
||||
scenario: "current" | "quit-jobs" | "start-production" | "custom"
|
||||
) {
|
||||
this.scenario = scenario;
|
||||
},
|
||||
|
||||
// Stress test
|
||||
updateStress(updates: Partial<typeof this.stress>) {
|
||||
this.stress = { ...this.stress, ...updates };
|
||||
},
|
||||
|
||||
// Policy updates
|
||||
setPolicy(relationship: "equal-pay" | "needs-weighted" | "hours-weighted" | "role-banded") {
|
||||
this.policy.relationship = relationship;
|
||||
},
|
||||
|
||||
setRoleBands(bands: Record<string, number>) {
|
||||
this.policy.roleBands = bands;
|
||||
},
|
||||
|
||||
setEqualWage(wage: number) {
|
||||
this.equalHourlyWage = wage;
|
||||
},
|
||||
|
||||
setOncostPct(pct: number) {
|
||||
this.payrollOncostPct = pct;
|
||||
},
|
||||
|
||||
// Overhead costs
|
||||
addOverheadCost(cost: any) {
|
||||
const withDefaults = {
|
||||
id: cost.id || Date.now().toString(),
|
||||
name: cost.name || "",
|
||||
amount: cost.amount || 0,
|
||||
category: cost.category ?? "",
|
||||
};
|
||||
this.overheadCosts.push(withDefaults);
|
||||
},
|
||||
|
||||
upsertOverheadCost(cost: any) {
|
||||
const i = this.overheadCosts.findIndex((c) => c.id === cost.id);
|
||||
const withDefaults = {
|
||||
id: cost.id || Date.now().toString(),
|
||||
name: cost.name || "",
|
||||
amount: cost.amount || 0,
|
||||
category: cost.category ?? "",
|
||||
};
|
||||
if (i === -1) {
|
||||
this.overheadCosts.push(withDefaults);
|
||||
} else {
|
||||
this.overheadCosts[i] = withDefaults;
|
||||
}
|
||||
},
|
||||
|
||||
removeOverheadCost(id: string) {
|
||||
this.overheadCosts = this.overheadCosts.filter((c) => c.id !== id);
|
||||
},
|
||||
|
||||
// Initialize with default data if empty - DISABLED
|
||||
// NO automatic initialization - stores should start empty
|
||||
initializeDefaults() {
|
||||
// DISABLED: No automatic data loading
|
||||
// User must explicitly choose to load demo data
|
||||
return;
|
||||
},
|
||||
|
||||
// Clear ALL data - no exceptions
|
||||
clearAll() {
|
||||
// Reset ALL state to initial empty values
|
||||
this._wasCleared = true;
|
||||
this.operatingMode = "min";
|
||||
this.members = [];
|
||||
this.streams = [];
|
||||
this.milestones = [];
|
||||
this.scenario = "current";
|
||||
this.stress = {
|
||||
revenueDelay: 0,
|
||||
costShockPct: 0,
|
||||
grantLost: false,
|
||||
};
|
||||
this.policy = {
|
||||
relationship: "equal-pay",
|
||||
roleBands: {},
|
||||
};
|
||||
this.equalHourlyWage = 0;
|
||||
this.payrollOncostPct = 0;
|
||||
this.savingsTargetMonths = 0;
|
||||
this.minCashCushion = 0;
|
||||
this.currentCash = 0;
|
||||
this.currentSavings = 0;
|
||||
this.overheadCosts = [];
|
||||
|
||||
// Clear ALL localStorage data
|
||||
if (typeof window !== "undefined") {
|
||||
// Save cleared flag first
|
||||
localStorage.setItem("urgent-tools-cleared-flag", "true");
|
||||
|
||||
// Remove all known keys
|
||||
const keysToRemove = [
|
||||
"coop_builder_v1",
|
||||
"urgent-tools-members",
|
||||
"urgent-tools-policies",
|
||||
"urgent-tools-streams",
|
||||
"urgent-tools-budget",
|
||||
"urgent-tools-cash",
|
||||
"urgent-tools-schema-version",
|
||||
];
|
||||
|
||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||
|
||||
// Clear any other urgent-tools or coop keys
|
||||
const allKeys = Object.keys(localStorage);
|
||||
allKeys.forEach((key) => {
|
||||
if (key.startsWith("urgent-tools-") || key.startsWith("coop_")) {
|
||||
if (key !== "urgent-tools-cleared-flag") {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
persist: {
|
||||
key: "coop_builder_v1",
|
||||
storage: typeof window !== "undefined" ? localStorage : undefined,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from 'vue';
|
||||
import { coverage, teamCoverageStats } from "~/types/members";
|
||||
|
||||
export const useMembersStore = defineStore(
|
||||
"members",
|
||||
|
|
@ -34,10 +36,16 @@ export const useMembersStore = defineStore(
|
|||
|
||||
// Normalize a member object to ensure required structure and sane defaults
|
||||
function normalizeMember(raw) {
|
||||
// Calculate hoursPerWeek from targetHours (monthly) if not explicitly set
|
||||
const targetHours = Number(raw.capacity?.targetHours) || 0;
|
||||
const hoursPerWeek = raw.hoursPerWeek ?? (targetHours > 0 ? targetHours / 4.33 : 0);
|
||||
|
||||
const normalized = {
|
||||
id: raw.id || Date.now().toString(),
|
||||
displayName: typeof raw.displayName === "string" ? raw.displayName : "",
|
||||
roleFocus: typeof raw.roleFocus === "string" ? raw.roleFocus : "",
|
||||
role: raw.role || raw.roleFocus || "",
|
||||
hoursPerWeek: hoursPerWeek,
|
||||
payRelationship: raw.payRelationship || "FullyPaid",
|
||||
capacity: {
|
||||
minHours: Number(raw.capacity?.minHours) || 0,
|
||||
|
|
@ -49,6 +57,11 @@ export const useMembersStore = defineStore(
|
|||
privacyNeeds: raw.privacyNeeds || "aggregate_ok",
|
||||
deferredHours: Number(raw.deferredHours ?? 0),
|
||||
quarterlyDeferredCap: Number(raw.quarterlyDeferredCap ?? 240),
|
||||
// NEW fields for needs coverage
|
||||
minMonthlyNeeds: Number(raw.minMonthlyNeeds) || 0,
|
||||
targetMonthlyPay: Number(raw.targetMonthlyPay) || 0,
|
||||
externalMonthlyIncome: Number(raw.externalMonthlyIncome) || 0,
|
||||
monthlyPayPlanned: Number(raw.monthlyPayPlanned) || 0,
|
||||
...raw,
|
||||
};
|
||||
return normalized;
|
||||
|
|
@ -187,6 +200,56 @@ export const useMembersStore = defineStore(
|
|||
members.value = [];
|
||||
}
|
||||
|
||||
// Coverage calculations for individual members
|
||||
function getMemberCoverage(memberId) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (!member) return { minPct: undefined, targetPct: undefined };
|
||||
|
||||
return coverage(
|
||||
member.minMonthlyNeeds || 0,
|
||||
member.targetMonthlyPay || 0,
|
||||
member.monthlyPayPlanned || 0,
|
||||
member.externalMonthlyIncome || 0
|
||||
);
|
||||
}
|
||||
|
||||
// Team-wide coverage statistics
|
||||
const teamStats = computed(() => teamCoverageStats(members.value));
|
||||
|
||||
// Pay policy configuration
|
||||
const payPolicy = ref({
|
||||
relationship: 'equal-pay' as const,
|
||||
notes: '',
|
||||
equalBase: 0,
|
||||
needsWeight: 0.5,
|
||||
roleBands: {},
|
||||
hoursRate: 0,
|
||||
customFormula: ''
|
||||
});
|
||||
|
||||
// Setters for new fields
|
||||
function setMonthlyNeeds(memberId, minNeeds, targetPay) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.minMonthlyNeeds = Number(minNeeds) || 0;
|
||||
member.targetMonthlyPay = Number(targetPay) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setExternalIncome(memberId, income) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.externalMonthlyIncome = Number(income) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setPlannedPay(memberId, planned) {
|
||||
const member = members.value.find((m) => m.id === memberId);
|
||||
if (member) {
|
||||
member.monthlyPayPlanned = Number(planned) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
members,
|
||||
capacityTotals,
|
||||
|
|
@ -194,6 +257,8 @@ export const useMembersStore = defineStore(
|
|||
validationDetails,
|
||||
isValid,
|
||||
schemaVersion,
|
||||
payPolicy,
|
||||
teamStats,
|
||||
// Wizard actions
|
||||
upsertMember,
|
||||
setCapacity,
|
||||
|
|
@ -202,6 +267,11 @@ export const useMembersStore = defineStore(
|
|||
setExternalCoveragePct,
|
||||
setPrivacy,
|
||||
resetMembers,
|
||||
// New coverage actions
|
||||
setMonthlyNeeds,
|
||||
setExternalIncome,
|
||||
setPlannedPay,
|
||||
getMemberCoverage,
|
||||
// Legacy actions
|
||||
addMember,
|
||||
updateMember,
|
||||
|
|
@ -211,7 +281,7 @@ export const useMembersStore = defineStore(
|
|||
{
|
||||
persist: {
|
||||
key: "urgent-tools-members",
|
||||
paths: ["members", "privacyFlags"],
|
||||
paths: ["members", "privacyFlags", "payPolicy"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,13 @@ export const usePoliciesStore = defineStore(
|
|||
const payrollOncostPct = ref(0);
|
||||
const savingsTargetMonths = ref(0);
|
||||
const minCashCushionAmount = ref(0);
|
||||
const operatingMode = ref<'minimum' | 'target'>('minimum');
|
||||
|
||||
// Pay policy for member needs coverage
|
||||
const payPolicy = ref({
|
||||
relationship: 'equal-pay' as 'equal-pay' | 'needs-weighted' | 'role-banded' | 'hours-weighted',
|
||||
roleBands: [] as Array<{ role: string; hourlyWage: number }>
|
||||
});
|
||||
|
||||
// Deferred pay limits
|
||||
const deferredCapHoursPerQtr = ref(0);
|
||||
|
|
@ -95,6 +102,13 @@ export const usePoliciesStore = defineStore(
|
|||
function setPaymentPriority(priority) {
|
||||
paymentPriority.value = [...priority];
|
||||
}
|
||||
|
||||
function setPayPolicy(relationship, roleBands = []) {
|
||||
payPolicy.value = {
|
||||
relationship,
|
||||
roleBands: [...roleBands]
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy actions
|
||||
function updatePolicy(key, value) {
|
||||
|
|
@ -120,6 +134,8 @@ export const usePoliciesStore = defineStore(
|
|||
payrollOncostPct.value = 0;
|
||||
savingsTargetMonths.value = 0;
|
||||
minCashCushionAmount.value = 0;
|
||||
operatingMode.value = 'minimum';
|
||||
payPolicy.value = { relationship: 'equal-pay', roleBands: [] };
|
||||
deferredCapHoursPerQtr.value = 0;
|
||||
deferredSunsetMonths.value = 0;
|
||||
surplusOrder.value = [
|
||||
|
|
@ -139,6 +155,8 @@ export const usePoliciesStore = defineStore(
|
|||
payrollOncostPct,
|
||||
savingsTargetMonths,
|
||||
minCashCushionAmount,
|
||||
operatingMode,
|
||||
payPolicy,
|
||||
deferredCapHoursPerQtr,
|
||||
deferredSunsetMonths,
|
||||
surplusOrder,
|
||||
|
|
@ -151,6 +169,7 @@ export const usePoliciesStore = defineStore(
|
|||
setOncostPct,
|
||||
setSavingsTargetMonths,
|
||||
setMinCashCushion,
|
||||
setPayPolicy,
|
||||
setDeferredCap,
|
||||
setDeferredSunset,
|
||||
setVolunteerScope,
|
||||
|
|
@ -171,6 +190,8 @@ export const usePoliciesStore = defineStore(
|
|||
"payrollOncostPct",
|
||||
"savingsTargetMonths",
|
||||
"minCashCushionAmount",
|
||||
"operatingMode",
|
||||
"payPolicy",
|
||||
"deferredCapHoursPerQtr",
|
||||
"deferredSunsetMonths",
|
||||
"surplusOrder",
|
||||
|
|
|
|||
|
|
@ -87,30 +87,6 @@ export const useStreamsStore = defineStore(
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize with fixture data if empty
|
||||
async function initializeWithFixtures() {
|
||||
if (streams.value.length === 0) {
|
||||
const { useFixtures } = await import('~/composables/useFixtures');
|
||||
const fixtures = useFixtures();
|
||||
const { revenueStreams } = await fixtures.loadStreams();
|
||||
|
||||
revenueStreams.forEach(stream => {
|
||||
upsertStream(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load realistic demo data (for better user experience)
|
||||
async function loadDemoData() {
|
||||
resetStreams();
|
||||
const { useFixtures } = await import('~/composables/useFixtures');
|
||||
const fixtures = useFixtures();
|
||||
const { revenueStreams } = await fixtures.loadStreams();
|
||||
|
||||
revenueStreams.forEach(stream => {
|
||||
upsertStream(stream);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset function
|
||||
function resetStreams() {
|
||||
|
|
@ -127,8 +103,6 @@ export const useStreamsStore = defineStore(
|
|||
// Wizard actions
|
||||
upsertStream,
|
||||
resetStreams,
|
||||
initializeWithFixtures,
|
||||
loadDemoData,
|
||||
// Legacy actions
|
||||
addStream,
|
||||
updateStream,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue