Files
alva/src/felsokning/domain.ts
T
Claude 5428684cca Kundgodkännande före arbete — förslag, besked och spärrat arbete
Verkstaden kan inte längre utföra föreslaget arbete utan spårbart besked
från kunden.

- Ny händelse atgardsforslag: föreslagen åtgärd (förifylld ur
  felorsaksanalysen) med uppskattad kostnad — kunddelbar och visad i
  Live Share, där kunden ser förslaget och sitt registrerade besked
- Ny händelse kundbeslut: godkänt/avböjt/delvis med obligatorisk kanal
  (telefon, på plats, e-post, SMS, delningslänk) och motivering vid
  avböjt/delvis; loggposten bär vem i verkstaden som tog emot beskedet
- "Dokumentera utförd åtgärd" är låst medan ett förslag saknar besked
  och förblir låst vid avböjt — vägen "Ingen åtgärd utförd" är öppen
  och hänvisar till beskedet
- Kvalitetsgrinden: besked obligatoriskt när arbete utförts, plus hård
  flagga "Utfört arbete trots avböjt åtgärdsförslag"
- Rapporten får avsnittet "Åtgärdsförslag och kundens besked";
  demoärendet visar hela kedjan förslag → godkännande → åtgärd

Avgränsning: kunden lämnar sitt besked via kontakt med verkstaden som
registrerar det. Publikt godkännande direkt i delningslänken kräver en
skrivande publik endpoint och hanteras separat.

69 vitest-tester, integrationstest, OpenAPI-validering och klicktest
(blockerat arbete utan/vid avböjt besked) gröna.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 10:34:44 +00:00

