soc2-cockpit/src/features/hooks/useSoc2Registry.js

192 lines
5.6 KiB
JavaScript
Raw Normal View History

2026-04-17 10:17:56 -04:00
import { useEffect, useState } from "react";
import { INITIAL_DB } from "../../model/seed.js";
import { STORAGE_KEY, TODAY } from "../../model/schema.js";
import { clone } from "../../lib/utils.js";
import { normalizeDb } from "../../lib/validation.js";
function loadDb() {
if (typeof window === "undefined") return clone(INITIAL_DB);
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return clone(INITIAL_DB);
return normalizeDb(JSON.parse(raw));
} catch {
return clone(INITIAL_DB);
}
}
export function useSoc2Registry() {
const [db, setDb] = useState(() => loadDb());
const [lastSavedAt, setLastSavedAt] = useState(null);
const [importError, setImportError] = useState("");
useEffect(() => {
if (typeof window === "undefined") return;
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(db, null, 2));
setLastSavedAt(new Date().toLocaleString("fr-CA"));
}, [db]);
const updateMilestoneStage = (pillarId, milestoneId, nextStage) => {
setDb((current) => ({
...current,
pillars: current.pillars.map((pillar) =>
pillar.id !== pillarId
? pillar
: {
...pillar,
milestones: pillar.milestones.map((milestone) =>
milestone.id === milestoneId ? { ...milestone, stage: nextStage } : milestone
),
}
),
}));
};
const updateEvidence = (evidenceId, patch) => {
setDb((current) => ({
...current,
evidence: current.evidence.map((item) =>
item.id === evidenceId ? { ...item, ...patch, updatedAt: patch.updatedAt || TODAY } : item
),
}));
};
const toggleEvidenceMilestoneLink = (evidenceId, pillarId, milestoneId) => {
setDb((current) => {
const evidence = current.evidence.find((item) => item.id === evidenceId);
if (!evidence) return current;
const hasLink = evidence.linkedMilestones.includes(milestoneId);
return {
...current,
evidence: current.evidence.map((item) =>
item.id !== evidenceId
? item
: {
...item,
linkedMilestones: hasLink
? item.linkedMilestones.filter((id) => id !== milestoneId)
: [...item.linkedMilestones, milestoneId],
updatedAt: TODAY,
}
),
pillars: current.pillars.map((pillar) =>
pillar.id !== pillarId
? pillar
: {
...pillar,
milestones: pillar.milestones.map((milestone) => {
if (milestone.id !== milestoneId) return milestone;
const alreadyLinked = milestone.evidenceRefs.includes(evidenceId);
return {
...milestone,
evidenceRefs: hasLink
? milestone.evidenceRefs.filter((id) => id !== evidenceId)
: alreadyLinked
? milestone.evidenceRefs
: [...milestone.evidenceRefs, evidenceId],
};
}),
}
),
};
});
};
const createEvidence = (selectedPillar) => {
const maxId = db.evidence
.map((item) => Number(String(item.id).replace("ev-", "")))
.filter((n) => Number.isFinite(n))
.reduce((max, n) => Math.max(max, n), 0);
const nextId = `ev-${String(maxId + 1).padStart(3, "0")}`;
const seed = {
id: nextId,
title: "Nouvelle preuve",
category: "General",
pillar: selectedPillar,
owner: "Ops",
freshness: "Brouillon",
updatedAt: TODAY,
auditorReady: false,
visibility: "internal",
source: "Source à définir",
tags: [],
linkedMilestones: [],
summary: "",
};
setDb((current) => ({ ...current, evidence: [seed, ...current.evidence] }));
return nextId;
};
const deleteEvidence = (evidenceId) => {
setDb((current) => ({
...current,
evidence: current.evidence.filter((item) => item.id !== evidenceId),
pillars: current.pillars.map((pillar) => ({
...pillar,
milestones: pillar.milestones.map((milestone) => ({
...milestone,
evidenceRefs: milestone.evidenceRefs.filter((ref) => ref !== evidenceId),
})),
})),
auditorPacks: current.auditorPacks.map((pack) => ({
...pack,
requiredEvidence: pack.requiredEvidence.filter((ref) => ref !== evidenceId),
})),
}));
};
const exportDb = () => {
const blob = new Blob([JSON.stringify(db, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "chezlepro-soc2-cockpit.json";
a.click();
URL.revokeObjectURL(url);
};
const copyDb = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(db, null, 2));
} catch {
// clipboard may be unavailable
}
};
const importDb = (payload) => {
setImportError("");
try {
const parsed = JSON.parse(payload);
const normalized = normalizeDb(parsed);
setDb(normalized);
return { ok: true, data: normalized };
} catch {
const error = "Import invalide : le JSON na pas pu être interprété.";
setImportError(error);
return { ok: false, error };
}
};
const resetDemo = () => {
setDb(clone(INITIAL_DB));
};
return {
db,
lastSavedAt,
importError,
updateMilestoneStage,
updateEvidence,
toggleEvidenceMilestoneLink,
createEvidence,
deleteEvidence,
exportDb,
copyDb,
importDb,
resetDemo,
};
}