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:
Claude
2026-08-03 09:26:29 +00:00
parent bfed5dbd7f
commit ddc11c6ac2
11 changed files with 553 additions and 6 deletions
+65
View File
@@ -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);
});
});
+14
View File
@@ -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 80100 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",
+25
View File
@@ -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": {
+119
View File
@@ -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({
+12
View File
@@ -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" },
],
+269 -6
View File
@@ -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 92108 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) => (