diff --git a/docs/MVP.md b/docs/MVP.md index f863ee9..7058f5b 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -36,6 +36,8 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st | Inställningar | ✅ Systemadministratören väljer vilka objekttyper och identifieringsmetoder som visas när ett ärende startas (`/felsokning/installningar`). På plattformen gäller valet hela organisationen (sparas på organisationen, endast admin får ändra — verifierat i integrationstestet); i lokalt läge gäller valet enheten. Okända värden filtreras och tomma listor faller tillbaka till standard. | | Evidensmotor (ECM) | ✅ Eget subsystem med sex motorer ([moduler/evidensmotor.md](moduler/evidensmotor.md), `src/felsokning/ecm.ts`, **ECM v2.0**): Evidence (evidensposter med nivå E0–E6, tekniker och innehållshash), Rule (dokumentationskrav + undantagsregeln med obligatorisk orsak), Compliance (ärendetypen — garanti/försäkring/reklamation m.fl. — styr extra krav), Validation ("Evidens saknas" i stället för antaganden, kodat i orkesterns grundprompt), Completion (kvalitetsgrind som spärrar slutrapporten) och Traceability (spårbarhetspaket med regelversion + hash i varje export). | | Pre-diagnostik | ✅ Ingen felsökning förrän grundkontrollerna är gjorda eller motiverade: fordonshistorik (Ja med ev. orsakskedja / Nej med obligatorisk orsak → kvalitetsvarning), **ingående mätarställning** (foto av instrumentpanelen, bildtolkningen föreslår värdet), kundens felbeskrivning verifierad och tidiga observationer hanterade. Metodiken låses upp först därefter. **Utgående mätarställning** fotograferas inför avslut och blir obligatorisk i grinden när ärendet stängs. | +| Symptomverifiering (SVP) | ✅ Kundens beskrivning ≠ konstaterat fel: beskrivningen dokumenteras ordagrant, förtydligas via metodikens symptomfrågor (generiska metodiken har SVP-setet när/var/hur) och reproduceras — Ja (hur/förhållanden), Delvis (vad kunde/kunde inte) eller Nej (obligatorisk motivering). Rapportens beviskedja skiljer kundens beskrivning, verifierad observation, felorsaksanalys och rekommenderad åtgärd; formuleringen "kunde inte reproduceras under de förhållanden som rådde" används i stället för "felet konstaterat" (kodat även i orkesterns grundprompt). | +| Felorsaksanalys | ✅ Obligatorisk före avslut: konstaterad avvikelse (kvalitetsregeln avvisar "trasig/defekt/sliten" utan förklaring), orsakskategorier (inkl. Okänd orsak med krav på motivering), minst en evidenskälla som valideras mot loggen, säkerhetsnivå (medel/låg kräver stärkande kontroller) och rekommenderad åtgärd. Avslutsknappen spärrad tills SVP + felorsak finns; kvalitetsgrinden gör båda obligatoriska vid stängning. Eget avsnitt i slutrapporten. | | Ärendeidentitet | ✅ Fordonsobjektet som röd tråd: identiteten (AO-nummer, claim-/garantinummer, skadenummer, regnr, VIN, miltal, kund) registreras en gång — normalt via arbetsorderskanningen — och återanvänds i identitetsraden i arbetsytan (med ärendetypsval), låst panel överst i Live Share, slutrapportens första sida (Ärendeinformation + Fordonsinformation) och exporten. | | Instrumentavläsning (visual-first) | ✅ Kameran som universellt gränssnitt: `📷 Instrument` i Dokumentera-panelen fotograferar multimetrar, diagnosskärmar, batteritestare m.m. — bildtolkningen identifierar instrumenttyp och extraherar värden/enheter/felkoder med konfidens per värde; teknikern bekräftar innan något loggas. Originalbilden loggas alltid tillsammans med de strukturerade mätvärdena — strukturerad data ersätter aldrig originalevidensen. Ingen integration mot diagnossystem krävs. | | Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. Utskriften går genom ECM-kvalitetsgrinden. | diff --git a/docs/moduler/evidensmotor.md b/docs/moduler/evidensmotor.md index e30920c..2d6e90c 100644 --- a/docs/moduler/evidensmotor.md +++ b/docs/moduler/evidensmotor.md @@ -104,6 +104,49 @@ dokumenterat motiverade — metodiken låses upp först därefter: **Utgående mätarställning** fotograferas inför avslut och blir obligatorisk i grinden när ärendet stängs. Rapporten visar in/ut. +## Symptom Verification Protocol (SVP) + +Ett fel diagnostiseras aldrig direkt från en vag kundbeskrivning. +Kedjan är alltid: **dokumenterats → förtydligats → reproducerats eller +dokumenterats som ej reproducerbart.** + +- Kundens beskrivning registreras ordagrant vid ärendestart och + verifieras i pre-diagnostiken; nya symptom blir separata observationer. +- Förtydligandet sker genom metodikens symptomfrågor (när/var/hur/ + förhållanden/frekvens — generiska metodiken har hela SVP-frågesetet). +- **Reproducering** (Ja/Delvis/Nej) dokumenteras innan avslut: Ja kräver + hur och under vilka förhållanden; Delvis vad som kunde respektive inte + kunde återskapas; Nej kräver motivering. Systemet skriver aldrig + "felet konstaterat" utan reproducering eller annan verifiering — i + stället: *"Kundens beskrivning kunde inte reproduceras under de + förhållanden som rådde vid undersökningen."* (kodat även i orkesterns + grundprompt). +- Rapportens beviskedja skiljer alltid: kundens beskrivning → + verifierad observation → felorsaksanalys → rekommenderad åtgärd. + +## Felorsaksanalys (Root Cause Analysis) + +Ett ärende avslutas aldrig med enbart "komponent defekt, byt komponent". +Varje konstaterat fel kräver fyra obligatoriska svar: + +1. **Konstaterad avvikelse** — kvalitetsregeln avvisar generella + formuleringar ("trasig", "defekt", "sliten", "behöver bytas") utan + förklaring. +2. **Mest sannolik orsak** — en eller flera kategorier (normalt slitage, + materialutmattning, tillverkningsfel, bristande underhåll, felaktig + tidigare reparation, yttre påverkan, korrosion, överhettning, + modifiering … samt *Okänd orsak*, som kräver motivering). +3. **Underlag** — minst en evidenskälla, och källan valideras mot + loggen: "Foto" godtas bara om ett foto faktiskt finns. +4. **Säkerhetsnivå** — hög/medel/låg; vid medel/låg krävs vilka + ytterligare kontroller som skulle stärka bedömningen. + +Avslutsknappen är spärrad tills SVP + felorsaksanalys är dokumenterade, +och kvalitetsgrinden gör båda obligatoriska när ärendet stängs. Efter +tusentals ärenden ger orsakskategorierna dessutom flottdata: vilka +komponenter fallerar av slitage, vilka efter tidigare reparationer, +vilka tyder på konstruktionsproblem. + ## Ärendeidentitet (Case Identity & Vehicle Context) Fordonsobjektet är den röda tråden: identiteten registreras **en gång** diff --git a/services/ai-orkester/server.mjs b/services/ai-orkester/server.mjs index e6a6049..141009f 100644 --- a/services/ai-orkester/server.mjs +++ b/services/ai-orkester/server.mjs @@ -33,6 +33,7 @@ Absoluta regler: - "rekommendation": nästa verifierbara kontroll eller mätning. - Om underlaget är otillräckligt: säg det uttryckligen i en observation och rekommendera vad som behöver dokumenteras. - Skriv aldrig "OK", "kontrollerad", "inga fel" eller "åtgärdad" om evidens saknas i underlaget — skriv i stället "Evidens saknas" och begär rätt underlag: foto för det synliga, video med ljud för det som låter, video för det som rör sig, mätvärde för det som mäts, foto av skärmen när ett instrument eller en diagnosdator visar informationen. +- Skriv aldrig "felet konstaterat" om symptomet varken reproducerats eller verifierats med dokumentation — skriv då "Kundens beskrivning kunde inte reproduceras under de förhållanden som rådde vid undersökningen." - Svara på svenska, konsekvent, kortfattat och metodiskt. "nastaSteg" är EN konkret, verifierbar åtgärd.`; const SVARS_SCHEMA = { diff --git a/services/plattform/openapi.yaml b/services/plattform/openapi.yaml index ab67b41..06a240e 100644 --- a/services/plattform/openapi.yaml +++ b/services/plattform/openapi.yaml @@ -571,6 +571,8 @@ components: - arendetyp_satt - historik_kontrollerad - matarstallning + - reproducering + - felorsak - export_skapad - ai_svar - arende_avslutat diff --git a/src/felsokning/__tests__/ecm.test.ts b/src/felsokning/__tests__/ecm.test.ts index d2ac8d7..11f6020 100644 --- a/src/felsokning/__tests__/ecm.test.ts +++ b/src/felsokning/__tests__/ecm.test.ts @@ -189,3 +189,68 @@ describe("Traceability Engine + ärendeidentitet", () => { expect(evidensNiva(demo)).toBe("E6"); }); }); + +describe("Felorsaksanalys (Root Cause Analysis)", () => { + it("kvalitetsregeln avvisar generella formuleringar utan förklaring", async () => { + const { granskaAvvikelse } = await import("../ecm"); + for (const daligt of ["Trasig.", "defekt", "Sliten", "Behöver bytas", "kort"]) { + expect(granskaAvvikelse(daligt), daligt).not.toBeNull(); + } + expect( + granskaAvvikelse("Startmotorn aktiverar inte trots korrekt matningsspänning och god jordförbindelse."), + ).toBeNull(); + }); + + it("valda evidenskällor måste finnas i loggen", async () => { + const { underlagFinns } = await import("../ecm"); + const utanFoto = byggArende([OBJEKT, { typ: "matvarde", beskrivning: "U", varde: "12" }]); + expect(underlagFinns(utanFoto, "Foto")).toBe(false); + expect(underlagFinns(utanFoto, "Mätresultat")).toBe(true); + expect(underlagFinns(utanFoto, "Servicehistorik")).toBe(false); + const medHistorik = byggArende([{ typ: "historik_kontrollerad", kontrollerad: true }]); + expect(underlagFinns(medHistorik, "Servicehistorik")).toBe(true); + }); + + it("avslut kräver reproducering och felorsak — grinden blir obligatorisk vid stängning", () => { + const stangdUtan = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat" }]); + const rader = kvalitetsgrind(stangdUtan, VIBRATION_METODIK); + expect(rader.find((r) => r.id === "svp")?.kravs).toBe(true); + expect(rader.find((r) => r.id === "svp")?.ok).toBe(false); + expect(rader.find((r) => r.id === "felorsak")?.ok).toBe(false); + + const stangdMed = byggArende([ + OBJEKT, + ...PREDIAG, + { typ: "reproducering", status: "nej", beskrivning: "Felet uppträdde inte vid provkörning; fordonet var varmt." }, + { + typ: "felorsak", + avvikelse: "Ingen avvikelse kunde konstateras vid undersökningen trots systematisk genomgång.", + orsaker: ["Okänd orsak"], + underlag: ["Direkt observation"], + sakerhet: "lag", + atgard: "Återkom vid kall motor så att symptomet kan reproduceras.", + motivering: "Symptomet kunde inte reproduceras under rådande förhållanden.", + }, + { typ: "observation", text: "Systematisk genomgång utan anmärkning" }, + { typ: "arende_avslutat" }, + ]); + const rader2 = kvalitetsgrind(stangdMed, VIBRATION_METODIK); + expect(rader2.find((r) => r.id === "svp")?.ok).toBe(true); + expect(rader2.find((r) => r.id === "svp")?.detalj).toContain("kunde inte reproduceras"); + expect(rader2.find((r) => r.id === "felorsak")?.ok).toBe(true); + }); + + it("rapportformuleringen skiljer reproducerat från ej reproducerat", async () => { + const { reproduceringsText } = await import("../ecm"); + expect(reproduceringsText("ja")).toContain("reproducerades"); + expect(reproduceringsText("nej")).toContain("kunde inte reproduceras under de förhållanden"); + }); + + it("demoärendet bär hela beviskedjan: reproducering + felorsak med underlag", () => { + const demo = byggDemoArende(1); + const repro = demo.handelser.find((p) => p.handelse.typ === "reproducering"); + expect(repro).toBeDefined(); + const orsak = demo.handelser.find((p) => p.handelse.typ === "felorsak"); + expect(orsak?.handelse.typ === "felorsak" && orsak.handelse.underlag.length > 0).toBe(true); + }); +}); diff --git a/src/felsokning/demo.ts b/src/felsokning/demo.ts index 36c9625..c37da03 100644 --- a/src/felsokning/demo.ts +++ b/src/felsokning/demo.ts @@ -105,6 +105,20 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende { post(82, { typ: "matvarde", beskrivning: "Obalans höger framhjul", varde: "38", enhet: "g" }), post(83, { typ: "kontroll_utford", stegId: "kontroller", kontrollId: "balansering", text: "Kontrollera hjulbalansering", resultat: "38 g obalans höger fram — en balansvikt saknas. Övriga hjul inom 5 g." }), post(85, { typ: "observation", text: "Spår efter lossnad klistervikt på höger framfälgs insida" }), + post(85, { + typ: "reproducering", + status: "ja", + beskrivning: "Reproducerad vid provkörning på plan väg: vibrationen återskapades tre gånger mellan 92 och 108 km/h, tydligast i ratten.", + }), + post(86, { + typ: "felorsak", + avvikelse: "Framhjulen uppvisar obalans (38 g höger fram). Ingen deformation av fälgar eller däck kunde konstateras.", + orsaker: ["Yttre påverkan"], + underlag: ["Mätresultat", "Foto", "Direkt observation"], + sakerhet: "medel", + atgard: "Balansera framhjulen och genomför ny provkörning i 80–100 km/h.", + ytterligareKontroller: "Ny provkörning efter balansering bekräftar att symptomet är åtgärdat.", + }), // Exempel på AI-handledning, märkt "demo" — riktiga svar kräver API-nyckel. post(86, { typ: "ai_svar", diff --git a/src/felsokning/domain.ts b/src/felsokning/domain.ts index 82ba0c1..1f41e84 100644 --- a/src/felsokning/domain.ts +++ b/src/felsokning/domain.ts @@ -80,6 +80,23 @@ export type Handelse = | { typ: "historik_kontrollerad"; kontrollerad: boolean; kommentar?: string } // Officiell mätarställning in/ut, normalt med foto av instrumentpanelen. | { typ: "matarstallning"; lage: "ingaende" | "utgaende"; varde: string; dataUrl?: string; undantag?: string } + // Symptomverifiering (SVP): kundens beskrivning är inte ett konstaterat + // fel förrän den reproducerats — eller dokumenterats som ej + // reproducerbar med motivering. + | { typ: "reproducering"; status: "ja" | "delvis" | "nej"; beskrivning: string } + // Felorsaksanalys: varje konstaterat fel kräver avvikelse, bedömd + // grundorsak, underlag och säkerhetsnivå — eller motivering till + // varför orsaken inte kunnat fastställas. + | { + typ: "felorsak"; + avvikelse: string; + orsaker: string[]; + underlag: string[]; + sakerhet: Tillforlitlighet; + atgard: string; + motivering?: string; + ytterligareKontroller?: string; + } | { typ: "export_skapad"; format: string; version: number } | { typ: "ai_svar"; @@ -162,6 +179,14 @@ export function handelseRubrik(post: LoggPost): string { case "matarstallning": if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`; return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`; + case "reproducering": + return h.status === "ja" + ? `Symptomet reproducerat: ${h.beskrivning}` + : h.status === "delvis" + ? `Symptomet delvis reproducerat: ${h.beskrivning}` + : `Symptomet kunde inte reproduceras — ${h.beskrivning}`; + case "felorsak": + return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse} — ${h.orsaker.join(", ")}`; case "export_skapad": return `Export skapad: ${h.format}, version ${h.version}`; case "ai_svar": { diff --git a/src/felsokning/ecm.ts b/src/felsokning/ecm.ts index df34c43..cc6505b 100644 --- a/src/felsokning/ecm.ts +++ b/src/felsokning/ecm.ts @@ -121,6 +121,98 @@ export const MARKOR_FELBESKRIVNING_VERIFIERAD = "Kundens felbeskrivning verifier export const MARKOR_INGA_TIDIGA_OBSERVATIONER = "Inga ytterligare observationer vid mottagandet"; export const MARKOR_TIDIGA_OBSERVATIONER_KLARA = "Tidiga observationer vid mottagandet dokumenterade"; +// -- Felorsaksanalys (Root Cause Analysis) -- +// Ett ärende avslutas aldrig med enbart "komponent trasig, byt komponent": +// varje konstaterat fel kräver avvikelse, bedömd grundorsak, underlag och +// säkerhetsnivå — eller en motivering till varför orsaken inte fastställts. + +export const ORSAKSKATEGORIER = [ + "Normalt slitage", + "Ålder", + "Körsträcka", + "Materialutmattning", + "Tillverkningsfel", + "Bristande underhåll", + "Felaktig tidigare reparation", + "Yttre påverkan", + "Korrosion", + "Överhettning", + "Förorening", + "Felaktig användning", + "Modifiering", + "Olycka eller skada", + "Okänd orsak", +]; + +export const UNDERLAGSKALLOR = [ + "Foto", + "Video", + "Mätresultat", + "Diagnosutläsning", + "Tidigare historik", + "Servicehistorik", + "Teknisk dokumentation", + "Direkt observation", +]; + +// Kvalitetsregeln: generella formuleringar utan förklaring avvisas. +const GENERISKA_FRASER = /^(trasig|defekt|sliten|utsliten|kass|död|behöver bytas|byt(es)?( ut)?|fungerar inte|går sönder|sönder)[.!]?$/i; + +export function granskaAvvikelse(text: string): string | null { + const ren = text.trim(); + if (ren.length < 20 || GENERISKA_FRASER.test(ren)) { + return "Felorsak saknas. Beskriv den konstaterade avvikelsen — inte bara komponenten. Exempel: ”Startmotorn aktiverar inte trots korrekt matningsspänning och god jordförbindelse.”"; + } + return null; +} + +// Underlaget måste finnas: en vald evidenskälla godtas bara om loggen +// faktiskt innehåller den sortens evidens. +export function underlagFinns(arende: Arende, kalla: string): boolean { + const h = arende.handelser.map((p) => p.handelse); + switch (kalla) { + case "Foto": + return h.some((x) => x.typ === "foto"); + case "Mätresultat": + return h.some((x) => x.typ === "matvarde"); + case "Diagnosutläsning": + return h.some((x) => x.typ === "foto" && /instrument|diagnos/i.test(x.beskrivning)); + case "Tidigare historik": + case "Servicehistorik": + return h.some((x) => x.typ === "historik_kontrollerad" && x.kontrollerad); + case "Direkt observation": + return h.some((x) => x.typ === "observation" || x.typ === "kontroll_utford"); + default: + // Video/teknisk dokumentation: kan ännu inte verifieras maskinellt. + return true; + } +} + +export function felorsaker(arende: Arende) { + return arende.handelser.filter((p) => p.handelse.typ === "felorsak"); +} + +// -- Symptom Verification Protocol (SVP) -- +// Kundens beskrivning → förtydligande (metodikens symptomfrågor) → +// reproducering (eller dokumenterat ej reproducerbar). Rapporten skiljer +// alltid kundens upplevelse från teknikerns verifierade observationer. + +export function reproducering(arende: Arende) { + let senaste: { status: "ja" | "delvis" | "nej"; beskrivning: string } | undefined; + for (const post of arende.handelser) { + if (post.handelse.typ === "reproducering") senaste = post.handelse; + } + return senaste; +} + +// Rapportformuleringen styrs av verifieringsläget — aldrig "felet +// konstaterat" utan reproducering eller annan dokumenterad verifiering. +export function reproduceringsText(status: "ja" | "delvis" | "nej"): string { + if (status === "ja") return "Symptomet reproducerades vid undersökningen."; + if (status === "delvis") return "Symptomet kunde delvis reproduceras vid undersökningen."; + return "Kundens beskrivning kunde inte reproduceras under de förhållanden som rådde vid undersökningen."; +} + // ---- 3. Compliance Engine --------------------------------------------- // Ärendetypen styr vilka dokumentationskrav som gäller utöver metodiken. @@ -315,6 +407,33 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { detalj: felb.klar ? undefined : "Bekräfta att kundens beskrivning är korrekt återgiven.", }); + // SVP: symptomet reproducerat eller dokumenterat ej reproducerbart — + // obligatoriskt innan ärendet avslutas. + const repro = reproducering(arende); + rader.push({ + id: "svp", + rubrik: "Symptomverifiering: reproducerat eller dokumenterat ej reproducerbart", + ok: !!repro, + kravs: avslutat, + detalj: repro + ? reproduceringsText(repro.status) + : "Dokumentera reproduceringen (Ja/Delvis/Nej med motivering) innan ärendet avslutas.", + }); + + // Felorsaksanalys: minst en dokumenterad felorsak (eller motiverad + // okänd orsak) krävs för att avsluta — aldrig bara komponent + åtgärd. + const orsaker = felorsaker(arende); + rader.push({ + id: "felorsak", + rubrik: "Felorsaksanalys dokumenterad", + ok: orsaker.length > 0, + kravs: avslutat, + detalj: + orsaker.length > 0 + ? undefined + : "Beskriv varför felet uppstått — eller ange varför orsaken inte kunnat fastställas.", + }); + // Utgående mätarställning: obligatorisk först när ärendet avslutas. const matUt = handelser.some((h) => h.typ === "matarstallning" && h.lage === "utgaende"); rader.push({ diff --git a/src/felsokning/metodik.ts b/src/felsokning/metodik.ts index 77c69a6..bbf2695 100644 --- a/src/felsokning/metodik.ts +++ b/src/felsokning/metodik.ts @@ -121,6 +121,18 @@ export const GENERISK_METODIK: Metodik = { svarstyp: "val", val: ["Alltid", "Ibland", "Endast under drift", "Endast vid start"], }, + { + id: "var", + text: "Var upplevs felet?", + svarstyp: "val", + val: ["Fram", "Bak", "Höger", "Vänster", "Motor/drivkälla", "Kupé/förarplats", "Okänt"], + }, + { + id: "hur", + text: "Hur upplevs felet?", + svarstyp: "val", + val: ["Skakar/vibrerar", "Missljud (knack/gnissel/vinande)", "Rycker", "Tappar kraft", "Stannar helt", "Annat"], + }, { id: "forlopp", text: "Beskriv förloppet när felet uppstår", svarstyp: "text" }, { id: "forandring", text: "Har något förändrats nyligen (service, reparation, miljö)?", svarstyp: "text" }, ], diff --git a/src/pages/felsokning/ArendeSida.tsx b/src/pages/felsokning/ArendeSida.tsx index 79776ee..e498c9c 100644 --- a/src/pages/felsokning/ArendeSida.tsx +++ b/src/pages/felsokning/ArendeSida.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; -import type { Arende, Handelse, TidKategori } from "@/felsokning/domain"; -import { TIDKATEGORI_LABEL, handelseRubrik } from "@/felsokning/domain"; +import type { Arende, Handelse, TidKategori, Tillforlitlighet } from "@/felsokning/domain"; +import { TIDKATEGORI_LABEL, TILLFORLITLIGHET_LABEL, handelseRubrik } from "@/felsokning/domain"; import type { Metodik, NastaSteg } from "@/felsokning/metodik"; import { nastaSteg } from "@/felsokning/metodik"; import { @@ -42,12 +42,19 @@ import { MARKOR_FELBESKRIVNING_VERIFIERAD, MARKOR_INGA_TIDIGA_OBSERVATIONER, MARKOR_TIDIGA_OBSERVATIONER_KLARA, + ORSAKSKATEGORIER, UNDANTAGSORSAKER, + UNDERLAGSKALLOR, arendetyp, + felorsaker, + granskaAvvikelse, grindGodkand, kvalitetsgrind, preDiagnostik, + reproducering, + reproduceringsText, sparbarhetspaket, + underlagFinns, } from "@/felsokning/ecm"; import { FelsokningSkal, NivaBadge, Panel, StorKnapp, TextFalt } from "@/felsokning/ui"; import { skalaNerFoto, tidKlockslag } from "@/felsokning/format"; @@ -417,6 +424,204 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha ); } +// Symptom Verification Protocol: kundens beskrivning blir aldrig ett +// konstaterat fel förrän den reproducerats — eller dokumenterats som ej +// reproducerbar med obligatorisk motivering. +function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) { + const [status, setStatus] = useState<"" | "ja" | "delvis" | "nej">(""); + const [text, setText] = useState(""); + const etikett = + status === "ja" + ? "Hur reproducerades felet? (förhållanden, hastighet, temperatur, belastning …)" + : status === "delvis" + ? "Vad kunde respektive kunde inte reproduceras?" + : "Motivering (obligatorisk) — varför kunde felet inte reproduceras?"; + return ( + +

