ECM v2.0 som subsystem: sex motorer, pre-diagnostik och ärendeidentitet
Regelmotorn är nu plattformens styrande subsystem — systemet kan inte skriva en slutsats som ECM inte godkänt. Sex motorer (src/felsokning/ecm.ts): - Evidence: evidensposter ur loggen med nivå E0–E6, tekniker och deterministisk innehållshash - Rule: dokumentationskrav + undantagsregeln med obligatorisk orsak - Compliance: ärendetypen (garanti/goodwill/försäkring/reklamation/ begagnatgaranti …) styr extra krav — claim, skadenummer, historik, miltal, bildbevis - Validation: "Evidens saknas" i stället för antaganden (orkesterns grundprompt + projektioner + grind) - Completion: utökad kvalitetsgrind som spärrar slutrapporten - Traceability: spårbarhetspaket (regelversion, grindstatus per regel-id, evidensposter med hash) i varje export Pre-Diagnostic Validation — metodiken låses upp först när: - fordonshistoriken kontrollerats (eller motiverats: kvalitetsvarning), med orsakskedja för tidigare arbeten - ingående mätarställning fotograferats (bildtolkningen föreslår värdet) - kundens felbeskrivning verifierats - tidiga observationer hanterats Utgående mätarställning fotograferas inför avslut och blir obligatorisk i grinden när ärendet stängs. Ärendeidentitet (Case Identity): fordonsobjektet utökat med VIN, miltal, AO-, claim- och skadenummer (läses ur arbetsordern) — registreras en gång, synligt i identitetsraden i arbetsytan (med ärendetypsval), låst panel i Live Share, slutrapportens första sida och exporten. 49 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:
+3
-1
@@ -34,7 +34,9 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
||||
| Dashboard | ✅ Enligt direktivet: räknare och filter för Alla/Pågående/Klara plus Starta nytt ärende. |
|
||||
| Ärendestart via arbetsorder | ✅ Primärvägen när ett ärende startas: fota arbetsorderns framsida — orkesterns dokumenttolkning (Claude Sonnet 5, vision) läser kund-, fordons- och verkstadsuppgifter oavsett layout och sätter konfidens per fält. 🟢 ≥95 % godkänns automatiskt, 🟡 80–95 % markeras för genomläsning, 🔴 <80 % kräver aktiv bekräftelse — teknikern granskar bara osäkra fält. Visuell granskning med dokumentet bredvid fälten (klick markerar ungefärlig position), sedan skapas hela ärendet med ett tryck. Tolkningen loggas som organisationsintern händelse (`arbetsorder_skannad`) och delas aldrig i kund-/partnervyer. Manuell inmatning finns kvar som andrahandsväg; i lokalt läge visas en tydligt märkt demo-tolkning. Inloggade användare tillfrågas aldrig om namn — kontot vet redan. |
|
||||
| Inställningar | ✅ Systemadministratören väljer vilka objekttyper och identifieringsmetoder som visas när ett ärende startas (`/felsokning/installningar`). På plattformen gäller valet hela organisationen (sparas på organisationen, endast admin får ändra — verifierat i integrationstestet); i lokalt läge gäller valet enheten. Okända värden filtreras och tomma listor faller tillbaka till standard. |
|
||||
| Evidensmotor (ECM) | ✅ Versionshanterat regelbibliotek ([moduler/evidensmotor.md](moduler/evidensmotor.md), `src/felsokning/ecm.ts`, ECM v1.0): evidensnivåer E0–E6 härledda ur loggen, fullbordansregeln *evidens eller dokumenterat undantag med obligatorisk orsak* ("Underlag kan inte tas fram" i guiden, flaggas ⚠ i brief/rapport), och **kvalitetsgrind före slutrapport** — utskrift spärrad tills objektidentifiering, kontroller, fotokrav och evidensnivå är gröna. Regeln "skriv aldrig OK/kontrollerad/inga fel utan evidens — skriv Evidens saknas" är kodad i orkesterns grundprompt. |
|
||||
| Evidensmotor (ECM) | ✅ Eget subsystem med sex motorer ([moduler/evidensmotor.md](moduler/evidensmotor.md), `src/felsokning/ecm.ts`, **ECM v2.0**): Evidence (evidensposter med nivå E0–E6, tekniker och innehållshash), Rule (dokumentationskrav + undantagsregeln med obligatorisk orsak), Compliance (ärendetypen — garanti/försäkring/reklamation m.fl. — styr extra krav), Validation ("Evidens saknas" i stället för antaganden, kodat i orkesterns grundprompt), Completion (kvalitetsgrind som spärrar slutrapporten) och Traceability (spårbarhetspaket med regelversion + hash i varje export). |
|
||||
| Pre-diagnostik | ✅ Ingen felsökning förrän grundkontrollerna är gjorda eller motiverade: fordonshistorik (Ja med ev. orsakskedja / Nej med obligatorisk orsak → kvalitetsvarning), **ingående mätarställning** (foto av instrumentpanelen, bildtolkningen föreslår värdet), kundens felbeskrivning verifierad och tidiga observationer hanterade. Metodiken låses upp först därefter. **Utgående mätarställning** fotograferas inför avslut och blir obligatorisk i grinden när ärendet stängs. |
|
||||
| Ärendeidentitet | ✅ Fordonsobjektet som röd tråd: identiteten (AO-nummer, claim-/garantinummer, skadenummer, regnr, VIN, miltal, kund) registreras en gång — normalt via arbetsorderskanningen — och återanvänds i identitetsraden i arbetsytan (med ärendetypsval), låst panel överst i Live Share, slutrapportens första sida (Ärendeinformation + Fordonsinformation) och exporten. |
|
||||
| Instrumentavläsning (visual-first) | ✅ Kameran som universellt gränssnitt: `📷 Instrument` i Dokumentera-panelen fotograferar multimetrar, diagnosskärmar, batteritestare m.m. — bildtolkningen identifierar instrumenttyp och extraherar värden/enheter/felkoder med konfidens per värde; teknikern bekräftar innan något loggas. Originalbilden loggas alltid tillsammans med de strukturerade mätvärdena — strukturerad data ersätter aldrig originalevidensen. Ingen integration mot diagnossystem krävs. |
|
||||
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. Utskriften går genom ECM-kvalitetsgrinden. |
|
||||
| Öppet API | ✅ Plattforms-API:t är dokumenterat med OpenAPI 3.0 (`services/plattform/openapi.yaml`) — auth, användare, ärenden/händelser (append-only), översikt, publik delning och AI-orkestern, med scheman för alla händelsetyper. Specen valideras maskinellt, paritetstestas mot serverns rutter och serveras live på `GET /api/openapi.yaml`. |
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Modul: Evidensmotorn (ECM — Evidence & Compliance Matrix)
|
||||
|
||||
**Version: ECM v1.0** · Regelbiblioteket är versionshanterat och skilt från
|
||||
applikationslogiken (`src/felsokning/ecm.ts`). Vyerna anropar bara motorns
|
||||
rena funktioner — regler kan uppdateras utan att applikationen byggs om.
|
||||
**Version: ECM v2.0** · ECM är ett eget subsystem — inte en tabell i
|
||||
databasen — och motorn som styr hela plattformen: den avgör vilken
|
||||
dokumentation som krävs, när dokumentation saknas, vilken bevisnivå som
|
||||
uppnåtts, vilka regler som gäller och om ett ärende kan avslutas.
|
||||
**Systemet kan aldrig skriva en slutsats som ECM inte har godkänt.**
|
||||
|
||||
## Grundprincip
|
||||
Regelbiblioteket är versionshanterat och skilt från applikationslogiken
|
||||
(`src/felsokning/ecm.ts`); vyerna anropar bara motorns rena funktioner.
|
||||
|
||||
**Inget underlag = ingen slutsats.**
|
||||
## De sex motorerna
|
||||
|
||||
Systemet får aldrig anta att en kontroll är utförd eller påstå att
|
||||
dokumentation finns om den inte faktiskt är insamlad. Varje påstående i
|
||||
diagnos, brief och slutrapport ska kunna härledas till minst en
|
||||
evidenspost i händelseloggen. Systemet skriver aldrig "OK",
|
||||
"kontrollerad", "inga fel" eller "åtgärdad" utan evidens — i stället:
|
||||
**"Evidens saknas."** Regeln är kodad i orkesterns grundprompt och kan
|
||||
inte kringgås från klienten.
|
||||
|
||||
## Evidensnivåer
|
||||
### 1. Evidence Engine
|
||||
Katalogiserar all bevisning ur händelseloggen. Varje evidenspost får
|
||||
id, tidpunkt, tekniker, kategori, evidensnivå, sammanfattning och
|
||||
**innehållshash** — samma post ger alltid samma hash, och den
|
||||
append-only-låsta loggen (databastriggers) gör varje ändringsförsök
|
||||
omöjligt.
|
||||
|
||||
| Nivå | Typ | Bevisvärde |
|
||||
| --- | --- | --- |
|
||||
@@ -25,70 +25,98 @@ inte kringgås från klienten.
|
||||
| E2 | Foto | Medel |
|
||||
| E3 | Video | Högt |
|
||||
| E4 | Mätvärde | Högt |
|
||||
| E5 | Diagnosdata/dokument (t.ex. skannad arbetsorder) | Mycket högt |
|
||||
| E5 | Diagnosdata/dokument | Mycket högt |
|
||||
| E6 | Flera oberoende källor | Högsta |
|
||||
|
||||
Ärendets nivå härleds ur loggens faktiska innehåll (`evidensNiva`) och
|
||||
visas i kvalitetsgrinden.
|
||||
### 2. Rule Engine
|
||||
Dokumentationskraven: metodikens `krav`-fält per kontroll plus de
|
||||
automatiska reglerna — *kan det fotograferas → begär foto; låter det →
|
||||
video med ljud; rör det sig → video; mäts det → mätvärde; visar en
|
||||
display informationen → fota displayen; finns ett dokument → fota
|
||||
dokumentet.* Undantagsorsakerna ("Underlag kan inte tas fram") ligger
|
||||
här.
|
||||
|
||||
## Fullbordansregler
|
||||
### 3. Compliance Engine
|
||||
Ärendetypen styr vilka regler som gäller utöver metodiken. Ärendetyp
|
||||
väljs i identitetsraden och loggas (`arendetyp_satt`):
|
||||
|
||||
En kontrollpunkt kan bara slutföras när något av följande är sant:
|
||||
| Ärendetyp | Extra krav (v2.0) |
|
||||
| --- | --- |
|
||||
| Garanti | Miltal dokumenterat · servicehistorik kontrollerad · claim-/garantinummer |
|
||||
| Goodwill | Miltal · servicehistorik |
|
||||
| Försäkring | Skadenummer · bildbevis |
|
||||
| Reklamation | Historik och tidigare försök kontrollerade |
|
||||
| Begagnatgaranti | Miltal |
|
||||
|
||||
1. **Krävd evidens är insamlad** — foto för det synliga, mätvärde för det
|
||||
som mäts, kommentar där inget bättre är möjligt (metodikens
|
||||
`krav`-fält per kontroll).
|
||||
2. **Teknikern dokumenterar ett undantag**: *"Underlag kan inte tas
|
||||
fram"* med **obligatorisk orsak** (komponenten oåtkomlig, fordonet kan
|
||||
inte lyftas säkert, kunden avböjde demontering, dålig sikt, utrustning
|
||||
saknas — eller fri text). Undantaget loggas i händelseloggen och
|
||||
flaggas ⚠ i brief, överlämning och rapport — det redovisas aldrig som
|
||||
"utförd".
|
||||
Här ansluter det framtida **ECM Knowledge Library**: serverdistribuerade,
|
||||
versionerade regelpaket (garantivillkor per tillverkare, försäkringsbolagens
|
||||
krav, reklamationslagstiftning, OEM-kontrollpunkter) som laddas dynamiskt
|
||||
utan appändring.
|
||||
|
||||
## Kvalitetsgrind före slutrapport
|
||||
### 4. Validation Engine
|
||||
Inga påståenden utan underlag, i tre lager: (a) orkesterns grundprompt —
|
||||
aldrig "OK/kontrollerad/inga fel/åtgärdad" utan evidens, i stället
|
||||
"Evidens saknas" plus begäran om rätt underlag; (b) projektionerna —
|
||||
hypoteser kan aldrig bli konstaterade fel; (c) kvalitetsgrinden nedan.
|
||||
|
||||
Slutrapporten kan inte genereras förrän grinden är godkänd
|
||||
(`kvalitetsgrind`/`grindGodkand`):
|
||||
### 5. Completion Engine
|
||||
Kvalitetsgrinden före slutrapport/avslut — utskriften är spärrad tills
|
||||
alla obligatoriska rader är gröna:
|
||||
|
||||
| Kontroll | Krav |
|
||||
| --- | --- |
|
||||
| Fordons-/objektidentifiering verifierad | Obligatorisk |
|
||||
| Arbetsorder inläst | Rekommenderas |
|
||||
| Fordonshistorik kontrollerad eller motiverad | Obligatorisk |
|
||||
| Ingående mätarställning dokumenterad | Obligatorisk |
|
||||
| Kundens felbeskrivning verifierad | Rekommenderas |
|
||||
| Utgående mätarställning | Obligatorisk vid avslut |
|
||||
| Metodikens kontroller: evidens eller dokumenterat undantag | Obligatorisk |
|
||||
| Foton finns för fotokrävande kontroller | Obligatorisk |
|
||||
| Hypoteser redovisas som ej verifierade | Informativ (alltid sant per konstruktion) |
|
||||
| Foton för fotokrävande kontroller | Obligatorisk |
|
||||
| Ärendetypens compliance-krav | Obligatoriska |
|
||||
| Evidensnivå över E0 | Obligatorisk |
|
||||
|
||||
Utskriftsknappen är spärrad tills varje obligatorisk rad är grön; varje
|
||||
röd rad visar exakt vad som saknas.
|
||||
### 6. Traceability Engine
|
||||
Varje export bär ett spårbarhetspaket: ECM-version, ärendetyp,
|
||||
evidensnivå, grindstatus per regel-id och samtliga evidensposter med
|
||||
hash. Tillsammans med loggen kan varje slutsats härledas: vilken bild →
|
||||
vilken mätning → vilken tekniker → vilken regel → vilken regelverksversion
|
||||
→ när.
|
||||
|
||||
## Visual-first: kameran är integrationslagret
|
||||
## Pre-Diagnostic Validation
|
||||
|
||||
Plattformen prioriterar visuell insamling framför systemintegrationer.
|
||||
När information redan visas på en skärm, ett instrument, en utskrift
|
||||
eller en etikett fotograferas den — bildtolkningen extraherar, validerar
|
||||
och strukturerar informationen automatiskt. Ingen specialintegration
|
||||
behövs mot Bosch, TEXA, Autel, Launch, Hella Gutmann m.fl. så länge en
|
||||
människa kan läsa informationen.
|
||||
Ingen felsökning påbörjas förrän grundkontrollerna är genomförda eller
|
||||
dokumenterat motiverade — metodiken låses upp först därefter:
|
||||
|
||||
- **Arbetsorder** → ärendestartens dokumenttolkning (fält + konfidens).
|
||||
- **Instrument/diagnosskärmar** → `📷 Instrument` i Dokumentera-panelen:
|
||||
foto → typidentifiering (multimeter, diagnosdator, batteritestare,
|
||||
mätarkluster, manometer …) → värden med enhet och konfidens → teknikern
|
||||
bekräftar → **originalbilden loggas alltid tillsammans med de
|
||||
strukturerade värdena** — strukturerad data ersätter aldrig
|
||||
originalevidensen.
|
||||
1. **Fordonshistorik** — kontrollerad (tidigare arbeten, återkommande
|
||||
fel, TSB, kampanjer; relevanta samband kan noteras som orsakskedja)
|
||||
eller Nej med obligatorisk orsak → kvalitetsvarning.
|
||||
2. **Ingående mätarställning** — instrumentpanelen fotograferas;
|
||||
bildtolkningen föreslår värdet, teknikern bekräftar. Fotot blir den
|
||||
officiella ingående mätarställningen.
|
||||
3. **Kundens felbeskrivning verifierad** — ytterligare symptom
|
||||
dokumenteras som separata observationer, aldrig hopblandade med
|
||||
kundens beskrivning.
|
||||
4. **Tidiga observationer** — reparationsspår, modifieringar, skador,
|
||||
läckage m.m. dokumenteras med foto/observation, eller kvitteras
|
||||
"inga ytterligare".
|
||||
|
||||
Värden med hög säkerhet godkänns automatiskt; osäkra markeras för
|
||||
granskning — samma konfidensmodell som arbetsorderskanningen
|
||||
(🟢 ≥95 %, 🟡 80–95 %, 🔴 <80 %).
|
||||
**Utgående mätarställning** fotograferas inför avslut och blir
|
||||
obligatorisk i grinden när ärendet stängs. Rapporten visar in/ut.
|
||||
|
||||
## Spårbarhet
|
||||
## Ärendeidentitet (Case Identity & Vehicle Context)
|
||||
|
||||
Varje evidenspost är en händelse i den append-only-loggen med tidpunkt,
|
||||
tekniker, ärende och innehåll — omöjlig att ändra i efterhand (triggers i
|
||||
databasen). Det ger varje slutsats juridiskt spårbart underlag för kund,
|
||||
försäkringsbolag eller domstol.
|
||||
Fordonsobjektet är den röda tråden: identiteten registreras **en gång**
|
||||
(normalt via arbetsorderskanningen, som nu även läser claim-/garantinummer
|
||||
och skadenummer) och återanvänds sedan överallt:
|
||||
|
||||
- **Identitetsrad i arbetsytan** — AO, claim, skadenummer, fordon, regnr,
|
||||
VIN, miltal, ansvarig tekniker + ärendetypsval.
|
||||
- **Live Share** — låst panel överst med fordon, referenser och status,
|
||||
härledd ur det nivåfiltrerade underlaget.
|
||||
- **Slutrapportens första sida** — Ärendeinformation + Fordonsinformation
|
||||
automatiskt.
|
||||
- **Exporten** — identitet + spårbarhetspaket i varje JSON.
|
||||
|
||||
## Terminologi
|
||||
|
||||
@@ -96,13 +124,4 @@ Produkten beskrivs aldrig som en "AI-app" utan som ett **evidensbaserat
|
||||
diagnossystem** / **intelligent beslutsstöd**. I användargränssnitt och
|
||||
dokument används *systemet, analysen, bedömningen, tolkningen,
|
||||
bildtolkningen, beslutsstödet, regelmotorn* — inte "AI", om det inte är
|
||||
tekniskt nödvändigt (t.ex. i arkitekturdokumentation om modellorkestern).
|
||||
|
||||
## Kommande (regelbibliotekets väg framåt)
|
||||
|
||||
- Områdesregler per komponent (däck: fyra bilder + DOT + dimension;
|
||||
bromsar: närbild per ok; motorljud: video med ljud …) som
|
||||
serverdistribuerade, versionerade regelpaket.
|
||||
- Video- och ljudevidens (E3) med analys.
|
||||
- Garanti-/försäkrings-/reklamationsprofiler med egna obligatoriska
|
||||
fält i kvalitetsgrinden.
|
||||
tekniskt nödvändigt.
|
||||
|
||||
@@ -72,7 +72,7 @@ const ARBETSORDER_FALT_ID = [
|
||||
"fordon_regnr", "fordon_vin", "fordon_marke", "fordon_modell",
|
||||
"fordon_motor", "fordon_arsmodell", "fordon_matarstallning",
|
||||
"fordon_motorkod", "fordon_vaxellada",
|
||||
"ao_nummer", "ao_referens", "ao_serviceradgivare", "ao_bokningsdatum",
|
||||
"ao_nummer", "ao_claim", "ao_skadenummer", "ao_referens", "ao_serviceradgivare", "ao_bokningsdatum",
|
||||
"felbeskrivning",
|
||||
];
|
||||
|
||||
|
||||
@@ -568,6 +568,9 @@ components:
|
||||
- overlamning
|
||||
- ansvarig_satt
|
||||
- arbetsorder_skannad
|
||||
- arendetyp_satt
|
||||
- historik_kontrollerad
|
||||
- matarstallning
|
||||
- export_skapad
|
||||
- ai_svar
|
||||
- arende_avslutat
|
||||
|
||||
@@ -6,11 +6,39 @@
|
||||
|
||||
import type { Arende } from "./domain";
|
||||
import { handelseRubrik } from "./domain";
|
||||
import { arAvslutat, brief, foton, tidsfordelningsRader } from "./projektioner";
|
||||
import { arAvslutat, arendeidentitet, brief, foton, tidsfordelningsRader } from "./projektioner";
|
||||
import { metodikForArende } from "./store";
|
||||
import { tidDatum, tidKlockslag } from "./format";
|
||||
import { FelsokningSkal, Panel } from "./ui";
|
||||
|
||||
function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boolean }) {
|
||||
const idn = arendeidentitet(arende);
|
||||
const falt: [string, string | undefined][] = [
|
||||
["Arbetsorder", idn.arbetsorder],
|
||||
["Claim", idn.claim],
|
||||
["Skadenr", idn.skadenummer],
|
||||
["Regnr", idn.identifierare],
|
||||
["VIN", idn.vin],
|
||||
["Miltal", idn.miltal],
|
||||
["Ansvarig tekniker", idn.ansvarig],
|
||||
["Status", avslutat ? "Avslutat" : "Felsökning pågår"],
|
||||
];
|
||||
return (
|
||||
<div className="sticky top-11 z-10 mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
|
||||
{idn.beskrivning && <p className="text-[13px] font-semibold">{idn.beskrivning}</p>}
|
||||
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
|
||||
{falt
|
||||
.filter(([, varde]) => varde)
|
||||
.map(([etikett, varde]) => (
|
||||
<span key={etikett} className="whitespace-nowrap">
|
||||
<span className="text-[#707070]">{etikett}:</span> <span className="font-medium">{varde}</span>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DelatArendeVy({
|
||||
arende,
|
||||
nu,
|
||||
@@ -48,6 +76,11 @@ export function DelatArendeVy({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{/* Ärendeidentiteten: låst överst — mottagaren (kund, försäkrings-
|
||||
handläggare, kollega) ska aldrig tveka om vilket fordon och
|
||||
ärende vyn avser. Härledd ur det nivåfiltrerade underlaget. */}
|
||||
<IdentitetsPanel arende={arende} avslutat={avslutat} />
|
||||
|
||||
<p className="mb-4 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
|
||||
{notis}
|
||||
</p>
|
||||
|
||||
@@ -2,7 +2,16 @@ import { describe, expect, it } from "vitest";
|
||||
import type { Arende, Handelse } from "../domain";
|
||||
import { nyLoggPost } from "../domain";
|
||||
import { VIBRATION_METODIK } from "../metodik";
|
||||
import { evidensNiva, grindGodkand, kvalitetsgrind } from "../ecm";
|
||||
import {
|
||||
arendetyp,
|
||||
evidensNiva,
|
||||
evidensposter,
|
||||
grindGodkand,
|
||||
kvalitetsgrind,
|
||||
preDiagnostik,
|
||||
sparbarhetspaket,
|
||||
} from "../ecm";
|
||||
import { arendeidentitet } from "../projektioner";
|
||||
import { byggDemoArende } from "../demo";
|
||||
|
||||
function byggArende(handelser: Handelse[]): Arende {
|
||||
@@ -16,10 +25,28 @@ function byggArende(handelser: Handelse[]): Arende {
|
||||
|
||||
const OBJEKT: Handelse = {
|
||||
typ: "objekt_identifierat",
|
||||
objekt: { typ: "Personbil", identifierare: "ABC123", identifieringsmetod: "Regnr", beskrivning: "Volvo XC60" },
|
||||
objekt: {
|
||||
typ: "Personbil",
|
||||
identifierare: "ABC123",
|
||||
identifieringsmetod: "Regnr",
|
||||
beskrivning: "Volvo XC60",
|
||||
vin: "YV1DZ8256F2123456",
|
||||
miltal: "8 432 mil",
|
||||
arbetsorder: "AO-2496",
|
||||
claim: "G-2026-00481",
|
||||
skadenummer: "SK-77812",
|
||||
},
|
||||
};
|
||||
|
||||
describe("evidensnivåer", () => {
|
||||
// Grundkontroller som gör pre-diagnostiken klar.
|
||||
const PREDIAG: Handelse[] = [
|
||||
{ typ: "historik_kontrollerad", kontrollerad: true },
|
||||
{ typ: "matarstallning", lage: "ingaende", varde: "84 320 km" },
|
||||
{ typ: "kommentar", text: "Kundens felbeskrivning verifierad vid mottagandet." },
|
||||
{ typ: "kommentar", text: "Inga ytterligare observationer vid mottagandet." },
|
||||
];
|
||||
|
||||
describe("Evidence Engine", () => {
|
||||
it("härleder nivån ur loggens faktiska innehåll", () => {
|
||||
expect(evidensNiva(byggArende([]))).toBe("E0");
|
||||
expect(evidensNiva(byggArende([{ typ: "observation", text: "x" }]))).toBe("E1");
|
||||
@@ -35,25 +62,62 @@ describe("evidensnivåer", () => {
|
||||
),
|
||||
).toBe("E6");
|
||||
});
|
||||
|
||||
it("katalogiserar evidensposter med tekniker, nivå och innehållshash", () => {
|
||||
const arende = byggArende([
|
||||
{ typ: "foto", beskrivning: "Höger framhjul", dataUrl: "data:x" },
|
||||
{ typ: "matvarde", beskrivning: "Obalans", varde: "38", enhet: "g" },
|
||||
]);
|
||||
const poster = evidensposter(arende);
|
||||
expect(poster).toHaveLength(2);
|
||||
expect(poster[0].niva).toBe("E2");
|
||||
expect(poster[1].sammanfattning).toBe("Obalans = 38 g");
|
||||
expect(poster[0].tekniker).toBe("Anna");
|
||||
expect(poster[0].hash).toMatch(/^[0-9a-f]{8}$/);
|
||||
// Samma innehåll → samma hash (deterministisk spårbarhet).
|
||||
expect(evidensposter(arende)[0].hash).toBe(poster[0].hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("kvalitetsgrind före slutrapport", () => {
|
||||
describe("Pre-Diagnostic Validation", () => {
|
||||
it("ingen felsökning utan grundkontroller — allt härleds ur loggen", () => {
|
||||
const tomt = byggArende([OBJEKT]);
|
||||
expect(preDiagnostik(tomt).every((r) => r.klar)).toBe(false);
|
||||
|
||||
const klart = byggArende([OBJEKT, ...PREDIAG]);
|
||||
expect(preDiagnostik(klart).every((r) => r.klar)).toBe(true);
|
||||
});
|
||||
|
||||
it("ej kontrollerad historik godtas med orsak men flaggas som kvalitetsvarning", () => {
|
||||
const arende = byggArende([
|
||||
OBJEKT,
|
||||
{ typ: "historik_kontrollerad", kontrollerad: false, kommentar: "Historiksystemet otillgängligt" },
|
||||
]);
|
||||
const rad = preDiagnostik(arende).find((r) => r.id === "historik")!;
|
||||
expect(rad.klar).toBe(true);
|
||||
expect(rad.varning).toContain("Historiksystemet otillgängligt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Completion Engine (kvalitetsgrind)", () => {
|
||||
it("utan evidens är grinden stängd — inga påståenden utan underlag", () => {
|
||||
const tomt = byggArende([]);
|
||||
expect(grindGodkand(tomt, VIBRATION_METODIK)).toBe(false);
|
||||
const rader = kvalitetsgrind(tomt, VIBRATION_METODIK);
|
||||
expect(rader.find((r) => r.id === "objekt")?.ok).toBe(false);
|
||||
expect(rader.find((r) => r.id === "kontroller")?.ok).toBe(false);
|
||||
expect(rader.find((r) => r.id === "kontroller")?.detalj).toContain("Evidens saknas");
|
||||
});
|
||||
|
||||
it("ett dokumenterat undantag räknas som hanterat men flaggas med orsak", () => {
|
||||
// Alla kontroller får undantag med orsak — grinden öppnas, men
|
||||
// undantagen redovisas i detaljraden (aldrig som "utförd").
|
||||
const undantag: Handelse[] = [OBJEKT, { typ: "foto", beskrivning: "Översikt", dataUrl: "data:" }, { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4", enhet: "bar" }];
|
||||
it("dokumenterade undantag öppnar grinden men redovisas med orsak", () => {
|
||||
const handelser: Handelse[] = [
|
||||
OBJEKT,
|
||||
...PREDIAG,
|
||||
{ typ: "foto", beskrivning: "Översikt", dataUrl: "data:" },
|
||||
{ typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4", enhet: "bar" },
|
||||
];
|
||||
for (const steg of VIBRATION_METODIK.steg) {
|
||||
for (const kontroll of steg.kontroller ?? []) {
|
||||
undantag.push({
|
||||
handelser.push({
|
||||
typ: "kontroll_utford",
|
||||
stegId: steg.id,
|
||||
kontrollId: kontroll.id,
|
||||
@@ -62,47 +126,66 @@ describe("kvalitetsgrind före slutrapport", () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
const arende = byggArende(undantag);
|
||||
const arende = byggArende(handelser);
|
||||
expect(grindGodkand(arende, VIBRATION_METODIK)).toBe(true);
|
||||
const rad = kvalitetsgrind(arende, VIBRATION_METODIK).find((r) => r.id === "kontroller");
|
||||
expect(rad?.ok).toBe(true);
|
||||
expect(rad?.detalj).toContain("Undantag med orsak");
|
||||
expect(rad?.detalj).toContain("Kunden avböjde demontering");
|
||||
});
|
||||
|
||||
it("demoärendet blockeras tills alla kontroller har evidens eller undantag", () => {
|
||||
const demo = byggDemoArende(1);
|
||||
expect(grindGodkand(demo, VIBRATION_METODIK)).toBe(false);
|
||||
|
||||
const komplett: Arende = {
|
||||
...demo,
|
||||
handelser: [...demo.handelser],
|
||||
};
|
||||
const hanterade = new Set(
|
||||
demo.handelser
|
||||
.filter((p) => p.handelse.typ === "kontroll_utford")
|
||||
.map((p) => {
|
||||
const h = p.handelse;
|
||||
return h.typ === "kontroll_utford" ? `${h.stegId}/${h.kontrollId}` : "";
|
||||
}),
|
||||
);
|
||||
for (const steg of VIBRATION_METODIK.steg) {
|
||||
for (const kontroll of steg.kontroller ?? []) {
|
||||
if (!hanterade.has(`${steg.id}/${kontroll.id}`)) {
|
||||
komplett.handelser.push(
|
||||
nyLoggPost("Johan", {
|
||||
typ: "kontroll_utford",
|
||||
stegId: steg.id,
|
||||
kontrollId: kontroll.id,
|
||||
text: kontroll.text,
|
||||
undantag: "Utrustning saknas",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(grindGodkand(komplett, VIBRATION_METODIK)).toBe(true);
|
||||
// Demoärendet har foto + mätvärden + flera källor → högsta evidensnivån.
|
||||
expect(evidensNiva(komplett)).toBe("E6");
|
||||
it("utgående mätarställning blir obligatorisk när ärendet avslutas", () => {
|
||||
const bas: Handelse[] = [OBJEKT, ...PREDIAG];
|
||||
const oppen = byggArende(bas);
|
||||
expect(kvalitetsgrind(oppen, VIBRATION_METODIK).find((r) => r.id === "matarstallning_ut")?.kravs).toBe(false);
|
||||
const stangd = byggArende([...bas, { typ: "arende_avslutat" }]);
|
||||
expect(kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "matarstallning_ut")?.kravs).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Compliance Engine", () => {
|
||||
it("ärendetypen styr dokumentationskraven", () => {
|
||||
const privat = byggArende([OBJEKT]);
|
||||
expect(arendetyp(privat)).toBe("Privat kund");
|
||||
expect(kvalitetsgrind(privat, VIBRATION_METODIK).some((r) => r.id.startsWith("garanti_"))).toBe(false);
|
||||
|
||||
const garanti = byggArende([OBJEKT, { typ: "arendetyp_satt", arendetyp: "Garanti" }]);
|
||||
const rader = kvalitetsgrind(garanti, VIBRATION_METODIK);
|
||||
expect(rader.find((r) => r.id === "garanti_claim")?.ok).toBe(true); // claim finns i objektet
|
||||
expect(rader.find((r) => r.id === "garanti_historik")?.ok).toBe(false); // historik ej kontrollerad
|
||||
});
|
||||
|
||||
it("försäkringsärenden kräver skadenummer och bildbevis", () => {
|
||||
const utanBild = byggArende([OBJEKT, { typ: "arendetyp_satt", arendetyp: "Försäkring" }]);
|
||||
const rader = kvalitetsgrind(utanBild, VIBRATION_METODIK);
|
||||
expect(rader.find((r) => r.id === "forsakring_skadenummer")?.ok).toBe(true);
|
||||
expect(rader.find((r) => r.id === "forsakring_bildbevis")?.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Traceability Engine + ärendeidentitet", () => {
|
||||
it("spårbarhetspaketet innehåller version, grindstatus och evidensposter", () => {
|
||||
const arende = byggArende([OBJEKT, { typ: "foto", beskrivning: "x", dataUrl: "data:" }]);
|
||||
const paket = sparbarhetspaket(arende, VIBRATION_METODIK);
|
||||
expect(paket.ecmVersion).toBe("2.0");
|
||||
expect(paket.arendetyp).toBe("Privat kund");
|
||||
expect(paket.evidensposter).toHaveLength(1);
|
||||
expect(paket.kvalitetsgrind.some((r) => r.id === "objekt" && r.ok)).toBe(true);
|
||||
});
|
||||
|
||||
it("ärendeidentiteten registreras en gång och återanvänds överallt", () => {
|
||||
const arende = byggArende([OBJEKT, { typ: "matarstallning", lage: "ingaende", varde: "84 320 km" }]);
|
||||
const idn = arendeidentitet(arende);
|
||||
expect(idn.arbetsorder).toBe("AO-2496");
|
||||
expect(idn.claim).toBe("G-2026-00481");
|
||||
expect(idn.skadenummer).toBe("SK-77812");
|
||||
expect(idn.vin).toBe("YV1DZ8256F2123456");
|
||||
// Den fotograferade mätarställningen ersätter arbetsorderns uppgift.
|
||||
expect(idn.miltal).toBe("84 320 km");
|
||||
});
|
||||
|
||||
it("demoärendet passerar pre-diagnostiken men blockeras av okontrollerade metodiksteg", () => {
|
||||
const demo = byggDemoArende(1);
|
||||
expect(preDiagnostik(demo).every((r) => r.klar)).toBe(true);
|
||||
expect(grindGodkand(demo, VIBRATION_METODIK)).toBe(false);
|
||||
expect(evidensNiva(demo)).toBe("E6");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,6 +128,8 @@ export const ARBETSORDER_FALT: { id: string; grupp: ArbetsorderGrupp; etikett: s
|
||||
{ id: "fordon_motorkod", grupp: "Fordon", etikett: "Motorkod" },
|
||||
{ id: "fordon_vaxellada", grupp: "Fordon", etikett: "Växellåda" },
|
||||
{ id: "ao_nummer", grupp: "Verkstad", etikett: "Arbetsordernr" },
|
||||
{ id: "ao_claim", grupp: "Verkstad", etikett: "Claim-/garantinr" },
|
||||
{ id: "ao_skadenummer", grupp: "Verkstad", etikett: "Skadenummer" },
|
||||
{ id: "ao_referens", grupp: "Verkstad", etikett: "Intern referens" },
|
||||
{ id: "ao_serviceradgivare", grupp: "Verkstad", etikett: "Servicerådgivare" },
|
||||
{ id: "ao_bokningsdatum", grupp: "Verkstad", etikett: "Bokningsdatum" },
|
||||
|
||||
@@ -63,9 +63,18 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
||||
identifieringsmetod: "Regnr",
|
||||
beskrivning: "Volvo XC60 D4 2019",
|
||||
kund: "Anders Svensson",
|
||||
vin: "YV1DZ8256F2123456",
|
||||
miltal: "8 432 mil",
|
||||
arbetsorder: "AO-2496",
|
||||
},
|
||||
}),
|
||||
post(2, { typ: "felbeskrivning", text: "Bilen vibrerar runt 88 km/h. Symptomet uppträder endast under körning." }),
|
||||
// Pre-diagnostiken: historik, ingående mätarställning, verifierad
|
||||
// felbeskrivning och tidiga observationer — innan metodiken börjar.
|
||||
post(3, { typ: "historik_kontrollerad", kontrollerad: true, kommentar: "Däck roterade vid service för 2 månader sedan" }),
|
||||
post(3, { typ: "matarstallning", lage: "ingaende", varde: "84 320 km", dataUrl: demoFoto("Instrumentpanel 84 320 km") }),
|
||||
post(4, { typ: "kommentar", text: "Kundens felbeskrivning verifierad vid mottagandet." }),
|
||||
post(4, { typ: "kommentar", text: "Inga ytterligare observationer vid mottagandet." }),
|
||||
post(4, { typ: "fraga_besvarad", stegId: "symptom", frageId: "hastighetsberoende", fraga: "Är vibrationen hastighetsberoende?", svar: "Ja" }),
|
||||
post(5, { typ: "fraga_besvarad", stegId: "symptom", frageId: "var", fraga: "Var känns vibrationen?", svar: "I ratten" }),
|
||||
post(6, { typ: "fraga_besvarad", stegId: "symptom", frageId: "nar", fraga: "När uppstår vibrationen?", svar: "Vid jämn fart" }),
|
||||
|
||||
@@ -29,12 +29,21 @@ export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
|
||||
paus: "Paus",
|
||||
};
|
||||
|
||||
// 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. "Fordon", "Industrimaskin"
|
||||
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.
|
||||
@@ -63,6 +72,14 @@ export type Handelse =
|
||||
| { 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 }
|
||||
| { typ: "export_skapad"; format: string; version: number }
|
||||
| {
|
||||
typ: "ai_svar";
|
||||
@@ -136,6 +153,15 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
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 "export_skapad":
|
||||
return `Export skapad: ${h.format}, version ${h.version}`;
|
||||
case "ai_svar": {
|
||||
|
||||
+293
-35
@@ -1,22 +1,31 @@
|
||||
// Evidence & Compliance Matrix (ECM) — plattformens regelmotor.
|
||||
//
|
||||
// Grundprincip: varje påstående måste kunna härledas till evidens i
|
||||
// händelseloggen. Systemet får aldrig skriva "kontrollerad", "OK" eller
|
||||
// "inga fel" utan underlag — utan evidens skrivs "Evidens saknas".
|
||||
// ECM är ett eget subsystem, inte en tabell i databasen: den avgör vilken
|
||||
// dokumentation som krävs, när dokumentation saknas, vilken bevisnivå som
|
||||
// uppnåtts, vilka regler som gäller (per ärendetyp) och om ett ärende kan
|
||||
// avslutas. Systemet kan aldrig skriva en slutsats som ECM inte godkänt.
|
||||
//
|
||||
// Regelbiblioteket är versionshanterat och skilt från applikationslogiken:
|
||||
// evidensnivåer, krav och kvalitetsgrind ändras här (och i kommande
|
||||
// versioner via serverdistribuerade regler) utan att vyerna byggs om.
|
||||
// Vyerna anropar bara de rena funktionerna nedan.
|
||||
// Sex motorer:
|
||||
// 1. Evidence Engine — katalogiserar all bevisning ur loggen
|
||||
// 2. Rule Engine — dokumentationskraven (metodik + automatiska regler)
|
||||
// 3. Compliance Engine — vilka regler som gäller per ärendetyp
|
||||
// 4. Validation Engine — inga påståenden utan underlag ("Evidens saknas")
|
||||
// 5. Completion Engine — kvalitetsgrind: får ärendet avslutas/rapporteras?
|
||||
// 6. Traceability Engine — varje evidenspost hash:as och versionsmärks
|
||||
//
|
||||
// Regelbiblioteket är versionshanterat och skilt från applikationslogiken;
|
||||
// vyerna anropar bara de rena funktionerna nedan. Nästa steg är server-
|
||||
// distribuerade regelpaket (garantivillkor per tillverkare, försäkrings-
|
||||
// krav, reklamationsregler) som laddas dynamiskt utan appändring.
|
||||
|
||||
import type { Arende } from "./domain";
|
||||
import type { Arende, LoggPost } from "./domain";
|
||||
import type { Metodik } from "./metodik";
|
||||
|
||||
export const ECM_VERSION = "1.0";
|
||||
export const ECM_VERSION = "2.0";
|
||||
|
||||
// ---- 1. Evidence Engine -----------------------------------------------
|
||||
|
||||
// Evidensnivåer: bevisvärdet för ett påstående, härlett ur loggen.
|
||||
// E0 inget underlag · E1 teknikerns observation · E2 foto · E3 video
|
||||
// E4 mätvärde · E5 diagnosdata/dokument · E6 flera oberoende källor
|
||||
export type EvidensNiva = "E0" | "E1" | "E2" | "E3" | "E4" | "E5" | "E6";
|
||||
|
||||
export const EVIDENS_LABEL: Record<EvidensNiva, string> = {
|
||||
@@ -29,8 +38,76 @@ export const EVIDENS_LABEL: Record<EvidensNiva, string> = {
|
||||
E6: "E6 · Flera oberoende källor",
|
||||
};
|
||||
|
||||
// Godkända orsaker när underlag inte kan tas fram. Fri text tillåts
|
||||
// också — men en orsak är alltid obligatorisk.
|
||||
export interface Evidenspost {
|
||||
id: string;
|
||||
tidpunkt: string;
|
||||
tekniker: string;
|
||||
kategori: string;
|
||||
niva: Exclude<EvidensNiva, "E0" | "E6">;
|
||||
sammanfattning: string;
|
||||
// Innehållshash för spårbarhet: samma post ger alltid samma hash,
|
||||
// varje ändringsförsök syns (loggen är dessutom append-only i DB:n).
|
||||
hash: string;
|
||||
}
|
||||
|
||||
// FNV-1a — snabb, deterministisk innehållshash (spårbarhet, inte krypto;
|
||||
// manipulationsskyddet ligger i databasens append-only-triggers).
|
||||
export function innehallsHash(innehall: string): string {
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < innehall.length; i++) {
|
||||
h ^= innehall.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return (h >>> 0).toString(16).padStart(8, "0");
|
||||
}
|
||||
|
||||
// Katalogiserar varje evidenspost i loggen med nivå, kategori och hash.
|
||||
export function evidensposter(arende: Arende): Evidenspost[] {
|
||||
const poster: Evidenspost[] = [];
|
||||
const lagg = (post: LoggPost, kategori: string, niva: Evidenspost["niva"], sammanfattning: string) =>
|
||||
poster.push({
|
||||
id: post.id,
|
||||
tidpunkt: post.tidpunkt,
|
||||
tekniker: post.anvandare,
|
||||
kategori,
|
||||
niva,
|
||||
sammanfattning,
|
||||
hash: innehallsHash(JSON.stringify(post.handelse)),
|
||||
});
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "foto") lagg(post, "foto", "E2", h.beskrivning);
|
||||
if (h.typ === "matvarde") lagg(post, "mätvärde", "E4", `${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`);
|
||||
if (h.typ === "matarstallning" && !h.undantag) lagg(post, "mätarställning", "E2", `${h.lage === "ingaende" ? "In" : "Ut"}: ${h.varde}`);
|
||||
if (h.typ === "arbetsorder_skannad") lagg(post, "dokument", "E5", `Arbetsorder, ${h.falt.length} fält`);
|
||||
if (h.typ === "observation") lagg(post, "observation", "E1", h.text);
|
||||
if (h.typ === "kontroll_utford" && !h.undantag) lagg(post, "kontroll", "E1", h.text);
|
||||
}
|
||||
return poster;
|
||||
}
|
||||
|
||||
// Ärendets samlade evidensnivå: starkaste kombination loggen innehåller.
|
||||
export function evidensNiva(arende: Arende): EvidensNiva {
|
||||
const poster = evidensposter(arende);
|
||||
const har = (n: string) => poster.some((p) => p.niva === n);
|
||||
const kallor = [har("E2"), har("E4"), har("E5")].filter(Boolean).length;
|
||||
if (kallor >= 2) return "E6";
|
||||
if (har("E5")) return "E5";
|
||||
if (har("E4")) return "E4";
|
||||
if (har("E2")) return "E2";
|
||||
if (har("E1")) return "E1";
|
||||
return "E0";
|
||||
}
|
||||
|
||||
// ---- 2. Rule Engine ---------------------------------------------------
|
||||
|
||||
// Automatiska regler (kodade i orkesterns grundprompt och i metodikens
|
||||
// krav-fält): kan det fotograferas → begär foto; låter det → video med
|
||||
// ljud; rör det sig → video; mäts det → mätvärde; visar en display
|
||||
// informationen → fota displayen; finns ett dokument → fota dokumentet.
|
||||
|
||||
// Godkända orsaker när underlag inte kan tas fram (fri text tillåts
|
||||
// också — men en orsak är alltid obligatorisk).
|
||||
export const UNDANTAGSORSAKER = [
|
||||
"Komponenten är oåtkomlig",
|
||||
"Fordonet kan inte lyftas säkert",
|
||||
@@ -39,29 +116,142 @@ export const UNDANTAGSORSAKER = [
|
||||
"Utrustning saknas",
|
||||
];
|
||||
|
||||
// Evidensnivå för ärendet som helhet: den starkaste kombination som
|
||||
// loggen faktiskt innehåller.
|
||||
export function evidensNiva(arende: Arende): EvidensNiva {
|
||||
let foto = false;
|
||||
let matvarde = false;
|
||||
let dokument = false;
|
||||
let observation = false;
|
||||
// Markörtexter för pre-diagnostikens kvitteringar (loggas som kommentar).
|
||||
export const MARKOR_FELBESKRIVNING_VERIFIERAD = "Kundens felbeskrivning verifierad vid mottagandet";
|
||||
export const MARKOR_INGA_TIDIGA_OBSERVATIONER = "Inga ytterligare observationer vid mottagandet";
|
||||
export const MARKOR_TIDIGA_OBSERVATIONER_KLARA = "Tidiga observationer vid mottagandet dokumenterade";
|
||||
|
||||
// ---- 3. Compliance Engine ---------------------------------------------
|
||||
|
||||
// Ärendetypen styr vilka dokumentationskrav som gäller utöver metodiken.
|
||||
export const ARENDETYPER = [
|
||||
"Privat kund",
|
||||
"Företagskund",
|
||||
"Garanti",
|
||||
"Goodwill",
|
||||
"Försäkring",
|
||||
"Reklamation",
|
||||
"Begagnatgaranti",
|
||||
"Intern kvalitetskontroll",
|
||||
"Teknisk utredning",
|
||||
] as const;
|
||||
|
||||
export type Arendetyp = (typeof ARENDETYPER)[number];
|
||||
|
||||
export function arendetyp(arende: Arende): Arendetyp {
|
||||
let typ: Arendetyp = "Privat kund";
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "foto") foto = true;
|
||||
if (h.typ === "matvarde") matvarde = true;
|
||||
if (h.typ === "arbetsorder_skannad") dokument = true;
|
||||
if (h.typ === "observation" || h.typ === "kontroll_utford") observation = true;
|
||||
if (h.typ === "arendetyp_satt" && (ARENDETYPER as readonly string[]).includes(h.arendetyp)) {
|
||||
typ = h.arendetyp as Arendetyp;
|
||||
}
|
||||
}
|
||||
const kallor = [foto, matvarde, dokument].filter(Boolean).length;
|
||||
if (kallor >= 2) return "E6";
|
||||
if (dokument) return "E5";
|
||||
if (matvarde) return "E4";
|
||||
if (foto) return "E2";
|
||||
if (observation) return "E1";
|
||||
return "E0";
|
||||
return typ;
|
||||
}
|
||||
|
||||
interface ComplianceKrav {
|
||||
id: string;
|
||||
rubrik: string;
|
||||
uppfyllt: (arende: Arende) => boolean;
|
||||
detaljVidBrist: string;
|
||||
}
|
||||
|
||||
function objektFalt(arende: Arende, falt: "miltal" | "claim" | "skadenummer" | "arbetsorder"): string | undefined {
|
||||
for (const post of arende.handelser) {
|
||||
if (post.handelse.typ === "objekt_identifierat") return post.handelse.objekt[falt];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const harMatarstallning = (arende: Arende) =>
|
||||
arende.handelser.some((p) => p.handelse.typ === "matarstallning" && !p.handelse.undantag) ||
|
||||
!!objektFalt(arende, "miltal");
|
||||
const harHistorik = (arende: Arende) =>
|
||||
arende.handelser.some((p) => p.handelse.typ === "historik_kontrollerad" && p.handelse.kontrollerad);
|
||||
const harFoto = (arende: Arende) => arende.handelser.some((p) => p.handelse.typ === "foto");
|
||||
|
||||
// Regelpaket per ärendetyp — här ansluter framtida serverdistribuerade
|
||||
// regler (garantivillkor per tillverkare, försäkringsbolagens krav …).
|
||||
const COMPLIANCE_REGLER: Partial<Record<Arendetyp, ComplianceKrav[]>> = {
|
||||
Garanti: [
|
||||
{ id: "garanti_miltal", rubrik: "Miltal dokumenterat", uppfyllt: harMatarstallning, detaljVidBrist: "Garantiärenden kräver dokumenterad mätarställning." },
|
||||
{ id: "garanti_historik", rubrik: "Servicehistorik kontrollerad", uppfyllt: harHistorik, detaljVidBrist: "Garantiärenden kräver kontrollerad servicehistorik." },
|
||||
{ id: "garanti_claim", rubrik: "Claim-/garantinummer registrerat", uppfyllt: (a) => !!objektFalt(a, "claim"), detaljVidBrist: "Ange claim-/garantinummer (läses ur arbetsordern)." },
|
||||
],
|
||||
Goodwill: [
|
||||
{ id: "goodwill_miltal", rubrik: "Miltal dokumenterat", uppfyllt: harMatarstallning, detaljVidBrist: "Goodwillärenden kräver dokumenterad mätarställning." },
|
||||
{ id: "goodwill_historik", rubrik: "Servicehistorik kontrollerad", uppfyllt: harHistorik, detaljVidBrist: "Goodwillärenden kräver kontrollerad servicehistorik." },
|
||||
],
|
||||
Försäkring: [
|
||||
{ id: "forsakring_skadenummer", rubrik: "Skadenummer registrerat", uppfyllt: (a) => !!objektFalt(a, "skadenummer"), detaljVidBrist: "Försäkringsärenden kräver skadenummer (läses ur arbetsordern)." },
|
||||
{ id: "forsakring_bildbevis", rubrik: "Bildbevis finns", uppfyllt: harFoto, detaljVidBrist: "Försäkringsärenden kräver bilddokumentation." },
|
||||
],
|
||||
Reklamation: [
|
||||
{ id: "reklamation_historik", rubrik: "Historik och tidigare försök kontrollerade", uppfyllt: harHistorik, detaljVidBrist: "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." },
|
||||
],
|
||||
Begagnatgaranti: [
|
||||
{ id: "begagnat_miltal", rubrik: "Miltal dokumenterat", uppfyllt: harMatarstallning, detaljVidBrist: "Begagnatgaranti kräver dokumenterad mätarställning." },
|
||||
],
|
||||
};
|
||||
|
||||
// ---- 4. Validation Engine ---------------------------------------------
|
||||
|
||||
// Kärnprincipen — inga påståenden utan underlag — verkar i tre lager:
|
||||
// (a) orkesterns grundprompt: aldrig "OK/kontrollerad/inga fel" utan
|
||||
// evidens; skriv "Evidens saknas" och begär rätt underlag,
|
||||
// (b) projektionerna: hypoteser kan aldrig bli konstaterade fel,
|
||||
// (c) kvalitetsgrinden nedan: rapport/avslut blockeras tills varje
|
||||
// obligatoriskt påstående har evidens eller dokumenterat undantag.
|
||||
|
||||
// ---- Pre-Diagnostic Validation ----------------------------------------
|
||||
|
||||
export interface PreDiagRad {
|
||||
id: "historik" | "matarstallning_in" | "felbeskrivning" | "tidiga_observationer";
|
||||
rubrik: string;
|
||||
klar: boolean;
|
||||
varning?: string;
|
||||
}
|
||||
|
||||
// Ingen felsökning påbörjas förrän grundkontrollerna är genomförda —
|
||||
// eller dokumenterat motiverade. Allt härleds ur loggen.
|
||||
export function preDiagnostik(arende: Arende): PreDiagRad[] {
|
||||
let historik: PreDiagRad = { id: "historik", rubrik: "Fordonshistorik kontrollerad", klar: false };
|
||||
let matarstallning = false;
|
||||
let felbeskrivningVerifierad = false;
|
||||
let tidiga = false;
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "historik_kontrollerad") {
|
||||
historik = {
|
||||
id: "historik",
|
||||
rubrik: "Fordonshistorik kontrollerad",
|
||||
klar: true,
|
||||
varning: h.kontrollerad ? undefined : `Ej kontrollerad — orsak: ${h.kommentar ?? "saknas"}`,
|
||||
};
|
||||
}
|
||||
if (h.typ === "matarstallning" && h.lage === "ingaende") matarstallning = true;
|
||||
if (h.typ === "kommentar" && h.text.startsWith(MARKOR_FELBESKRIVNING_VERIFIERAD)) felbeskrivningVerifierad = true;
|
||||
if (
|
||||
h.typ === "kommentar" &&
|
||||
(h.text.startsWith(MARKOR_INGA_TIDIGA_OBSERVATIONER) || h.text.startsWith(MARKOR_TIDIGA_OBSERVATIONER_KLARA))
|
||||
) {
|
||||
tidiga = true;
|
||||
}
|
||||
}
|
||||
return [
|
||||
historik,
|
||||
{ id: "matarstallning_in", rubrik: "Ingående mätarställning dokumenterad", klar: matarstallning },
|
||||
{ id: "felbeskrivning", rubrik: "Kundens felbeskrivning verifierad", klar: felbeskrivningVerifierad },
|
||||
{ id: "tidiga_observationer", rubrik: "Tidiga observationer hanterade", klar: tidiga },
|
||||
];
|
||||
}
|
||||
|
||||
export function preDiagnostikKlar(arende: Arende): boolean {
|
||||
return preDiagnostik(arende).every((rad) => rad.klar);
|
||||
}
|
||||
|
||||
// ---- 5. Completion Engine ---------------------------------------------
|
||||
|
||||
export interface GrindRad {
|
||||
id: string;
|
||||
rubrik: string;
|
||||
@@ -71,12 +261,13 @@ export interface GrindRad {
|
||||
detalj?: string;
|
||||
}
|
||||
|
||||
// Kvalitetsgrind före slutrapport: varje rad är en verifierbar kontroll
|
||||
// mot händelseloggen. Rapporten kan inte genereras förrän alla
|
||||
// obligatoriska rader är gröna — evidens eller dokumenterat undantag.
|
||||
// Kvalitetsgrind före slutrapport/avslut. Varje rad är en verifierbar
|
||||
// regel mot händelseloggen; regel-id och ECM-version följer med i
|
||||
// exporten (Traceability Engine).
|
||||
export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
const rader: GrindRad[] = [];
|
||||
const handelser = arende.handelser.map((p) => p.handelse);
|
||||
const avslutat = handelser.some((h) => h.typ === "arende_avslutat");
|
||||
|
||||
const objektFinns = handelser.some((h) => h.typ === "objekt_identifierat");
|
||||
rader.push({
|
||||
@@ -87,7 +278,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
detalj: objektFinns ? undefined : "Evidens saknas — identifiera objektet.",
|
||||
});
|
||||
|
||||
const arbetsorder = handelser.some((h) => h.typ === "arbetsorder_skannad");
|
||||
const arbetsorder = handelser.some((h) => h.typ === "arbetsorder_skannad") || !!objektFalt(arende, "arbetsorder");
|
||||
rader.push({
|
||||
id: "arbetsorder",
|
||||
rubrik: "Arbetsorder inläst",
|
||||
@@ -96,6 +287,44 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
detalj: arbetsorder ? undefined : "Ärendet startades utan skannad arbetsorder.",
|
||||
});
|
||||
|
||||
// Pre-diagnostiken ingår i grinden: historik + ingående mätarställning
|
||||
// är obligatoriska (dokumenterade eller motiverade).
|
||||
const pre = preDiagnostik(arende);
|
||||
const historik = pre.find((r) => r.id === "historik")!;
|
||||
rader.push({
|
||||
id: "historik",
|
||||
rubrik: "Fordonshistorik kontrollerad eller motiverad",
|
||||
ok: historik.klar,
|
||||
kravs: true,
|
||||
detalj: historik.klar ? historik.varning : "Kontrollera historiken eller dokumentera varför det inte gått.",
|
||||
});
|
||||
const matIn = pre.find((r) => r.id === "matarstallning_in")!;
|
||||
rader.push({
|
||||
id: "matarstallning_in",
|
||||
rubrik: "Ingående mätarställning dokumenterad",
|
||||
ok: matIn.klar,
|
||||
kravs: true,
|
||||
detalj: matIn.klar ? undefined : "Fotografera instrumentpanelen (eller dokumentera undantag).",
|
||||
});
|
||||
const felb = pre.find((r) => r.id === "felbeskrivning")!;
|
||||
rader.push({
|
||||
id: "felbeskrivning_verifierad",
|
||||
rubrik: "Kundens felbeskrivning verifierad",
|
||||
ok: felb.klar,
|
||||
kravs: false,
|
||||
detalj: felb.klar ? undefined : "Bekräfta att kundens beskrivning är korrekt återgiven.",
|
||||
});
|
||||
|
||||
// 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({
|
||||
id: "matarstallning_ut",
|
||||
rubrik: "Utgående mätarställning dokumenterad",
|
||||
ok: matUt,
|
||||
kravs: avslutat,
|
||||
detalj: matUt ? undefined : "Fotografera instrumentpanelen när arbetet är klart.",
|
||||
});
|
||||
|
||||
// Metodikens kontroller: evidens eller dokumenterat undantag per kontroll.
|
||||
const hanterade = new Map<string, { undantag?: string }>();
|
||||
for (const h of handelser) {
|
||||
@@ -134,6 +363,19 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined,
|
||||
});
|
||||
|
||||
// Compliance Engine: ärendetypens regelpaket.
|
||||
const typ = arendetyp(arende);
|
||||
for (const krav of COMPLIANCE_REGLER[typ] ?? []) {
|
||||
const ok = krav.uppfyllt(arende);
|
||||
rader.push({
|
||||
id: krav.id,
|
||||
rubrik: `${typ}: ${krav.rubrik}`,
|
||||
ok,
|
||||
kravs: true,
|
||||
detalj: ok ? undefined : krav.detaljVidBrist,
|
||||
});
|
||||
}
|
||||
|
||||
const hypoteser = handelser.filter((h) => h.typ === "hypotes").length;
|
||||
rader.push({
|
||||
id: "hypoteser",
|
||||
@@ -159,3 +401,19 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
export function grindGodkand(arende: Arende, metodik: Metodik): boolean {
|
||||
return kvalitetsgrind(arende, metodik).every((rad) => rad.ok || !rad.kravs);
|
||||
}
|
||||
|
||||
// ---- 6. Traceability Engine -------------------------------------------
|
||||
|
||||
// Spårbarhetspaketet som följer med varje export: regelverkets version,
|
||||
// ärendets evidensnivå och samtliga evidensposter med innehållshash.
|
||||
// Tillsammans med den append-only-loggen kan varje slutsats härledas:
|
||||
// vilken bild, vilken mätning, vilken tekniker, vilken regel, när.
|
||||
export function sparbarhetspaket(arende: Arende, metodik: Metodik) {
|
||||
return {
|
||||
ecmVersion: ECM_VERSION,
|
||||
arendetyp: arendetyp(arende),
|
||||
evidensniva: evidensNiva(arende),
|
||||
kvalitetsgrind: kvalitetsgrind(arende, metodik).map(({ id, rubrik, ok, kravs }) => ({ id, rubrik, ok, kravs })),
|
||||
evidensposter: evidensposter(arende),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,46 @@ export function ansvarig(arende: Arende): string | undefined {
|
||||
return namn;
|
||||
}
|
||||
|
||||
// Ärendeidentiteten (Case Identity): registreras en gång och återanvänds
|
||||
// i felsökningsvyn, Live Share, slutrapporten och exporten. Fordons-
|
||||
// objektet är den röda tråden — det ska alltid vara omedelbart tydligt
|
||||
// vilket fordon och vilket ärende informationen avser.
|
||||
export interface Arendeidentitet {
|
||||
nummer: number;
|
||||
arbetsorder?: string;
|
||||
claim?: string;
|
||||
skadenummer?: string;
|
||||
identifierare?: string;
|
||||
vin?: string;
|
||||
beskrivning?: string;
|
||||
miltal?: string;
|
||||
kund?: string;
|
||||
ansvarig?: string;
|
||||
avslutat: boolean;
|
||||
}
|
||||
|
||||
export function arendeidentitet(arende: Arende): Arendeidentitet {
|
||||
const o = objekt(arende);
|
||||
let miltal = o?.miltal;
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ === "matarstallning" && h.lage === "ingaende" && !h.undantag) miltal = h.varde;
|
||||
}
|
||||
return {
|
||||
nummer: arende.nummer,
|
||||
arbetsorder: o?.arbetsorder,
|
||||
claim: o?.claim,
|
||||
skadenummer: o?.skadenummer,
|
||||
identifierare: o?.identifierare,
|
||||
vin: o?.vin,
|
||||
beskrivning: o?.beskrivning,
|
||||
miltal,
|
||||
kund: o?.kund,
|
||||
ansvarig: ansvarig(arende),
|
||||
avslutat: arAvslutat(arende),
|
||||
};
|
||||
}
|
||||
|
||||
export function arAvslutat(arende: Arende): boolean {
|
||||
return arende.handelser.some((p) => p.handelse.typ === "arende_avslutat");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
||||
import { nastaSteg } from "@/felsokning/metodik";
|
||||
import {
|
||||
arAvslutat,
|
||||
arendeidentitet,
|
||||
brief,
|
||||
felbeskrivning,
|
||||
formateraTid,
|
||||
@@ -35,7 +36,19 @@ import {
|
||||
type InstrumentTolkning,
|
||||
} from "@/felsokning/ai";
|
||||
import { byggDemoInstrument } from "@/felsokning/demo";
|
||||
import { ECM_VERSION, UNDANTAGSORSAKER, grindGodkand, kvalitetsgrind } from "@/felsokning/ecm";
|
||||
import {
|
||||
ARENDETYPER,
|
||||
ECM_VERSION,
|
||||
MARKOR_FELBESKRIVNING_VERIFIERAD,
|
||||
MARKOR_INGA_TIDIGA_OBSERVATIONER,
|
||||
MARKOR_TIDIGA_OBSERVATIONER_KLARA,
|
||||
UNDANTAGSORSAKER,
|
||||
arendetyp,
|
||||
grindGodkand,
|
||||
kvalitetsgrind,
|
||||
preDiagnostik,
|
||||
sparbarhetspaket,
|
||||
} from "@/felsokning/ecm";
|
||||
import { FelsokningSkal, NivaBadge, Panel, StorKnapp, TextFalt } from "@/felsokning/ui";
|
||||
import { skalaNerFoto, tidKlockslag } from "@/felsokning/format";
|
||||
|
||||
@@ -127,6 +140,10 @@ export default function ArendeSida() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* Ärendeidentiteten: registreras en gång, alltid synlig — det ska
|
||||
aldrig råda tvekan om vilket fordon och ärende som avses. */}
|
||||
<IdentitetsRad arende={arende} skicka={skicka} />
|
||||
|
||||
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
|
||||
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
|
||||
skärmar: flikrad + en kolumn. */}
|
||||
@@ -168,6 +185,238 @@ export default function ArendeSida() {
|
||||
);
|
||||
}
|
||||
|
||||
// Nyckel/värde-rader för identitetspanelerna (rapport + delade vyer).
|
||||
function identitetsRader(rader: [string, string | undefined][]) {
|
||||
return rader
|
||||
.filter(([, varde]) => varde)
|
||||
.map(([etikett, varde]) => (
|
||||
<div key={etikett} className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
<span className="text-[#4A5560]">{etikett}</span>
|
||||
<span className="text-right font-medium">{varde}</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
// Ärendeidentiteten (Case Identity): kompakt rad med fordonsobjektet och
|
||||
// ärendereferenserna — alltid synlig i arbetsytan. Ärendetypen väljs här
|
||||
// och styr vilka dokumentationskrav ECM ställer.
|
||||
function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
|
||||
const idn = arendeidentitet(arende);
|
||||
const typ = arendetyp(arende);
|
||||
const del = (etikett: string, varde?: string) =>
|
||||
varde ? (
|
||||
<span className="whitespace-nowrap">
|
||||
{etikett && <span className="text-[#707070]">{etikett} </span>}
|
||||
<span className="font-semibold text-[#1A1A1A]">{varde}</span>
|
||||
</span>
|
||||
) : null;
|
||||
return (
|
||||
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
|
||||
{del("AO", idn.arbetsorder)}
|
||||
{del("Claim", idn.claim)}
|
||||
{del("Skadenr", idn.skadenummer)}
|
||||
{del("", idn.beskrivning)}
|
||||
{del("Regnr", idn.identifierare)}
|
||||
{del("VIN", idn.vin)}
|
||||
{del("Miltal", idn.miltal)}
|
||||
{del("Ansvarig", idn.ansvarig)}
|
||||
<label className="ml-auto flex items-center gap-1">
|
||||
<span className="text-[#707070]">Ärendetyp</span>
|
||||
<select
|
||||
value={typ}
|
||||
disabled={idn.avslutat}
|
||||
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
|
||||
className="rounded border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
|
||||
>
|
||||
{ARENDETYPER.map((t) => (
|
||||
<option key={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Mätarställning in/ut: instrumentpanelen fotograferas, bildtolkningen
|
||||
// föreslår värdet och teknikern bekräftar. Fotot blir den officiella
|
||||
// mätarställningen (E2-evidens). Undantag kräver dokumenterad orsak.
|
||||
function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; skicka: (h: Handelse) => void }) {
|
||||
const filRef = useRef<HTMLInputElement>(null);
|
||||
const [foto, setFoto] = useState<string | null>(null);
|
||||
const [varde, setVarde] = useState("");
|
||||
const [laser, setLaser] = useState(false);
|
||||
const [demo, setDemo] = useState(false);
|
||||
|
||||
const fota = async (fil: File) => {
|
||||
setLaser(true);
|
||||
const dataUrl = await skalaNerFoto(fil);
|
||||
setFoto(dataUrl);
|
||||
try {
|
||||
const res = await lasAvInstrument(dataUrl);
|
||||
const km = res?.tolkning.varden.find((v) => /mätar|odo|km|mil/i.test(`${v.beskrivning} ${v.enhet ?? ""}`)) ?? res?.tolkning.varden[0];
|
||||
if (km) setVarde(`${km.varde}${km.enhet ? ` ${km.enhet}` : ""}`);
|
||||
if (!res) {
|
||||
setDemo(true);
|
||||
setVarde(lage === "ingaende" ? "84 320 km" : "84 512 km");
|
||||
}
|
||||
} catch {
|
||||
// Avläsningen kunde inte nås — värdet anges manuellt.
|
||||
}
|
||||
setLaser(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
ref={filRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const fil = e.target.files?.[0];
|
||||
if (fil) fota(fil);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{!foto && !laser && (
|
||||
<StorKnapp variant="sekundar" onClick={() => filRef.current?.click()}>
|
||||
📷 Fotografera instrumentpanelen
|
||||
</StorKnapp>
|
||||
)}
|
||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av mätarställningen …</p>}
|
||||
{foto && !laser && (
|
||||
<div className="mt-2">
|
||||
{demo && (
|
||||
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
Demo-avläsning — bildtolkningen kräver inloggning. Kontrollera värdet.
|
||||
</p>
|
||||
)}
|
||||
<img src={foto} alt="Instrumentpanel" className="mb-2 max-h-40 rounded border border-[#C6C6C6]" />
|
||||
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "ingående" : "utgående"})`} varde={varde} satt={setVarde} platshallare="T.ex. 84 320 km" />
|
||||
<StorKnapp
|
||||
disabled={!varde.trim()}
|
||||
onClick={() => {
|
||||
skicka({ typ: "matarstallning", lage, varde: varde.trim(), dataUrl: foto });
|
||||
setFoto(null);
|
||||
setVarde("");
|
||||
}}
|
||||
>
|
||||
Spara mätarställning
|
||||
</StorKnapp>
|
||||
</div>
|
||||
)}
|
||||
<Undantag vidUndantag={(orsak) => skicka({ typ: "matarstallning", lage, varde: "", undantag: orsak })} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-Diagnostic Validation: ingen felsökning påbörjas förrän
|
||||
// grundkontrollerna är genomförda — eller dokumenterat motiverade.
|
||||
function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
|
||||
const rader = preDiagnostik(arende);
|
||||
const rad = (id: string) => rader.find((r) => r.id === id)!;
|
||||
const [historikVal, setHistorikVal] = useState<"" | "ja" | "nej">("");
|
||||
const [historikText, setHistorikText] = useState("");
|
||||
const fb = felbeskrivning(arende);
|
||||
const harTidiga = arende.handelser.some((p) => p.handelse.typ === "observation" || p.handelse.typ === "foto");
|
||||
|
||||
return (
|
||||
<Panel rubrik="Pre-diagnostik — innan felsökningen börjar">
|
||||
{rader.map((r) => (
|
||||
<p key={r.id} className="py-0.5 text-[13px]">
|
||||
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? "✅" : "☐"}</span> {r.rubrik}
|
||||
{r.varning && <span className="ml-1 text-[11px] font-semibold text-[#9A6700]">⚠ {r.varning}</span>}
|
||||
</p>
|
||||
))}
|
||||
|
||||
{!rad("historik").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Har fordonets historik kontrollerats? (tidigare arbeten, återkommande fel, TSB, kampanjer)
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant={historikVal === "ja" ? "primar" : "sekundar"} onClick={() => setHistorikVal("ja")}>
|
||||
Ja — kontrollerad
|
||||
</StorKnapp>
|
||||
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
||||
Nej
|
||||
</StorKnapp>
|
||||
</div>
|
||||
{historikVal && (
|
||||
<>
|
||||
<TextFalt
|
||||
label={historikVal === "ja" ? "Relevanta tidigare arbeten (valfritt — orsakskedja)" : "Orsak till att historiken inte kontrollerats (obligatoriskt)"}
|
||||
varde={historikText}
|
||||
satt={setHistorikText}
|
||||
platshallare={historikVal === "ja" ? "T.ex. Vattenpump bytt för 8 mån sedan — misstänkt läckage samma område" : ""}
|
||||
rost
|
||||
/>
|
||||
<StorKnapp
|
||||
disabled={historikVal === "nej" && !historikText.trim()}
|
||||
onClick={() => {
|
||||
skicka({ typ: "historik_kontrollerad", kontrollerad: historikVal === "ja", kommentar: historikText.trim() || undefined });
|
||||
setHistorikVal("");
|
||||
setHistorikText("");
|
||||
}}
|
||||
>
|
||||
Dokumentera
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rad("matarstallning_in").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Ingående mätarställning — fotografera instrumentpanelen
|
||||
</p>
|
||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rad("felbeskrivning").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Är kundens felbeskrivning korrekt återgiven?
|
||||
</p>
|
||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||
Stämmer — verifierad
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Ytterligare symptom dokumenteras som separata observationer — blanda dem inte med kundens beskrivning.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rad("tidiga_observationer").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Något ytterligare vid mottagandet? (reparationsspår, modifieringar, skador, läckage, korrosion …)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}>
|
||||
Inga ytterligare observationer
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
variant="sekundar"
|
||||
disabled={!harTidiga}
|
||||
onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })}
|
||||
>
|
||||
Observationerna är dokumenterade
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Dokumentera med foto eller observation i panelen nedan — knappen låses upp när något loggats.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
// Navigationsträd (vänsterkolumnen): ärendets vyer plus metodikens steg
|
||||
// med status — som en mappstruktur i ett klassiskt verkstadssystem.
|
||||
function VyTrad({
|
||||
@@ -370,6 +619,18 @@ function GuideFlik({
|
||||
);
|
||||
}
|
||||
|
||||
// Pre-Diagnostic Validation: metodiken öppnas först när grund-
|
||||
// kontrollerna är genomförda eller dokumenterat motiverade.
|
||||
if (!preDiagnostik(arende).every((r) => r.klar)) {
|
||||
return (
|
||||
<>
|
||||
<KategoriRad arende={arende} skicka={skicka} />
|
||||
<PreDiagnostikPanel arende={arende} skicka={skicka} />
|
||||
<SnabbDokumentation skicka={skicka} paSparad={fragaAiOmDokumentation} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<KategoriRad arende={arende} skicka={skicka} />
|
||||
@@ -422,6 +683,14 @@ function GuideFlik({
|
||||
|
||||
<SnabbDokumentation skicka={skicka} paSparad={fragaAiOmDokumentation} />
|
||||
|
||||
{/* 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") && (
|
||||
<Panel rubrik="Utgående mätarställning — inför avslut">
|
||||
<MatarstallningSteg lage="utgaende" skicka={skicka} />
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||
Lämna över arbete
|
||||
@@ -1174,6 +1443,17 @@ function RapportFlik({
|
||||
const b = brief(arende, metodik, nu);
|
||||
const bilder = foton(arende);
|
||||
const fordelning = tidsfordelningsRader(arende, nu);
|
||||
const idn = arendeidentitet(arende);
|
||||
const typ = arendetyp(arende);
|
||||
let matIn: string | undefined;
|
||||
let matUt: string | undefined;
|
||||
for (const p of arende.handelser) {
|
||||
const h = p.handelse;
|
||||
if (h.typ === "matarstallning" && !h.undantag) {
|
||||
if (h.lage === "ingaende") matIn = h.varde;
|
||||
else matUt = h.varde;
|
||||
}
|
||||
}
|
||||
// Kategoribyten är interna; hypoteser och AI-dialogen är arbetsmaterial
|
||||
// och ingår inte i det som delas med kund.
|
||||
const kundposter = arende.handelser.filter(
|
||||
@@ -1181,11 +1461,13 @@ function RapportFlik({
|
||||
);
|
||||
|
||||
// Alla exporter bygger på samma händelselogg och versionsmärks:
|
||||
// version = antal händelser vid exporttillfället.
|
||||
// version = antal händelser vid exporttillfället. Spårbarhetspaketet
|
||||
// (ECM-version, evidensposter med hash, grindstatus) följer med.
|
||||
const exporteraJson = () => {
|
||||
const version = arende.handelser.length;
|
||||
const data = {
|
||||
export: { format: "JSON", version, exporteradAv: anvandare, tidpunkt: new Date().toISOString() },
|
||||
ecm: sparbarhetspaket(arende, metodik),
|
||||
arende,
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
@@ -1262,13 +1544,30 @@ function RapportFlik({
|
||||
Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare).
|
||||
</p>
|
||||
</Panel>
|
||||
<Panel rubrik={`Ärende #${arende.nummer}`}>
|
||||
{b.objekt && (
|
||||
<p className="text-[14px] font-semibold">
|
||||
{b.objekt.beskrivning} · {b.objekt.identifierare}
|
||||
</p>
|
||||
)}
|
||||
{b.felbeskrivning && <p className="mt-1 text-[14px]">Felbeskrivning: ”{b.felbeskrivning}”</p>}
|
||||
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
|
||||
återanvänd här automatiskt. */}
|
||||
<Panel rubrik="Ärendeinformation">
|
||||
{identitetsRader([
|
||||
["Ärende", `#${arende.nummer}`],
|
||||
["Arbetsorder", idn.arbetsorder],
|
||||
["Claim-/garantinr", idn.claim],
|
||||
["Skadenummer", idn.skadenummer],
|
||||
["Ärendetyp", typ],
|
||||
["Ansvarig tekniker", idn.ansvarig],
|
||||
])}
|
||||
</Panel>
|
||||
<Panel rubrik="Fordonsinformation">
|
||||
{identitetsRader([
|
||||
["Fordon", idn.beskrivning],
|
||||
["Regnr", idn.identifierare],
|
||||
["VIN", idn.vin],
|
||||
["Mätarställning in", matIn],
|
||||
["Mätarställning ut", matUt],
|
||||
["Kund", idn.kund],
|
||||
])}
|
||||
</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>
|
||||
{fordelning.length > 0 && (
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -83,6 +83,12 @@ export default function NyttArende() {
|
||||
identifieringsmetod: regnr ? "Regnr" : vin ? "VIN" : "Manuell inmatning",
|
||||
beskrivning: beskr || "Se arbetsorder",
|
||||
kund: v("kund_namn"),
|
||||
// Ärendeidentiteten: registreras en gång här, återanvänds i alla vyer.
|
||||
vin: vin?.toUpperCase(),
|
||||
miltal: v("fordon_matarstallning"),
|
||||
arbetsorder: v("ao_nummer"),
|
||||
claim: v("ao_claim"),
|
||||
skadenummer: v("ao_skadenummer"),
|
||||
};
|
||||
const felText = v("felbeskrivning") ?? "Se skannad arbetsorder";
|
||||
let metodikId: string | undefined;
|
||||
|
||||
@@ -71,7 +71,7 @@ const ARBETSORDER_FALT_ID = [
|
||||
"fordon_regnr", "fordon_vin", "fordon_marke", "fordon_modell",
|
||||
"fordon_motor", "fordon_arsmodell", "fordon_matarstallning",
|
||||
"fordon_motorkod", "fordon_vaxellada",
|
||||
"ao_nummer", "ao_referens", "ao_serviceradgivare", "ao_bokningsdatum",
|
||||
"ao_nummer", "ao_claim", "ao_skadenummer", "ao_referens", "ao_serviceradgivare", "ao_bokningsdatum",
|
||||
"felbeskrivning",
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user