- 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.
- 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
- Tidigare ärenden på {identifierare} — granska innan historiken kvitteras
+ Previous cases for {identifierare} — review before signing off the history
@@ -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 && (
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.
- 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.
);
@@ -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.
- 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
@@ -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.
@@ -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.
- 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 &&
- 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.
@@ -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.
- 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 &&
- 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
)}
- 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() {
- Rikta kameran mot QR-koden, streckkoden eller VIN-etiketten.
+ Point the camera at the QR code, barcode or VIN label.
- Avbryt skanning
+ Cancel scanning
) : (
{stodStreckkod() && (
- Skanna QR/streckkod
+ Scan QR or barcode
)}
typskyltRef.current?.click()}>
@@ -295,8 +294,8 @@ export default function NyttArende() {
)}
{avlastKod && (
{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."}
- {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.
@@ -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}”}
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