192 lines
5.6 KiB
JavaScript
192 lines
5.6 KiB
JavaScript
|
|
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 n’a 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,
|
|||
|
|
};
|
|||
|
|
}
|