feat(iace/verification): derived view on relevant mitigations + 2 actions
Task #21. The verification page used to manage a separate VerificationItem entity that the expert had to populate by hand — disjoint from the actual mitigations list. With the is_relevant flag from migration 029, the verification step has a natural definition: confirm completion for every mitigation the expert flagged as relevant for this project. Page is now a derived view on useMitigations(): filter is_relevant=true, group by title (same dedupe as Massnahmen page), expose two actions per hazard×mitigation row: 1. "Kundenstandard" — already implemented at the customer's site, no evidence file required. Sets is_customer_standard=true and status='verified'. 2. "Verifizieren…" — opens a modal asking for a textual evidence reference (Prüfprotokoll-Nr, audit reference, etc.). Calls the existing POST /mitigations/:mid/verify with verification_result. File upload is deferred to phase 2 once an object-storage backend is in place — the modal explains this. When a row is verified, a "Zurücksetzen" link reverts status to 'implemented' for accidental confirmations. Header counters: total relevant / open / verified / Kundenstandard. Maßnahmen-page polish (same commit): - "Lösch."-column header removed — the trash icon is self-explanatory - groupByTitle now additionally deduplicates by hazard_id within a group (engine occasionally emits duplicate (name, hazard_id) pairs when Reinit is clicked twice; a follow-up migration 030 will add a UNIQUE constraint to prevent these upstream) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,14 @@ export default function MitigationsPage() {
|
||||
// Mitigations sharing the same title (e.g. "Sicherheitszeichen nach ISO 7010"
|
||||
// applied to 21 hazards) collapse into a single group row. Each instance
|
||||
// keeps its own DB id, status and notes — the grouping is presentation-only.
|
||||
//
|
||||
// Within a group we additionally deduplicate by hazard_id: the engine
|
||||
// sometimes emits the same (name, hazard_id) pair twice when "Neu
|
||||
// initialisieren" is clicked repeatedly. We pick the row that already
|
||||
// carries user state (is_relevant=true preferred, then newest created_at)
|
||||
// so the expert's decisions are not lost. The DB still holds both rows;
|
||||
// a separate migration adds a UNIQUE(hazard_id, name) constraint to
|
||||
// prevent the duplicates upstream.
|
||||
function groupByTitle(items: Mitigation[]): Array<{ title: string; instances: Mitigation[] }> {
|
||||
const map = new Map<string, Mitigation[]>()
|
||||
for (const m of items) {
|
||||
@@ -71,7 +79,18 @@ export default function MitigationsPage() {
|
||||
const arr = map.get(key)
|
||||
if (arr) arr.push(m); else map.set(key, [m])
|
||||
}
|
||||
return Array.from(map.entries()).map(([title, instances]) => ({ title, instances }))
|
||||
return Array.from(map.entries()).map(([title, instances]) => {
|
||||
const byHazard = new Map<string, Mitigation>()
|
||||
for (const m of instances) {
|
||||
const hid = (m.linked_hazard_ids || []).join('|') || m.id
|
||||
const prev = byHazard.get(hid)
|
||||
if (!prev) { byHazard.set(hid, m); continue }
|
||||
// Tie-break: prefer is_relevant=true, then newest created_at
|
||||
const score = (x: Mitigation) => (x.is_relevant ? 2 : 0) + (x.created_at > (prev.created_at || '') ? 1 : 0)
|
||||
if (score(m) > score(prev)) byHazard.set(hid, m)
|
||||
}
|
||||
return { title, instances: Array.from(byHazard.values()) }
|
||||
})
|
||||
}
|
||||
|
||||
// Compact status distribution: returns counts for the three known states.
|
||||
@@ -187,7 +206,7 @@ export default function MitigationsPage() {
|
||||
{/* Table header */}
|
||||
<div className="grid grid-cols-[36px_36px_2fr_120px_110px] gap-2 px-4 py-2 bg-gray-50 dark:bg-gray-750 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<div title="Relevant fuer dieses Projekt">Relev.</div>
|
||||
<div title="Loeschen">Lösch.</div>
|
||||
<div />
|
||||
<div>Massnahme</div>
|
||||
<div className="text-right pr-2">Gefährdungen</div>
|
||||
<div>Status (P · I · V)</div>
|
||||
|
||||
Reference in New Issue
Block a user