217 lines
6.5 KiB
TypeScript
217 lines
6.5 KiB
TypeScript
import { defineStore } from "pinia";
|
|
|
|
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) {
|
|
const normalized = {
|
|
id: raw.id || Date.now().toString(),
|
|
displayName: typeof raw.displayName === "string" ? raw.displayName : "",
|
|
roleFocus: typeof raw.roleFocus === "string" ? raw.roleFocus : "",
|
|
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),
|
|
...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 = [];
|
|
}
|
|
|
|
return {
|
|
members,
|
|
capacityTotals,
|
|
privacyFlags,
|
|
validationDetails,
|
|
isValid,
|
|
schemaVersion,
|
|
// Wizard actions
|
|
upsertMember,
|
|
setCapacity,
|
|
setPayRelationship,
|
|
setRiskBand,
|
|
setExternalCoveragePct,
|
|
setPrivacy,
|
|
resetMembers,
|
|
// Legacy actions
|
|
addMember,
|
|
updateMember,
|
|
removeMember,
|
|
};
|
|
},
|
|
{
|
|
persist: {
|
|
key: "urgent-tools-members",
|
|
paths: ["members", "privacyFlags"],
|
|
},
|
|
}
|
|
);
|