app/stores/coopBuilder.ts

277 lines
7.1 KiB
TypeScript

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