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

View file

@ -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"
],
},
});

View file

@ -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
View 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,
},
});

View file

@ -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"],
},
}
);

View file

@ -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",

View file

@ -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,