268 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Domänmodell för Guidad Felsökning.
// Händelseloggen är append-only och är ärendets enda sanningskälla.
// Allt annat (brief, tidrapport, kundrapport) är projektioner av loggen.
export type Tillforlitlighet = "hog" | "medel" | "lag";
export const TILLFORLITLIGHET_LABEL: Record<Tillforlitlighet, string> = {
hog: "Hög",
medel: "Medel",
lag: "Låg",
};
export type TidKategori =
| "aktiv_felsokning"
| "vantetid"
| "administration"
| "reservdelssokning"
| "provkorning"
| "kundkontakt"
| "paus";
export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
aktiv_felsokning: "Aktiv felsökning",
vantetid: "Väntetid",
administration: "Administration",
reservdelssokning: "Reservdelssökning",
provkorning: "Provkörning",
kundkontakt: "Kundkontakt",
paus: "Paus",
};
// Kundens besked på ett lämnat åtgärdsförslag.
export const KUNDBESLUT_LABEL: Record<"godkant" | "avbojt" | "delvis", string> = {
godkant: "Godkänt",
avbojt: "Avböjt",
delvis: "Delvis godkänt",
};
// 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
// registreras en gång och återanvänds i felsökning, Live Share, rapport
// och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet
// så att de följer med i alla vyer utan att kunduppgifter läcker.
export interface Objekt {
typ: string; // t.ex. "Personbil", "Industrimaskin"
identifierare: string; // reg.nr, VIN, serienummer …
identifieringsmetod: string; // "Regnr", "VIN", "Serienummer", "Manuell inmatning"
beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
kund?: string;
vin?: string;
miltal?: string;
arbetsorder?: string;
claim?: string;
skadenummer?: string;
}
// Ett fält tolkat ur en skannad arbetsorder, med AI:ns läs-säkerhet.
export interface ArbetsorderFalt {
id: string;
etikett: string;
varde: string;
konfidens: number; // 01
}
// Varje händelse är en av dessa typer. Ingen händelse ändras eller tas bort.
export type Handelse =
| { typ: "objekt_identifierat"; objekt: Objekt }
| { typ: "arbetsorder_skannad"; falt: ArbetsorderFalt[]; dataUrl?: string }
| { typ: "felbeskrivning"; text: string }
| { typ: "fraga_besvarad"; stegId: string; frageId: string; fraga: string; svar: string }
// undantag: underlaget kunde inte tas fram — obligatorisk orsak i stället
// för evidens. Kontrollen räknas som hanterad men flaggas i brief/rapport.
| { typ: "kontroll_utford"; stegId: string; kontrollId: string; text: string; resultat?: string; undantag?: string }
| { typ: "observation"; text: string }
| { typ: "matvarde"; beskrivning: string; varde: string; enhet?: string }
| { typ: "hypotes"; text: string; niva: Exclude<Tillforlitlighet, "hog"> }
| { typ: "foto"; beskrivning: string; dataUrl: string }
// Video med ljud (E3-evidens): för det som låter eller rör sig —
// motorljud, hjullager, fjädringsrörelse. Kort och komprimerad;
// originalfilen bevaras som evidens precis som foton.
| { typ: "video"; beskrivning: string; dataUrl: string }
| { typ: "kommentar"; text: string }
| { typ: "kategori_byte"; kategori: TidKategori }
| { typ: "inaktivitet_forklarad"; text: string; minuter: number }
| { typ: "overlamning"; fran: string; till?: string }
| { typ: "ansvarig_satt"; ansvarig: string }
// Ärendetypen styr vilka dokumentationskrav ECM ställer (garanti,
// försäkring, reklamation …).
| { typ: "arendetyp_satt"; arendetyp: string }
// Pre-diagnostik: fordonshistoriken kontrollerad — eller motiverat
// varför inte (kvalitetsvarning).
| { 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;
}
// Kundkommunikation: åtgärdsförslag lämnat till kund för godkännande
// innan arbetet påbörjas. Kunddelbart — visas i Live Share.
| {
typ: "atgardsforslag";
beskrivning: string;
uppskattadKostnad?: string;
uppskattadTid?: string;
}
// Kundens besked på förslaget, med kanal och tidpunkt (loggposten bär
// vem i verkstaden som tog emot beskedet).
| {
typ: "kundbeslut";
beslut: "godkant" | "avbojt" | "delvis";
kanal: string;
kommentar?: 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: "ai_svar";
rader: { typ: "observation" | "verifierat" | "hypotes" | "rekommendation"; text: string }[];
nastaSteg: string;
modell: string;
}
// Avslutet signeras av teknikern — slutsatsen får en ansvarig avsändare.
| { typ: "arende_avslutat"; signatur?: string };
export interface LoggPost {
id: string;
tidpunkt: string; // ISO 8601
anvandare: string;
handelse: Handelse;
}
export interface Arende {
id: string;
nummer: number;
skapad: string; // ISO 8601
// Slumpad kod för Live Share-länken; sätts vid skapande och kan aldrig
// ändras (ärenderaden är append-only även i databasen).
delningskod?: string;
// Explicit vald metodik (t.ex. via AI-klassificering vid skapandet).
// Saknas den härleds metodiken ur felbeskrivningen.
metodikId?: string;
handelser: LoggPost[];
}
let raknare = 0;
export function nyLoggPost(anvandare: string, handelse: Handelse, tidpunkt?: string): LoggPost {
raknare += 1;
return {
id: `${Date.now().toString(36)}-${raknare.toString(36)}`,
tidpunkt: tidpunkt ?? new Date().toISOString(),
anvandare,
handelse,
};
}
export function handelseRubrik(post: LoggPost): string {
const h = post.handelse;
switch (h.typ) {
case "objekt_identifierat":
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
case "arbetsorder_skannad":
return `Arbetsorder skannad — ${h.falt.length} fält tolkade`;
case "felbeskrivning":
return `Felbeskrivning registrerad`;
case "fraga_besvarad":
return `${h.fraga}${h.svar}`;
case "kontroll_utford":
if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`;
return h.resultat ? `${h.text}${h.resultat}` : `${h.text} — utförd`;
case "observation":
return `Observation: ${h.text}`;
case "matvarde":
return `Mätvärde: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`;
case "hypotes":
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
case "foto":
return `Foto: ${h.beskrivning}`;
case "video":
return `Video: ${h.beskrivning}`;
case "kommentar":
return h.text;
case "kategori_byte":
return `Arbetskategori: ${TIDKATEGORI_LABEL[h.kategori]}`;
case "inaktivitet_forklarad":
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
case "overlamning":
return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`;
case "ansvarig_satt":
return `Ansvarig tekniker: ${h.ansvarig}`;
case "arendetyp_satt":
return `Ärendetyp: ${h.arendetyp}`;
case "historik_kontrollerad":
return h.kontrollerad
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
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 "atgardsforslag":
return `Åtgärdsförslag till kund: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`;
case "kundbeslut":
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? `${h.kommentar}` : ""}`;
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":
return `Export skapad: ${h.format}, version ${h.version}`;
case "ai_svar": {
const forsta = h.rader[0];
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`;
}
case "arende_avslutat":
return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Felsökning avslutad";
}
}