From 3faa312b316cedddd4cc361c4726ea4472fbefd7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 17 May 2026 14:49:56 +0200 Subject: [PATCH] feat(iace/verification): derived view on relevant mitigations + 2 actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../sdk/iace/[projectId]/mitigations/page.tsx | 23 +- .../iace/[projectId]/verification/page.tsx | 344 +++++++++++------- 2 files changed, 225 insertions(+), 142 deletions(-) diff --git a/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx index 2de91320..af128dc0 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx @@ -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() 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() + 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 */}
Relev.
-
Lösch.
+
Massnahme
Gefährdungen
Status (P · I · V)
diff --git a/admin-compliance/app/sdk/iace/[projectId]/verification/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/verification/page.tsx index 69b3aed8..71cc8b4d 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/verification/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/verification/page.tsx @@ -1,86 +1,41 @@ 'use client' -import React, { useState, useEffect } from 'react' +import { useState } from 'react' import { useParams } from 'next/navigation' -import type { VerificationItem, VerificationFormData } from './_components/verification-types' -import { VerificationForm } from './_components/VerificationForm' -import { CompleteModal } from './_components/CompleteModal' -import { SuggestEvidenceModal } from './_components/SuggestEvidenceModal' -import { VerificationTable } from './_components/VerificationTable' +import { useMitigations } from '../mitigations/_hooks/useMitigations' +import type { Mitigation } from '../mitigations/_components/types' + +// Verifikations-Page (Phase-1 Workflow): +// +// Diese Seite ist eine abgeleitete View auf die Maßnahmen-Liste. Sie zeigt +// nur diejenigen Maßnahmen, die der Fachmann auf der Maßnahmen-Seite als +// `is_relevant = true` markiert hat. Pro Maßnahme stehen zwei Aktionen +// zur Verfügung: +// +// 1. "Beim Kunden Standard" — Die Maßnahme ist beim Kunden bereits +// umgesetzt (z.B. firmenweite Vorgabe, identische Vor-Anlage). +// Setzt is_customer_standard = true und status = verified. +// Es ist kein Nachweis-Dokument erforderlich. +// +// 2. "Verifizieren (mit Nachweis)" — Öffnet ein Modal, in dem der +// Verifizierer einen Text-Nachweis hinterlegt (Prüfprotokoll-Nummer, +// Abnahme-Referenz, etc.). Setzt status = verified. Die File-Upload- +// Variante folgt in Phase 2, sobald ein Object-Storage-Backend +// verfügbar ist. +// +// Wenn die Maßnahme bereits verifiziert ist, wird ein "Zurücksetzen"-Link +// angeboten — er stellt status auf 'implemented' zurück, damit der +// Fachmann eine versehentliche Bestätigung rückgängig machen kann. export default function VerificationPage() { const params = useParams() const projectId = params.projectId as string - const [items, setItems] = useState([]) - const [hazards, setHazards] = useState<{ id: string; name: string }[]>([]) - const [mitigations, setMitigations] = useState<{ id: string; title: string }[]>([]) - const [loading, setLoading] = useState(true) - const [showForm, setShowForm] = useState(false) - const [completingItem, setCompletingItem] = useState(null) - const [showSuggest, setShowSuggest] = useState(false) - useEffect(() => { fetchData() }, [projectId]) + const { byType, loading, handleSetCustomerStandard } = useMitigations(projectId) - async function fetchData() { - try { - // Only load verifications initially — hazards/mitigations loaded on demand - const verRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`) - if (verRes.ok) { const j = await verRes.json(); setItems(j.verifications || j || []) } - } catch (err) { console.error('Failed to fetch data:', err) } - finally { setLoading(false) } - } - - async function loadMitigationsIfNeeded() { - if (mitigations.length > 0) return - try { - const mitRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`) - if (mitRes.ok) { - const j = await mitRes.json() - const mits = (j.mitigations || j || []).map((m: Record) => ({ id: m.id, title: m.title || m.name || '' })) - setMitigations(mits) - } - } catch { /* ignore */ } - } - - async function handleSubmit(data: VerificationFormData) { - try { - const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), - }) - if (res.ok) { setShowForm(false); await fetchData() } - } catch (err) { console.error('Failed to add verification:', err) } - } - - async function handleAddSuggestedEvidence(title: string, description: string, method: string, mitigationId: string) { - try { - const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title, description, method, linked_mitigation_id: mitigationId }), - }) - if (res.ok) await fetchData() - } catch (err) { console.error('Failed to add suggested evidence:', err) } - } - - async function handleComplete(id: string, result: string, passed: boolean) { - try { - const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications/${id}/complete`, { - method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ result, passed }), - }) - if (res.ok) { setCompletingItem(null); await fetchData() } - } catch (err) { console.error('Failed to complete verification:', err) } - } - - async function handleDelete(id: string) { - if (!confirm('Verifikation wirklich loeschen?')) return - try { - const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/verifications/${id}`, { method: 'DELETE' }) - if (res.ok) await fetchData() - } catch (err) { console.error('Failed to delete verification:', err) } - } - - const completed = items.filter(i => i.status === 'completed').length - const failed = items.filter(i => i.status === 'failed').length - const pending = items.filter(i => i.status === 'pending' || i.status === 'in_progress').length + const [verifyTarget, setVerifyTarget] = useState(null) + const [verifyResult, setVerifyResult] = useState('') + const [submitting, setSubmitting] = useState(false) if (loading) return (
@@ -88,82 +43,191 @@ export default function VerificationPage() {
) + const allRelevant = [...byType.design, ...byType.protection, ...byType.information].filter((m) => m.is_relevant) + const groups = groupByTitle(allRelevant) + const totals = { + total: allRelevant.length, + verified: allRelevant.filter((m) => m.status === 'verified').length, + customerStd: allRelevant.filter((m) => m.is_customer_standard).length, + pending: allRelevant.filter((m) => m.status !== 'verified').length, + } + + async function setStatus(id: string, value: 'implemented' | 'verified') { + await fetch(`/api/sdk/v1/iace/mitigations/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: value }), + }) + } + + async function submitVerify() { + if (!verifyTarget) return + setSubmitting(true) + try { + await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${verifyTarget.id}/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ verification_result: verifyResult }), + }) + // Refetch via window-reload of just the data — useMitigations refreshes on mount. + window.location.reload() + } finally { + setSubmitting(false) + setVerifyTarget(null) + setVerifyResult('') + } + } + return ( -
-
-
-

Verifikationsplan

-

Nachweisfuehrung fuer alle Schutzmassnahmen und Sicherheitsanforderungen.

-
-
- {true && ( - - )} - -
+
+
+

Verifikation

+

+ Bestätige die Umsetzung jeder als relevant markierten Maßnahme — entweder als + Kundenstandard (keine Nachweis-Datei nötig) oder mit hinterlegtem Nachweis. +

- {items.length > 0 && ( -
-
-
{items.length}
-
Gesamt
-
-
-
{completed}
-
Abgeschlossen
-
-
-
{failed}
-
Fehlgeschlagen
-
-
-
{pending}
-
Ausstehend
-
+ {totals.total === 0 ? ( +
+ Keine als relevant markierten Maßnahmen vorhanden. Gehe zurück zur + {' '}Maßnahmen-Seite{' '} + und kreuze die anwendbaren Maßnahmen an.
+ ) : ( + <> +
+ + + + +
+ + {groups.map(({ title, instances }) => { + const verifiedCount = instances.filter((m) => m.status === 'verified').length + const allDone = verifiedCount === instances.length + return ( +
+
+
+
{title}
+
{verifiedCount}/{instances.length} verifiziert
+
+ {allDone && ( + + + + )} +
+
+ {instances.map((m) => { + const isVerified = m.status === 'verified' + return ( +
+
+
+ {(m.linked_hazard_names || []).join(', ') || '— (keine Gefährdung verknüpft)'} +
+ {m.is_customer_standard && ( +
Beim Kunden Standard
+ )} +
+
+ {!isVerified ? ( + <> + + + + ) : ( + <> + ✓ Verifiziert + + + )} +
+
+ ) + })} +
+
+ ) + })} + )} - {showForm && setShowForm(false)} hazards={hazards} mitigations={mitigations} />} - {completingItem && setCompletingItem(null)} />} - {showSuggest && setShowSuggest(false)} />} - - {items.length > 0 ? ( - - ) : !showForm && ( -
-
- - - -
-

Kein Verifikationsplan vorhanden

-

- Definieren Sie Verifikationsschritte fuer Ihre Schutzmassnahmen. - Jede Massnahme sollte durch mindestens eine Verifikation abgedeckt sein. -

-
- {mitigations.length > 0 && ( -