Åtgärdsfas med kvalitetskontroll — loopen sluts

Ärendet slutade tidigare vid rekommenderad åtgärd. Nu dokumenteras hela
kedjan: symptomverifiering → felorsak → åtgärd → kvalitetskontroll.

- Ny händelse atgard_utford: vad som gjordes (med delar) — eller varför
  ingen åtgärd utfördes, med orsak ur regelpaketet (kunden avböjde,
  väntar på reservdel, endast utredning beställd …)
- Ny händelse kvalitetskontroll: symptomet borta / kvarstår / delvis /
  kunde inte verifieras, med beskrivning av hur verifieringen gjordes —
  under samma förhållanden som symptomet en gång reproducerades
- Kvalitetsgrinden: "Åtgärd dokumenterad eller motiverad" obligatorisk
  vid avslut, "Kvalitetskontroll genomförd" obligatorisk när en åtgärd
  faktiskt utförts. Kvarstående symptom flaggas — ärendet bör inte
  avslutas som åtgärdat
- Avslutsknappen spärrad tills kedjan är komplett; rapporten har eget
  avsnitt "Utförd åtgärd och verifiering" i beviskedjan
- Demoärendet bär hela kedjan (ombalansering → verifierad provkörning)

65 vitest-tester, integrationstest och OpenAPI-validering gröna;
klicktestet bekräftar att avslut öppnas först när kedjan är hel.

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 10:15:54 +00:00
parent 786e2159f5
commit 636df0559d
8 changed files with 435 additions and 5 deletions
+1
View File
@@ -38,6 +38,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
| Pre-diagnostik | ✅ Ingen felsökning förrän grundkontrollerna är gjorda eller motiverade: fordonshistorik — tidigare ärenden på samma fordon hämtas automatiskt med sina felorsaker (server i inloggat läge, lokala storen annars) och orsakskedjan kopplas med ett tryck; Ja/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. | | Pre-diagnostik | ✅ Ingen felsökning förrän grundkontrollerna är gjorda eller motiverade: fordonshistorik — tidigare ärenden på samma fordon hämtas automatiskt med sina felorsaker (server i inloggat läge, lokala storen annars) och orsakskedjan kopplas med ett tryck; Ja/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). | | 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. | | 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. |
| Åtgärd och kvalitetskontroll | ✅ Arbetsflödets sista led: åtgärden dokumenteras (vad som gjordes + delar) eller motiveras varför den uteblev (kunden avböjde, väntar på reservdel …). Har en åtgärd utförts krävs **kvalitetskontroll** — symptomet borta / kvarstår / delvis / kunde inte verifieras, med beskrivning av hur det verifierades under samma förhållanden som symptomet reproducerades. Kvarstående symptom flaggas i grinden i stället för att döljas. Avslutsknappen är spärrad tills hela kedjan symptomverifiering → felorsak → åtgärd → kvalitetskontroll är komplett; rapporten har ett eget avsnitt ”Utförd åtgärd och verifiering”. |
| Ä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. | | Ä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. | | 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. | | Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. Utskriften går genom ECM-kvalitetsgrinden. |
+21
View File
@@ -76,6 +76,8 @@ alla obligatoriska rader är gröna:
| Fordonshistorik kontrollerad eller motiverad | Obligatorisk | | Fordonshistorik kontrollerad eller motiverad | Obligatorisk |
| Ingående mätarställning dokumenterad | Obligatorisk | | Ingående mätarställning dokumenterad | Obligatorisk |
| Kundens felbeskrivning verifierad | Rekommenderas | | Kundens felbeskrivning verifierad | Rekommenderas |
| Åtgärd dokumenterad eller motiverad | Obligatorisk vid avslut |
| Kvalitetskontroll genomförd | Obligatorisk vid avslut efter utförd åtgärd |
| Utgående mätarställning | Obligatorisk vid avslut | | Utgående mätarställning | Obligatorisk vid avslut |
| Metodikens kontroller: evidens eller dokumenterat undantag | Obligatorisk | | Metodikens kontroller: evidens eller dokumenterat undantag | Obligatorisk |
| Foton för fotokrävande kontroller | Obligatorisk | | Foton för fotokrävande kontroller | Obligatorisk |
@@ -159,6 +161,25 @@ Flottdatan är redan igång: **felorsaksstatistiken** i arbetsledarvyn
organisationen — vilka komponenter fallerar av slitage, vilka efter organisationen — vilka komponenter fallerar av slitage, vilka efter
tidigare reparationer, vilka tyder på konstruktionsproblem. tidigare reparationer, vilka tyder på konstruktionsproblem.
## Åtgärdsfasen (Repair & Verification)
Loopen som symptomverifieringen öppnade sluts här — ett ärende kan inte
avslutas utan att det framgår vad som gjordes och om det hjälpte:
1. **Åtgärd dokumenterad eller motiverad** — antingen vad som faktiskt
utfördes (med eventuella delar), eller varför ingen åtgärd gjordes
(kunden avböjde, väntar på reservdel, endast utredning beställd,
kostnadsförslag lämnat, åtgärd hos annan verkstad).
2. **Kvalitetskontroll** — obligatorisk när en åtgärd faktiskt utförts:
är symptomet borta, kvarstår det helt eller delvis, eller kunde det
inte verifieras? Utfallet dokumenteras med hur verifieringen gick
till (samma förhållanden som symptomet reproducerades under).
Kvarstående symptom döljs aldrig: grinden skriver ut att ärendet inte
bör avslutas som åtgärdat. Avslutsknappen är spärrad tills kedjan
**symptomverifiering → felorsaksanalys → åtgärd → kvalitetskontroll** är
komplett, och rapporten redovisar den i egna avsnitt.
## Ärendeidentitet (Case Identity & Vehicle Context) ## Ärendeidentitet (Case Identity & Vehicle Context)
Fordonsobjektet är den röda tråden: identiteten registreras **en gång** Fordonsobjektet är den röda tråden: identiteten registreras **en gång**
+2
View File
@@ -654,6 +654,8 @@ components:
- historik_kontrollerad - historik_kontrollerad
- matarstallning - matarstallning
- video - video
- atgard_utford
- kvalitetskontroll
- reproducering - reproducering
- felorsak - felorsak
- export_skapad - export_skapad
+82
View File
@@ -363,3 +363,85 @@ describe("Videoevidens (E3) och signerat avslut", () => {
expect(rad?.detalj).toContain("Erik"); expect(rad?.detalj).toContain("Erik");
}); });
}); });
describe("Åtgärdsfasen och kvalitetskontroll", () => {
it("avslut kräver dokumenterad åtgärd — och kvalitetskontroll när åtgärd utförts", async () => {
const { atgarder, kvalitetskontroll } = await import("../ecm");
const bas: Handelse[] = [
OBJEKT,
...PREDIAG,
{ typ: "reproducering", status: "ja", beskrivning: "Reproducerad vid provkörning." },
{
typ: "felorsak",
avvikelse: "Framhjulen uppvisar obalans om 38 g på höger sida.",
orsaker: ["Yttre påverkan"],
underlag: ["Direkt observation"],
sakerhet: "hog",
atgard: "Balansera om hjulet.",
},
{ typ: "observation", text: "Obalans uppmätt" },
];
// Utan åtgärd: grinden kräver den vid avslut.
const utanAtgard = byggArende([...bas, { typ: "arende_avslutat", signatur: "Erik" }]);
const rad = kvalitetsgrind(utanAtgard, VIBRATION_METODIK).find((r) => r.id === "atgard");
expect(rad?.kravs).toBe(true);
expect(rad?.ok).toBe(false);
// Utförd åtgärd utan kvalitetskontroll: kontrollen blir obligatorisk.
const utanKk = byggArende([
...bas,
{ typ: "atgard_utford", beskrivning: "Balanserade om höger framhjul.", utford: true },
{ typ: "arende_avslutat", signatur: "Erik" },
]);
expect(atgarder(utanKk)).toHaveLength(1);
const kkRad = kvalitetsgrind(utanKk, VIBRATION_METODIK).find((r) => r.id === "kvalitetskontroll");
expect(kkRad?.kravs).toBe(true);
expect(kkRad?.ok).toBe(false);
// Komplett kedja: åtgärd + verifierat borta.
const komplett = byggArende([
...bas,
{ typ: "atgard_utford", beskrivning: "Balanserade om höger framhjul.", utford: true },
{ typ: "kvalitetskontroll", resultat: "symptomet_borta", beskrivning: "Ny provkörning utan vibration." },
{ typ: "arende_avslutat", signatur: "Erik" },
]);
expect(kvalitetskontroll(komplett)?.resultat).toBe("symptomet_borta");
const rader = kvalitetsgrind(komplett, VIBRATION_METODIK);
expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true);
expect(rader.find((r) => r.id === "kvalitetskontroll")?.detalj).toContain("verifierat borta");
});
it("motiverat uteblivande godtas utan kvalitetskontroll", () => {
const ingenAtgard = byggArende([
OBJEKT,
...PREDIAG,
{ typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: "Kunden avböjde åtgärd" },
{ typ: "arende_avslutat", signatur: "Erik" },
]);
const rader = kvalitetsgrind(ingenAtgard, VIBRATION_METODIK);
expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true);
expect(rader.find((r) => r.id === "atgard")?.detalj).toContain("orsaken är dokumenterad");
// Inget att verifiera → kvalitetskontrollen är inte obligatorisk.
expect(rader.find((r) => r.id === "kvalitetskontroll")?.kravs).toBe(false);
});
it("kvarstående symptom flaggas i grinden i stället för att döljas", () => {
const kvarstar = byggArende([
OBJEKT,
...PREDIAG,
{ typ: "atgard_utford", beskrivning: "Bytte hjullager höger fram.", utford: true },
{ typ: "kvalitetskontroll", resultat: "kvarstar", beskrivning: "Missljudet finns kvar vid 60 km/h." },
]);
const rad = kvalitetsgrind(kvarstar, VIBRATION_METODIK).find((r) => r.id === "kvalitetskontroll");
expect(rad?.detalj).toContain("bör inte avslutas som åtgärdat");
});
it("demoärendet bär hela kedjan symptom → orsak → åtgärd → verifiering", () => {
const demo = byggDemoArende(1);
const typer = demo.handelser.map((p) => p.handelse.typ);
for (const typ of ["reproducering", "felorsak", "atgard_utford", "kvalitetskontroll"]) {
expect(typer, typ).toContain(typ);
}
});
});
+11
View File
@@ -130,6 +130,17 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
nastaSteg: "Balansera om höger framhjul och genomför ny provkörning i 80100 km/h.", nastaSteg: "Balansera om höger framhjul och genomför ny provkörning i 80100 km/h.",
modell: "demo", modell: "demo",
}), }),
post(87, {
typ: "atgard_utford",
beskrivning: "Balanserade om höger framhjul och monterade ny balansvikt på fälgens insida.",
delar: "Balansvikter 40 g",
utford: true,
}),
post(88, {
typ: "kvalitetskontroll",
resultat: "symptomet_borta",
beskrivning: "Ny provkörning 80110 km/h på samma vägsträcka — ingen vibration i ratten.",
}),
post(90, { typ: "overlamning", fran: anvandare, till: "Johan" }), post(90, { typ: "overlamning", fran: anvandare, till: "Johan" }),
post(92, { typ: "kategori_byte", kategori: "administration" }, "Johan"), post(92, { typ: "kategori_byte", kategori: "administration" }, "Johan"),
post(95, { typ: "kommentar", text: "Balanserat om höger framhjul, ny provkörning planerad." }, "Johan"), post(95, { typ: "kommentar", text: "Balanserat om höger framhjul, ny provkörning planerad." }, "Johan"),
+34
View File
@@ -29,6 +29,17 @@ export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
paus: "Paus", paus: "Paus",
}; };
// Kvalitetskontrollens utfall efter utförd åtgärd.
export const KVALITETSKONTROLL_LABEL: Record<
"symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar",
string
> = {
symptomet_borta: "Symptomet är borta",
kvarstar: "Symptomet kvarstår",
delvis: "Symptomet delvis kvar",
ej_verifierbar: "Kunde inte verifieras",
};
// Fordonsobjektet är den röda tråden genom hela ärendet: identiteten // Fordonsobjektet är den röda tråden genom hela ärendet: identiteten
// registreras en gång och återanvänds i felsökning, Live Share, rapport // registreras en gång och återanvänds i felsökning, Live Share, rapport
// och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet // och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet
@@ -101,6 +112,23 @@ export type Handelse =
motivering?: string; motivering?: string;
ytterligareKontroller?: string; ytterligareKontroller?: string;
} }
// Åtgärdsfasen: vad som faktiskt gjordes — eller varför ingen åtgärd
// utfördes (kunden avböjde, väntar på delar …). Kopplas till den
// felorsak åtgärden svarar mot.
| {
typ: "atgard_utford";
beskrivning: string;
delar?: string;
utford: boolean;
motivering?: string;
}
// Kvalitetskontroll efter åtgärd: är symptomet borta? Slutar loopen
// som symptomverifieringen (SVP) öppnade.
| {
typ: "kvalitetskontroll";
resultat: "symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar";
beskrivning: string;
}
| { typ: "export_skapad"; format: string; version: number } | { typ: "export_skapad"; format: string; version: number }
| { | {
typ: "ai_svar"; typ: "ai_svar";
@@ -194,6 +222,12 @@ export function handelseRubrik(post: LoggPost): string {
: `Symptomet kunde inte reproduceras — ${h.beskrivning}`; : `Symptomet kunde inte reproduceras — ${h.beskrivning}`;
case "felorsak": case "felorsak":
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse}${h.orsaker.join(", ")}`; return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse}${h.orsaker.join(", ")}`;
case "atgard_utford":
return h.utford
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
: `Ingen åtgärd utförd — ${h.motivering ?? "orsak saknas"}`;
case "kvalitetskontroll":
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]}${h.beskrivning}`;
case "export_skapad": case "export_skapad":
return `Export skapad: ${h.format}, version ${h.version}`; return `Export skapad: ${h.format}, version ${h.version}`;
case "ai_svar": { case "ai_svar": {
+61
View File
@@ -196,6 +196,31 @@ export function felorsaker(arende: Arende) {
return arende.handelser.filter((p) => p.handelse.typ === "felorsak"); return arende.handelser.filter((p) => p.handelse.typ === "felorsak");
} }
// -- Åtgärdsfasen (Repair & Verification) --
// Loopen som symptomverifieringen öppnade sluts här: åtgärden
// dokumenteras (eller motiveras varför den uteblev) och kvalitets-
// kontrollen visar om symptomet faktiskt är borta.
export const INGEN_ATGARD_ORSAKER = [
"Kunden avböjde åtgärd",
"Väntar på reservdel",
"Åtgärd utförs av annan verkstad",
"Endast utredning beställd",
"Kostnadsförslag lämnat, inväntar besked",
];
export function atgarder(arende: Arende) {
return arende.handelser.filter((p) => p.handelse.typ === "atgard_utford");
}
export function kvalitetskontroll(arende: Arende) {
let senaste: { resultat: "symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar"; beskrivning: string } | undefined;
for (const post of arende.handelser) {
if (post.handelse.typ === "kvalitetskontroll") senaste = post.handelse;
}
return senaste;
}
// -- Symptom Verification Protocol (SVP) -- // -- Symptom Verification Protocol (SVP) --
// Kundens beskrivning → förtydligande (metodikens symptomfrågor) → // Kundens beskrivning → förtydligande (metodikens symptomfrågor) →
// reproducering (eller dokumenterat ej reproducerbar). Rapporten skiljer // reproducering (eller dokumenterat ej reproducerbar). Rapporten skiljer
@@ -526,6 +551,42 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
: "Beskriv varför felet uppstått — eller ange varför orsaken inte kunnat fastställas.", : "Beskriv varför felet uppstått — eller ange varför orsaken inte kunnat fastställas.",
}); });
// Åtgärdsfasen: reparationen dokumenterad — eller motiverat varför
// ingen åtgärd utfördes. Obligatoriskt innan ärendet avslutas.
const atgardsposter = atgarder(arende);
const utfordAtgard = atgardsposter.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford);
rader.push({
id: "atgard",
rubrik: "Åtgärd dokumenterad eller motiverad",
ok: atgardsposter.length > 0,
kravs: avslutat,
detalj:
atgardsposter.length === 0
? "Dokumentera vad som gjordes — eller varför ingen åtgärd utfördes."
: utfordAtgard
? undefined
: "Ingen åtgärd utförd; orsaken är dokumenterad.",
});
// Kvalitetskontroll: är symptomet borta? Krävs när en åtgärd faktiskt
// utförts — annars finns inget att verifiera.
const kk = kvalitetskontroll(arende);
rader.push({
id: "kvalitetskontroll",
rubrik: "Kvalitetskontroll genomförd — symptomet verifierat",
ok: !!kk,
kravs: avslutat && utfordAtgard,
detalj: kk
? kk.resultat === "symptomet_borta"
? "Symptomet är verifierat borta efter åtgärd."
: kk.resultat === "ej_verifierbar"
? `Kunde inte verifieras: ${kk.beskrivning}`
: `Symptomet kvarstår helt eller delvis — ärendet bör inte avslutas som åtgärdat.`
: utfordAtgard
? "Verifiera att symptomet är borta efter åtgärden (provkörning/återtest)."
: "Ingen åtgärd utförd — inget att verifiera.",
});
// Utgående mätarställning: obligatorisk först när ärendet avslutas. // Utgående mätarställning: obligatorisk först när ärendet avslutas.
const matUt = handelser.some((h) => h.typ === "matarstallning" && h.lage === "utgaende"); const matUt = handelser.some((h) => h.typ === "matarstallning" && h.lage === "utgaende");
rader.push({ rader.push({
+223 -5
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import type { Arende, Handelse, TidKategori, Tillforlitlighet } from "@/felsokning/domain"; import type { Arende, Handelse, TidKategori, Tillforlitlighet } from "@/felsokning/domain";
import { TIDKATEGORI_LABEL, TILLFORLITLIGHET_LABEL, handelseRubrik } from "@/felsokning/domain"; import { KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLITLIGHET_LABEL, handelseRubrik } from "@/felsokning/domain";
import type { Metodik, NastaSteg } from "@/felsokning/metodik"; import type { Metodik, NastaSteg } from "@/felsokning/metodik";
import { nastaSteg } from "@/felsokning/metodik"; import { nastaSteg } from "@/felsokning/metodik";
import { import {
@@ -44,14 +44,17 @@ import {
ECM_VERSION, ECM_VERSION,
MARKOR_FELBESKRIVNING_VERIFIERAD, MARKOR_FELBESKRIVNING_VERIFIERAD,
MARKOR_INGA_TIDIGA_OBSERVATIONER, MARKOR_INGA_TIDIGA_OBSERVATIONER,
INGEN_ATGARD_ORSAKER,
MARKOR_TIDIGA_OBSERVATIONER_KLARA, MARKOR_TIDIGA_OBSERVATIONER_KLARA,
aktivtRegelpaket, aktivtRegelpaket,
atgarder,
arendetyp, arendetyp,
felorsaker, felorsaker,
granskaAvvikelse, granskaAvvikelse,
grindGodkand, grindGodkand,
kvalitetsgrind, kvalitetsgrind,
hamtaRegelpaket, hamtaRegelpaket,
kvalitetskontroll,
preDiagnostik, preDiagnostik,
reproducering, reproducering,
reproduceringsText, reproduceringsText,
@@ -702,6 +705,182 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
); );
} }
// Åtgärdsfasen: reparationen dokumenteras (eller motiveras varför den
// uteblev) och kvalitetskontrollen sluter loopen som symptom-
// verifieringen öppnade — är symptomet faktiskt borta?
function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
const poster = atgarder(arende);
const utford = poster.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford);
const kk = kvalitetskontroll(arende);
const repro = reproducering(arende);
const [lage, setLage] = useState<"" | "utford" | "ingen">("");
const [beskrivning, setBeskrivning] = useState("");
const [delar, setDelar] = useState("");
const [orsak, setOrsak] = useState("");
const [kkResultat, setKkResultat] = useState<"" | "symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar">("");
const [kkText, setKkText] = useState("");
return (
<Panel rubrik="Åtgärd och kvalitetskontroll">
{poster.map((p) => {
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
return (
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
{h.utford ? (
<>
<span className="font-semibold">Utförd åtgärd:</span> {h.beskrivning}
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
</>
) : (
<span className="text-[#9A6700]">Ingen åtgärd utförd {h.motivering}</span>
)}
</p>
);
})}
{kk && (
<p className={`py-1 text-[13px] font-semibold ${kk.resultat === "symptomet_borta" ? "text-[#1E6B34]" : "text-[#9A6700]"}`}>
Kvalitetskontroll: {KVALITETSKONTROLL_LABEL[kk.resultat]} <span className="font-normal">{kk.beskrivning}</span>
</p>
)}
{poster.length === 0 && !lage && (
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setLage("utford")}>
Dokumentera utförd åtgärd
</StorKnapp>
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
Ingen åtgärd utförd
</StorKnapp>
</div>
)}
{lage === "utford" && (
<div className="mt-2">
<TextFalt
label="Vad gjordes? (kopplat till den dokumenterade felorsaken)"
varde={beskrivning}
satt={setBeskrivning}
flerRad
rost
platshallare="T.ex. Balanserade om höger framhjul, ny balansvikt 40 g på insidan."
/>
<TextFalt label="Delar (valfritt)" varde={delar} satt={setDelar} platshallare="T.ex. Balansvikter, artikelnr 30-1234" rost />
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
Avbryt
</StorKnapp>
<StorKnapp
disabled={beskrivning.trim().length < 10}
onClick={() => {
skicka({ typ: "atgard_utford", beskrivning: beskrivning.trim(), delar: delar.trim() || undefined, utford: true });
setLage("");
setBeskrivning("");
setDelar("");
}}
>
Spara åtgärd
</StorKnapp>
</div>
</div>
)}
{lage === "ingen" && (
<div className="mt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
Orsak till att ingen åtgärd utfördes (obligatorisk)
</p>
<div className="mb-2 grid gap-1">
{INGEN_ATGARD_ORSAKER.map((val) => (
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
{val}
</button>
))}
</div>
<TextFalt label="Eller egen orsak" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
Avbryt
</StorKnapp>
<StorKnapp
disabled={!orsak.trim()}
onClick={() => {
skicka({ typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: orsak.trim() });
setLage("");
setOrsak("");
}}
>
Dokumentera
</StorKnapp>
</div>
</div>
)}
{utford && !kk && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
Kvalitetskontroll är symptomet borta efter åtgärden?
</p>
{repro?.status === "ja" && (
<p className="mb-2 text-[12px] text-[#707070]">
Symptomet reproducerades vid undersökningen återskapa samma förhållanden för att verifiera.
</p>
)}
<div className="mb-2 grid grid-cols-2 gap-2">
{(["symptomet_borta", "kvarstar", "delvis", "ej_verifierbar"] as const).map((val) => (
<button
key={val}
onClick={() => setKkResultat(val)}
className={`min-h-9 rounded border px-2 text-[12px] font-semibold ${
kkResultat === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
{KVALITETSKONTROLL_LABEL[val]}
</button>
))}
</div>
{kkResultat && (
<>
<TextFalt
label={
kkResultat === "symptomet_borta"
? "Hur verifierades det? (förhållanden, hastighet, provkörning …)"
: kkResultat === "ej_verifierbar"
? "Varför kunde det inte verifieras? (obligatorisk)"
: "Vad kvarstår? (obligatorisk)"
}
varde={kkText}
satt={setKkText}
flerRad
rost
platshallare="T.ex. Ny provkörning 80110 km/h på samma vägsträcka — ingen vibration."
/>
<StorKnapp
disabled={!kkText.trim()}
onClick={() => {
skicka({ typ: "kvalitetskontroll", resultat: kkResultat, beskrivning: kkText.trim() });
setKkResultat("");
setKkText("");
}}
>
Spara kvalitetskontroll
</StorKnapp>
</>
)}
</div>
)}
</Panel>
);
}
// Navigationsträd (vänsterkolumnen): ärendets vyer plus metodikens steg // Navigationsträd (vänsterkolumnen): ärendets vyer plus metodikens steg
// med status — som en mappstruktur i ett klassiskt verkstadssystem. // med status — som en mappstruktur i ett klassiskt verkstadssystem.
function VyTrad({ function VyTrad({
@@ -895,8 +1074,16 @@ function GuideFlik({
const senasteAiSvar = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar"); const senasteAiSvar = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar");
// Avslut kräver SVP (reproducering dokumenterad) + felorsaksanalys. // Avslut kräver hela kedjan: symptomverifiering, felorsaksanalys,
const kanAvslutas = !!reproducering(arende) && felorsaker(arende).length > 0; // dokumenterad åtgärd (eller motiverat uteblivande) och — när en
// åtgärd faktiskt utförts — kvalitetskontroll av att symptomet är borta.
const atgardsposter = atgarder(arende);
const utfordAtgard = atgardsposter.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford);
const kanAvslutas =
!!reproducering(arende) &&
felorsaker(arende).length > 0 &&
atgardsposter.length > 0 &&
(!utfordAtgard || !!kvalitetskontroll(arende));
if (avslutat) { if (avslutat) {
return ( return (
@@ -936,7 +1123,7 @@ function GuideFlik({
</StorKnapp> </StorKnapp>
{!kanAvslutas && ( {!kanAvslutas && (
<p className="mt-2 text-[12px] font-semibold text-[#9A6700]"> <p className="mt-2 text-[12px] font-semibold text-[#9A6700]">
Avslut kräver dokumenterad symptomverifiering och felorsaksanalys se panelerna nedan. Avslut kräver symptomverifiering, felorsaksanalys, åtgärd och kvalitetskontroll se panelerna nedan.
</p> </p>
)} )}
</> </>
@@ -986,6 +1173,10 @@ function GuideFlik({
säkerhetsnivå. Obligatorisk för avslut. */} säkerhetsnivå. Obligatorisk för avslut. */}
<FelorsaksPanel arende={arende} skicka={skicka} /> <FelorsaksPanel arende={arende} skicka={skicka} />
{/* Åtgärdsfasen: vad som gjordes — och kvalitetskontrollen som
verifierar att symptomet faktiskt är borta. */}
{felorsaker(arende).length > 0 && <AtgardsPanel arende={arende} skicka={skicka} />}
{/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när {/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när
ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */} ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */}
{!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && ( {!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && (
@@ -996,7 +1187,8 @@ function GuideFlik({
{!kanAvslutas && ( {!kanAvslutas && (
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]"> <p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
Avslut kräver dokumenterad symptomverifiering och felorsaksanalys annars är slutsatsen inte spårbar. Avslut kräver hela kedjan: symptomverifiering, felorsaksanalys, dokumenterad åtgärd och vid utförd åtgärd
kvalitetskontroll av att symptomet är borta.
</p> </p>
)} )}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -1811,6 +2003,8 @@ function RapportFlik({
const typ = arendetyp(arende); const typ = arendetyp(arende);
const repro = reproducering(arende); const repro = reproducering(arende);
const orsakerLista = felorsaker(arende); const orsakerLista = felorsaker(arende);
const atgardsRader = atgarder(arende);
const kkRad = kvalitetskontroll(arende);
let matIn: string | undefined; let matIn: string | undefined;
let matUt: string | undefined; let matUt: string | undefined;
for (const p of arende.handelser) { for (const p of arende.handelser) {
@@ -1967,6 +2161,30 @@ function RapportFlik({
); );
})} })}
</Panel> </Panel>
<Panel rubrik="Utförd åtgärd och verifiering">
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4A5560]">Ingen åtgärd dokumenterad ännu.</p>}
{atgardsRader.map((p) => {
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
return (
<div key={p.id} className="mb-2">
{h.utford ? (
identitetsRader([
["Utförd åtgärd", h.beskrivning],
["Delar", h.delar],
])
) : (
<p className="text-[14px] text-[#9A6700]">Ingen åtgärd utförd {h.motivering}</p>
)}
</div>
);
})}
{kkRad &&
identitetsRader([
["Kvalitetskontroll", KVALITETSKONTROLL_LABEL[kkRad.resultat]],
["Verifiering", kkRad.beskrivning],
])}
</Panel>
<Panel rubrik="Sammanfattning"> <Panel rubrik="Sammanfattning">
<p className="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 && ( {fordelning.length > 0 && (