diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts new file mode 100644 index 00000000..616762b1 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { calculateAP } from './useFMEA' + +describe('calculateAP — AIAG-VDA 2019 Handbook Action Priority', () => { + it('returns H for severity 10 with mid occurrence', () => { + expect(calculateAP(10, 5, 5)).toBe('H') + }) + + it('returns H for severity 9 with low detection', () => { + expect(calculateAP(9, 4, 7)).toBe('H') + }) + + it('returns M for severity 9 with low occurrence and good detection', () => { + expect(calculateAP(9, 2, 5)).toBe('M') + }) + + it('returns L for severity 9 with very low occurrence and detection', () => { + expect(calculateAP(9, 1, 4)).toBe('L') + }) + + it('returns H for severity 7 with high occurrence', () => { + expect(calculateAP(7, 5, 1)).toBe('H') + }) + + it('returns M for severity 7 with mid occurrence', () => { + expect(calculateAP(7, 3, 5)).toBe('M') + }) + + it('returns L for low-severity well-controlled mode', () => { + expect(calculateAP(3, 1, 1)).toBe('L') + }) + + it('returns L for severity 5 with very low occurrence and detection', () => { + expect(calculateAP(5, 1, 1)).toBe('L') + }) +}) diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts index b95be21f..adb83e5d 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts @@ -156,5 +156,52 @@ export function useFMEA(projectId: string) { // Get unique components for the suggest button const components = [...new Map(rows.map((r) => [r.component.id, r.component])).values()] - return { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions } + /** + * Accept a suggested FM: build an FMEA row from the FM defaults, prepend it + * to the table state, and remove the FM from the suggestion list. + * Returns false if the (component, fm.id) combo already exists in rows. + */ + function acceptSuggestion(fm: FailureMode, componentId: string): boolean { + const comp = components.find((c) => c.id === componentId) + if (!comp) return false + const dup = rows.find((r) => r.component.id === componentId && r.failureMode.id === fm.id) + if (dup) { + // Still drop the suggestion so the UI does not keep offering it. + setSuggestions((prev) => prev.filter((s) => s.id !== fm.id)) + return false + } + const s = fm.default_severity || 5 + const o = fm.default_occurrence || 5 + const d = fm.default_detection || 5 + const newRow: FMEARow = { + component: comp, + failureMode: fm, + severity: s, + occurrence: o, + detection: d, + rpz: s * o * d, + ap: calculateAP(s, o, d), + } + setRows((prev) => [newRow, ...prev].sort((a, b) => b.rpz - a.rpz)) + setSuggestions((prev) => prev.filter((sg) => sg.id !== fm.id)) + return true + } + + function rejectSuggestion(fmId: string) { + setSuggestions((prev) => prev.filter((sg) => sg.id !== fmId)) + } + + return { + rows, + loading, + stats, + components, + suggestFMs, + suggesting, + suggestions, + suggestSource, + setSuggestions, + acceptSuggestion, + rejectSuggestion, + } } diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx index 44cb7310..d4f2fac7 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useParams } from 'next/navigation' import { useFMEA, type FMEARow } from './_hooks/useFMEA' @@ -27,8 +27,17 @@ function rpzLabel(rpz: number): string { export default function FMEAPage() { const { projectId } = useParams<{ projectId: string }>() - const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions } = useFMEA(projectId) + const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions, acceptSuggestion, rejectSuggestion } = useFMEA(projectId) const [suggestComp, setSuggestComp] = useState(null) + const [acceptedCount, setAcceptedCount] = useState(0) + + // Reset accepted-count when a fresh suggestion run is loaded or the panel closes. + useEffect(() => { + if (suggesting) setAcceptedCount(0) + }, [suggesting]) + useEffect(() => { + if (suggestions.length === 0) setAcceptedCount(0) + }, [suggestions.length]) if (loading) { return ( @@ -97,26 +106,60 @@ export default function FMEAPage() { {suggestions.length > 0 && (
-

- KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek'} -

+
+

+ KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek-Fallback'} +

+ {acceptedCount > 0 && ( +
+ {acceptedCount} Vorschlag{acceptedCount > 1 ? 'e' : ''} uebernommen +
+ )} +
- {suggestions.map((fm, i) => ( -
-
-
{fm.name_de}
-
{fm.effect}
-
- S={fm.default_severity} - O={fm.default_occurrence} - D={fm.default_detection} - RPZ={fm.default_severity * fm.default_occurrence * fm.default_detection} + {suggestions.map((fm) => { + const rpz = fm.default_severity * fm.default_occurrence * fm.default_detection + return ( +
+
+
{fm.name_de}
+
{fm.effect}
+
+ S={fm.default_severity} + O={fm.default_occurrence} + D={fm.default_detection} + 200 ? 'text-red-600' : rpz > 100 ? 'text-orange-600' : 'text-gray-500'}`}>RPZ={rpz} +
+
+
+ +
-
- ))} + ) + })} +
+
+ Hinweis: Uebernommene Fehlermodi erscheinen sofort in der Tabelle unten. Bewertung (S/O/D) ist anpassbar — Standardwerte aus der Bibliothek.
)}