import { defineStore } from "pinia"; import { ref, computed } from 'vue'; import { coverage, teamCoverageStats } from "~/types/members"; export const useMembersStore = defineStore( "members", () => { // Member list and profiles const members = ref([]); // Schema version for persistence const schemaVersion = "1.0"; // Capacity totals across all members const capacityTotals = computed(() => ({ minHours: members.value.reduce( (sum, m) => sum + (m.capacity?.minHours || 0), 0 ), targetHours: members.value.reduce( (sum, m) => sum + (m.capacity?.targetHours || 0), 0 ), maxHours: members.value.reduce( (sum, m) => sum + (m.capacity?.maxHours || 0), 0 ), })); // Privacy flags - aggregate vs individual visibility const privacyFlags = ref({ showIndividualNeeds: false, showIndividualCapacity: false, stewardOnlyDetails: true, }); // 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 : "", hoursPerWeek: hoursPerWeek, payRelationship: raw.payRelationship || "FullyPaid", capacity: { minHours: Number(raw.capacity?.minHours) || 0, targetHours: Number(raw.capacity?.targetHours) || 0, maxHours: Number(raw.capacity?.maxHours) || 0, }, riskBand: raw.riskBand || "Medium", externalCoveragePct: Number(raw.externalCoveragePct ?? 0), privacyNeeds: raw.privacyNeeds || "aggregate_ok", deferredHours: Number(raw.deferredHours ?? 0), quarterlyDeferredCap: Number(raw.quarterlyDeferredCap ?? 240), // Simplified - only minimum needs for allocation minMonthlyNeeds: Number(raw.minMonthlyNeeds) || 0, monthlyPayPlanned: Number(raw.monthlyPayPlanned) || 0, ...raw, }; return normalized; } // Initialize normalization for any persisted members if (Array.isArray(members.value) && members.value.length) { members.value = members.value.map(normalizeMember); } // Validation details for debugging const validationDetails = computed(() => members.value.map((m) => { const nameOk = typeof m.displayName === "string" && m.displayName.trim().length > 0; const relOk = Boolean(m.payRelationship); const target = Number(m.capacity?.targetHours); const targetOk = Number.isFinite(target) && target > 0; const pctRaw = m.externalCoveragePct; const pct = pctRaw === undefined || pctRaw === null ? 0 : Number(pctRaw); const pctOk = pctRaw === undefined || pctRaw === null || (Number.isFinite(pct) && pct >= 0 && pct <= 100); return { id: m.id, displayName: m.displayName, payRelationship: m.payRelationship, targetHours: m.capacity?.targetHours, externalCoveragePct: m.externalCoveragePct, checks: { nameOk, relOk, targetOk, pctOk }, valid: nameOk && relOk && targetOk && pctOk, }; }) ); // Validation computed (robust against NaN/empty values) const isValid = computed(() => { // A member is valid if core required fields pass const isMemberValid = (m: any) => { const nameOk = typeof m.displayName === "string" && m.displayName.trim().length > 0; const relOk = Boolean(m.payRelationship); const target = Number(m.capacity?.targetHours); const targetOk = Number.isFinite(target) && target > 0; // External coverage is optional; when present it must be within 0..100 const pctRaw = m.externalCoveragePct; const pct = pctRaw === undefined || pctRaw === null ? 0 : Number(pctRaw); const pctOk = pctRaw === undefined || pctRaw === null || (Number.isFinite(pct) && pct >= 0 && pct <= 100); return nameOk && relOk && targetOk && pctOk; }; // Require at least one valid member return members.value.some(isMemberValid); }); // Wizard-required actions function upsertMember(member) { const existingIndex = members.value.findIndex((m) => m.id === member.id); if (existingIndex > -1) { members.value[existingIndex] = normalizeMember({ ...members.value[existingIndex], ...member, }); } else { members.value.push( normalizeMember({ id: member.id || Date.now().toString(), ...member, }) ); } } function setCapacity(memberId, capacity) { const member = members.value.find((m) => m.id === memberId); if (member) { member.capacity = { ...member.capacity, ...capacity }; } } function setPayRelationship(memberId, payRelationship) { const member = members.value.find((m) => m.id === memberId); if (member) { member.payRelationship = payRelationship; } } function setRiskBand(memberId, riskBand) { const member = members.value.find((m) => m.id === memberId); if (member) { member.riskBand = riskBand; } } function setExternalCoveragePct(memberId, pct) { const member = members.value.find((m) => m.id === memberId); if (member && pct >= 0 && pct <= 100) { member.externalCoveragePct = pct; } } function setPrivacy(memberId, privacyNeeds) { const member = members.value.find((m) => m.id === memberId); if (member) { member.privacyNeeds = privacyNeeds; } } // Actions function addMember(member) { upsertMember(member); } function updateMember(id, updates) { const member = members.value.find((m) => m.id === id); if (member) { Object.assign(member, updates); } } function removeMember(id) { const index = members.value.findIndex((m) => m.id === id); if (index > -1) { members.value.splice(index, 1); } } // Reset function function resetMembers() { members.value = []; } // Coverage calculations for individual members function getMemberCoverage(memberId) { const member = members.value.find((m) => m.id === memberId); if (!member) return { coveragePct: undefined }; return coverage( member.minMonthlyNeeds || 0, member.monthlyPayPlanned || 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, hoursRate: 0, customFormula: '' }); // Setter for minimum needs only function setMonthlyNeeds(memberId, minNeeds) { const member = members.value.find((m) => m.id === memberId); if (member) { member.minMonthlyNeeds = Number(minNeeds) || 0; } } // Removed setExternalIncome - no longer needed function setPlannedPay(memberId, planned) { const member = members.value.find((m) => m.id === memberId); if (member) { member.monthlyPayPlanned = Number(planned) || 0; } } return { members, capacityTotals, privacyFlags, validationDetails, isValid, schemaVersion, payPolicy, teamStats, // Wizard actions upsertMember, setCapacity, setPayRelationship, setRiskBand, setExternalCoveragePct, setPrivacy, resetMembers, // New coverage actions setMonthlyNeeds, setPlannedPay, getMemberCoverage, // Legacy actions addMember, updateMember, removeMember, }; }, { persist: { key: "urgent-tools-members", paths: ["members", "privacyFlags", "payPolicy"], }, } );