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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user