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:
@@ -160,8 +160,17 @@ type Mitigation struct {
|
||||
VerificationResult string `json:"verification_result,omitempty"`
|
||||
VerifiedAt *time.Time `json:"verified_at,omitempty"`
|
||||
VerifiedBy uuid.UUID `json:"verified_by,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// IsRelevant marks the mitigation as applicable for this concrete project.
|
||||
// Engine-suggested mitigations start with IsRelevant = false; the expert
|
||||
// flips it to true (or deletes the mitigation) when walking through the
|
||||
// Massnahmen tab. Only relevant mitigations surface in verification.
|
||||
IsRelevant bool `json:"is_relevant"`
|
||||
// IsCustomerStandard means the customer site already has this mitigation
|
||||
// implemented as company-wide standard, so no evidence upload is needed.
|
||||
// Implies IsRelevant = true (DB CHECK constraint).
|
||||
IsCustomerStandard bool `json:"is_customer_standard"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Evidence represents an uploaded file that serves as evidence for compliance
|
||||
|
||||
@@ -31,17 +31,20 @@ func (s *Store) CreateMitigation(ctx context.Context, req CreateMitigationReques
|
||||
id, hazard_id, reduction_type, name, description,
|
||||
status, verification_method, verification_result,
|
||||
verified_at, verified_by,
|
||||
is_relevant, is_customer_standard,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8,
|
||||
$9, $10,
|
||||
$11, $12
|
||||
$11, $12,
|
||||
$13, $14
|
||||
)
|
||||
`,
|
||||
m.ID, m.HazardID, string(m.ReductionType), m.Name, m.Description,
|
||||
string(m.Status), "", "",
|
||||
nil, uuid.Nil,
|
||||
m.IsRelevant, m.IsCustomerStandard,
|
||||
m.CreatedAt, m.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -79,6 +82,23 @@ func (s *Store) UpdateMitigation(ctx context.Context, id uuid.UUID, updates map[
|
||||
query += fmt.Sprintf(", verification_method = $%d", argIdx)
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
case "is_relevant":
|
||||
query += fmt.Sprintf(", is_relevant = $%d", argIdx)
|
||||
args = append(args, val)
|
||||
argIdx++
|
||||
case "is_customer_standard":
|
||||
// CHECK constraint requires is_relevant=true when this is true,
|
||||
// so we flip is_relevant on as well when the caller sets the
|
||||
// customer-standard flag.
|
||||
b, _ := val.(bool)
|
||||
query += fmt.Sprintf(", is_customer_standard = $%d", argIdx)
|
||||
args = append(args, b)
|
||||
argIdx++
|
||||
if b {
|
||||
query += fmt.Sprintf(", is_relevant = $%d", argIdx)
|
||||
args = append(args, true)
|
||||
argIdx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +143,7 @@ func (s *Store) ListMitigations(ctx context.Context, hazardID uuid.UUID) ([]Miti
|
||||
id, hazard_id, reduction_type, name, description,
|
||||
status, verification_method, verification_result,
|
||||
verified_at, verified_by,
|
||||
is_relevant, is_customer_standard,
|
||||
created_at, updated_at
|
||||
FROM iace_mitigations WHERE hazard_id = $1
|
||||
ORDER BY created_at ASC
|
||||
@@ -141,6 +162,7 @@ func (s *Store) ListMitigations(ctx context.Context, hazardID uuid.UUID) ([]Miti
|
||||
&m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
|
||||
&status, &verificationMethod, &m.VerificationResult,
|
||||
&m.VerifiedAt, &m.VerifiedBy,
|
||||
&m.IsRelevant, &m.IsCustomerStandard,
|
||||
&m.CreatedAt, &m.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -164,6 +186,7 @@ func (s *Store) ListMitigationsByProject(ctx context.Context, projectID uuid.UUI
|
||||
m.id, m.hazard_id, m.reduction_type, m.name, m.description,
|
||||
m.status, m.verification_method, m.verification_result,
|
||||
m.verified_at, m.verified_by,
|
||||
m.is_relevant, m.is_customer_standard,
|
||||
m.created_at, m.updated_at
|
||||
FROM iace_mitigations m
|
||||
JOIN iace_hazards h ON h.id = m.hazard_id
|
||||
@@ -184,6 +207,7 @@ func (s *Store) ListMitigationsByProject(ctx context.Context, projectID uuid.UUI
|
||||
&m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
|
||||
&status, &verificationMethod, &m.VerificationResult,
|
||||
&m.VerifiedAt, &m.VerifiedBy,
|
||||
&m.IsRelevant, &m.IsCustomerStandard,
|
||||
&m.CreatedAt, &m.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -224,12 +248,14 @@ func (s *Store) getMitigation(ctx context.Context, id uuid.UUID) (*Mitigation, e
|
||||
id, hazard_id, reduction_type, name, description,
|
||||
status, verification_method, verification_result,
|
||||
verified_at, verified_by,
|
||||
is_relevant, is_customer_standard,
|
||||
created_at, updated_at
|
||||
FROM iace_mitigations WHERE id = $1
|
||||
`, id).Scan(
|
||||
&m.ID, &m.HazardID, &reductionType, &m.Name, &m.Description,
|
||||
&status, &verificationMethod, &m.VerificationResult,
|
||||
&m.VerifiedAt, &m.VerifiedBy,
|
||||
&m.IsRelevant, &m.IsCustomerStandard,
|
||||
&m.CreatedAt, &m.UpdatedAt,
|
||||
)
|
||||
if err == pgx.ErrNoRows {
|
||||
|
||||
Reference in New Issue
Block a user