diff --git a/felsokning/app/e2e/genomgang.mjs b/felsokning/app/e2e/genomgang.mjs index db2a3a6..724ae86 100644 --- a/felsokning/app/e2e/genomgang.mjs +++ b/felsokning/app/e2e/genomgang.mjs @@ -172,16 +172,16 @@ async function kor(sida, fall) { // ---- Starta ärendet --------------------------------------------------- await sida.goto(`${BAS}/#/felsokning/nytt`, { waitUntil: "networkidle" }); await vanta(300); - await klicka(btn(/Fyll i manuellt/i)); + await klicka(btn(/Fill in manually/i)); const start = sida.locator('input:not([type="file"])'); await fyll(start.nth(0), fall.regnr); await fyll(start.nth(1), fall.objekt); - await klicka(btn(/^Fortsätt$/)); + await klicka(btn(/^Continue$/)); await vanta(250); - await klicka(btn(/Rätt objekt/i)); + await klicka(btn(/Correct object/i)); await vanta(250); await fyll(sida.locator("textarea").first(), fall.fel); - await klicka(btn(/Starta arbetslogg/i)); + await klicka(btn(/Start work log/i)); await vanta(700); if (!sida.url().includes("/arende/")) throw new Error(`${fall.id}: kom aldrig in i ärendet`); @@ -193,50 +193,50 @@ async function kor(sida, fall) { for (let varv = 0; varv < 200; varv++) { const text = await sida.locator("body").innerText(); - if (!metodik) metodik = text.match(/METODIK:\s*([^\n·]+)/i)?.[1]?.trim() ?? ""; - if (text.includes("FELSÖKNINGEN ÄR AVSLUTAD")) break; + if (!metodik) metodik = text.match(/METHODOLOGY:\s*([^\n·]+)/i)?.[1]?.trim() ?? ""; + if (text.includes("THE DIAGNOSIS IS CLOSED")) break; - const pre = panel(/Pre-diagnostik/i); + const pre = panel(/Pre-diagnostics/i); if (await finns(pre)) { - const ja = btn(/Ja — kontrollerad/i, pre); + const ja = btn(/Yes — checked/i, pre); if (await finns(ja)) { // Historikkontrollen är en SPÄRR, inte en påminnelse: metodiken // får inte gå att nå förbi den. Det prövas här, i det ögonblick // frågan står obesvarad på skärmen — senare går det inte att // pröva, eftersom svaret då redan är avgivet. - if (await finns(panel(/Metodik|Steg \d/i))) { + if (await finns(panel(/Methodology|Step \d/i))) { throw new Error("metodiken var nåbar innan fordonets historik besvarats"); } await klicka(ja); - await klicka(btn(/^Dokumentera$/i, pre)); + await klicka(btn(/^Document$/i, pre)); continue; } - if (await finns(btn(/Fotografera instrumentpanelen/i, pre))) { + if (await finns(btn(/Photograph the instrument panel/i, pre))) { await foto(pre.locator('input[type="file"]')); const falt = pre.locator('input:not([type="file"])'); if (await finns(falt)) { await fyll(falt, "84 320 km"); - await klicka(btn(/Registrera|Spara/i, pre)); + await klicka(btn(/Record|Save/i, pre)); } continue; } - if (await finns(btn(/Stämmer — verifierad/i, pre))) { - await klicka(btn(/Stämmer — verifierad/i, pre)); + if (await finns(btn(/Correct — verified/i, pre))) { + await klicka(btn(/Correct — verified/i, pre)); continue; } - if (await finns(btn(/Inga ytterligare observationer/i, pre))) { - await klicka(btn(/Inga ytterligare observationer/i, pre)); + if (await finns(btn(/No further observations/i, pre))) { + await klicka(btn(/No further observations/i, pre)); continue; } } - const guide = panel(/^Metodik:/i); + const guide = panel(/^Methodology:/i); if (await finns(guide)) { - if (await finns(btn(/Ta foto/i, guide))) { + if (await finns(btn(/Take a photograph/i, guide))) { await foto(guide.locator('input[type="file"]')); continue; } - const verifiera = btn(/Markera verifierad/i, guide); + const verifiera = btn(/Mark as verified/i, guide); if (await finns(verifiera)) { const falt = guide.locator('input[type="text"], textarea').first(); if (await finns(falt)) await fyll(falt, fall.matvarde); @@ -245,86 +245,86 @@ async function kor(sida, fall) { continue; } } - if (await finns(btn(/Spara svar/i, guide))) { + if (await finns(btn(/Save answer/i, guide))) { const falt = guide.locator('input[type="text"], textarea').first(); if (await finns(falt)) await fyll(falt, `Enligt kundens uppgift: ${fall.fel}`); - await klicka(btn(/Spara svar/i, guide)); + await klicka(btn(/Save answer/i, guide)); continue; } // Säkerhetsfrågor besvaras jakande; spärren vid Nej provas i // enhetstesterna, inte här. - if (await finns(btn(/^Ja$/, guide))) { - await klicka(btn(/^Ja$/, guide)); + if (await finns(btn(/^Yes$/, guide))) { + await klicka(btn(/^Yes$/, guide)); continue; } const val = guide.locator("button").filter({ hasNot: sida.locator("svg") }); - if ((await val.count()) > 0 && !text.includes("Samtliga steg i metodiken")) { + if ((await val.count()) > 0 && !text.includes("Every step in the methodology")) { await klicka(val.first()); continue; } } - const repro = panel(/reproducering/i); + const repro = panel(/reproduction/i); if (await finns(repro)) { - await klicka(btn(/^Ja$/, repro)); + await klicka(btn(/^Yes$/, repro)); await fyll(repro.locator("textarea"), `${fall.fel} Återskapat vid provkörning under samma förhållanden.`); - await klicka(btn(/Dokumentera reproducering/i, repro)); + await klicka(btn(/Document reproduction/i, repro)); continue; } - const fo = panel(/Felorsaksanalys/i); - if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Dokumentera felorsak/i, fo)))) { - await klicka(btn(/Dokumentera felorsak/i, fo)); + const fo = panel(/Root cause analysis/i); + if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Document root cause/i, fo)))) { + await klicka(btn(/Document root cause/i, fo)); await fyll(fo.locator("textarea").first(), fall.avvikelse); - await klicka(btn(/^Normalt slitage$/, fo)); - await klicka(btn(/^Mätresultat$/, fo)); + await klicka(btn(/^Normal wear$/, fo)); + await klicka(btn(/^Measurement result$/, fo)); await fyll(fo.locator('input[type="text"], textarea').last(), fall.atgard); - await klicka(btn(/Spara felorsak/i, fo)); + await klicka(btn(/Save root cause/i, fo)); // En metodik utan mätkontroller har inga mätresultat att luta sig // mot; systemet nekar då källan och teknikern byter. - if (await finns(btn(/Spara felorsak/i, fo))) { - await klicka(btn(/^Mätresultat$/, fo)); - await klicka(btn(/^Direkt observation$/, fo)); - await klicka(btn(/Spara felorsak/i, fo)); + if (await finns(btn(/Save root cause/i, fo))) { + await klicka(btn(/^Measurement result$/, fo)); + await klicka(btn(/^Direct observation$/, fo)); + await klicka(btn(/Save root cause/i, fo)); } felorsakGjord = true; continue; } - const at = panel(/Åtgärd och kvalitetskontroll/i); + const at = panel(/Action and quality check/i); if (await finns(at)) { - if (await finns(btn(/Lämna åtgärdsförslag till kund/i, at))) { - await klicka(btn(/Lämna åtgärdsförslag till kund/i, at)); - await klicka(btn(/^Lämna förslag$/i, at)); + if (await finns(btn(/Give the customer a proposed action/i, at))) { + await klicka(btn(/Give the customer a proposed action/i, at)); + await klicka(btn(/^Give the proposal$/i, at)); continue; } - if (await finns(btn(/^Godkänt$/i, at))) { - await klicka(btn(/^Godkänt$/i, at)); - await klicka(btn(/^Telefon$/i, at)); - await klicka(btn(/Registrera besked/i, at)); + if (await finns(btn(/^Approved$/i, at))) { + await klicka(btn(/^Approved$/i, at)); + await klicka(btn(/^Telephone$/i, at)); + await klicka(btn(/Record the decision/i, at)); continue; } - if (await finns(btn(/Dokumentera utförd åtgärd/i, at))) { - await klicka(btn(/Dokumentera utförd åtgärd/i, at)); + if (await finns(btn(/Document the action performed/i, at))) { + await klicka(btn(/Document the action performed/i, at)); await fyll(at.locator("textarea").first(), `${fall.atgard} Utfört enligt den dokumenterade felorsaken.`); - await klicka(btn(/Spara åtgärd/i, at)); + await klicka(btn(/Save action/i, at)); continue; } - if (await finns(btn(/Symptomet är borta/i, at))) { - await klicka(btn(/Symptomet är borta/i, at)); + if (await finns(btn(/Symptom is gone/i, at))) { + await klicka(btn(/Symptom is gone/i, at)); await fyll(at.locator("textarea").first(), "Ny provkörning på samma vägsträcka — symptomet uppträder inte längre."); - await klicka(btn(/Spara kvalitetskontroll/i, at)); + await klicka(btn(/Save quality check/i, at)); continue; } } - const mat = panel(/Utgående mätarställning/i); + const mat = panel(/Outgoing odometer reading/i); if (await finns(mat)) { await foto(mat.locator('input[type="file"]')); const falt = mat.locator('input:not([type="file"])'); if (await finns(falt)) { await fyll(falt, "84 520 km"); - await klicka(btn(/Registrera|Spara/i, mat)); + await klicka(btn(/Record|Save/i, mat)); } continue; } @@ -336,7 +336,7 @@ async function kor(sida, fall) { const f = sida.locator(`#slutsats-${id}`); if (await finns(f)) await fyll(f, txt); } - const reg = btn(/Registrera slutsats|Uppdatera slutsats/i); + const reg = btn(/Record closing statement|Update closing statement/i); if ((await finns(reg)) && !(await reg.first().isDisabled())) { await klicka(reg); slutsatsfalt = synliga; @@ -345,7 +345,7 @@ async function kor(sida, fall) { } } - const avsluta = btn(/^Avsluta felsökning$/i); + const avsluta = btn(/^Close diagnosis$/i); if ((await finns(avsluta)) && !(await avsluta.last().isDisabled())) { await klicka(avsluta.last()); continue; @@ -374,7 +374,7 @@ async function kor(sida, fall) { return { metodik, - avslutat: slut.includes("FELSÖKNINGEN ÄR AVSLUTAD"), + avslutat: slut.includes("THE DIAGNOSIS IS CLOSED"), interaktioner, handelser: handelser.length, avvisade: handelser.map((h) => [h?.typ, granskaHändelse(h)]).filter(([, fel]) => fel !== null), diff --git a/felsokning/app/src/felsokning/DelatArendeVy.tsx b/felsokning/app/src/felsokning/DelatArendeVy.tsx index 8e1c2d8..56cd270 100644 --- a/felsokning/app/src/felsokning/DelatArendeVy.tsx +++ b/felsokning/app/src/felsokning/DelatArendeVy.tsx @@ -30,14 +30,14 @@ function BeslutsKnappar({ return (

- Ditt besked till verkstaden + Your decision to the workshop

setVal("godkant")}> - Godkänn åtgärden + Approve the action setVal("avbojt")}> - Avböj + Decline
{val && ( @@ -67,7 +67,7 @@ function BeslutsKnappar({ {skickar ? "Sending …" : "Send decision"}

- Beskedet registreras i ärendet och kan inte ändras här — kontakta verkstaden om du ändrar dig. + The decision is recorded in the case and cannot be changed here — contact the workshop if you change your mind.

)} @@ -226,7 +226,7 @@ export function DelatArendeVy({ ) : (

- Inväntar ditt besked — kontakta verkstaden för att godkänna eller avböja. + Awaiting your decision — contact the workshop to approve or decline.

)} @@ -312,7 +312,7 @@ export function DelatArendeVy({

- Ärende #{arende.nummer} · startat {tidDatum(arende.skapad)} · genererad ur ärendets händelselogg + Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log

); diff --git a/felsokning/app/src/felsokning/Mikrofon.tsx b/felsokning/app/src/felsokning/Mikrofon.tsx index 14704c0..6cdc173 100644 --- a/felsokning/app/src/felsokning/Mikrofon.tsx +++ b/felsokning/app/src/felsokning/Mikrofon.tsx @@ -58,7 +58,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) { {lyssnar && ( - ● Inspelning pågår{interim ? ` — ”${interim}”` : "…"} + ● Recording{interim ? ` — ”${interim}”` : "…"} )} diff --git a/felsokning/app/src/felsokning/Slutsats.tsx b/felsokning/app/src/felsokning/Slutsats.tsx index 560266d..c807a29 100644 --- a/felsokning/app/src/felsokning/Slutsats.tsx +++ b/felsokning/app/src/felsokning/Slutsats.tsx @@ -56,7 +56,7 @@ const FALT = [ id: "kvarstaende" as const, etikett: "Remaining uncertainty", fraga: "What remains uncertain?", - hjalp: 'Får vara "inget" — men det måste sägas aktivt.', + hjalp: 'May be "nothing" — but it has to be said actively.', rader: 2, }, ]; @@ -114,8 +114,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:

- Underlaget visar vad som mättes. Slutsatsen visar vad som är fel. Det här är raden som säger varför det - ena följer av det andra — och den enda en garantihandläggare eller försäkringsbedömare behöver granska. + The evidence shows what was measured. The conclusion shows what is wrong. This is the line that says why the one follows from the other — and the only one a warranty or insurance assessor needs to review.

{/* Den ärliga vägen. Att orsaken inte kunnat fastställas är ett @@ -155,7 +154,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h: ))}

- Ange under Uteslutna alternativ varför de föll bort — eller varför de kvarstår. + State under Dismissed alternatives why they were ruled out — or why they remain open.

)} diff --git a/felsokning/app/src/felsokning/__tests__/dataskydd.test.ts b/felsokning/app/src/felsokning/__tests__/dataskydd.test.ts index c3d8b54..bb56000 100644 --- a/felsokning/app/src/felsokning/__tests__/dataskydd.test.ts +++ b/felsokning/app/src/felsokning/__tests__/dataskydd.test.ts @@ -90,12 +90,24 @@ describe("C-3 · krypto-shredding gör append-only och radering förenliga", () it("gallringstiden följer ärendetypen, och okänd typ får standardtiden", () => { const avslut = "2026-01-15T10:00:00.000Z"; - const garanti = gallringsdatum("Garanti", avslut); - const okand = gallringsdatum("Hittepå", avslut); - expect(garanti.getFullYear()).toBe(2036); - expect(okand.getFullYear()).toBe(2029); - // Reklamation följer konsumentköplagens reklamationsrätt. - expect(GALLRING_MANADER.Reklamation).toBe(36); + expect(gallringsdatum("Warranty", avslut).getFullYear()).toBe(2036); + expect(gallringsdatum("Hittepå", avslut).getFullYear()).toBe(2029); + // Complaint följer konsumentköplagens reklamationsrätt. + expect(GALLRING_MANADER.Complaint).toBe(36); + }); + + it("ett ärende som öppnades på svenska gallras inte sju år för tidigt", () => { + // Den farligaste följden av att ärendetyperna bytte språk. Ett + // garantiärende vars logg säger "Garanti" hade fallit ur tabellen och + // tagit standardtiden 36 månader i stället för 120 — alltså raderat + // garantiunderlaget sju år i förtid, tyst och oåterkalleligt. + // + // Ett uppslag som missar ska ge det FÖRSIKTIGASTE utfallet. Här gav + // det det farligaste, och det syns inte förrän underlaget behövs. + const avslut = "2026-01-15T10:00:00.000Z"; + expect(gallringsdatum("Garanti", avslut).getTime()).toBe(gallringsdatum("Warranty", avslut).getTime()); + expect(gallringsdatum("Försäkring", avslut).getFullYear()).toBe(2036); + expect(gallringsdatum("Begagnatgaranti", avslut).getFullYear()).toBe(2031); }); }); diff --git a/felsokning/app/src/felsokning/__tests__/ecm.test.ts b/felsokning/app/src/felsokning/__tests__/ecm.test.ts index 89c643b..1af102d 100644 --- a/felsokning/app/src/felsokning/__tests__/ecm.test.ts +++ b/felsokning/app/src/felsokning/__tests__/ecm.test.ts @@ -138,7 +138,7 @@ describe("Completion Engine (kvalitetsgrind)", () => { stegId: steg.id, kontrollId: kontroll.id, text: kontroll.text, - undantag: "Kunden avböjde demontering", + undantag: "The customer declined disassembly", }); } } @@ -160,17 +160,17 @@ describe("Completion Engine (kvalitetsgrind)", () => { describe("Compliance Engine", () => { it("ärendetypen styr dokumentationskraven", () => { const privat = byggArende([OBJEKT]); - expect(arendetyp(privat)).toBe("Privat kund"); + expect(arendetyp(privat)).toBe("Private customer"); expect(kvalitetsgrind(privat, VIBRATION_METODIK).some((r) => r.id.startsWith("garanti_"))).toBe(false); - const garanti = byggArende([OBJEKT, { typ: "arendetyp_satt", arendetyp: "Garanti" }]); + const garanti = byggArende([OBJEKT, { typ: "arendetyp_satt", arendetyp: "Warranty" }]); 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 utanBild = byggArende([OBJEKT, { typ: "arendetyp_satt", arendetyp: "Insurance" }]); 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); @@ -182,7 +182,7 @@ describe("Traceability Engine + ärendeidentitet", () => { 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.arendetyp).toBe("Private customer"); expect(paket.evidensposter).toHaveLength(1); expect(paket.kvalitetsgrind.some((r) => r.id === "objekt" && r.ok)).toBe(true); }); @@ -221,10 +221,10 @@ describe("Felorsaksanalys (Root Cause Analysis)", () => { const { underlagFinns } = await import("../ecm"); const utanFoto = byggArende([OBJEKT, { typ: "matvarde", beskrivning: "U", varde: "12" }]); expect(underlagFinns(utanFoto, "Foto")).toBe(false); - expect(underlagFinns(utanFoto, "Mätresultat")).toBe(true); - expect(underlagFinns(utanFoto, "Servicehistorik")).toBe(false); + expect(underlagFinns(utanFoto, "Measurement result")).toBe(true); + expect(underlagFinns(utanFoto, "Service history")).toBe(false); const medHistorik = byggArende([{ typ: "historik_kontrollerad", kontrollerad: true }]); - expect(underlagFinns(medHistorik, "Servicehistorik")).toBe(true); + expect(underlagFinns(medHistorik, "Service history")).toBe(true); }); it("avslut kräver reproducering och felorsak — grinden blir obligatorisk vid stängning", () => { @@ -241,8 +241,8 @@ describe("Felorsaksanalys (Root Cause Analysis)", () => { { typ: "felorsak", avvikelse: "Ingen avvikelse kunde konstateras vid undersökningen trots systematisk genomgång.", - orsaker: ["Okänd orsak"], - underlag: ["Direkt observation"], + orsaker: ["Unknown cause"], + underlag: ["Direct observation"], sakerhet: "lag", atgard: "Återkom vid kall motor så att symptomet kan reproduceras.", motivering: "Symptomet kunde inte reproduceras under rådande förhållanden.", @@ -252,14 +252,14 @@ describe("Felorsaksanalys (Root Cause Analysis)", () => { ]); const rader2 = kvalitetsgrind(stangdMed, VIBRATION_METODIK); expect(rader2.find((r) => r.id === "svp")?.ok).toBe(true); - expect(rader2.find((r) => r.id === "svp")?.detalj).toContain("kunde inte reproduceras"); + expect(rader2.find((r) => r.id === "svp")?.detalj).toContain("could not be reproduced"); expect(rader2.find((r) => r.id === "felorsak")?.ok).toBe(true); }); it("rapportformuleringen skiljer reproducerat från ej reproducerat", async () => { const { reproduceringsText } = await import("../ecm"); - expect(reproduceringsText("ja")).toContain("reproducerades"); - expect(reproduceringsText("nej")).toContain("kunde inte reproduceras under de förhållanden"); + expect(reproduceringsText("ja")).toContain("was reproduced"); + expect(reproduceringsText("nej")).toContain("could not be reproduced under the conditions"); }); it("demoärendet bär hela beviskedjan: reproducering + felorsak med underlag", () => { @@ -306,7 +306,7 @@ describe("ECM Knowledge Library (regelpaket)", () => { normaliseraRegelpaket({ version: "2.1-test", arendetypRegler: { - "Privat kund": [ + "Private customer": [ { id: "test_foto", rubrik: "Foto krävs för alla privatkunder", krav: "foto", detaljVidBrist: "Testregel." }, ], }, @@ -317,7 +317,7 @@ describe("ECM Knowledge Library (regelpaket)", () => { 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"); + expect(rad?.rubrik).toContain("Private customer"); // Spårbarheten bär paketversionen. expect(sparbarhetspaket(utanFoto, VIBRATION_METODIK).regelpaketVersion).toBe("2.1-test"); } finally { @@ -334,7 +334,7 @@ describe("Fordonshistorik (lokal projektion)", () => { { typ: "felorsak", avvikelse: "Vattenpumpen läcker vid axeltätningen.", - orsaker: ["Normalt slitage"], + orsaker: ["Normal wear"], underlag: ["Foto"], sakerhet: "hog", atgard: "Byt vattenpump.", @@ -346,7 +346,7 @@ describe("Fordonshistorik (lokal projektion)", () => { const historik = lokalFordonshistorik({ [tidigare.id]: tidigare, [aktuellt.id]: aktuellt }, "abc123", aktuellt.id); expect(historik).toHaveLength(1); expect(historik[0].avslutat).toBe(true); - expect(historik[0].felorsaker[0].orsaker).toContain("Normalt slitage"); + expect(historik[0].felorsaker[0].orsaker).toContain("Normal wear"); // Annat fordon → ingen träff. expect(lokalFordonshistorik({ [tidigare.id]: tidigare }, "ZZZ111")).toHaveLength(0); }); @@ -398,8 +398,8 @@ describe("Åtgärdsfasen och kvalitetskontroll", () => { { typ: "felorsak", avvikelse: "Framhjulen uppvisar obalans om 38 g på höger sida.", - orsaker: ["Yttre påverkan"], - underlag: ["Direkt observation"], + orsaker: ["External influence"], + underlag: ["Direct observation"], sakerhet: "hog", atgard: "Balansera om hjulet.", }, @@ -433,19 +433,19 @@ describe("Åtgärdsfasen och kvalitetskontroll", () => { expect(kvalitetskontroll(komplett)?.resultat).toBe("symptomet_borta"); const rader = kvalitetsgrind(komplett, VIBRATION_METODIK); expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true); - expect(rader.find((r) => r.id === "kvalitetskontroll")?.detalj).toContain("verifierat borta"); + expect(rader.find((r) => r.id === "kvalitetskontroll")?.detalj).toContain("verified gone"); }); it("motiverat uteblivande godtas utan kvalitetskontroll", () => { const ingenAtgard = byggArende([ OBJEKT, ...PREDIAG, - { typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: "Kunden avböjde åtgärd" }, + { typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: "The customer declined the action" }, { typ: "arende_avslutat", signatur: "Erik" }, ]); const rader = kvalitetsgrind(ingenAtgard, VIBRATION_METODIK); expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true); - expect(rader.find((r) => r.id === "atgard")?.detalj).toContain("orsaken är dokumenterad"); + expect(rader.find((r) => r.id === "atgard")?.detalj).toContain("the reason is documented"); // Inget att verifiera → kvalitetskontrollen är inte obligatorisk. expect(rader.find((r) => r.id === "kvalitetskontroll")?.kravs).toBe(false); }); @@ -458,7 +458,7 @@ describe("Åtgärdsfasen och kvalitetskontroll", () => { { typ: "kvalitetskontroll", resultat: "kvarstar", beskrivning: "Missljudet finns kvar vid 60 km/h." }, ]); const rad = kvalitetsgrind(kvarstar, VIBRATION_METODIK).find((r) => r.id === "kvalitetskontroll"); - expect(rad?.detalj).toContain("bör inte avslutas som åtgärdat"); + expect(rad?.detalj).toContain("should not be closed as resolved"); }); it("demoärendet bär hela kedjan symptom → orsak → åtgärd → verifiering", () => { @@ -486,7 +486,7 @@ describe("Kundgodkännande före arbete", () => { const rad = kvalitetsgrind(utanBesked, VIBRATION_METODIK).find((r) => r.id === "kundgodkannande"); expect(rad?.kravs).toBe(true); expect(rad?.ok).toBe(false); - expect(rad?.detalj).toContain("registrera kundens besked"); + expect(rad?.detalj).toContain("record the customer's decision"); const medBesked = byggArende([ OBJEKT, diff --git a/felsokning/app/src/felsokning/__tests__/grind.test.ts b/felsokning/app/src/felsokning/__tests__/grind.test.ts index b89e389..a2cf6e6 100644 --- a/felsokning/app/src/felsokning/__tests__/grind.test.ts +++ b/felsokning/app/src/felsokning/__tests__/grind.test.ts @@ -26,7 +26,7 @@ function komplettLogg(extra: Record[] = []) { { typ: "matarstallning", lage: "ingaende", varde: "14 200", bilagaId: "bil-in" }, { typ: "matarstallning", lage: "utgaende", varde: "14 205", bilagaId: "bil-ut" }, { typ: "reproducering", status: "ja", beskrivning: "Reproducerat vid 88 km/h." }, - { typ: "felorsak", avvikelse: "Obalans", orsaker: ["Normalt slitage"], underlag: ["Mätvärde"], sakerhet: "hog" }, + { typ: "felorsak", avvikelse: "Obalans", orsaker: ["Normal wear"], underlag: ["Mätvärde"], sakerhet: "hog" }, { typ: "atgard_utford", beskrivning: "Balanserade hjulen.", utford: true }, { typ: "kundbeslut", beslut: "godkant", kanal: "Telefon" }, { typ: "kvalitetskontroll", resultat: "symptomet_borta", beskrivning: "Provkört, symptomet borta." }, @@ -190,20 +190,61 @@ describe("M-2 · högvoltsspärren är ett hinder, inte en varning", () => { }); }); +// Säkerhetsspärren avgörs av en TEXTSTRÄNG i loggen. När knappen bytte +// från "Ja" till "Yes" gick klient och server isär: klienten skrev "Yes", +// servern jämförde mot "Ja". Utfallet blev fail-closed — högvoltsärenden +// gick inte att avsluta alls — vilket är rätt riktning för ett fel att +// falla åt, men det var en slump och inte en konstruktion. +describe("säkerhetsspärren känner igen ett ja oavsett språk", () => { + const HOGVOLT = ALLA_METODIKER.find((m) => m.id === "hogvolt")!; + const svar = (s: string) => [ + { typ: "fraga_besvarad", stegId: "sakerhet", frageId: "behorighet", fraga: "?", svar: s }, + { typ: "fraga_besvarad", stegId: "sakerhet", frageId: "avstangt", fraga: "?", svar: s }, + ]; + const sparrar = (s: string) => + grinda(svar(s), HOGVOLT).map((h) => h.id).filter((id) => id.startsWith("sparr_")); + + it("engelskt ja passerar", () => expect(sparrar("Yes")).toEqual([])); + it("svenskt ja passerar — gamla ärenden", () => expect(sparrar("Ja")).toEqual([])); + it("tyskt ja passerar", () => expect(sparrar("Ja")).toEqual([])); + + it("ett nej spärrar fortfarande", () => { + expect(sparrar("No")).toEqual(["sparr_behorighet", "sparr_avstangt"]); + expect(sparrar("Nej")).toEqual(["sparr_behorighet", "sparr_avstangt"]); + }); + + it("ett obesvarat eller tomt svar spärrar", () => { + expect(sparrar("")).toEqual(["sparr_behorighet", "sparr_avstangt"]); + expect(grinda([], HOGVOLT).map((h) => h.id)).toContain("sparr_behorighet"); + }); +}); + describe("ärendetypens regelpaket", () => { - const paket = { arendetyper: { Garanti: { krav: [{ typ: "miltal" }, { typ: "historik" }] } } }; + const paket = { arendetyper: { Warranty: { krav: [{ typ: "miltal" }, { typ: "historik" }] } } }; it("garantiärende kräver miltal och historik", () => { - const utan = [{ typ: "arendetyp_satt", arendetyp: "Garanti" }]; + const utan = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }]; expect(grindaArendetyp(utan, paket).map((h) => h.id)).toEqual([ "arendetyp_miltal", "arendetyp_historik", ]); }); + it("ett ärende som öppnades på svenska behåller sina extra krav", () => { + // Ärendetypen ligger i loggen och slår upp regelpaketet. När typerna + // bytte språk hade ett gammalt garantiärende annars fått noll extra + // krav och släppts igenom grinden med FÄRRE krav än när det öppnades + // — utan att någonting i gränssnittet visade det. + const gammalt = [{ typ: "arendetyp_satt", arendetyp: "Garanti" }]; + expect(grindaArendetyp(gammalt, paket).map((h) => h.id)).toEqual([ + "arendetyp_miltal", + "arendetyp_historik", + ]); + }); + it("en okänd kravtyp spärrar i stället för att tolkas som uppfylld", () => { - const trasigt = { arendetyper: { Garanti: { krav: [{ typ: "hittepa" }] } } }; - const logg = [{ typ: "arendetyp_satt", arendetyp: "Garanti" }]; + const trasigt = { arendetyper: { Warranty: { krav: [{ typ: "hittepa" }] } } }; + const logg = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }]; expect(grindaArendetyp(logg, trasigt)[0].nyckel).toBe("grind.arendetyp.okant"); expect(grindaArendetyp(logg, trasigt)[0].rubrik).toContain("hittepa"); }); @@ -255,7 +296,7 @@ describe("schemat följer domänmodellen", () => { // Bakgrund: en kontroll vars krav är "matvarde" loggade bara // kontroll_utford. Följden var att 56 av metodikernas 153 kontroller — // drygt en tredjedel — mätte utan att det syntes någonstans: -// felorsaksanalysen nekade "Mätresultat" som underlag trots att +// felorsaksanalysen nekade "Measurement result" som underlag trots att // teknikern just hade mätt, och evidensprofilen i analysvyn underskattade // systematiskt hur mycket som faktiskt mäts i verkstaden. // @@ -280,12 +321,12 @@ describe("metodikernas mätkontroller producerar mätevidens", () => { expect(kod).toMatch(/skicka\(\{ typ: "matvarde", beskrivning: kontroll\.text/); }); - it('underlagskällan "Mätresultat" godtar just den händelsetypen', async () => { + it('underlagskällan "Measurement result" godtar just den händelsetypen', async () => { const { underlagFinns } = await import("../ecm"); const arende = { handelser: [{ id: "1", tidpunkt: "", anvandare: "A", handelse: { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4 bar" } }], } as never; - expect(underlagFinns(arende, "Mätresultat")).toBe(true); + expect(underlagFinns(arende, "Measurement result")).toBe(true); }); }); diff --git a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts index d10317e..e6bf31c 100644 --- a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts +++ b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts @@ -249,8 +249,8 @@ describe("klientens avslutsvillkor täcker hela grindens åtgärdskedja", () => { typ: "felorsak", avvikelse: "Höger framhjul har 38 g obalans mot gränsen 10 g.", - orsaker: ["Normalt slitage"], - underlag: ["Mätresultat"], + orsaker: ["Normal wear"], + underlag: ["Measurement result"], sakerhet: "hog", atgard: "Balansera om höger framhjul.", }, diff --git a/felsokning/app/src/felsokning/ecm.ts b/felsokning/app/src/felsokning/ecm.ts index 666d639..597687f 100644 --- a/felsokning/app/src/felsokning/ecm.ts +++ b/felsokning/app/src/felsokning/ecm.ts @@ -20,6 +20,7 @@ import type { Arende, LoggPost } from "./domain"; import type { Metodik } from "./metodik"; +import { arendetypNu } from "../../../services/gemensam/sprak/index.mjs"; export const ECM_VERSION = "2.0"; @@ -29,13 +30,13 @@ export const ECM_VERSION = "2.0"; export type EvidensNiva = "E0" | "E1" | "E2" | "E3" | "E4" | "E5" | "E6"; export const EVIDENS_LABEL: Record = { - E0: "E0 · Inget underlag", - E1: "E1 · Teknikerns observation", - E2: "E2 · Foto", + E0: "E0 · No evidence", + E1: "E1 · Technician's observation", + E2: "E2 · Photograph", E3: "E3 · Video", - E4: "E4 · Mätvärde", - E5: "E5 · Diagnosdata/dokument", - E6: "E6 · Flera oberoende källor", + E4: "E4 · Measurement", + E5: "E5 · Diagnostic data or document", + E6: "E6 · Several independent sources", }; export interface Evidenspost { @@ -165,17 +166,38 @@ export function evidensNiva(arende: Arende): EvidensNiva { // 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", - "Kunden avböjde demontering", - "Dålig sikt/åtkomst", - "Utrustning saknas", + "The component is inaccessible", + "The vehicle cannot be lifted safely", + "The customer declined disassembly", + "Poor visibility or access", + "Equipment not available", ]; // 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"; +export const MARKOR_FELBESKRIVNING_VERIFIERAD = "Customer's fault description verified on receipt"; +export const MARKOR_INGA_TIDIGA_OBSERVATIONER = "No further observations on receipt"; +export const MARKOR_TIDIGA_OBSERVATIONER_KLARA = "Early observations on receipt documented"; + +// Markörernas svenska lydelse, från tiden före engelska som källspråk. +// +// Kvitteringen ligger i loggen som en kommentar och känns igen på sin +// TEXT. När texten byter språk slutar gamla ärenden att räknas som +// kvitterade — pre-diagnostiken hade då visat en ogjord kontroll för +// arbete som faktiskt utförts, och teknikern hade kvitterat en gång till +// för att komma vidare. Loggen hade då fått två kvitteringar för samma +// sak, vilket är precis vad en append-only logg inte ska innehålla. +// +// Att känna igen en kvittering på fritext är i sig svagt. Rätt lösning är +// en egen händelsetyp med ett fält; tills den finns står arvet här, synligt. +const ARV_MARKORER: Record = { + [MARKOR_FELBESKRIVNING_VERIFIERAD]: ["Kundens felbeskrivning verifierad vid mottagandet"], + [MARKOR_INGA_TIDIGA_OBSERVATIONER]: ["Inga ytterligare observationer vid mottagandet"], + [MARKOR_TIDIGA_OBSERVATIONER_KLARA]: ["Tidiga observationer vid mottagandet dokumenterade"], +}; + +/** Bär texten den här markören, på något av de språk den har haft? */ +const markerad = (text: string, markor: string) => + [markor, ...(ARV_MARKORER[markor] ?? [])].some((m) => text.startsWith(m)); // -- Felorsaksanalys (Root Cause Analysis) -- // Ett ärende avslutas aldrig med enbart "komponent trasig, byt komponent": @@ -183,32 +205,32 @@ export const MARKOR_TIDIGA_OBSERVATIONER_KLARA = "Tidiga observationer vid motta // säkerhetsnivå — eller en motivering till varför orsaken inte fastställts. export const ORSAKSKATEGORIER = [ - "Normalt slitage", - "Ålder", - "Körsträcka", - "Materialutmattning", - "Tillverkningsfel", - "Bristande underhåll", - "Felaktig tidigare reparation", - "Yttre påverkan", - "Korrosion", - "Överhettning", - "Förorening", - "Felaktig användning", - "Modifiering", - "Olycka eller skada", - "Okänd orsak", + "Normal wear", + "Age", + "Distance travelled", + "Material fatigue", + "Manufacturing defect", + "Inadequate maintenance", + "Faulty previous repair", + "External influence", + "Corrosion", + "Overheating", + "Contamination", + "Improper use", + "Modification", + "Accident or damage", + "Unknown cause", ]; export const UNDERLAGSKALLOR = [ "Foto", "Video", - "Mätresultat", - "Diagnosutläsning", - "Tidigare historik", - "Servicehistorik", - "Teknisk dokumentation", - "Direkt observation", + "Measurement result", + "Diagnostic readout", + "Previous history", + "Service history", + "Technical documentation", + "Direct observation", ]; // Kvalitetsregeln: generella formuleringar utan förklaring avvisas. @@ -231,14 +253,14 @@ export function underlagFinns(arende: Arende, kalla: string): boolean { return h.some((x) => x.typ === "foto"); case "Video": return h.some((x) => x.typ === "video"); - case "Mätresultat": + case "Measurement result": return h.some((x) => x.typ === "matvarde"); - case "Diagnosutläsning": + case "Diagnostic readout": return h.some((x) => x.typ === "foto" && /instrument|diagnos/i.test(x.beskrivning)); - case "Tidigare historik": - case "Servicehistorik": + case "Previous history": + case "Service history": return h.some((x) => x.typ === "historik_kontrollerad" && x.kontrollerad); - case "Direkt observation": + case "Direct observation": return h.some((x) => x.typ === "observation" || x.typ === "kontroll_utford"); default: // Teknisk dokumentation: kan ännu inte verifieras maskinellt. @@ -256,16 +278,16 @@ export function felorsaker(arende: Arende) { // kontrollen visar om symptomet faktiskt är borta. export const INGEN_ATGARD_ORSAKER = [ - "Kunden avböjde åtgärd", - "Väntar på reservdel", - "Åtgärd utförs av annan verkstad", - "Endast utredning beställd", - "Kostnadsförslag lämnat, inväntar besked", + "The customer declined the action", + "Awaiting a spare part", + "The action is performed by another workshop", + "Investigation only was ordered", + "Estimate given, awaiting a decision", ]; // Kanaler för kundens besked — beskedet ska alltid gå att härleda till // ett faktiskt samtal, besök eller meddelande. -export const KUNDKANALER = ["Telefon", "På plats", "E-post", "SMS", "Delningslänk"]; +export const KUNDKANALER = ["Telephone", "In person", "E-mail", "SMS", "Share link"]; export function atgardsforslag(arende: Arende) { return arende.handelser.filter((p) => p.handelse.typ === "atgardsforslag"); @@ -307,35 +329,37 @@ export function reproducering(arende: Arende) { // Rapportformuleringen styrs av verifieringsläget — aldrig "felet // konstaterat" utan reproducering eller annan dokumenterad verifiering. export function reproduceringsText(status: "ja" | "delvis" | "nej"): string { - if (status === "ja") return "Symptomet reproducerades vid undersökningen."; - if (status === "delvis") return "Symptomet kunde delvis reproduceras vid undersökningen."; - return "Kundens beskrivning kunde inte reproduceras under de förhållanden som rådde vid undersökningen."; + if (status === "ja") return "The symptom was reproduced during the examination."; + if (status === "delvis") return "The symptom could be partly reproduced during the examination."; + return "The customer's description could not be reproduced under the conditions present during the examination."; } // ---- 3. Compliance Engine --------------------------------------------- // Ärendetypen styr vilka dokumentationskrav som gäller utöver metodiken. export const ARENDETYPER = [ - "Privat kund", - "Företagskund", - "Garanti", + "Private customer", + "Business customer", + "Warranty", "Goodwill", - "Försäkring", - "Reklamation", - "Begagnatgaranti", - "Intern kvalitetskontroll", - "Teknisk utredning", + "Insurance", + "Complaint", + "Used-vehicle warranty", + "Internal quality check", + "Technical investigation", ] as const; export type Arendetyp = (typeof ARENDETYPER)[number]; export function arendetyp(arende: Arende): Arendetyp { - let typ: Arendetyp = "Privat kund"; + let typ: Arendetyp = "Private customer"; for (const post of arende.handelser) { const h = post.handelse; - if (h.typ === "arendetyp_satt" && (ARENDETYPER as readonly string[]).includes(h.arendetyp)) { - typ = h.arendetyp as Arendetyp; - } + if (h.typ !== "arendetyp_satt") continue; + // Gamla ärenden bär den svenska typen. Den översätts vid uppslaget, + // aldrig i loggen — se ARENDETYP_ARV i sprak/index.mjs. + const satt = arendetypNu(h.arendetyp) as string; + if ((ARENDETYPER as readonly string[]).includes(satt)) typ = satt as Arendetyp; } return typ; } @@ -388,24 +412,24 @@ const KRAV_KONTROLLER: Record boolean> = 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)." }, + "Warranty": [ + { id: "garanti_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Warranty cases require a documented odometer reading." }, + { id: "garanti_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Warranty cases require a checked service history." }, + { id: "garanti_claim", rubrik: "Claim or warranty number recorded", krav: "claim", detaljVidBrist: "State the claim or warranty number (read from the work order)." }, ], 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." }, + { id: "goodwill_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Goodwill cases require a documented odometer reading." }, + { id: "goodwill_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Goodwill cases require a checked service history." }, ], - 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." }, + "Insurance": [ + { id: "forsakring_skadenummer", rubrik: "Claim number recorded", krav: "skadenummer", detaljVidBrist: "Insurance cases require a claim number (read from the work order)." }, + { id: "forsakring_bildbevis", rubrik: "Photographic evidence present", krav: "foto", detaljVidBrist: "Insurance cases require photographic documentation." }, ], - 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)." }, + "Complaint": [ + { id: "reklamation_historik", rubrik: "History and previous attempts checked", krav: "historik", detaljVidBrist: "Complaint cases require a checked history (previous repairs and attempts)." }, ], - Begagnatgaranti: [ - { id: "begagnat_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Begagnatgaranti kräver dokumenterad mätarställning." }, + "Used-vehicle warranty": [ + { id: "begagnat_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Used-vehicle warranty cases require a documented odometer reading." }, ], }, undantagsorsaker: UNDANTAGSORSAKER, @@ -494,7 +518,7 @@ export interface PreDiagRad { // 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 historik: PreDiagRad = { id: "historik", rubrik: "Vehicle history checked", klar: false }; let matarstallning = false; let felbeskrivningVerifierad = false; let tidiga = false; @@ -503,25 +527,25 @@ export function preDiagnostik(arende: Arende): PreDiagRad[] { if (h.typ === "historik_kontrollerad") { historik = { id: "historik", - rubrik: "Fordonshistorik kontrollerad", + rubrik: "Vehicle history checked", 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" && markerad(h.text, MARKOR_FELBESKRIVNING_VERIFIERAD)) felbeskrivningVerifierad = true; if ( h.typ === "kommentar" && - (h.text.startsWith(MARKOR_INGA_TIDIGA_OBSERVATIONER) || h.text.startsWith(MARKOR_TIDIGA_OBSERVATIONER_KLARA)) + (markerad(h.text, MARKOR_INGA_TIDIGA_OBSERVATIONER) || markerad(h.text, 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 }, + { id: "matarstallning_in", rubrik: "Incoming odometer reading documented", klar: matarstallning }, + { id: "felbeskrivning", rubrik: "Customer's fault description verified", klar: felbeskrivningVerifierad }, + { id: "tidiga_observationer", rubrik: "Early observations handled", klar: tidiga }, ]; } @@ -551,19 +575,19 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const objektFinns = handelser.some((h) => h.typ === "objekt_identifierat"); rader.push({ id: "objekt", - rubrik: "Fordons-/objektidentifiering verifierad", + rubrik: "Vehicle or object identification verified", ok: objektFinns, kravs: true, - detalj: objektFinns ? undefined : "Evidens saknas — identifiera objektet.", + detalj: objektFinns ? undefined : "Evidence missing — identify the object.", }); const arbetsorder = handelser.some((h) => h.typ === "arbetsorder_skannad") || !!objektFalt(arende, "arbetsorder"); rader.push({ id: "arbetsorder", - rubrik: "Arbetsorder inläst", + rubrik: "Work order read", ok: arbetsorder, kravs: false, - detalj: arbetsorder ? undefined : "Ärendet startades utan skannad arbetsorder.", + detalj: arbetsorder ? undefined : "The case was started without a scanned work order.", }); // Pre-diagnostiken ingår i grinden: historik + ingående mätarställning @@ -572,26 +596,26 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const historik = pre.find((r) => r.id === "historik")!; rader.push({ id: "historik", - rubrik: "Fordonshistorik kontrollerad eller motiverad", + rubrik: "Vehicle history checked or justified", ok: historik.klar, kravs: true, - detalj: historik.klar ? historik.varning : "Kontrollera historiken eller dokumentera varför det inte gått.", + detalj: historik.klar ? historik.varning : "Check the history, or document why that was not possible.", }); const matIn = pre.find((r) => r.id === "matarstallning_in")!; rader.push({ id: "matarstallning_in", - rubrik: "Ingående mätarställning dokumenterad", + rubrik: "Incoming odometer reading documented", ok: matIn.klar, kravs: true, - detalj: matIn.klar ? undefined : "Fotografera instrumentpanelen (eller dokumentera undantag).", + detalj: matIn.klar ? undefined : "Photograph the instrument panel (or document an exemption).", }); const felb = pre.find((r) => r.id === "felbeskrivning")!; rader.push({ id: "felbeskrivning_verifierad", - rubrik: "Kundens felbeskrivning verifierad", + rubrik: "Customer's fault description verified", ok: felb.klar, kravs: false, - detalj: felb.klar ? undefined : "Bekräfta att kundens beskrivning är korrekt återgiven.", + detalj: felb.klar ? undefined : "Confirm that the customer's description is correctly recorded.", }); // SVP: symptomet reproducerat eller dokumenterat ej reproducerbart — @@ -599,12 +623,12 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const repro = reproducering(arende); rader.push({ id: "svp", - rubrik: "Symptomverifiering: reproducerat eller dokumenterat ej reproducerbart", + rubrik: "Symptom verification: reproduced, or documented as not reproducible", ok: !!repro, kravs: avslutat, detalj: repro ? reproduceringsText(repro.status) - : "Dokumentera reproduceringen (Ja/Delvis/Nej med motivering) innan ärendet avslutas.", + : "Document the reproduction (Yes / Partly / No with a reason) before closing the case.", }); // Felorsaksanalys: minst en dokumenterad felorsak (eller motiverad @@ -612,13 +636,13 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const orsaker = felorsaker(arende); rader.push({ id: "felorsak", - rubrik: "Felorsaksanalys dokumenterad", + rubrik: "Root cause analysis documented", ok: orsaker.length > 0, kravs: avslutat, detalj: orsaker.length > 0 ? undefined - : "Beskriv varför felet uppstått — eller ange varför orsaken inte kunnat fastställas.", + : "Describe why the fault occurred — or state why the cause could not be established.", }); // Åtgärdsfasen: reparationen dokumenterad — eller motiverat varför @@ -627,15 +651,15 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const utfordAtgard = atgardsposter.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford); rader.push({ id: "atgard", - rubrik: "Åtgärd dokumenterad eller motiverad", + rubrik: "Corrective action documented or justified", ok: atgardsposter.length > 0, kravs: avslutat, detalj: atgardsposter.length === 0 - ? "Dokumentera vad som gjordes — eller varför ingen åtgärd utfördes." + ? "Document what was done — or why no action was performed." : utfordAtgard ? undefined - : "Ingen åtgärd utförd; orsaken är dokumenterad.", + : "No action performed; the reason is documented.", }); // Kundgodkännande: har ett åtgärdsförslag lämnats ska kundens besked @@ -645,20 +669,20 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { if (forslag.length > 0 || beslut) { rader.push({ id: "kundgodkannande", - rubrik: "Kundens besked på åtgärdsförslaget registrerat", + rubrik: "Customer decision on the proposed action recorded", ok: !!beslut, kravs: utfordAtgard, detalj: beslut ? `${beslut.beslut === "godkant" ? "Godkänt" : beslut.beslut === "avbojt" ? "Avböjt" : "Delvis godkänt"} via ${beslut.kanal}.` - : "Åtgärdsförslag lämnat — registrera kundens besked innan arbetet utförs.", + : "A proposal has been given — record the customer's decision before the work is performed.", }); if (beslut?.beslut === "avbojt" && utfordAtgard) { rader.push({ id: "godkannande_konflikt", - rubrik: "Utfört arbete trots avböjt åtgärdsförslag", + rubrik: "Work performed despite a declined proposal", ok: false, kravs: true, - detalj: "Kunden avböjde åtgärden men arbete är dokumenterat som utfört — kontrollera underlaget.", + detalj: "The customer declined the action, but work is documented as performed — check the evidence.", }); } } @@ -668,28 +692,28 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const kk = kvalitetskontroll(arende); rader.push({ id: "kvalitetskontroll", - rubrik: "Kvalitetskontroll genomförd — symptomet verifierat", + rubrik: "Quality check performed — symptom verified", ok: !!kk, kravs: avslutat && utfordAtgard, detalj: kk ? kk.resultat === "symptomet_borta" - ? "Symptomet är verifierat borta efter åtgärd." + ? "The symptom is verified gone after the action." : kk.resultat === "ej_verifierbar" ? `Kunde inte verifieras: ${kk.beskrivning}` - : `Symptomet kvarstår helt eller delvis — ärendet bör inte avslutas som åtgärdat.` + : `The symptom remains wholly or partly — the case should not be closed as resolved.` : utfordAtgard - ? "Verifiera att symptomet är borta efter åtgärden (provkörning/återtest)." - : "Ingen åtgärd utförd — inget att verifiera.", + ? "Verify that the symptom is gone after the action (road test or retest)." + : "No action performed — nothing to verify.", }); // 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", + rubrik: "Outgoing odometer reading documented", ok: matUt, kravs: avslutat, - detalj: matUt ? undefined : "Fotografera instrumentpanelen när arbetet är klart.", + detalj: matUt ? undefined : "Photograph the instrument panel when the work is complete.", }); // Metodikens kontroller: evidens eller dokumenterat undantag per kontroll. @@ -711,7 +735,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { } rader.push({ id: "kontroller", - rubrik: "Metodikens kontroller: evidens eller dokumenterat undantag", + rubrik: "Methodology checks: evidence, or a documented exemption", ok: saknade.length === 0, kravs: true, detalj: @@ -724,7 +748,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { rader.push({ id: "fotokrav", - rubrik: "Foton finns för fotokrävande kontroller", + rubrik: "Photographs present for checks that require them", ok: fotoKravUtanFoto === 0, kravs: true, detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined, @@ -748,21 +772,21 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { const avslutEvent = handelser.find((h) => h.typ === "arende_avslutat"); rader.push({ id: "signering", - rubrik: "Teknikerns slutsats signerad", + rubrik: "The technician's closing statement is signed", ok: !avslutat || (avslutEvent?.typ === "arende_avslutat" && !!avslutEvent.signatur), kravs: false, detalj: avslutat && avslutEvent?.typ === "arende_avslutat" && avslutEvent.signatur ? `Signerad av ${avslutEvent.signatur}.` : avslutat - ? "Avslutet saknar signatur (äldre ärende)." - : "Signeras automatiskt när ärendet avslutas.", + ? "The closing has no signature (older case)." + : "Signed automatically when the case is closed.", }); const hypoteser = handelser.filter((h) => h.typ === "hypotes").length; rader.push({ id: "hypoteser", - rubrik: "Hypoteser redovisas som ej verifierade", + rubrik: "Hypotheses are reported as unverified", ok: true, kravs: false, detalj: hypoteser > 0 ? `${hypoteser} hypotes(er) markeras som ej verifierade i rapporten — aldrig som konstaterade fel.` : undefined, @@ -773,7 +797,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] { rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`, ok: evidensNiva(arende) !== "E0", kravs: true, - detalj: evidensNiva(arende) === "E0" ? "Ingen evidens i loggen ännu." : undefined, + detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined, }); return rader; diff --git a/felsokning/app/src/felsokning/metodik.ts b/felsokning/app/src/felsokning/metodik.ts index 357d119..27295e4 100644 --- a/felsokning/app/src/felsokning/metodik.ts +++ b/felsokning/app/src/felsokning/metodik.ts @@ -8,6 +8,7 @@ import type { Arende } from "./domain"; import { ALLA_METODIKER, ELSYSTEM, GENERISK, VIBRATION } from "./metodiker"; +import { arJakande } from "../../../services/gemensam/sprak/index.mjs"; export interface Fraga { id: string; @@ -197,7 +198,7 @@ export function nastaSteg(arende: Arende, metodik: Metodik): NastaSteg { // Säkerhetsspärr: ett nekande svar stoppar metodiken här i stället // för att räknas som besvarat och släppa fram nästa steg. const sparr = SPARRFRAGOR[`${metodik.id}/${steg.id}/${fraga.id}`]; - if (sparr && svar.get(`${steg.id}/${fraga.id}`) !== "Yes") { + if (sparr && !arJakande(svar.get(`${steg.id}/${fraga.id}`))) { return { steg, fraga, klart: false, sparr }; } } diff --git a/felsokning/app/src/pages/felsokning/ArendeSida.tsx b/felsokning/app/src/pages/felsokning/ArendeSida.tsx index 0816ac7..1895560 100644 --- a/felsokning/app/src/pages/felsokning/ArendeSida.tsx +++ b/felsokning/app/src/pages/felsokning/ArendeSida.tsx @@ -323,7 +323,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s /> {!foto && !laser && ( filRef.current?.click()}> - Fotografera instrumentpanelen + Photograph the instrument panel )} {laser &&

Reading the odometer …

} @@ -331,11 +331,11 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
{demo && (

- Demo-avläsning — bildtolkningen kräver inloggning. Kontrollera värdet. + Demo reading — image interpretation requires sign-in. Check the value.

)} Instrument panel - + { @@ -344,7 +344,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s setVarde(""); }} > - Spara mätarställning + Save odometer reading
)} @@ -393,12 +393,12 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande return (

- Tidigare ärenden på {identifierare} — granska innan historiken kvitteras + Previous cases for {identifierare} — review before signing off the history

{rader.map((rad) => (

- Ärende #{rad.nummer} · {tidDatum(rad.skapad)} ·{" "} + Case #{rad.nummer} · {tidDatum(rad.skapad)} ·{" "} {rad.avslutat ? "closed" : "in progress"} {rad.felbeskrivning && — ”{rad.felbeskrivning}”}

@@ -412,13 +412,13 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande onClick={() => skicka({ typ: "kommentar", - text: `Orsakskedja: kopplat till tidigare ärende #${rad.nummer}${ + text: `Causal chain: linked to previous case #${rad.nummer}${ rad.felorsaker[0] ? ` — ${rad.felorsaker[0].avvikelse}` : rad.felbeskrivning ? ` — ${rad.felbeskrivning}` : "" }`, }) } > - + Koppla till orsakskedjan + + Link to the causal chain
))} @@ -448,12 +448,12 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha {!rad("historik").klar && (

- Har fordonets historik kontrollerats? (tidigare arbeten, återkommande fel, TSB, kampanjer) + Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)

setHistorikVal("ja")}> - Ja — kontrollerad + Yes — checked setHistorikVal("nej")}> No @@ -486,7 +486,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha {!rad("matarstallning_in").klar && (

- Ingående mätarställning — fotografera instrumentpanelen + Incoming odometer reading — photograph the instrument panel

@@ -495,11 +495,11 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha {!rad("felbeskrivning").klar && (

- Är kundens felbeskrivning korrekt återgiven? + Is the customer's fault description correctly recorded?

{fb &&

”{fb}”

} skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}> - Stämmer — verifierad + Correct — verified

Ytterligare symptom dokumenteras som separata observationer — blanda dem inte med kundens beskrivning. @@ -510,22 +510,22 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha {!rad("tidiga_observationer").klar && (

- Något ytterligare vid mottagandet? (reparationsspår, modifieringar, skador, läckage, korrosion …) + Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)

skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}> - Inga ytterligare observationer + No further observations skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })} > - Observationerna är dokumenterade + The observations are documented

- Dokumentera med foto eller observation i panelen nedan — knappen låses upp när något loggats. + Document with a photograph or an observation in the panel below — the button unlocks once something is logged.

)} @@ -569,7 +569,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) { setText(""); }} > - Dokumentera reproducering + Document reproduction )} @@ -638,15 +638,15 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel

