From a37cc027ae2b30b6cfb3861915ed12007fc20e71 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 09:38:44 +0000 Subject: [PATCH] Personbil-etikett i granskningen + ECM Knowledge Library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arbetsordergranskningens fältgrupp heter nu Personbil (tidigare Fordon), liksom raden i rapportens fordonsinformation. ECM Knowledge Library: regelbiblioteket är nu serverdistribuerat — regler uppdateras i driften utan appändring, precis som rekommenderat: - Compliance-reglerna är deklarativ data (krav-typ i stället för kod): services/plattform/ecm-regler.json serveras på GET /api/ecm/regler, i klustret utbytbar via ConfigMap (ECM_REGLER_FIL) - Klienten hämtar paketet vid sidladdning, cachar det och faller tillbaka till inbyggt standardpaket offline; trasiga paket och okända krav-typer filtreras (normaliseraRegelpaket) - Kvalitetsgrinden, orsakskategorierna, evidenskällorna och undantagsorsakerna läses ur det aktiva paketet; spårbarhetspaketet bär regelpaketets version - Testat: paritet server/standardpaket, fallback, att ett uppdaterat paket ändrar grinden utan appändring; integrationstestet verifierar serveringen (57 vitest-tester gröna) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt --- docs/MVP.md | 2 +- docs/moduler/evidensmotor.md | 14 ++- services/plattform/Dockerfile | 2 +- services/plattform/ecm-regler.json | 59 +++++++++ services/plattform/integrationstest.sh | 5 + services/plattform/openapi.yaml | 23 ++++ services/plattform/server.mjs | 18 +++ src/felsokning/__tests__/ai.test.ts | 3 +- src/felsokning/__tests__/ecm.test.ts | 55 +++++++++ src/felsokning/ai.ts | 20 +-- src/felsokning/demo.ts | 12 +- src/felsokning/ecm.ts | 165 +++++++++++++++++++------ src/pages/felsokning/ArendeSida.tsx | 16 +-- src/pages/felsokning/NyttArende.tsx | 2 +- 14 files changed, 328 insertions(+), 68 deletions(-) create mode 100644 services/plattform/ecm-regler.json diff --git a/docs/MVP.md b/docs/MVP.md index 7058f5b..2a28d0f 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -34,7 +34,7 @@ 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) | ✅ 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). | +| 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). **ECM Knowledge Library**: compliance-reglerna är deklarativ data som serveras av plattformen (`GET /api/ecm/regler`, utbytbar via ConfigMap) — uppdateras i driften utan appändring; klienten cachar och faller tillbaka till inbyggt standardpaket offline. | | 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. | | 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. | diff --git a/docs/moduler/evidensmotor.md b/docs/moduler/evidensmotor.md index 2d6e90c..a581784 100644 --- a/docs/moduler/evidensmotor.md +++ b/docs/moduler/evidensmotor.md @@ -48,10 +48,16 @@ väljs i identitetsraden och loggas (`arendetyp_satt`): | Reklamation | Historik och tidigare försök kontrollerade | | Begagnatgaranti | Miltal | -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. +**ECM Knowledge Library är implementerat**: reglerna är deklarativa data +(krav-typ, inte kod) och distribueras från plattformen via +`GET /api/ecm/regler` (`services/plattform/ecm-regler.json` — i klustret +utbytbar via ConfigMap och miljövariabeln `ECM_REGLER_FIL`). Klienten +hämtar paketet vid sidladdning, cachar det och faller tillbaka till sitt +inbyggda standardpaket offline; trasiga paket och okända krav-typer +filtreras. Regelpaketets version följer med i varje spårbarhetspaket. +Nya regler — garantivillkor per tillverkare, försäkringsbolagens krav, +reklamationslagstiftning, OEM-kontrollpunkter — läggs till i driften +utan att applikationen byggs om. ### 4. Validation Engine Inga påståenden utan underlag, i tre lager: (a) orkesterns grundprompt — diff --git a/services/plattform/Dockerfile b/services/plattform/Dockerfile index 04ea18b..2715dad 100644 --- a/services/plattform/Dockerfile +++ b/services/plattform/Dockerfile @@ -4,7 +4,7 @@ FROM node:22-alpine WORKDIR /app COPY package.json ./ RUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force -COPY server.mjs openapi.yaml ./ +COPY server.mjs openapi.yaml ecm-regler.json ./ ENV NODE_ENV=production PORT=8080 USER node diff --git a/services/plattform/ecm-regler.json b/services/plattform/ecm-regler.json new file mode 100644 index 0000000..0a90a73 --- /dev/null +++ b/services/plattform/ecm-regler.json @@ -0,0 +1,59 @@ +{ + "version": "2.0", + "beskrivning": "ECM Knowledge Library — serverdistribuerat regelpaket. Uppdateras utan appändring: redigera filen (eller montera en ConfigMap över den i klustret) och starta om tjänsten. Klienten hämtar paketet vid inloggning och faller tillbaka till sitt inbyggda standardpaket offline.", + "arendetypRegler": { + "Garanti": [ + { "id": "garanti_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Garantiärenden kräver dokumenterad mätarställning." }, + { "id": "garanti_historik", "rubrik": "Servicehistorik kontrollerad", "krav": "historik", "detaljVidBrist": "Garantiärenden kräver kontrollerad servicehistorik." }, + { "id": "garanti_claim", "rubrik": "Claim-/garantinummer registrerat", "krav": "claim", "detaljVidBrist": "Ange claim-/garantinummer (läses ur arbetsordern)." } + ], + "Goodwill": [ + { "id": "goodwill_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Goodwillärenden kräver dokumenterad mätarställning." }, + { "id": "goodwill_historik", "rubrik": "Servicehistorik kontrollerad", "krav": "historik", "detaljVidBrist": "Goodwillärenden kräver kontrollerad servicehistorik." } + ], + "Försäkring": [ + { "id": "forsakring_skadenummer", "rubrik": "Skadenummer registrerat", "krav": "skadenummer", "detaljVidBrist": "Försäkringsärenden kräver skadenummer (läses ur arbetsordern)." }, + { "id": "forsakring_bildbevis", "rubrik": "Bildbevis finns", "krav": "foto", "detaljVidBrist": "Försäkringsärenden kräver bilddokumentation." } + ], + "Reklamation": [ + { "id": "reklamation_historik", "rubrik": "Historik och tidigare försök kontrollerade", "krav": "historik", "detaljVidBrist": "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." } + ], + "Begagnatgaranti": [ + { "id": "begagnat_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Begagnatgaranti kräver dokumenterad mätarställning." } + ] + }, + "undantagsorsaker": [ + "Komponenten är oåtkomlig", + "Fordonet kan inte lyftas säkert", + "Kunden avböjde demontering", + "Dålig sikt/åtkomst", + "Utrustning saknas" + ], + "orsakskategorier": [ + "Normalt slitage", + "Ålder", + "Körsträcka", + "Materialutmattning", + "Tillverkningsfel", + "Bristande underhåll", + "Felaktig tidigare reparation", + "Yttre påverkan", + "Korrosion", + "Överhettning", + "Förorening", + "Felaktig användning", + "Modifiering", + "Olycka eller skada", + "Okänd orsak" + ], + "underlagskallor": [ + "Foto", + "Video", + "Mätresultat", + "Diagnosutläsning", + "Tidigare historik", + "Servicehistorik", + "Teknisk dokumentation", + "Direkt observation" + ] +} diff --git a/services/plattform/integrationstest.sh b/services/plattform/integrationstest.sh index bedf023..953b242 100755 --- a/services/plattform/integrationstest.sh +++ b/services/plattform/integrationstest.sh @@ -166,6 +166,11 @@ curl -s -X POST "$BAS/api/delningar/$PARTNERKOD/aterkalla" -H "Authorization: Be KOD=$(curl -s -o /dev/null -w "%{http_code}" "$BAS/api/delad/$PARTNERKOD") kontroll "återkallad delning ger 404" "$KOD" "404" +# 10b. ECM Knowledge Library: regelpaketet serveras till inloggade klienter +REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J") +kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0" +kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Garanti.length')" "3" + # 11. API-first: OpenAPI-specen serveras live, utan inloggning SPEC=$(curl -s "$BAS/api/openapi.yaml") case "$SPEC" in diff --git a/services/plattform/openapi.yaml b/services/plattform/openapi.yaml index 06a240e..b8d6e0a 100644 --- a/services/plattform/openapi.yaml +++ b/services/plattform/openapi.yaml @@ -147,6 +147,29 @@ paths: "403": { $ref: "#/components/responses/Fel" } "409": { $ref: "#/components/responses/Fel" } + /api/ecm/regler: + get: + tags: [Drift] + summary: ECM Knowledge Library — aktuellt regelpaket + description: > + Serverdistribuerat, versionerat regelpaket (compliance-regler per + ärendetyp, orsakskategorier, evidenskällor, undantagsorsaker). + Uppdateras i driften utan appändring; klienten cachar och faller + tillbaka till sitt inbyggda standardpaket offline. + responses: + "200": + description: Regelpaketet. + content: + application/json: + schema: + type: object + properties: + version: { type: string } + arendetypRegler: { type: object, additionalProperties: true } + undantagsorsaker: { type: array, items: { type: string } } + orsakskategorier: { type: array, items: { type: string } } + underlagskallor: { type: array, items: { type: string } } + /api/organisation: get: tags: [Organisation] diff --git a/services/plattform/server.mjs b/services/plattform/server.mjs index 60286da..91a78d9 100644 --- a/services/plattform/server.mjs +++ b/services/plattform/server.mjs @@ -24,6 +24,15 @@ import pg from "pg"; // API-first: OpenAPI-specen är en versionerad artefakt och serveras live. const OPENAPI = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "openapi.yaml"), "utf8"); +// ECM Knowledge Library: regelpaketet är serverägd konfiguration — inte +// appkod. Uppdateras genom att byta filen (ECM_REGLER_FIL kan peka på en +// ConfigMap-mount i klustret) och starta om tjänsten; klienterna hämtar +// det nya paketet vid nästa inloggning/sidladdning. +const ECM_REGLER = readFileSync( + process.env.ECM_REGLER_FIL ?? join(dirname(fileURLToPath(import.meta.url)), "ecm-regler.json"), + "utf8", +); + const PORT = Number(process.env.PORT ?? 8080); const MAX_KROPP = 4 * 1024 * 1024; const TOKEN_LIVSTID_S = 12 * 60 * 60; @@ -266,6 +275,15 @@ export function skapaServer() { } } + // ECM Knowledge Library: aktuellt regelpaket för inloggade klienter. + if (req.method === "GET" && vag === "/api/ecm/regler") { + res.writeHead(200, { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }); + return res.end(ECM_REGLER); + } + // Organisationens inställningar: vad som visas när ett ärende // startas (objekttyper, identifieringsmetoder). Alla inloggade // läser; endast systemadministratören ändrar. diff --git a/src/felsokning/__tests__/ai.test.ts b/src/felsokning/__tests__/ai.test.ts index 2013842..f6d47ae 100644 --- a/src/felsokning/__tests__/ai.test.ts +++ b/src/felsokning/__tests__/ai.test.ts @@ -72,6 +72,7 @@ describe("AI-orkestern", () => { ["/api/auth/registrera", "/api/auth/registrera"], ["/api/auth/logga-in", "/api/auth/logga-in"], ["/api/anvandare", "/api/anvandare"], + ["/api/ecm/regler", "/api/ecm/regler"], ["/api/organisation", "/api/organisation"], ["/api/organisation/installningar", "/api/organisation/installningar"], ["/api/arenden", "/api/arenden"], @@ -151,7 +152,7 @@ describe("arbetsorderskanning", () => { expect(falt.map((f) => f.id)).toEqual(["fordon_regnr", "fordon_vin"]); expect(falt[0].konfidens).toBe(1); expect(falt[0].varde).toBe("abc123"); - expect(falt[0].grupp).toBe("Fordon"); + expect(falt[0].grupp).toBe("Personbil"); expect(falt[1].omrade?.x).toBe(0.5); expect(() => normaliseraArbetsorder({})).toThrow(); }); diff --git a/src/felsokning/__tests__/ecm.test.ts b/src/felsokning/__tests__/ecm.test.ts index 11f6020..3c0d2fa 100644 --- a/src/felsokning/__tests__/ecm.test.ts +++ b/src/felsokning/__tests__/ecm.test.ts @@ -254,3 +254,58 @@ describe("Felorsaksanalys (Root Cause Analysis)", () => { expect(orsak?.handelse.typ === "felorsak" && orsak.handelse.underlag.length > 0).toBe(true); }); }); + +describe("ECM Knowledge Library (regelpaket)", () => { + it("serverpaketet och det inbyggda standardpaketet är samma regler", async () => { + const { readFileSync } = await import("node:fs"); + const { STANDARD_REGELPAKET, normaliseraRegelpaket } = await import("../ecm"); + const server = normaliseraRegelpaket(JSON.parse(readFileSync("services/plattform/ecm-regler.json", "utf8"))); + expect(server.version).toBe(STANDARD_REGELPAKET.version); + expect(server.arendetypRegler).toEqual(STANDARD_REGELPAKET.arendetypRegler); + expect(server.orsakskategorier).toEqual(STANDARD_REGELPAKET.orsakskategorier); + }); + + it("trasiga paket faller tillbaka till standard, okända krav-typer filtreras", async () => { + const { STANDARD_REGELPAKET, normaliseraRegelpaket } = await import("../ecm"); + expect(normaliseraRegelpaket(null)).toEqual(STANDARD_REGELPAKET); + const paket = normaliseraRegelpaket({ + version: "2.1", + arendetypRegler: { + Garanti: [ + { id: "x", rubrik: "Ny regel", krav: "foto", detaljVidBrist: "Kräver foto." }, + { id: "y", rubrik: "Trasig", krav: "pahittad_kravtyp", detaljVidBrist: "…" }, + ], + }, + }); + expect(paket.version).toBe("2.1"); + expect(paket.arendetypRegler.Garanti).toHaveLength(1); + expect(paket.undantagsorsaker).toEqual(STANDARD_REGELPAKET.undantagsorsaker); + }); + + it("ett uppdaterat regelpaket ändrar kvalitetsgrinden utan appändring", async () => { + const { normaliseraRegelpaket } = await import("../ecm"); + localStorage.setItem( + "gf-ecm-regelpaket", + JSON.stringify( + normaliseraRegelpaket({ + version: "2.1-test", + arendetypRegler: { + "Privat kund": [ + { id: "test_foto", rubrik: "Foto krävs för alla privatkunder", krav: "foto", detaljVidBrist: "Testregel." }, + ], + }, + }), + ), + ); + try { + const utanFoto = byggArende([OBJEKT]); + const rad = kvalitetsgrind(utanFoto, VIBRATION_METODIK).find((r) => r.id === "test_foto"); + expect(rad?.ok).toBe(false); + expect(rad?.rubrik).toContain("Privat kund"); + // Spårbarheten bär paketversionen. + expect(sparbarhetspaket(utanFoto, VIBRATION_METODIK).regelpaketVersion).toBe("2.1-test"); + } finally { + localStorage.removeItem("gf-ecm-regelpaket"); + } + }); +}); diff --git a/src/felsokning/ai.ts b/src/felsokning/ai.ts index ee2792d..84c5d69 100644 --- a/src/felsokning/ai.ts +++ b/src/felsokning/ai.ts @@ -101,7 +101,7 @@ export function tolkaAiSvar(data: unknown): AiSvar { // (OCR + layoutförståelse) och returnerar strukturerade fält med // konfidens per värde. Bara osäkra fält behöver granskas. -export type ArbetsorderGrupp = "Kund" | "Fordon" | "Verkstad" | "Ärende"; +export type ArbetsorderGrupp = "Kund" | "Personbil" | "Verkstad" | "Ärende"; export interface TolkatFalt { id: string; @@ -118,15 +118,15 @@ export const ARBETSORDER_FALT: { id: string; grupp: ArbetsorderGrupp; etikett: s { id: "kund_foretag", grupp: "Kund", etikett: "Företag" }, { id: "kund_telefon", grupp: "Kund", etikett: "Telefon" }, { id: "kund_epost", grupp: "Kund", etikett: "E-post" }, - { id: "fordon_regnr", grupp: "Fordon", etikett: "Regnr" }, - { id: "fordon_vin", grupp: "Fordon", etikett: "VIN" }, - { id: "fordon_marke", grupp: "Fordon", etikett: "Märke" }, - { id: "fordon_modell", grupp: "Fordon", etikett: "Modell" }, - { id: "fordon_motor", grupp: "Fordon", etikett: "Motor" }, - { id: "fordon_arsmodell", grupp: "Fordon", etikett: "Årsmodell" }, - { id: "fordon_matarstallning", grupp: "Fordon", etikett: "Mätarställning" }, - { id: "fordon_motorkod", grupp: "Fordon", etikett: "Motorkod" }, - { id: "fordon_vaxellada", grupp: "Fordon", etikett: "Växellåda" }, + { id: "fordon_regnr", grupp: "Personbil", etikett: "Regnr" }, + { id: "fordon_vin", grupp: "Personbil", etikett: "VIN" }, + { id: "fordon_marke", grupp: "Personbil", etikett: "Märke" }, + { id: "fordon_modell", grupp: "Personbil", etikett: "Modell" }, + { id: "fordon_motor", grupp: "Personbil", etikett: "Motor" }, + { id: "fordon_arsmodell", grupp: "Personbil", etikett: "Årsmodell" }, + { id: "fordon_matarstallning", grupp: "Personbil", etikett: "Mätarställning" }, + { id: "fordon_motorkod", grupp: "Personbil", etikett: "Motorkod" }, + { id: "fordon_vaxellada", grupp: "Personbil", 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" }, diff --git a/src/felsokning/demo.ts b/src/felsokning/demo.ts index c37da03..8b5d993 100644 --- a/src/felsokning/demo.ts +++ b/src/felsokning/demo.ts @@ -151,12 +151,12 @@ export function byggDemoTolkning(): TolkatFalt[] { return [ { id: "kund_namn", etikett: "Namn", grupp: "Kund", varde: "Anders Svensson", konfidens: 0.98 }, { id: "kund_telefon", etikett: "Telefon", grupp: "Kund", varde: "070-123 45 67", konfidens: 0.91, omrade: { x: 0.08, y: 0.22, bredd: 0.3, hojd: 0.04 } }, - { id: "fordon_regnr", etikett: "Regnr", grupp: "Fordon", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } }, - { id: "fordon_vin", etikett: "VIN", grupp: "Fordon", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } }, - { id: "fordon_marke", etikett: "Märke", grupp: "Fordon", varde: "Volvo", konfidens: 0.99 }, - { id: "fordon_modell", etikett: "Modell", grupp: "Fordon", varde: "XC60 D4", konfidens: 0.97 }, - { id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Fordon", varde: "2019", konfidens: 0.96 }, - { id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Fordon", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } }, + { id: "fordon_regnr", etikett: "Regnr", grupp: "Personbil", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } }, + { id: "fordon_vin", etikett: "VIN", grupp: "Personbil", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } }, + { id: "fordon_marke", etikett: "Märke", grupp: "Personbil", varde: "Volvo", konfidens: 0.99 }, + { id: "fordon_modell", etikett: "Modell", grupp: "Personbil", varde: "XC60 D4", konfidens: 0.97 }, + { id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Personbil", varde: "2019", konfidens: 0.96 }, + { id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Personbil", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } }, { id: "ao_nummer", etikett: "Arbetsordernr", grupp: "Verkstad", varde: "AO-2496", konfidens: 0.99 }, { id: "ao_serviceradgivare", etikett: "Servicerådgivare", grupp: "Verkstad", varde: "M. Lindqvist", konfidens: 0.82 }, { id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Ärende", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } }, diff --git a/src/felsokning/ecm.ts b/src/felsokning/ecm.ts index cc6505b..3111fd9 100644 --- a/src/felsokning/ecm.ts +++ b/src/felsokning/ecm.ts @@ -241,13 +241,31 @@ export function arendetyp(arende: Arende): Arendetyp { return typ; } -interface ComplianceKrav { +// ---- ECM Knowledge Library -------------------------------------------- +// Regelpaketet är data, inte kod: reglerna är deklarativa (krav-typ i +// stället för funktioner) och kan därför distribueras från servern +// (GET /api/ecm/regler, filen services/plattform/ecm-regler.json — i +// klustret utbytbar via ConfigMap) och uppdateras utan appändring. +// Klienten cachar senast hämtade paket och faller tillbaka till det +// inbyggda standardpaketet offline/i lokalt läge. + +export type ComplianceKravTyp = "miltal" | "historik" | "claim" | "skadenummer" | "foto"; + +export interface ComplianceRegel { id: string; rubrik: string; - uppfyllt: (arende: Arende) => boolean; + krav: ComplianceKravTyp; detaljVidBrist: string; } +export interface RegelPaket { + version: string; + arendetypRegler: Partial>; + undantagsorsaker: string[]; + orsakskategorier: string[]; + underlagskallor: 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]; @@ -255,37 +273,107 @@ function objektFalt(arende: Arende, falt: "miltal" | "claim" | "skadenummer" | " 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> = { - 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." }, - ], +// Krav-typ → verifiering mot händelseloggen. Nya krav-typer läggs till +// här; vilka regler som använder dem styrs helt av regelpaketet. +const KRAV_KONTROLLER: Record boolean> = { + miltal: (a) => + a.handelser.some((p) => p.handelse.typ === "matarstallning" && !p.handelse.undantag) || + !!objektFalt(a, "miltal"), + historik: (a) => a.handelser.some((p) => p.handelse.typ === "historik_kontrollerad" && p.handelse.kontrollerad), + claim: (a) => !!objektFalt(a, "claim"), + skadenummer: (a) => !!objektFalt(a, "skadenummer"), + foto: (a) => a.handelser.some((p) => p.handelse.typ === "foto"), }; +// Inbyggt standardpaket = fallback när servern inte nåtts ännu. +export const STANDARD_REGELPAKET: RegelPaket = { + version: ECM_VERSION, + arendetypRegler: { + Garanti: [ + { id: "garanti_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Garantiärenden kräver dokumenterad mätarställning." }, + { id: "garanti_historik", rubrik: "Servicehistorik kontrollerad", krav: "historik", detaljVidBrist: "Garantiärenden kräver kontrollerad servicehistorik." }, + { id: "garanti_claim", rubrik: "Claim-/garantinummer registrerat", krav: "claim", detaljVidBrist: "Ange claim-/garantinummer (läses ur arbetsordern)." }, + ], + Goodwill: [ + { id: "goodwill_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Goodwillärenden kräver dokumenterad mätarställning." }, + { id: "goodwill_historik", rubrik: "Servicehistorik kontrollerad", krav: "historik", detaljVidBrist: "Goodwillärenden kräver kontrollerad servicehistorik." }, + ], + Försäkring: [ + { id: "forsakring_skadenummer", rubrik: "Skadenummer registrerat", krav: "skadenummer", detaljVidBrist: "Försäkringsärenden kräver skadenummer (läses ur arbetsordern)." }, + { id: "forsakring_bildbevis", rubrik: "Bildbevis finns", krav: "foto", detaljVidBrist: "Försäkringsärenden kräver bilddokumentation." }, + ], + Reklamation: [ + { id: "reklamation_historik", rubrik: "Historik och tidigare försök kontrollerade", krav: "historik", detaljVidBrist: "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." }, + ], + Begagnatgaranti: [ + { id: "begagnat_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Begagnatgaranti kräver dokumenterad mätarställning." }, + ], + }, + undantagsorsaker: UNDANTAGSORSAKER, + orsakskategorier: ORSAKSKATEGORIER, + underlagskallor: UNDERLAGSKALLOR, +}; + +const REGELPAKET_NYCKEL = "gf-ecm-regelpaket"; + +// Validerar och normaliserar ett paket från servern/cachen — trasiga +// eller ofullständiga paket faller tillbaka till standard per fält. +export function normaliseraRegelpaket(data: unknown): RegelPaket { + const d = (data ?? {}) as Partial; + const lista = (varden: unknown, standard: string[]) => + Array.isArray(varden) && varden.length > 0 && varden.every((v) => typeof v === "string") + ? (varden as string[]) + : standard; + const regler: RegelPaket["arendetypRegler"] = {}; + if (d.arendetypRegler && typeof d.arendetypRegler === "object") { + for (const [typ, rader] of Object.entries(d.arendetypRegler)) { + if (!Array.isArray(rader)) continue; + const giltiga = rader.filter( + (r): r is ComplianceRegel => + !!r && typeof r.id === "string" && typeof r.rubrik === "string" && + typeof r.detaljVidBrist === "string" && typeof r.krav === "string" && r.krav in KRAV_KONTROLLER, + ); + if (giltiga.length > 0) regler[typ] = giltiga; + } + } + return { + version: typeof d.version === "string" && d.version ? d.version : STANDARD_REGELPAKET.version, + arendetypRegler: Object.keys(regler).length > 0 ? regler : STANDARD_REGELPAKET.arendetypRegler, + undantagsorsaker: lista(d.undantagsorsaker, STANDARD_REGELPAKET.undantagsorsaker), + orsakskategorier: lista(d.orsakskategorier, STANDARD_REGELPAKET.orsakskategorier), + underlagskallor: lista(d.underlagskallor, STANDARD_REGELPAKET.underlagskallor), + }; +} + +export function aktivtRegelpaket(): RegelPaket { + try { + const rad = localStorage.getItem(REGELPAKET_NYCKEL); + return rad ? normaliseraRegelpaket(JSON.parse(rad)) : STANDARD_REGELPAKET; + } catch { + return STANDARD_REGELPAKET; + } +} + +// Hämtar aktuellt regelpaket från plattformen (inloggat läge) och cachar +// det. Anropas vid sidladdning — misslyckas hämtningen gäller cachen +// eller standardpaketet. +export async function hamtaRegelpaket(): Promise { + try { + const { plattformAktiv, plattformToken, plattformFetch } = await import("./plattform"); + if (plattformAktiv() && plattformToken()) { + const res = await plattformFetch("/api/ecm/regler"); + if (res.ok) { + const paket = normaliseraRegelpaket(await res.json()); + localStorage.setItem(REGELPAKET_NYCKEL, JSON.stringify(paket)); + return paket; + } + } + } catch { + // Nätverksfel: cache/standard gäller. + } + return aktivtRegelpaket(); +} + // ---- 4. Validation Engine --------------------------------------------- // Kärnprincipen — inga påståenden utan underlag — verkar i tre lager: @@ -482,16 +570,18 @@ 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. + // Compliance Engine: ärendetypens regler ur det aktiva regelpaketet + // (serverdistribuerat via ECM Knowledge Library, standard som fallback). + const paket = aktivtRegelpaket(); const typ = arendetyp(arende); - for (const krav of COMPLIANCE_REGLER[typ] ?? []) { - const ok = krav.uppfyllt(arende); + for (const regel of paket.arendetypRegler[typ] ?? []) { + const ok = KRAV_KONTROLLER[regel.krav](arende); rader.push({ - id: krav.id, - rubrik: `${typ}: ${krav.rubrik}`, + id: regel.id, + rubrik: `${typ}: ${regel.rubrik}`, ok, kravs: true, - detalj: ok ? undefined : krav.detaljVidBrist, + detalj: ok ? undefined : regel.detaljVidBrist, }); } @@ -530,6 +620,7 @@ export function grindGodkand(arende: Arende, metodik: Metodik): boolean { export function sparbarhetspaket(arende: Arende, metodik: Metodik) { return { ecmVersion: ECM_VERSION, + regelpaketVersion: aktivtRegelpaket().version, arendetyp: arendetyp(arende), evidensniva: evidensNiva(arende), kvalitetsgrind: kvalitetsgrind(arende, metodik).map(({ id, rubrik, ok, kravs }) => ({ id, rubrik, ok, kravs })), diff --git a/src/pages/felsokning/ArendeSida.tsx b/src/pages/felsokning/ArendeSida.tsx index e498c9c..b4f9e7c 100644 --- a/src/pages/felsokning/ArendeSida.tsx +++ b/src/pages/felsokning/ArendeSida.tsx @@ -42,14 +42,13 @@ import { MARKOR_FELBESKRIVNING_VERIFIERAD, MARKOR_INGA_TIDIGA_OBSERVATIONER, MARKOR_TIDIGA_OBSERVATIONER_KLARA, - ORSAKSKATEGORIER, - UNDANTAGSORSAKER, - UNDERLAGSKALLOR, + aktivtRegelpaket, arendetyp, felorsaker, granskaAvvikelse, grindGodkand, kvalitetsgrind, + hamtaRegelpaket, preDiagnostik, reproducering, reproduceringsText, @@ -115,6 +114,9 @@ export default function ArendeSida() { useEffect(() => { const timer = setInterval(() => setNu(new Date().toISOString()), 30000); + // ECM Knowledge Library: färskt regelpaket från plattformen (cache/ + // standardpaket gäller offline). + hamtaRegelpaket(); return () => clearInterval(timer); }, []); @@ -551,7 +553,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel />

2. Mest sannolik orsak (en eller flera)

- {ORSAKSKATEGORIER.map((o) => ( + {aktivtRegelpaket().orsakskategorier.map((o) => (

3. Underlag som stöder bedömningen

- {UNDERLAGSKALLOR.map((k) => ( + {aktivtRegelpaket().underlagskallor.map((k) => (