304 lines
8.1 KiB
TypeScript
304 lines
8.1 KiB
TypeScript
import { defineStore } from "pinia";
|
|
|
|
export const useCoopBuilderStore = defineStore("coop", {
|
|
state: () => ({
|
|
// Currency preference
|
|
currency: "EUR" as string,
|
|
|
|
// Flag to track if data was intentionally cleared
|
|
_wasCleared: false,
|
|
|
|
members: [] as Array<{
|
|
id: string;
|
|
name: string;
|
|
hoursPerMonth?: number;
|
|
minMonthlyNeeds: number;
|
|
monthlyPayPlanned: number;
|
|
}>,
|
|
|
|
streams: [] as Array<{
|
|
id: string;
|
|
label: string;
|
|
monthly: number;
|
|
annual?: number;
|
|
amountType?: 'monthly' | 'annual';
|
|
category?: string;
|
|
certainty?: string;
|
|
}>,
|
|
|
|
milestones: [] as Array<{
|
|
id: string;
|
|
label: string;
|
|
date: string;
|
|
}>,
|
|
|
|
// Scenario and stress test state
|
|
scenario: "current" as
|
|
| "current"
|
|
| "start-production"
|
|
| "custom",
|
|
stress: {
|
|
revenueDelay: 0,
|
|
costShockPct: 0,
|
|
grantLost: false,
|
|
},
|
|
|
|
// Policy settings
|
|
policy: {
|
|
relationship: "equal-pay" as "equal-pay" | "needs-weighted" | "hours-weighted",
|
|
},
|
|
equalHourlyWage: 50,
|
|
payrollOncostPct: 25,
|
|
|
|
// Cash flow management
|
|
minCashThreshold: 5000, // Minimum cash balance to maintain
|
|
savingsTargetMonths: 6,
|
|
minCashCushion: 10000,
|
|
|
|
// Cash reserves
|
|
currentCash: 50000,
|
|
currentSavings: 15000,
|
|
|
|
// Overhead costs
|
|
overheadCosts: [] as Array<{
|
|
id?: string;
|
|
name: string;
|
|
amount: number;
|
|
annualAmount?: number;
|
|
amountType?: 'monthly' | 'annual';
|
|
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);
|
|
|
|
// Calculate monthly value based on amount type
|
|
let monthlyValue = s.monthly || s.targetMonthlyAmount || 0;
|
|
if (s.amountType === 'annual' && s.annual) {
|
|
monthlyValue = Math.round(s.annual / 12);
|
|
}
|
|
|
|
const withDefaults = {
|
|
id: s.id || Date.now().toString(),
|
|
label: s.label || s.name || "",
|
|
monthly: monthlyValue,
|
|
annual: s.annual || s.targetAnnualAmount || monthlyValue * 12,
|
|
amountType: s.amountType || 'monthly',
|
|
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);
|
|
},
|
|
|
|
// Scenario
|
|
setScenario(
|
|
scenario: "current" | "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") {
|
|
this.policy.relationship = relationship;
|
|
},
|
|
|
|
setEqualWage(wage: number) {
|
|
this.equalHourlyWage = wage;
|
|
},
|
|
|
|
setOncostPct(pct: number) {
|
|
this.payrollOncostPct = pct;
|
|
},
|
|
|
|
setMinCashThreshold(amount: number) {
|
|
this.minCashThreshold = amount;
|
|
},
|
|
|
|
setCurrency(currency: string) {
|
|
this.currency = currency;
|
|
},
|
|
|
|
// Overhead costs
|
|
addOverheadCost(cost: any) {
|
|
// Calculate monthly value based on amount type
|
|
let monthlyValue = cost.amount || 0;
|
|
if (cost.amountType === 'annual' && cost.annualAmount) {
|
|
monthlyValue = Math.round(cost.annualAmount / 12);
|
|
}
|
|
|
|
const withDefaults = {
|
|
id: cost.id || Date.now().toString(),
|
|
name: cost.name || "",
|
|
amount: monthlyValue,
|
|
annualAmount: cost.annualAmount || monthlyValue * 12,
|
|
amountType: cost.amountType || 'monthly',
|
|
category: cost.category ?? "",
|
|
};
|
|
this.overheadCosts.push(withDefaults);
|
|
},
|
|
|
|
upsertOverheadCost(cost: any) {
|
|
const i = this.overheadCosts.findIndex((c) => c.id === cost.id);
|
|
|
|
// Calculate monthly value based on amount type
|
|
let monthlyValue = cost.amount || 0;
|
|
if (cost.amountType === 'annual' && cost.annualAmount) {
|
|
monthlyValue = Math.round(cost.annualAmount / 12);
|
|
}
|
|
|
|
const withDefaults = {
|
|
id: cost.id || Date.now().toString(),
|
|
name: cost.name || "",
|
|
amount: monthlyValue,
|
|
annualAmount: cost.annualAmount || monthlyValue * 12,
|
|
amountType: cost.amountType || 'monthly',
|
|
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.currency = "EUR";
|
|
this.members = [];
|
|
this.streams = [];
|
|
this.milestones = [];
|
|
this.scenario = "current";
|
|
this.stress = {
|
|
revenueDelay: 0,
|
|
costShockPct: 0,
|
|
grantLost: false,
|
|
};
|
|
this.policy = {
|
|
relationship: "equal-pay",
|
|
};
|
|
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,
|
|
},
|
|
});
|