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:
@@ -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) => (
|
||||
|
||||
Reference in New Issue
Block a user