{h.avvikelse}

- Orsak: {h.orsaker.join(", ")} · Underlag: {h.underlag.join(", ")} · Säkerhet: {TILLFORLITLIGHET_LABEL[h.sakerhet]} + Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}

-

Åtgärd: {h.atgard}

+

Action: {h.atgard}

); })} {!oppen ? ( setOppen(true)}> - + Dokumentera felorsak + + Document root cause ) : (
@@ -721,7 +721,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel {fel &&

{fel}

}
setOppen(false)}> - Avbryt + Cancel Save root cause
@@ -774,7 +774,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse {h.delar && · Delar: {h.delar}} ) : ( - Ingen åtgärd utförd — {h.motivering} + No action performed — {h.motivering} )}

); @@ -820,7 +820,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse setForslagText(foreslagenAtgard); }} > - Lämna åtgärdsförslag till kund + Give the customer a proposed action )} @@ -837,7 +837,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
setForslagsLage(false)}> - Avbryt + Cancel - Lämna förslag + Give the proposal
@@ -861,7 +861,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse {forslag.length > 0 && !beslut && (

- Registrera kundens besked innan arbetet påbörjas + Record the customer's decision before the work begins

{(["godkant", "avbojt", "delvis"] as const).map((val) => ( @@ -912,7 +912,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse setBeslutsKommentar(""); }} > - Registrera besked + Record the decision )} @@ -926,7 +926,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse disabled={forslag.length > 0 && (!beslut || beslut.beslut === "avbojt")} onClick={() => setLage("utford")} > - Dokumentera utförd åtgärd + Document the action performed setLage("ingen")}> No action performed @@ -947,7 +947,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
setLage("")}> - Avbryt + Cancel - Spara åtgärd + Save action
@@ -967,7 +967,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse {lage === "ingen" && (

- Orsak till att ingen åtgärd utfördes (obligatorisk) + Reason no action was performed (required)

{INGEN_ATGARD_ORSAKER.map((val) => ( @@ -985,12 +985,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse {beslut?.beslut === "avbojt" && (

- Kunden har avböjt åtgärden via {beslut.kanal} — beskedet finns registrerat i loggen. + The customer has declined the action via {beslut.kanal} — the decision is recorded in the log.

)}
setLage("")}> - Avbryt + Cancel

- Kvalitetskontroll — är symptomet borta efter åtgärden? + Quality check — is the symptom gone after the action?

{repro?.status === "ja" && (

- Symptomet reproducerades vid undersökningen — återskapa samma förhållanden för att verifiera. + The symptom was reproduced during the examination — recreate the same conditions to verify.

)}
@@ -1053,7 +1053,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse setKkText(""); }} > - Spara kvalitetskontroll + Save quality check )} @@ -1081,7 +1081,7 @@ function VyTrad({ return (
); @@ -1286,7 +1286,7 @@ function GuideFlik({ return (

- Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export. + The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export.

); @@ -1335,16 +1335,16 @@ function GuideFlik({ först vid avslutsknappen vore att neka någon som redan är klar. */} {(steg.klart || underlagKlart) && } - + {steg.klart ? ( <>

Every step in the methodology is documented.

- Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet - med rekommenderade nästa steg. + If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case + with recommended next steps.

skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}> - Avsluta felsökning + Close diagnosis {!kanAvslutas && } @@ -1357,7 +1357,7 @@ function GuideFlik({

{steg.sparr.orsak}

{steg.sparr.atgard}

- Svaret är dokumenterat i loggen. Metodiken öppnas när förutsättningen är uppfylld och frågan besvaras + The answer is documented in the log. The methodology opens when the precondition is met and the question is answered jakande.

@@ -1422,10 +1422,10 @@ function GuideFlik({ {!kanAvslutas && }
setVisaOverlamning(true)}> - Lämna över arbete + Hand over the work skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}> - Avsluta felsökning + Close diagnosis
@@ -1448,7 +1448,7 @@ function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detal return (

- Ärendet kan inte avslutas ännu + The case cannot be closed yet

    {hinder.map((h) => ( @@ -1515,7 +1515,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) => <> svara(text.trim())}> - Spara svar + Save answer )} @@ -1602,7 +1602,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)

    )} - Markera verifierad + Mark as verified )} @@ -1634,7 +1634,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) { onClick={() => setOppen(true)} className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden" > - Underlag kan inte tas fram … + Evidence cannot be produced … ); } @@ -1659,10 +1659,10 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
    setOppen(false)}> - Avbryt + Cancel vidUndantag(orsak.trim())}> - Dokumentera undantag + Document an exemption
@@ -1733,7 +1733,7 @@ function SnabbDokumentation({ if (!avlasning) return; // Originalet först, sedan varje bekräftat värde som eget mätvärde — // strukturerad data ersätter aldrig originalevidensen. - skicka({ typ: "foto", beskrivning: `Instrumentavläsning (${avlasning.tolkning.instrumenttyp})`, dataUrl: avlasning.foto }); + skicka({ typ: "foto", beskrivning: `Instrument reading (${avlasning.tolkning.instrumenttyp})`, dataUrl: avlasning.foto }); for (const v of avlasning.tolkning.varden) { // `kalla` sätts därför att värdet AVLÄSTES av en modell ur fotot // (TÜV T-5). Teknikern bekräftar innan något skrivs, vilket täcker @@ -1750,7 +1750,7 @@ function SnabbDokumentation({ }); } paSparad?.( - `Instrumentavläsning (${avlasning.tolkning.instrumenttyp}): ${avlasning.tolkning.varden + `Instrument reading (${avlasning.tolkning.instrumenttyp}): ${avlasning.tolkning.varden .map((v) => `${v.beskrivning} = ${v.varde}${v.enhet ? ` ${v.enhet}` : ""}`) .join("; ")}`, ); @@ -1773,7 +1773,7 @@ function SnabbDokumentation({ } if (typ === "matvarde" && t && varde.trim()) { skicka({ typ: "matvarde", beskrivning: t, varde: varde.trim() }); - paSparad?.(`Mätvärde: ${t} = ${varde.trim()}`); + paSparad?.(`Measurement: ${t} = ${varde.trim()}`); } aterstall(); }; @@ -1860,7 +1860,7 @@ function SnabbDokumentation({ />
setVideo(null)}> - Förkasta + Discard - Spara video + Save video
@@ -1880,11 +1880,11 @@ function SnabbDokumentation({
{avlasning.demo && (

- Demo-avläsning — bildtolkningen kräver inloggning. Värdena är exempeldata. + Demo reading — image interpretation requires sign-in. The values are sample data.

)}

- Avläst: {avlasning.tolkning.instrumenttyp} — bekräfta innan något loggas + Read: {avlasning.tolkning.instrumenttyp} — confirm before anything is logged

{avlasning.tolkning.varden.map((v, i) => (
@@ -1898,7 +1898,7 @@ function SnabbDokumentation({ ))}
setAvlasning(null)}> - Förkasta + Discard Save photograph + values
@@ -1908,7 +1908,7 @@ function SnabbDokumentation({
{typ === "hypotes" && (

- En hypotes är en möjlig felorsak som kräver verifiering — den loggas aldrig som ett konstaterat fel. + A hypothesis is a possible root cause that requires verification — it is never logged as an established fault.

)} {typ === "matvarde" && } - Spara + Save
)} @@ -1974,7 +1974,7 @@ function OverlamningDialog({ {aiLage === "vilar" && ( - Komplettera analysen: risker & osäkerheter + Extend the analysis: risks & uncertainties )} {aiLage === "arbetar" &&

Summarizing …

} @@ -1993,7 +1993,7 @@ function OverlamningDialog({
- Avbryt + Cancel { @@ -2001,7 +2001,7 @@ function OverlamningDialog({ stang(); }} > - Bekräfta överlämning + Confirm handover
@@ -2042,7 +2042,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han uppdatera(); }} > - Delningslänkar (kund/partner/intern) + Share links (customer / partner / internal) ); } @@ -2064,17 +2064,17 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`) } > - Kopiera + Copy )} @@ -2088,7 +2088,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han onClick={async () => { try { await skapaDelning(arende.id, niva); - skicka({ typ: "kommentar", text: `Delningslänk skapad (${NIVA_LABEL[niva]}).` }); + skicka({ typ: "kommentar", text: `Share link created (${NIVA_LABEL[niva]}).` }); uppdatera(); } catch { setFel("Could not create a share — requires sign-in and a synchronized case."); @@ -2100,8 +2100,8 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han ))}

- Kund: det kunddelbara · Extern partner: även hypoteser (märkta ej verifierade) · Intern: full insyn. - Filtreringen sker på servern. + Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) · + Internal: full visibility. Filtering happens on the server.

); @@ -2164,8 +2164,8 @@ function BriefFlik({ {!b.avslutat && (

- Låt systemet gå igenom hela underlaget innan du lämnar det vidare: motsägelser mellan - observationer, luckor i dokumentationen och slutsatser som saknar stöd. + Have the system review the whole record before you hand it on: contradictions between + observations, gaps in the documentation, and conclusions that lack support.

{granskning === "arbetar" &&

Reviewing the evidence …

} {granskning === "fel" && ( @@ -2345,22 +2345,22 @@ function RapportFlik({ ))} {!godkand && (

- Rapporten kan inte genereras förrän varje underkänd rad har evidens eller ett dokumenterat undantag + The report cannot be generated until every failing row has evidence or a documented exemption (”Underlag kan inte tas fram” i guiden).

)}

- Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas — bilder kan - innehålla uppgifter om andra kunder. + A shareable summary of the work performed. Review the content before the report is shared — images may + contain details about other customers.

window.print()}> - Skriv ut / PDF + Print / PDF - Exportera JSON + Export JSON
@@ -2383,7 +2383,7 @@ function RapportFlik({ ) )}

- Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare). + The share link requires the case to be synchronized (signed-in user).

{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar @@ -2506,7 +2506,7 @@ function RapportFlik({ ["Parts", h.delar], ]) ) : ( -

Ingen åtgärd utförd — {h.motivering}

+

No action performed — {h.motivering}

)}
); @@ -2565,7 +2565,7 @@ function RapportFlik({ ) : null}

- Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd. + Generated from the case log · Observations and measurements are reported without conclusions that lack support.

@@ -2642,8 +2642,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu return (

- Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden - skickas. + Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.

{[ @@ -2664,14 +2663,14 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu platshallare="Whoever answers cannot see your screen." flerRad /> - {utfall.ok &&

Anmäld som {utfall.ok}.

} + {utfall.ok &&

Reported as {utfall.ok}.

} {utfall.fel &&

{utfall.fel}

}
{skickar ? "Sending" : "Send report"} setOppen(false)}> - Stäng + Close
diff --git a/felsokning/app/src/pages/felsokning/Arendelista.tsx b/felsokning/app/src/pages/felsokning/Arendelista.tsx index 3258783..adbec87 100644 --- a/felsokning/app/src/pages/felsokning/Arendelista.tsx +++ b/felsokning/app/src/pages/felsokning/Arendelista.tsx @@ -47,7 +47,7 @@ function PlattformInloggning() {

{konto.namn} · {ROLL_LABEL[konto.roll]} ·{" "} - {konto.organisation} — synk och beslutsstöd aktiva. + {konto.organisation} — synchronization and decision support active.

{(konto.roll === "arbetsledare" || konto.roll === "admin") && ( @@ -80,8 +80,7 @@ function PlattformInloggning() { {lage === "stangd" ? ( <>

- Logga in för synk mellan enheter, samarbete i ärenden och beslutsstöd. Utan inloggning - arbetar appen i lokalt läge. Ny organisation? Skapa konto — du blir systemadministratör. + Sign in for synchronization across devices, collaboration on cases and decision support. Without sign-in the app works in local mode. New organization? Create an account — you become the system administrator.

setLage("registrera")}> @@ -117,7 +116,7 @@ function PlattformInloggning() { {fel &&

{fel}

}
setLage("stangd")}> - Avbryt + Cancel - Hantera användare i organisationen + Manage users in the organization ); @@ -209,7 +208,7 @@ function AnvandarAdmin() {
))}

- En avstängning gäller omedelbart — pågående sessioner på personens enheter upphör direkt. + Disabling takes effect immediately — active sessions on that person's devices end at once.

{fel &&

{fel}

} - Skapa användare + Create user
@@ -273,7 +272,7 @@ export default function Arendelista() {

- Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja. + All work is logged with user and timestamp. Enter your name to begin.

{ @@ -283,7 +282,7 @@ export default function Arendelista() { > - Fortsätt + Continue
@@ -330,11 +329,10 @@ export default function Arendelista() { {alla.length === 0 && (

- Inga ärenden ännu. Starta med att identifiera ett objekt — eller utforska ett färdigt demoärende - med komplett arbetslogg, brief och kundrapport. + No cases yet. Start by identifying an object — or explore a finished demo case with a complete work log, brief and customer report.

laggInArende(byggDemoArende(nastaNummer, anvandare))}> - Skapa demoärende (Volvo XC60, vibration) + Create demo case (Volvo XC60, vibration)
)} @@ -353,11 +351,7 @@ export default function Arendelista() {

- När du är inloggad hjälper systemet dig medan du arbetar: föreslår nästa steg utifrån det du - dokumenterar, granskar hela ärendet på begäran och skriver utkast till överlämningen. Den - skiljer alltid på verifierat och hypotes — och slår aldrig fast en felorsak som inte är - bekräftad. Inget att installera eller ställa in; allt ingår i tjänsten. Utan inloggning - guidar checklistan dig steg för steg. + When you are signed in the system helps while you work: it proposes the next step from what you document, reviews the whole case on request and drafts the handover. It always separates verified from hypothesis — and never states a root cause that is not confirmed. Nothing to install or configure; it is part of the service. Without sign-in the checklist guides you step by step.

@@ -372,7 +366,7 @@ export default function Arendelista() {
- Ärende #{arende.nummer} + Case #{arende.nummer}

- Organisationens egna avtal hos tillverkare och dataleverantörer. Uppgifterna lagras krypterat på - plattformen och används bara av servern — de skickas aldrig till teknikernas enheter. + The organization's own agreements with manufacturers and data providers. The details are stored encrypted on the platform and used only by the server — they are never sent to technicians' devices.

{!krypteringKlar && (

- Kryptering är inte konfigurerad i driften (INTEGRATION_NYCKEL) — uppgifter kan inte sparas förrän - nyckeln finns. + Encryption is not configured in this deployment (INTEGRATION_NYCKEL) — details cannot be saved until the key exists.

)} @@ -132,7 +130,7 @@ function Integrationer() { {fel &&

{fel}

}
{ setOppen(null); setFel(""); }}> - Avbryt + Cancel - Spara uppgifter + Save details

- Hemliga fält visas aldrig igen efter sparande — fyll i på nytt för att byta värde. + Secret fields are never shown again after saving — enter a new value to replace one.

) : ( @@ -182,7 +180,7 @@ function Integrationer() { ladda(); }} > - Ta bort + Delete )}
@@ -190,7 +188,7 @@ function Integrationer() {
); })} - {sparat &&

Uppgifter sparade för {sparat}.

} + {sparat &&

Details saved for {sparat}.

} ); } @@ -213,7 +211,7 @@ export default function Installningar() {

- Organisationens inställningar hanteras av er systemadministratör. + Organization settings are managed by your system administrator.

@@ -246,7 +244,7 @@ export default function Installningar() {

- Välj vad som visas när ett nytt ärende startas.{" "} + Choose what is shown when a new case is started.{" "} {inloggad ? "The setting applies to everyone in the organization." : "In local mode the setting applies to this device; a signed-in system administrator governs the whole organization."} diff --git a/felsokning/app/src/pages/felsokning/NyttArende.tsx b/felsokning/app/src/pages/felsokning/NyttArende.tsx index e5a686e..1abe350 100644 --- a/felsokning/app/src/pages/felsokning/NyttArende.tsx +++ b/felsokning/app/src/pages/felsokning/NyttArende.tsx @@ -177,8 +177,7 @@ export default function NyttArende() {

- Fota arbetsorderns framsida. Systemet läser kund-, fordons- och ärendeuppgifter automatiskt — - du granskar bara osäkra fält och trycker Starta diagnos. + Photograph the front of the work order. The system reads customer, vehicle and case details automatically — you review only the uncertain fields and press Start diagnosis.

setSteg("identifiera")}> - Fyll i manuellt + Fill in manually
@@ -217,7 +216,7 @@ export default function NyttArende() { > {tolkning.demo && (

- Demo-tolkning — plattformens dokumenttolkning kräver inloggning. Värdena nedan är exempeldata. + Demo interpretation — the platform's document interpretation requires sign-in. The values below are sample data.

)}

- QR-kod, streckkod och typskylt kan läsas av i nästa steg — eller ange identiteten manuellt. + QR code, barcode and type plate can be read in the next step — or enter the identity manually.

@@ -275,17 +274,17 @@ export default function NyttArende() {
) : (
{stodStreckkod() && ( - Skanna QR/streckkod + Scan QR or barcode )} typskyltRef.current?.click()}> @@ -295,8 +294,8 @@ export default function NyttArende() { )} {avlastKod && (

- Avläst {avlastKod.typ}: {avlastKod.varde} - {avlastKod.format ? ` (${avlastKod.format})` : ""} — kontrollera värdet nedan. + Read {avlastKod.typ}: {avlastKod.varde} + {avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.

)} {kodFel &&

{kodFel}

} @@ -306,7 +305,7 @@ export default function NyttArende() { setSteg("bekrafta")}> - Fortsätt + Continue @@ -325,7 +324,7 @@ export default function NyttArende() {
setSteg("identifiera")}> - Ändra + Edit setSteg("felbeskrivning")}>Correct object — continue
@@ -420,7 +419,7 @@ function GranskaTolkning({

{vald?.omrade - ? `Markerat: ${vald.etikett} (ungefärlig position)` + ? `Markerat: ${vald.etikett} (approximate position)` : "Select a field to see where in the document the value was found."}

@@ -467,7 +466,7 @@ function GranskaTolkning({ }} className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white" > - Bekräfta + Confirm )}
@@ -480,12 +479,12 @@ function GranskaTolkning({ {obekraftade.length > 0 && (

- {obekraftade.length} osäkra fält kräver bekräftelse eller korrigering innan diagnosen startar. + {obekraftade.length} uncertain fields require confirmation or correction before the diagnosis starts.

)}
- Skanna om + Scan again 0 || startar} onClick={() => vidStart(rader)}> {startar ? "Starting …" : "Start diagnosis"} diff --git a/felsokning/app/src/pages/felsokning/Oversikt.tsx b/felsokning/app/src/pages/felsokning/Oversikt.tsx index 78e6bd1..96d3337 100644 --- a/felsokning/app/src/pages/felsokning/Oversikt.tsx +++ b/felsokning/app/src/pages/felsokning/Oversikt.tsx @@ -40,7 +40,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void }) hamtaAnvandare().then(setPersoner).catch(() => setPersoner([])); }} > - Omfördela + Reassign ); } @@ -72,7 +72,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void }) className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]" onClick={() => setOppen(false)} > - Avbryt + Cancel
@@ -102,7 +102,7 @@ export default function Oversikt() {

- Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen. + The overview requires supervisor or administrator rights on the platform.

@@ -177,7 +177,7 @@ export default function Oversikt() {
{avslutade.length > 0 && (

- Genomsnittlig ledtid för avslutade: {formateraTid(medelLedtidMs)} + Average lead time for closed cases: {formateraTid(medelLedtidMs)}

)} @@ -195,7 +195,7 @@ export default function Oversikt() {
))}

- Ur dokumenterade felorsaksanalyser — visar mönster som slitage, tidigare reparationer eller möjliga konstruktionsproblem. + From documented root cause analyses — shows patterns such as wear, previous repairs or possible design problems.

)} @@ -206,7 +206,7 @@ export default function Oversikt() {
- Ärende #{rad.nummer} + Case #{rad.nummer} ”{rad.felbeskrivning}”

}

Ansvarig: {rad.ansvarig ?? rad.skapare ?? "—"} - {` · ${rad.antal_handelser} händelser`} + {` · ${rad.antal_handelser} events`} {rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}

oppna(rad)}> - Öppna ärendet + Open the case {!rad.avslutat && hamtaOversikt().then(setRader)} />}
diff --git a/felsokning/services/gemensam/grind.mjs b/felsokning/services/gemensam/grind.mjs index 01d867c..a3e05d5 100644 --- a/felsokning/services/gemensam/grind.mjs +++ b/felsokning/services/gemensam/grind.mjs @@ -16,7 +16,7 @@ // samma utfall, vilket är vad som gör en spärr granskbar. import { granskaSlutsats } from "./motivering.mjs"; -import { STANDARD, t } from "./sprak/index.mjs"; +import { STANDARD, arJakande, arendetypNu, t } from "./sprak/index.mjs"; const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0; @@ -190,7 +190,7 @@ export function grinda(handelser, metodik, sprak = STANDARD) { const svaret = av(handelser, "fraga_besvarad").findLast( (h) => h.stegId === stegId && h.frageId === frageId, ); - krav(`sparr_${frageId}`, nyckel, svaret?.svar === "Ja", "grind.sparr.ej_uppfyllt"); + krav(`sparr_${frageId}`, nyckel, arJakande(svaret?.svar), "grind.sparr.ej_uppfyllt"); } // Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan @@ -210,7 +210,10 @@ export function grinda(handelser, metodik, sprak = STANDARD) { * (ECM Knowledge Library) och är alltså data, inte kod. */ export function grindaArendetyp(handelser, regelpaket, sprak = STANDARD) { - const typ = av(handelser, "arendetyp_satt").at(-1)?.arendetyp; + // Ärendetypen slås upp i sin nuvarande form. Ett ärende som öppnades + // när typerna hette "Garanti" ska få garantins extra krav även efter + // att de bytt språk — se ARENDETYP_ARV. + const typ = arendetypNu(av(handelser, "arendetyp_satt").at(-1)?.arendetyp); const regler = regelpaket?.arendetyper?.[typ]?.krav ?? []; const hinder = []; diff --git a/felsokning/services/gemensam/personuppgifter.mjs b/felsokning/services/gemensam/personuppgifter.mjs index 2161202..997aad2 100644 --- a/felsokning/services/gemensam/personuppgifter.mjs +++ b/felsokning/services/gemensam/personuppgifter.mjs @@ -24,6 +24,7 @@ // sämre i praktiken — då raderas beviset tillsammans med personuppgiften. import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; +import { arendetypNu } from "./sprak/index.mjs"; /** * Fält som räknas som identifierande och därför krypteras. @@ -134,16 +135,27 @@ export function öppnaHändelse(handelse, nycklar) { * den som inte gör ett aktivt val får det försiktigaste. */ export const GALLRING_MANADER = { - Garanti: 120, // Följer den längsta rimliga garantitiden på en drivlina. + Warranty: 120, // Följer den längsta rimliga garantitiden på en drivlina. Goodwill: 120, - Försäkring: 120, // Preskription för försäkringskrav. - Reklamation: 36, // Konsumentköplagens reklamationsrätt. - Begagnatgaranti: 60, + Insurance: 120, // Preskription för försäkringskrav. + Complaint: 36, // Konsumentköplagens reklamationsrätt. + "Used-vehicle warranty": 60, standard: 36, }; +/** + * Gallringsdatum för ett avslutat ärende. + * + * Ärendetypen normaliseras genom ARENDETYP_ARV innan den slås upp. Det + * är inte en artighet mot gamla data utan en spärr: när typerna bytte + * språk hade ett garantiärende vars logg säger "Garanti" fallit ur + * tabellen och tagit standardvärdet 36 månader i stället för 120 — + * alltså raderat garantiunderlaget sju år för tidigt, tyst, och + * oåterkalleligt. Ett uppslag som missar ska ge det FÖRSIKTIGASTE + * utfallet; här gav det det farligaste. + */ export function gallringsdatum(arendetyp, avslutat, manader = GALLRING_MANADER) { - const antal = manader[arendetyp] ?? manader.standard; + const antal = manader[arendetypNu(arendetyp)] ?? manader.standard; const d = new Date(avslutat); d.setMonth(d.getMonth() + antal); return d; diff --git a/felsokning/services/gemensam/sprak/index.mjs b/felsokning/services/gemensam/sprak/index.mjs index bdcc18d..38ba86a 100644 --- a/felsokning/services/gemensam/sprak/index.mjs +++ b/felsokning/services/gemensam/sprak/index.mjs @@ -187,3 +187,51 @@ export function metodikvarning(sprak) { if (!s || s.granskat) return null; return t(sprak, "metodik.ogranskad", { sprak: s.egetNamn }); } + +/** + * Ärendetyper som skrevs på svenska innan engelska blev källspråket. + * + * Ärendetypen är DATA: den ligger i händelseloggen och slår upp + * regelpaketets extra krav. Ett byte av språk får därför inte göras tyst + * — ett gammalt garantiärende vars typ heter "Garanti" hade annars inte + * matchat "Warranty", fått noll extra krav, och släppts igenom grinden + * med FÄRRE krav än när det öppnades. Ingenting i gränssnittet hade + * visat det. + * + * Tabellen är därför en del av regeln, inte en bekvämlighet, och den tas + * bort först när ingen logg innehåller de gamla värdena. + */ +export const ARENDETYP_ARV = { + "Privat kund": "Private customer", + Företagskund: "Business customer", + Garanti: "Warranty", + Försäkring: "Insurance", + Reklamation: "Complaint", + Begagnatgaranti: "Used-vehicle warranty", + "Intern kvalitetskontroll": "Internal quality check", + "Teknisk utredning": "Technical investigation", +}; + +/** Ärendetypen i sin nuvarande form, oavsett vilket språk den skrevs i. */ +export const arendetypNu = (typ) => ARENDETYP_ARV[typ] ?? typ; + +/** + * Jakande svar på en säkerhetsspärr. + * + * Svaret på en ja/nej-fråga ligger i loggen som text, och spärrfrågorna + * släpper bara igenom ett jakande svar. När knappen bytte från "Ja" till + * "Yes" gick de två isär: klienten skrev "Yes", servern jämförde mot + * "Ja". Utfallet blev fail-closed — högvoltsärenden gick inte att + * avsluta alls — vilket är rätt riktning för ett fel att falla åt, men + * det var en slump och inte en konstruktion. + * + * Det verkliga felet är att en säkerhetsspärr avgörs av en textsträng. + * Rätt lösning är ett booleskt fält i händelsen; tills det finns står + * båda formerna här, och de gäller alla språk samtidigt av samma skäl + * som ordlistorna i ord.mjs gör det: den som svarar ska inte kunna + * missas för att organisationens språkinställning var en annan. + */ +export const JAKANDE = new Set(["Yes", "Ja", "Oui", "Sí", "Si", "Sì", "Tak", "Da", "Sim", "Ja."]); + +/** Är svaret jakande, oavsett vilket språk knappen stod på? */ +export const arJakande = (svar) => JAKANDE.has(String(svar ?? "").trim()); diff --git a/felsokning/services/plattform/ecm-regler.json b/felsokning/services/plattform/ecm-regler.json index 0a90a73..b4293b0 100644 --- a/felsokning/services/plattform/ecm-regler.json +++ b/felsokning/services/plattform/ecm-regler.json @@ -2,58 +2,103 @@ "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)." } + "Warranty": [ + { + "id": "garanti_miltal", + "rubrik": "Mileage documented", + "krav": "miltal", + "detaljVidBrist": "Warranty cases require a documented odometer reading." + }, + { + "id": "garanti_historik", + "rubrik": "Service history checked", + "krav": "historik", + "detaljVidBrist": "Warranty cases require a checked service history." + }, + { + "id": "garanti_claim", + "rubrik": "Claim or warranty number recorded", + "krav": "claim", + "detaljVidBrist": "State the claim or warranty number (read from the work order)." + } ], "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." } + { + "id": "goodwill_miltal", + "rubrik": "Mileage documented", + "krav": "miltal", + "detaljVidBrist": "Goodwill cases require a documented odometer reading." + }, + { + "id": "goodwill_historik", + "rubrik": "Service history checked", + "krav": "historik", + "detaljVidBrist": "Goodwill cases require a checked service history." + } ], - "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." } + "Insurance": [ + { + "id": "forsakring_skadenummer", + "rubrik": "Claim number recorded", + "krav": "skadenummer", + "detaljVidBrist": "Insurance cases require a claim number (read from the work order)." + }, + { + "id": "forsakring_bildbevis", + "rubrik": "Photographic evidence present", + "krav": "foto", + "detaljVidBrist": "Insurance cases require photographic documentation." + } ], - "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)." } + "Complaint": [ + { + "id": "reklamation_historik", + "rubrik": "History and previous attempts checked", + "krav": "historik", + "detaljVidBrist": "Complaint cases require a checked history (previous repairs and attempts)." + } ], - "Begagnatgaranti": [ - { "id": "begagnat_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Begagnatgaranti kräver dokumenterad mätarställning." } + "Used-vehicle warranty": [ + { + "id": "begagnat_miltal", + "rubrik": "Mileage documented", + "krav": "miltal", + "detaljVidBrist": "Used-vehicle warranty cases require a documented odometer reading." + } ] }, "undantagsorsaker": [ - "Komponenten är oåtkomlig", - "Fordonet kan inte lyftas säkert", - "Kunden avböjde demontering", - "Dålig sikt/åtkomst", - "Utrustning saknas" + "The component is inaccessible", + "The vehicle cannot be lifted safely", + "The customer declined disassembly", + "Poor visibility or access", + "Equipment not available" ], "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" + "Normal wear", + "Age", + "Distance travelled", + "Material fatigue", + "Manufacturing defect", + "Inadequate maintenance", + "Faulty previous repair", + "External influence", + "Corrosion", + "Overheating", + "Contamination", + "Improper use", + "Modification", + "Accident or damage", + "Unknown cause" ], "underlagskallor": [ "Foto", "Video", - "Mätresultat", - "Diagnosutläsning", - "Tidigare historik", - "Servicehistorik", - "Teknisk dokumentation", - "Direkt observation" + "Measurement result", + "Diagnostic readout", + "Previous history", + "Service history", + "Technical documentation", + "Direct observation" ] } diff --git a/felsokning/services/plattform/integrationstest.sh b/felsokning/services/plattform/integrationstest.sh index 6110e5a..e0c7058 100755 --- a/felsokning/services/plattform/integrationstest.sh +++ b/felsokning/services/plattform/integrationstest.sh @@ -215,7 +215,7 @@ kontroll "statistiken är organisationsknuten data" "$(echo "$STAT" | falt '.ors # 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" +kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Warranty.length')" "3" # 10c. Publikt kundgodkännande — den enda skrivande publika vägen # Ärendets ursprungliga delningskod saknar registrerad nivå och får