soc2-cockpit/src/features/hooks/useSoc2Registry.js
2026-04-17 14:17:56 +00:00

191 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}