feat(iace/mitigations): is_relevant + is_customer_standard flags

[migration-approved]

Expert-driven workflow refinement on the Massnahmen page. The engine seeds
~80 mitigations per project, but for a concrete customer site most need a
relevance decision before they're meaningful in verification:

  status: 'planned' | 'implemented' | 'verified'   (existing — verification track)
  is_relevant          bool   (new)                (does this apply to *this* site?)
  is_customer_standard bool   (new)                (already in place at customer — no evidence)

Decision flow on the Mitigations tab:
  Engine-seeded → is_relevant=false (Default, waiting for expert)
  Expert checks "Relevant" → is_relevant=true → surfaces in verification
  Expert clicks trash       → DELETE (banner warns: do not click Reinit
                                       afterwards or seeds come back)
  In verification, customer_standard=true bypasses evidence upload

is_customer_standard implies is_relevant (DB CHECK constraint).

Migration 029_iace_mitigation_relevance.sql:
  ALTER TABLE iace_mitigations ADD COLUMN is_relevant ..., is_customer_standard ...
  + CHECK constraint + partial index on is_relevant for the verification
    page's filter.

Backend (Go):
  - Mitigation struct gains two bool fields
  - CreateMitigation: defaults to false/false (engine-seeded mitigations
    start unbewertet)
  - UpdateMitigation: new case clauses for both keys; setting
    is_customer_standard=true auto-flips is_relevant=true to satisfy
    the CHECK constraint
  - All three SELECT statements (ListMitigations, ListMitigationsByProject,
    getMitigation) extended with the two new columns

Frontend:
  - Maßnahmen-page columns: [Relev. ☑] [Lösch. 🗑] Title | #Hazards | P·I·V
  - Group-header checkbox shows tri-state (indeterminate when partial),
    flips all instances in the group at once
  - Banner above the table: "Markiere jede Maßnahme als Relevant oder
    lösche sie. Nach Löschen kein Neu initialisieren mehr drücken."
  - Relevant rows tinted emerald, customer-standard label visible
  - Legacy bulk-select state + helpers removed (the Relevant checkbox
    now IS the primary mass action)
  - useMitigations gains handleSetRelevant, handleSetCustomerStandard,
    handleDeleteSilent (for non-confirm bulk deletes)

Future use: is_customer_standard mitigations from a prior project at the
same customer can later be auto-suggested when commissioning the next
plant — turning expert knowledge into reusable customer-profile data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-17 14:35:56 +02:00
parent df7d83134b
commit 8f4f59f0e3
6 changed files with 191 additions and 96 deletions
@@ -45,6 +45,8 @@ export function useMitigations(projectId: string) {
created_at: (m.created_at || '') as string,
verified_at: (m.verified_at || null) as string | null,
verified_by: (m.verified_by || null) as string | null,
is_relevant: Boolean(m.is_relevant),
is_customer_standard: Boolean(m.is_customer_standard),
operational_states: (() => {
const ids = m.linked_hazard_ids ? (m.linked_hazard_ids as string[]) : m.hazard_id ? [m.hazard_id as string] : []
const states = new Set<string>()
@@ -151,6 +153,48 @@ export function useMitigations(projectId: string) {
}
}
// Bulk delete without per-row confirm; caller owns the confirm-step.
async function handleDeleteSilent(id: string) {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${id}`, { method: 'DELETE' })
if (!res.ok) console.error('delete failed for', id, res.status)
} catch (err) {
console.error('Failed to delete mitigation:', err)
}
}
// Flag a mitigation as relevant for this project (or unflag). Optimistic:
// updates local state immediately, refetches afterwards.
async function handleSetRelevant(id: string, value: boolean) {
setMitigations((prev) => prev.map((m) => m.id === id ? { ...m, status: m.status } : m))
try {
await fetch(`/api/sdk/v1/iace/mitigations/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_relevant: value }),
})
await fetchData()
} catch (err) {
console.error('Failed to set relevant flag:', err)
}
}
// Mark a mitigation as "customer standard" — already implemented at the
// customer's site, no evidence required. Implies is_relevant=true (server
// enforces this via the CHECK constraint).
async function handleSetCustomerStandard(id: string, value: boolean) {
try {
await fetch(`/api/sdk/v1/iace/mitigations/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_customer_standard: value }),
})
await fetchData()
} catch (err) {
console.error('Failed to set customer-standard flag:', err)
}
}
const byType = {
design: mitigations.filter((m) => m.reduction_type === 'design'),
protection: mitigations.filter((m) => m.reduction_type === 'protection'),
@@ -159,7 +203,8 @@ export function useMitigations(projectId: string) {
return {
mitigations, hazards, loading, hierarchyWarning, setHierarchyWarning,
measures, byType,
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
measures, byType, fetchData,
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify,
handleDelete, handleDeleteSilent, handleSetRelevant, handleSetCustomerStandard,
}
}