+ Har kundens fel kunnat reproduceras? +

+
+ {(["ja", "delvis", "nej"] as const).map((val) => ( + setStatus(val)}> + {val === "ja" ? "Ja" : val === "delvis" ? "Delvis" : "Nej"} + + ))} +
+ {status && ( + <> + + { + skicka({ typ: "reproducering", status, beskrivning: text.trim() }); + setStatus(""); + setText(""); + }} + > + Dokumentera reproducering + + + )} +
+ ); +} + +// Felorsaksanalys (Root Cause Analysis): vad är avvikelsen, varför har +// den uppstått, vilket underlag stöder bedömningen och hur säker är den. +// Kvalitetsregeln avvisar generella formuleringar ("trasig", "defekt") +// och evidenskällor som inte finns i loggen. +function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) { + const dokumenterade = felorsaker(arende); + const [oppen, setOppen] = useState(false); + const [avvikelse, setAvvikelse] = useState(""); + const [orsaker, setOrsaker] = useState([]); + const [underlag, setUnderlag] = useState([]); + const [sakerhet, setSakerhet] = useState("hog"); + const [atgard, setAtgard] = useState(""); + const [motivering, setMotivering] = useState(""); + const [ytterligare, setYtterligare] = useState(""); + const [fel, setFel] = useState(""); + + const vaxla = (lista: string[], satt: (v: string[]) => void, val: string) => + satt(lista.includes(val) ? lista.filter((v) => v !== val) : [...lista, val]); + + const spara = () => { + const avvikelseFel = granskaAvvikelse(avvikelse); + if (avvikelseFel) return setFel(avvikelseFel); + if (orsaker.length === 0) return setFel("Välj minst en orsakskategori."); + if (underlag.length === 0) return setFel("Koppla minst en evidenskälla till bedömningen."); + const saknat = underlag.find((k) => !underlagFinns(arende, k)); + if (saknat) return setFel(`Underlaget ”${saknat}” finns inte i ärendets logg — dokumentera det först eller välj en annan källa.`); + if (orsaker.includes("Okänd orsak") && !motivering.trim()) + return setFel("Okänd orsak kräver en motivering till varför orsaken inte kunnat fastställas."); + if (sakerhet !== "hog" && !ytterligare.trim()) + return setFel("Vid medel/låg säkerhet: ange vilka ytterligare kontroller som skulle stärka bedömningen."); + if (!atgard.trim()) return setFel("Ange rekommenderad åtgärd."); + skicka({ + typ: "felorsak", + avvikelse: avvikelse.trim(), + orsaker, + underlag, + sakerhet, + atgard: atgard.trim(), + motivering: motivering.trim() || undefined, + ytterligareKontroller: ytterligare.trim() || undefined, + }); + setOppen(false); + setAvvikelse(""); + setOrsaker([]); + setUnderlag([]); + setSakerhet("hog"); + setAtgard(""); + setMotivering(""); + setYtterligare(""); + setFel(""); + }; + + return ( + + {dokumenterade.map((p) => { + const h = p.handelse; + if (h.typ !== "felorsak") return null; + return ( +
+

{h.avvikelse}

+

+ Orsak: {h.orsaker.join(", ")} · Underlag: {h.underlag.join(", ")} · Säkerhet: {TILLFORLITLIGHET_LABEL[h.sakerhet]} +

+

Åtgärd: {h.atgard}

+
+ ); + })} + {!oppen ? ( + setOppen(true)}> + + Dokumentera felorsak + + ) : ( +
+ +

2. Mest sannolik orsak (en eller flera)

+
+ {ORSAKSKATEGORIER.map((o) => ( + + ))} +
+

3. Underlag som stöder bedömningen

+
+ {UNDERLAGSKALLOR.map((k) => ( + + ))} +
+

4. Säkerhet i bedömningen

+
+ {(["hog", "medel", "lag"] as const).map((n) => ( + + ))} +
+ {sakerhet !== "hog" && ( + + )} + {orsaker.includes("Okänd orsak") && ( + + )} + + {fel &&

{fel}

} +
+ setOppen(false)}> + Avbryt + + Spara felorsak +
+
+ )} +
+ ); +} + // Navigationsträd (vänsterkolumnen): ärendets vyer plus metodikens steg // med status — som en mappstruktur i ett klassiskt verkstadssystem. function VyTrad({ @@ -609,6 +814,9 @@ function GuideFlik({ const senasteAiSvar = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar"); + // Avslut kräver SVP (reproducering dokumenterad) + felorsaksanalys. + const kanAvslutas = !!reproducering(arende) && felorsaker(arende).length > 0; + if (avslutat) { return ( @@ -642,9 +850,14 @@ function GuideFlik({ Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet med rekommenderade nästa steg.

- skicka({ typ: "arende_avslutat" })}> + skicka({ typ: "arende_avslutat" })}> Avsluta felsökning + {!kanAvslutas && ( +

+ Avslut kräver dokumenterad symptomverifiering och felorsaksanalys — se panelerna nedan. +

+ )} ) : steg.fraga ? ( @@ -683,6 +896,15 @@ function GuideFlik({ + {/* SVP: symptomet ska reproduceras — eller dokumenteras som ej + reproducerbart med motivering — innan ärendet kan avslutas. */} + {!reproducering(arende) && } + + {/* Felorsaksanalys: aldrig bara "komponent trasig, byt komponent" — + varje konstaterat fel kräver avvikelse, orsak, underlag och + säkerhetsnivå. Obligatorisk för avslut. */} + + {/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */} {!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && ( @@ -691,11 +913,16 @@ function GuideFlik({
)} + {!kanAvslutas && ( +

+ Avslut kräver dokumenterad symptomverifiering och felorsaksanalys — annars är slutsatsen inte spårbar. +

+ )}
setVisaOverlamning(true)}> Lämna över arbete - skicka({ typ: "arende_avslutat" })}> + skicka({ typ: "arende_avslutat" })}> Avsluta felsökning
@@ -1445,6 +1672,8 @@ function RapportFlik({ const fordelning = tidsfordelningsRader(arende, nu); const idn = arendeidentitet(arende); const typ = arendetyp(arende); + const repro = reproducering(arende); + const orsakerLista = felorsaker(arende); let matIn: string | undefined; let matUt: string | undefined; for (const p of arende.handelser) { @@ -1566,9 +1795,43 @@ function RapportFlik({ ["Kund", idn.kund], ])} + {/* Beviskedjan: kundens upplevelse, teknikerns verifiering och den + tekniska slutsatsen hålls strikt åtskilda och spårbara. */} + +

{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}

+
+ + {repro ? ( + <> +

{reproduceringsText(repro.status)}

+

{repro.beskrivning}

+ + ) : ( +

Reproducering ej dokumenterad ännu.

+ )} +
+ + {orsakerLista.length === 0 &&

Felorsaksanalys ej dokumenterad ännu.

} + {orsakerLista.map((p) => { + const h = p.handelse; + if (h.typ !== "felorsak") return null; + return ( +
+

{h.avvikelse}

+ {identitetsRader([ + ["Bedömd grundorsak", h.orsaker.join(", ")], + ["Underlag för bedömningen", h.underlag.join(", ")], + ["Säkerhetsnivå", TILLFORLITLIGHET_LABEL[h.sakerhet]], + ["Rekommenderad åtgärd", h.atgard], + ["Motivering", h.motivering], + ["Stärkande kontroller", h.ytterligareKontroller], + ])} +
+ ); + })} +
- {b.felbeskrivning &&

Felbeskrivning: ”{b.felbeskrivning}”

} -

Total arbetstid: {b.totalArbetstid}

+

Total arbetstid: {b.totalArbetstid}

{fordelning.length > 0 && (
{fordelning.map((r) => ( diff --git a/supabase/functions/felsokning-ai/index.ts b/supabase/functions/felsokning-ai/index.ts index 0a94193..cfc28af 100644 --- a/supabase/functions/felsokning-ai/index.ts +++ b/supabase/functions/felsokning-ai/index.ts @@ -32,6 +32,7 @@ Absoluta regler: - "rekommendation": nästa verifierbara kontroll eller mätning. - Om underlaget är otillräckligt: säg det uttryckligen i en observation och rekommendera vad som behöver dokumenteras. - Skriv aldrig "OK", "kontrollerad", "inga fel" eller "åtgärdad" om evidens saknas i underlaget — skriv i stället "Evidens saknas" och begär rätt underlag: foto för det synliga, video med ljud för det som låter, video för det som rör sig, mätvärde för det som mäts, foto av skärmen när ett instrument eller en diagnosdator visar informationen. +- Skriv aldrig "felet konstaterat" om symptomet varken reproducerats eller verifierats med dokumentation — skriv då "Kundens beskrivning kunde inte reproduceras under de förhållanden som rådde vid undersökningen." - Svara på svenska, konsekvent, kortfattat och metodiskt. "nastaSteg" är EN konkret, verifierbar åtgärd.`; const SVARS_SCHEMA = {