feat: add initial application structure with configuration, UI components, and state management
This commit is contained in:
parent
fadf94002c
commit
0af6b17792
56 changed files with 6137 additions and 129 deletions
217
stores/members.ts
Normal file
217
stores/members.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
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"],
|
||||
},
|
||||
}
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue