Felorsaksanalys och symptomverifiering (SVP) som obligatoriska grindar
Skillnaden mellan VAD som är fel och VARFÖR felet uppstått är nu kodad:
ett ärende kan aldrig avslutas med enbart "komponent defekt, byt
komponent", och en kundbeskrivning blir aldrig ett konstaterat fel utan
verifiering.
Symptom Verification Protocol:
- Ny händelse reproducering (ja/delvis/nej): ja kräver hur/förhållanden,
delvis vad som kunde respektive inte kunde återskapas, nej kräver
motivering
- Generiska metodiken utökad med SVP-frågorna var/hur
- Rapportens beviskedja skiljer kundens beskrivning, verifierad
observation, felorsaksanalys och rekommenderad åtgärd; "kunde inte
reproduceras under de förhållanden som rådde" i stället för "felet
konstaterat" — kodat även i orkesterns grundprompt
Felorsaksanalys (Root Cause Analysis):
- Ny händelse felorsak: avvikelse + orsakskategorier + underlag +
säkerhetsnivå + rekommenderad åtgärd
- Kvalitetsregeln avvisar generella formuleringar ("trasig", "defekt",
"sliten", "behöver bytas") utan förklaring
- Valda evidenskällor valideras mot loggen — "Foto" godtas bara om ett
foto faktiskt finns
- Okänd orsak kräver motivering; medel/låg säkerhet kräver vilka
ytterligare kontroller som stärker bedömningen
- Avslutsknappen spärrad tills SVP + felorsak dokumenterats;
kvalitetsgrinden gör båda obligatoriska vid stängning
Demoärendet bär hela beviskedjan; 54 vitest-tester, integrationstest
och OpenAPI-validering gröna.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -571,6 +571,8 @@ components:
|
||||
- arendetyp_satt
|
||||
- historik_kontrollerad
|
||||
- matarstallning
|
||||
- reproducering
|
||||
- felorsak
|
||||
- export_skapad
|
||||
- ai_svar
|
||||
- arende_avslutat
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
|
||||
@@ -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 (
|
||||
<Panel rubrik="Symptomverifiering — reproducering">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Har kundens fel kunnat reproduceras?
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{(["ja", "delvis", "nej"] as const).map((val) => (
|
||||
<StorKnapp key={val} variant={status === val ? "primar" : "sekundar"} onClick={() => setStatus(val)}>
|
||||
{val === "ja" ? "Ja" : val === "delvis" ? "Delvis" : "Nej"}
|
||||
</StorKnapp>
|
||||
))}
|
||||
</div>
|
||||
{status && (
|
||||
<>
|
||||
<TextFalt label={etikett} varde={text} satt={setText} flerRad rost
|
||||
platshallare={status === "ja" ? "T.ex. Reproducerad tre gånger vid 92–108 km/h på plan väg, varm motor" : ""} />
|
||||
<StorKnapp
|
||||
disabled={!text.trim()}
|
||||
onClick={() => {
|
||||
skicka({ typ: "reproducering", status, beskrivning: text.trim() });
|
||||
setStatus("");
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
Dokumentera reproducering
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// 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<string[]>([]);
|
||||
const [underlag, setUnderlag] = useState<string[]>([]);
|
||||
const [sakerhet, setSakerhet] = useState<Tillforlitlighet>("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 (
|
||||
<Panel rubrik="Felorsaksanalys — obligatorisk före avslut">
|
||||
{dokumenterade.map((p) => {
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "felorsak") return null;
|
||||
return (
|
||||
<div key={p.id} className="mb-2 border-b border-[#EBEBEB] pb-2 text-[13px] last:border-0">
|
||||
<p className="font-semibold">{h.avvikelse}</p>
|
||||
<p className="text-[#4A5560]">
|
||||
Orsak: {h.orsaker.join(", ")} · Underlag: {h.underlag.join(", ")} · Säkerhet: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
</p>
|
||||
<p>Åtgärd: {h.atgard}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!oppen ? (
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(true)}>
|
||||
+ Dokumentera felorsak
|
||||
</StorKnapp>
|
||||
) : (
|
||||
<div>
|
||||
<TextFalt
|
||||
label="1. Konstaterad avvikelse (inte bara komponenten)"
|
||||
varde={avvikelse}
|
||||
satt={setAvvikelse}
|
||||
flerRad
|
||||
rost
|
||||
platshallare="T.ex. Startmotorn aktiverar inte trots korrekt matningsspänning och god jordförbindelse."
|
||||
/>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">2. Mest sannolik orsak (en eller flera)</p>
|
||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
|
||||
{ORSAKSKATEGORIER.map((o) => (
|
||||
<button
|
||||
key={o}
|
||||
onClick={() => vaxla(orsaker, setOrsaker, o)}
|
||||
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
|
||||
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
|
||||
}`}
|
||||
>
|
||||
{o}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Underlag som stöder bedömningen</p>
|
||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
|
||||
{UNDERLAGSKALLOR.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => vaxla(underlag, setUnderlag, k)}
|
||||
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
|
||||
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
|
||||
}`}
|
||||
>
|
||||
{k}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Säkerhet i bedömningen</p>
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
{(["hog", "medel", "lag"] as const).map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setSakerhet(n)}
|
||||
className={`min-h-9 rounded border text-[12px] font-semibold ${
|
||||
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
||||
}`}
|
||||
>
|
||||
{TILLFORLITLIGHET_LABEL[n]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{sakerhet !== "hog" && (
|
||||
<TextFalt
|
||||
label="Vilka ytterligare kontroller skulle stärka bedömningen?"
|
||||
varde={ytterligare}
|
||||
satt={setYtterligare}
|
||||
rost
|
||||
/>
|
||||
)}
|
||||
{orsaker.includes("Okänd orsak") && (
|
||||
<TextFalt
|
||||
label="Motivering — varför kunde orsaken inte fastställas?"
|
||||
varde={motivering}
|
||||
satt={setMotivering}
|
||||
flerRad
|
||||
rost
|
||||
/>
|
||||
)}
|
||||
<TextFalt label="Rekommenderad åtgärd" varde={atgard} satt={setAtgard} rost platshallare="T.ex. Balansera framhjulen och genomför ny provkörning." />
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Avbryt
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={spara}>Spara felorsak</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Panel rubrik="Felsökningen är avslutad">
|
||||
@@ -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.
|
||||
</p>
|
||||
<StorKnapp variant="fara" onClick={() => skicka({ typ: "arende_avslutat" })}>
|
||||
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat" })}>
|
||||
Avsluta felsökning
|
||||
</StorKnapp>
|
||||
{!kanAvslutas && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
Avslut kräver dokumenterad symptomverifiering och felorsaksanalys — se panelerna nedan.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : steg.fraga ? (
|
||||
<FrageKort key={`${steg.steg.id}/${steg.fraga.id}`} steg={steg} skicka={skicka} />
|
||||
@@ -683,6 +896,15 @@ function GuideFlik({
|
||||
|
||||
<SnabbDokumentation skicka={skicka} paSparad={fragaAiOmDokumentation} />
|
||||
|
||||
{/* SVP: symptomet ska reproduceras — eller dokumenteras som ej
|
||||
reproducerbart med motivering — innan ärendet kan avslutas. */}
|
||||
{!reproducering(arende) && <ReproduceringPanel skicka={skicka} />}
|
||||
|
||||
{/* Felorsaksanalys: aldrig bara "komponent trasig, byt komponent" —
|
||||
varje konstaterat fel kräver avvikelse, orsak, underlag och
|
||||
säkerhetsnivå. Obligatorisk för avslut. */}
|
||||
<FelorsaksPanel arende={arende} skicka={skicka} />
|
||||
|
||||
{/* 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({
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{!kanAvslutas && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
Avslut kräver dokumenterad symptomverifiering och felorsaksanalys — annars är slutsatsen inte spårbar.
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||
Lämna över arbete
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "arende_avslutat" })}>
|
||||
<StorKnapp variant="sekundar" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat" })}>
|
||||
Avsluta felsökning
|
||||
</StorKnapp>
|
||||
</div>
|
||||
@@ -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],
|
||||
])}
|
||||
</Panel>
|
||||
{/* Beviskedjan: kundens upplevelse, teknikerns verifiering och den
|
||||
tekniska slutsatsen hålls strikt åtskilda och spårbara. */}
|
||||
<Panel rubrik="Kundens beskrivning">
|
||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||
</Panel>
|
||||
<Panel rubrik="Verifierad observation">
|
||||
{repro ? (
|
||||
<>
|
||||
<p className="text-[14px] font-semibold">{reproduceringsText(repro.status)}</p>
|
||||
<p className="text-[13px] text-[#4A5560]">{repro.beskrivning}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[14px] text-[#4A5560]">Reproducering ej dokumenterad ännu.</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Felorsaksanalys">
|
||||
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4A5560]">Felorsaksanalys ej dokumenterad ännu.</p>}
|
||||
{orsakerLista.map((p) => {
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "felorsak") return null;
|
||||
return (
|
||||
<div key={p.id} className="mb-3 border-b border-[#EBEBEB] pb-2 last:mb-0 last:border-0">
|
||||
<p className="text-[14px] font-semibold">{h.avvikelse}</p>
|
||||
{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],
|
||||
])}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Panel>
|
||||
<Panel rubrik="Sammanfattning">
|
||||
{b.felbeskrivning && <p className="text-[14px]">Felbeskrivning: ”{b.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-[14px]">Total arbetstid: <span className="font-semibold">{b.totalArbetstid}</span></p>
|
||||
<p className="text-[14px]">Total arbetstid: <span className="font-semibold">{b.totalArbetstid}</span></p>
|
||||
{fordelning.length > 0 && (
|
||||
<div className="mt-2">
|
||||
{fordelning.map((r) => (
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user