{b.totalArbetstid}
{tidsfordelningsRader(arende, nu).map((r) => (
diff --git a/felsokning/app/src/felsokning/Mikrofon.tsx b/felsokning/app/src/felsokning/Mikrofon.tsx
index 4a7be7d..14704c0 100644
--- a/felsokning/app/src/felsokning/Mikrofon.tsx
+++ b/felsokning/app/src/felsokning/Mikrofon.tsx
@@ -47,7 +47,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
(lyssnar ? stoppa() : starta())}
- aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
+ aria-label={lyssnar ? "Stop recording" : "Start voice input"}
className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${
lyssnar
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
diff --git a/felsokning/app/src/felsokning/Slutsats.tsx b/felsokning/app/src/felsokning/Slutsats.tsx
index 6b16dd9..560266d 100644
--- a/felsokning/app/src/felsokning/Slutsats.tsx
+++ b/felsokning/app/src/felsokning/Slutsats.tsx
@@ -25,35 +25,37 @@ import { FARG, Etikett, Knapp, Statusmärke } from "@/alva/komponenter";
interface Brist {
falt: string;
+ /** Katalognyckeln. Saknas för brister som bara har en färdig text. */
+ nyckel?: string;
text: string;
}
const FALT = [
{
id: "motivering" as const,
- etikett: "Motivering",
- fraga: "Varför följer slutsatsen av underlaget?",
- hjalp: "Knyt mätvärden, foton och kontroller till slutsatsen. Vad i evidensen gör att just detta följer?",
+ etikett: "Rationale",
+ fraga: "Why does the conclusion follow from the evidence?",
+ hjalp: "Tie measurements, photographs and checks to the conclusion. What in the evidence makes this follow?",
rader: 4,
},
{
id: "uteslutet" as const,
- etikett: "Uteslutna alternativ",
- fraga: "Vad övervägdes, och varför föll det bort?",
- hjalp: "En felsökning utan uteslutna alternativ är en gissning som råkade stämma.",
+ etikett: "Dismissed alternatives",
+ fraga: "What was considered, and why was it dismissed?",
+ hjalp: "A diagnosis without dismissed alternatives is a guess that happened to be right.",
rader: 3,
},
{
id: "atgardsval" as const,
- etikett: "Val av åtgärd",
- fraga: "Varför denna åtgärd och inte en annan?",
- hjalp: "Byte eller justering? Hela enheten eller en del? Skälet läses av den som granskar kostnaden.",
+ etikett: "Choice of action",
+ fraga: "Why this action and not another?",
+ hjalp: "Replacement or adjustment? The whole unit or a part? The reason is read by whoever reviews the cost.",
rader: 2,
},
{
id: "kvarstaende" as const,
- etikett: "Kvarstående osäkerhet",
- fraga: "Vad är fortfarande osäkert?",
+ etikett: "Remaining uncertainty",
+ fraga: "What remains uncertain?",
hjalp: 'Får vara "inget" — men det måste sägas aktivt.',
rader: 2,
},
@@ -82,7 +84,13 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
);
const obemötta: string[] = useMemo(() => obemottaHypoteser(handelser, förslag), [handelser, förslag]);
- const brist = (falt: string) => brister.find((b) => b.falt === falt && !b.text.includes("bemöts inte"));
+ // Hypotesbristerna hör till fältet "uteslutet" men är inte en brist i
+ // fältet — de listas för sig nedan. De sållas därför bort här, på
+ // NYCKELN och inte på texten: texten är översatt och byter språk med
+ // organisationen, och en jämförelse mot en svensk fras slutade tyst
+ // att matcha när standardspråket blev engelska.
+ const brist = (falt: string) =>
+ brister.find((b) => b.falt === falt && b.nyckel !== "slutsats.hypotes_obemott");
const klar = brister.length === 0;
const arbeteUtfört = handelser.some((h) => h.typ === "atgard_utford" && h.utford);
@@ -115,8 +123,8 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
Men varför den inte kunde det är fortfarande ett varför. */}
{[
- { värde: true, text: "Orsak fastställd" },
- { värde: false, text: "Orsak kunde inte fastställas" },
+ { värde: true, text: "Cause established" },
+ { värde: false, text: "Cause could not be established" },
].map((val) => (
0 && (
-
Hypoteser i loggen som behöver bemötas
+
Hypotheses in the log that need addressing
{obemötta.map((text) => (
@@ -154,7 +162,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
{FALT.filter((f) => f.id !== "atgardsval" || arbeteUtfört).map((f) => {
const fel = brist(f.id);
- const etikett = f.id === "motivering" && !fastställd ? "Skäl till att orsaken inte fastställts" : f.etikett;
+ const etikett = f.id === "motivering" && !fastställd ? "Reason the cause could not be established" : f.etikett;
return (
skicka(förslag)}>
- {befintlig ? "Uppdatera slutsats" : "Registrera slutsats"}
+ {befintlig ? "Update closing statement" : "Record closing statement"}
{!klar && (
diff --git a/felsokning/app/src/felsokning/__tests__/ai.test.ts b/felsokning/app/src/felsokning/__tests__/ai.test.ts
index 5df408b..36b3647 100644
--- a/felsokning/app/src/felsokning/__tests__/ai.test.ts
+++ b/felsokning/app/src/felsokning/__tests__/ai.test.ts
@@ -192,7 +192,7 @@ describe("arbetsorderskanning", () => {
expect(falt.map((f) => f.id)).toEqual(["fordon_regnr", "fordon_vin"]);
expect(falt[0].konfidens).toBe(1);
expect(falt[0].varde).toBe("abc123");
- expect(falt[0].grupp).toBe("Personbil");
+ expect(falt[0].grupp).toBe("Vehicle");
expect(falt[1].omrade?.x).toBe(0.5);
expect(() => normaliseraArbetsorder({})).toThrow();
});
diff --git a/felsokning/app/src/felsokning/__tests__/installningar.test.ts b/felsokning/app/src/felsokning/__tests__/installningar.test.ts
index 5aa20e0..4090973 100644
--- a/felsokning/app/src/felsokning/__tests__/installningar.test.ts
+++ b/felsokning/app/src/felsokning/__tests__/installningar.test.ts
@@ -17,10 +17,10 @@ describe("organisationsinställningar", () => {
it("okända värden filtreras bort och ordningen följer standardlistan", () => {
const inst = normalisera({
- objekttyper: ["Hydraulik", "Påhittad typ", "Personbil"],
+ objekttyper: ["Hydraulics", "Påhittad typ", "Passenger car"],
identifieringsmetoder: ["VIN"],
});
- expect(inst.objekttyper).toEqual(["Personbil", "Hydraulik"]);
+ expect(inst.objekttyper).toEqual(["Passenger car", "Hydraulics"]);
expect(inst.identifieringsmetoder).toEqual(["VIN"]);
});
@@ -33,10 +33,10 @@ describe("organisationsinställningar", () => {
it("sparade val på enheten läses tillbaka", () => {
localStorage.setItem(
"gf-installningar",
- JSON.stringify({ objekttyper: ["Elsystem"], identifieringsmetoder: ["Serienummer", "Manuell inmatning"] }),
+ JSON.stringify({ objekttyper: ["Electrical system"], identifieringsmetoder: ["Serial number", "Manual entry"] }),
);
const inst = lastaInstallningar();
- expect(inst.objekttyper).toEqual(["Elsystem"]);
- expect(inst.identifieringsmetoder).toEqual(["Serienummer", "Manuell inmatning"]);
+ expect(inst.objekttyper).toEqual(["Electrical system"]);
+ expect(inst.identifieringsmetoder).toEqual(["Serial number", "Manual entry"]);
});
});
diff --git a/felsokning/app/src/felsokning/__tests__/projektioner.test.ts b/felsokning/app/src/felsokning/__tests__/projektioner.test.ts
index 7baf8f0..70609e8 100644
--- a/felsokning/app/src/felsokning/__tests__/projektioner.test.ts
+++ b/felsokning/app/src/felsokning/__tests__/projektioner.test.ts
@@ -18,7 +18,7 @@ function byggArende(poster: [string, Handelse][]): Arende {
const OBJEKT: Handelse = {
typ: "objekt_identifierat",
objekt: {
- typ: "Personbil",
+ typ: "Passenger car",
identifierare: "ABC123",
identifieringsmetod: "Registreringsnummer",
beskrivning: "Volvo XC60 D4 2019",
@@ -192,7 +192,7 @@ describe("brief", () => {
const b = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
expect(b.hypoteser).toEqual([{ text: "Obalans i höger framhjul", niva: "lag" }]);
expect(b.observationer).not.toContain("Obalans i höger framhjul");
- expect(b.tillforlitlighet.some((r) => r.niva === "medel" && /inte verifierad/.test(r.text))).toBe(true);
+ expect(b.tillforlitlighet.some((r) => r.niva === "medel" && /not yet verified/i.test(r.text))).toBe(true);
});
it("är regenererbar: samma logg ger alltid samma brief", () => {
diff --git a/felsokning/app/src/felsokning/__tests__/streckkod.test.ts b/felsokning/app/src/felsokning/__tests__/streckkod.test.ts
index 8a51472..f672556 100644
--- a/felsokning/app/src/felsokning/__tests__/streckkod.test.ts
+++ b/felsokning/app/src/felsokning/__tests__/streckkod.test.ts
@@ -7,15 +7,15 @@ describe("avläst kod klassificeras innan den används", () => {
// Mellanslag och bindestreck i streckkoder normaliseras bort.
expect(tolkaKod(" yv1dz8256f2123456 ")?.typ).toBe("VIN");
// 16 tecken är inget VIN — men kan vara ett serienummer.
- expect(tolkaKod("YV1DZ8256F212345")?.typ).toBe("Serienummer");
+ expect(tolkaKod("YV1DZ8256F212345")?.typ).toBe("Serial number");
// I/O/Q förekommer aldrig i VIN → klassas som serienummer.
- expect(tolkaKod("YV1DZ8256F212345O")?.typ).toBe("Serienummer");
+ expect(tolkaKod("YV1DZ8256F212345O")?.typ).toBe("Serial number");
});
it("känner igen svenska registreringsnummer i båda serierna", () => {
- expect(tolkaKod("ABC123")).toEqual({ varde: "ABC123", typ: "Regnr" });
- expect(tolkaKod("abc12a")).toEqual({ varde: "ABC12A", typ: "Regnr" });
- expect(tolkaKod("ABC 123")?.typ).toBe("Regnr");
+ expect(tolkaKod("ABC123")).toEqual({ varde: "ABC123", typ: "Reg. no." });
+ expect(tolkaKod("abc12a")).toEqual({ varde: "ABC12A", typ: "Reg. no." });
+ expect(tolkaKod("ABC 123")?.typ).toBe("Reg. no.");
});
it("plockar ut identifieraren ur QR-innehåll med fält eller URL", () => {
@@ -23,12 +23,12 @@ describe("avläst kod klassificeras innan den används", () => {
varde: "YV1DZ8256F2123456",
typ: "VIN",
});
- expect(tolkaKod("REGNR: ABC123")).toEqual({ varde: "ABC123", typ: "Regnr" });
+ expect(tolkaKod("REGNR: ABC123")).toEqual({ varde: "ABC123", typ: "Reg. no." });
expect(tolkaKod("vin YV1DZ8256F2123456")?.typ).toBe("VIN");
});
it("tar maskin-/serienummer som de är men avvisar fritext och nakna URL:er", () => {
- expect(tolkaKod("SN-4471/A")).toEqual({ varde: "SN4471/A", typ: "Serienummer" });
+ expect(tolkaKod("SN-4471/A")).toEqual({ varde: "SN4471/A", typ: "Serial number" });
expect(tolkaKod("https://exempel.se/nagot-helt-annat")).toBeNull();
expect(tolkaKod("Kontrollera oljenivån innan start")).toBeNull();
expect(tolkaKod(" ")).toBeNull();
diff --git a/felsokning/app/src/felsokning/ai.ts b/felsokning/app/src/felsokning/ai.ts
index 11def88..18f95b7 100644
--- a/felsokning/app/src/felsokning/ai.ts
+++ b/felsokning/app/src/felsokning/ai.ts
@@ -33,9 +33,9 @@ export type AiRadTyp = "observation" | "verifierat" | "hypotes" | "rekommendatio
export const AI_RADTYP_LABEL: Record = {
observation: "Observation",
- verifierat: "Verifierat",
- hypotes: "Hypotes (ej verifierad)",
- rekommendation: "Rekommendation",
+ verifierat: "Verified",
+ hypotes: "Hypothesis (unverified)",
+ rekommendation: "Recommendation",
};
export interface AiRad {
@@ -58,11 +58,11 @@ export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
if (brief.utfordaKontroller.length > 0) {
- rader.push("Utförda kontroller:");
+ rader.push("Checks performed:");
for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`);
}
if (brief.observationer.length > 0) {
- rader.push("Observationer och mätvärden:");
+ rader.push("Observations and measurements:");
for (const o of brief.observationer) rader.push(`- ${o}`);
}
if (brief.ejKontrollerat.length > 0) {
@@ -87,7 +87,7 @@ export function byggGranskningsPrompt(arende: Arende, metodik: Metodik): string
export function tolkaAiSvar(data: unknown): AiSvar {
const svar = data as AiSvar;
if (!svar || !Array.isArray(svar.rader) || typeof svar.nastaSteg !== "string") {
- throw new Error("Oväntat AI-svarsformat");
+ throw new Error("Unexpected AI response format");
}
const giltiga: AiRadTyp[] = ["observation", "verifierat", "hypotes", "rekommendation"];
const rader = svar.rader.filter(
@@ -101,7 +101,7 @@ export function tolkaAiSvar(data: unknown): AiSvar {
// (OCR + layoutförståelse) och returnerar strukturerade fält med
// konfidens per värde. Bara osäkra fält behöver granskas.
-export type ArbetsorderGrupp = "Kund" | "Personbil" | "Verkstad" | "Ärende";
+export type ArbetsorderGrupp = "Customer" | "Vehicle" | "Workshop" | "Case";
export interface TolkatFalt {
id: string;
@@ -114,36 +114,36 @@ export interface TolkatFalt {
}
export const ARBETSORDER_FALT: { id: string; grupp: ArbetsorderGrupp; etikett: string }[] = [
- { id: "kund_namn", grupp: "Kund", etikett: "Namn" },
- { id: "kund_foretag", grupp: "Kund", etikett: "Företag" },
- { id: "kund_telefon", grupp: "Kund", etikett: "Telefon" },
- { id: "kund_epost", grupp: "Kund", etikett: "E-post" },
- { id: "fordon_regnr", grupp: "Personbil", etikett: "Regnr" },
- { id: "fordon_vin", grupp: "Personbil", etikett: "VIN" },
- { id: "fordon_marke", grupp: "Personbil", etikett: "Märke" },
- { id: "fordon_modell", grupp: "Personbil", etikett: "Modell" },
- { id: "fordon_motor", grupp: "Personbil", etikett: "Motor" },
- { id: "fordon_arsmodell", grupp: "Personbil", etikett: "Årsmodell" },
- { id: "fordon_matarstallning", grupp: "Personbil", etikett: "Mätarställning" },
- { id: "fordon_motorkod", grupp: "Personbil", etikett: "Motorkod" },
- { id: "fordon_vaxellada", grupp: "Personbil", etikett: "Växellåda" },
- { id: "ao_nummer", grupp: "Verkstad", etikett: "Arbetsordernr" },
- { id: "ao_claim", grupp: "Verkstad", etikett: "Claim-/garantinr" },
- { id: "ao_skadenummer", grupp: "Verkstad", etikett: "Skadenummer" },
- { id: "ao_referens", grupp: "Verkstad", etikett: "Intern referens" },
- { id: "ao_serviceradgivare", grupp: "Verkstad", etikett: "Servicerådgivare" },
- { id: "ao_bokningsdatum", grupp: "Verkstad", etikett: "Bokningsdatum" },
- { id: "felbeskrivning", grupp: "Ärende", etikett: "Felbeskrivning" },
+ { id: "kund_namn", grupp: "Customer", etikett: "Name" },
+ { id: "kund_foretag", grupp: "Customer", etikett: "Company" },
+ { id: "kund_telefon", grupp: "Customer", etikett: "Telephone" },
+ { id: "kund_epost", grupp: "Customer", etikett: "E-mail" },
+ { id: "fordon_regnr", grupp: "Vehicle", etikett: "Reg. no." },
+ { id: "fordon_vin", grupp: "Vehicle", etikett: "VIN" },
+ { id: "fordon_marke", grupp: "Vehicle", etikett: "Make" },
+ { id: "fordon_modell", grupp: "Vehicle", etikett: "Model" },
+ { id: "fordon_motor", grupp: "Vehicle", etikett: "Motor" },
+ { id: "fordon_arsmodell", grupp: "Vehicle", etikett: "Model year" },
+ { id: "fordon_matarstallning", grupp: "Vehicle", etikett: "Odometer" },
+ { id: "fordon_motorkod", grupp: "Vehicle", etikett: "Engine code" },
+ { id: "fordon_vaxellada", grupp: "Vehicle", etikett: "Transmission" },
+ { id: "ao_nummer", grupp: "Workshop", etikett: "Work order no." },
+ { id: "ao_claim", grupp: "Workshop", etikett: "Claim / warranty no." },
+ { id: "ao_skadenummer", grupp: "Workshop", etikett: "Claim number" },
+ { id: "ao_referens", grupp: "Workshop", etikett: "Internal reference" },
+ { id: "ao_serviceradgivare", grupp: "Workshop", etikett: "Service advisor" },
+ { id: "ao_bokningsdatum", grupp: "Workshop", etikett: "Booking date" },
+ { id: "felbeskrivning", grupp: "Case", etikett: "Fault description" },
];
const ARBETSORDER_PROMPT =
- "Tolka den bifogade arbetsordern från en fordonsverkstad och extrahera fälten.";
+ "Interpret the attached work order from a vehicle workshop and extract the fields.";
// Rensar AI-svaret: okända fält-id filtreras, konfidens klipps till 0–1,
// tomma värden tas bort och etikett/grupp slås upp ur katalogen.
export function normaliseraArbetsorder(data: unknown): TolkatFalt[] {
const rader = (data as { falt?: unknown }).falt;
- if (!Array.isArray(rader)) throw new Error("Oväntat tolkningsformat");
+ if (!Array.isArray(rader)) throw new Error("Unexpected interpretation format");
const resultat: TolkatFalt[] = [];
for (const rad of rader as Partial[]) {
const def = ARBETSORDER_FALT.find((f) => f.id === rad?.id);
@@ -188,7 +188,7 @@ export interface InstrumentTolkning {
export function normaliseraInstrument(data: unknown): InstrumentTolkning {
const d = data as Partial;
if (!d || typeof d.instrumenttyp !== "string" || !Array.isArray(d.varden)) {
- throw new Error("Oväntat avläsningsformat");
+ throw new Error("Unexpected reading format");
}
const varden: InstrumentVarde[] = [];
for (const rad of d.varden as Partial[]) {
@@ -208,7 +208,7 @@ export async function lasAvInstrument(
): Promise<{ tolkning: InstrumentTolkning; modell: string } | null> {
const resultat = await anropa(
"instrumentavlasning",
- "Läs av instrumentet/skärmen i den bifogade bilden.",
+ "Read the instrument or display in the attached image.",
{ bild: dataUrl },
);
return resultat ? { tolkning: normaliseraInstrument(resultat.svar), modell: resultat.modell } : null;
@@ -265,7 +265,7 @@ async function anropa(
resultat = svar;
}
const { modell, svar } = resultat as { modell?: string; svar?: unknown };
- if (typeof modell !== "string") throw new Error("Oväntat svar från AI-endpointen");
+ if (typeof modell !== "string") throw new Error("Unexpected response from the AI endpoint");
return { modell, svar };
}
diff --git a/felsokning/app/src/felsokning/demo.ts b/felsokning/app/src/felsokning/demo.ts
index 2ef4b06..8be4076 100644
--- a/felsokning/app/src/felsokning/demo.ts
+++ b/felsokning/app/src/felsokning/demo.ts
@@ -188,17 +188,17 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
// fält → starta diagnos) kan provas ändå. Märks alltid som demo i UI:t.
export function byggDemoTolkning(): TolkatFalt[] {
return [
- { id: "kund_namn", etikett: "Namn", grupp: "Kund", varde: "Anders Svensson", konfidens: 0.98 },
- { id: "kund_telefon", etikett: "Telefon", grupp: "Kund", varde: "070-123 45 67", konfidens: 0.91, omrade: { x: 0.08, y: 0.22, bredd: 0.3, hojd: 0.04 } },
- { id: "fordon_regnr", etikett: "Regnr", grupp: "Personbil", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } },
- { id: "fordon_vin", etikett: "VIN", grupp: "Personbil", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } },
- { id: "fordon_marke", etikett: "Märke", grupp: "Personbil", varde: "Volvo", konfidens: 0.99 },
- { id: "fordon_modell", etikett: "Modell", grupp: "Personbil", varde: "XC60 D4", konfidens: 0.97 },
- { id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Personbil", varde: "2019", konfidens: 0.96 },
- { id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Personbil", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } },
- { id: "ao_nummer", etikett: "Arbetsordernr", grupp: "Verkstad", varde: "AO-2496", konfidens: 0.99 },
- { id: "ao_serviceradgivare", etikett: "Servicerådgivare", grupp: "Verkstad", varde: "M. Lindqvist", konfidens: 0.82 },
- { id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Ärende", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } },
+ { id: "kund_namn", etikett: "Namn", grupp: "Customer", varde: "Anders Svensson", konfidens: 0.98 },
+ { id: "kund_telefon", etikett: "Telefon", grupp: "Customer", varde: "070-123 45 67", konfidens: 0.91, omrade: { x: 0.08, y: 0.22, bredd: 0.3, hojd: 0.04 } },
+ { id: "fordon_regnr", etikett: "Regnr", grupp: "Vehicle", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } },
+ { id: "fordon_vin", etikett: "VIN", grupp: "Vehicle", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } },
+ { id: "fordon_marke", etikett: "Märke", grupp: "Vehicle", varde: "Volvo", konfidens: 0.99 },
+ { id: "fordon_modell", etikett: "Modell", grupp: "Vehicle", varde: "XC60 D4", konfidens: 0.97 },
+ { id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Vehicle", varde: "2019", konfidens: 0.96 },
+ { id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Vehicle", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } },
+ { id: "ao_nummer", etikett: "Arbetsordernr", grupp: "Workshop", varde: "AO-2496", konfidens: 0.99 },
+ { id: "ao_serviceradgivare", etikett: "Servicerådgivare", grupp: "Workshop", varde: "M. Lindqvist", konfidens: 0.82 },
+ { id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Case", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } },
];
}
diff --git a/felsokning/app/src/felsokning/domain.ts b/felsokning/app/src/felsokning/domain.ts
index afbd1f8..c846094 100644
--- a/felsokning/app/src/felsokning/domain.ts
+++ b/felsokning/app/src/felsokning/domain.ts
@@ -5,9 +5,9 @@
export type Tillforlitlighet = "hog" | "medel" | "lag";
export const TILLFORLITLIGHET_LABEL: Record = {
- hog: "Hög",
- medel: "Medel",
- lag: "Låg",
+ hog: "High",
+ medel: "Medium",
+ lag: "Low",
};
export type TidKategori =
@@ -20,20 +20,20 @@ export type TidKategori =
| "paus";
export const TIDKATEGORI_LABEL: Record = {
- aktiv_felsokning: "Aktiv felsökning",
- vantetid: "Väntetid",
+ aktiv_felsokning: "Active diagnosis",
+ vantetid: "Waiting time",
administration: "Administration",
- reservdelssokning: "Reservdelssökning",
- provkorning: "Provkörning",
- kundkontakt: "Kundkontakt",
- paus: "Paus",
+ reservdelssokning: "Parts search",
+ provkorning: "Road test",
+ kundkontakt: "Customer contact",
+ paus: "Break",
};
// Kundens besked på ett lämnat åtgärdsförslag.
export const KUNDBESLUT_LABEL: Record<"godkant" | "avbojt" | "delvis", string> = {
- godkant: "Godkänt",
- avbojt: "Avböjt",
- delvis: "Delvis godkänt",
+ godkant: "Approved",
+ avbojt: "Declined",
+ delvis: "Partly approved",
};
// Kvalitetskontrollens utfall efter utförd åtgärd.
@@ -41,10 +41,10 @@ export const KVALITETSKONTROLL_LABEL: Record<
"symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar",
string
> = {
- symptomet_borta: "Symptomet är borta",
- kvarstar: "Symptomet kvarstår",
- delvis: "Symptomet delvis kvar",
- ej_verifierbar: "Kunde inte verifieras",
+ symptomet_borta: "Symptom is gone",
+ kvarstar: "Symptom remains",
+ delvis: "Symptom partly remains",
+ ej_verifierbar: "Could not be verified",
};
// Fordonsobjektet är den röda tråden genom hela ärendet: identiteten
@@ -52,9 +52,9 @@ export const KVALITETSKONTROLL_LABEL: Record<
// och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet
// så att de följer med i alla vyer utan att kunduppgifter läcker.
export interface Objekt {
- typ: string; // t.ex. "Personbil", "Industrimaskin"
+ typ: string; // t.ex. "Passenger car", "Industrial machine"
identifierare: string; // reg.nr, VIN, serienummer …
- identifieringsmetod: string; // "Regnr", "VIN", "Serienummer", "Manuell inmatning"
+ identifieringsmetod: string; // "Reg. no.", "VIN", "Serial number", "Manual entry"
beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
kund?: string;
vin?: string;
@@ -331,7 +331,7 @@ export function handelseRubrik(post: LoggPost): string {
case "atgard_utford":
return h.utford
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
- : `Ingen åtgärd utförd — ${h.motivering ?? "orsak saknas"}`;
+ : `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`;
case "kvalitetskontroll":
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
case "export_skapad":
@@ -341,6 +341,6 @@ export function handelseRubrik(post: LoggPost): string {
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`;
}
case "arende_avslutat":
- return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Felsökning avslutad";
+ return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Diagnosis closed";
}
}
diff --git a/felsokning/app/src/felsokning/format.ts b/felsokning/app/src/felsokning/format.ts
index ebd2893..93911a4 100644
--- a/felsokning/app/src/felsokning/format.ts
+++ b/felsokning/app/src/felsokning/format.ts
@@ -26,10 +26,10 @@ export function tidDatum(iso: string): string {
export function skalaNerFoto(fil: File, maxSida = 1024): Promise {
return new Promise((resolve, reject) => {
const lasare = new FileReader();
- lasare.onerror = () => reject(new Error("Kunde inte läsa filen"));
+ lasare.onerror = () => reject(new Error("Could not read the file"));
lasare.onload = () => {
const bild = new Image();
- bild.onerror = () => reject(new Error("Kunde inte tolka bilden"));
+ bild.onerror = () => reject(new Error("Could not interpret the image"));
bild.onload = () => {
const skala = Math.min(1, maxSida / Math.max(bild.width, bild.height));
const canvas = document.createElement("canvas");
@@ -51,12 +51,12 @@ export const MAX_VIDEO_BYTE = 2_500_000;
export function lasVideo(fil: File): Promise {
return new Promise((los, avvisa) => {
if (fil.size > MAX_VIDEO_BYTE) {
- avvisa(new Error("Videon är för stor — spela in ett kort klipp (ca 10–15 sekunder)."));
+ avvisa(new Error("The video is too large — record a short clip (about 10–15 seconds)."));
return;
}
const lasare = new FileReader();
lasare.onload = () => los(lasare.result as string);
- lasare.onerror = () => avvisa(new Error("Videon kunde inte läsas."));
+ lasare.onerror = () => avvisa(new Error("The video could not be read."));
lasare.readAsDataURL(fil);
});
}
diff --git a/felsokning/app/src/felsokning/installningar.ts b/felsokning/app/src/felsokning/installningar.ts
index 7c94439..1a44786 100644
--- a/felsokning/app/src/felsokning/installningar.ts
+++ b/felsokning/app/src/felsokning/installningar.ts
@@ -7,21 +7,21 @@
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
export const ALLA_OBJEKTTYPER = [
- "Personbil",
- "Lastbil/Buss",
- "Entreprenadmaskin",
- "Industrimaskin",
- "Elsystem",
- "Hydraulik",
- "Övrigt",
+ "Passenger car",
+ "Truck / bus",
+ "Construction machine",
+ "Industrial machine",
+ "Electrical system",
+ "Hydraulics",
+ "Other",
];
export const ALLA_IDENTIFIERINGSMETODER = [
- "Regnr",
+ "Reg. no.",
"VIN",
- "Serienummer",
- "Maskinnummer",
- "Manuell inmatning",
+ "Serial number",
+ "Machine number",
+ "Manual entry",
];
export interface Installningar {
diff --git a/felsokning/app/src/felsokning/metodik.ts b/felsokning/app/src/felsokning/metodik.ts
index 30fe11a..357d119 100644
--- a/felsokning/app/src/felsokning/metodik.ts
+++ b/felsokning/app/src/felsokning/metodik.ts
@@ -168,12 +168,12 @@ export interface NastaSteg {
*/
export const SPARRFRAGOR: Record = {
"hogvolt/sakerhet/behorighet": {
- orsak: "Arbete på högvoltsystem kräver dokumenterad behörighet.",
- atgard: "Lämna över ärendet till en behörig tekniker. Arbetet får inte påbörjas utan behörighet.",
+ orsak: "Work on high-voltage systems requires documented authorisation.",
+ atgard: "Hand the case to an authorised technician. Work must not begin without authorisation.",
},
"hogvolt/sakerhet/avstangt": {
- orsak: "Fordonet är inte spänningslöst enligt tillverkarens rutin.",
- atgard: "Gör fordonet spänningslöst enligt tillverkarens anvisning innan något ingrepp påbörjas.",
+ orsak: "The vehicle is not de-energised per the manufacturer's procedure.",
+ atgard: "De-energise the vehicle per the manufacturer's instruction before any work begins.",
},
};
@@ -197,7 +197,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}`) !== "Ja") {
+ if (sparr && svar.get(`${steg.id}/${fraga.id}`) !== "Yes") {
return { steg, fraga, klart: false, sparr };
}
}
diff --git a/felsokning/app/src/felsokning/projektioner.ts b/felsokning/app/src/felsokning/projektioner.ts
index ba0dd6d..f863011 100644
--- a/felsokning/app/src/felsokning/projektioner.ts
+++ b/felsokning/app/src/felsokning/projektioner.ts
@@ -262,19 +262,19 @@ export interface TillforlitlighetsRad {
export function tillforlitlighet(arende: Arende): TillforlitlighetsRad[] {
const rader: TillforlitlighetsRad[] = [];
- if (objekt(arende)) rader.push({ niva: "hog", text: "Objekt identifierat" });
- if (foton(arende).length > 0) rader.push({ niva: "hog", text: "Bilder dokumenterade" });
+ if (objekt(arende)) rader.push({ niva: "hog", text: "Object identified" });
+ if (foton(arende).length > 0) rader.push({ niva: "hog", text: "Images documented" });
if (arende.handelser.some((p) => p.handelse.typ === "matvarde")) {
- rader.push({ niva: "hog", text: "Mätvärden registrerade" });
+ rader.push({ niva: "hog", text: "Measurements recorded" });
}
if (utfordaKontroller(arende).length > 0) {
- rader.push({ niva: "hog", text: "Kontroller dokumenterade" });
+ rader.push({ niva: "hog", text: "Checks documented" });
}
// Felorsaken är per definition inte verifierad förrän ärendet avslutats
// med en verifierad slutsats — MVP:t markerar den alltid som ej verifierad
// så länge ärendet är öppet.
if (!arAvslutat(arende)) {
- rader.push({ niva: "medel", text: "Felorsak ännu inte verifierad" });
+ rader.push({ niva: "medel", text: "Root cause not yet verified" });
}
return rader;
}
@@ -306,7 +306,7 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief {
if (!rekommenderat.includes(text)) rekommenderat.push(text);
}
if (rekommenderat.length === 0) {
- rekommenderat.push("Samtliga kontroller i metodiken utförda. Verifiera slutsats eller utöka felsökningen.");
+ rekommenderat.push("Every check in the methodology is complete. Verify the conclusion or extend the diagnosis.");
}
}
return {
@@ -332,25 +332,25 @@ export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string):
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`);
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
rader.push("");
- rader.push("Utförda kontroller:");
- if (b.utfordaKontroller.length === 0) rader.push(" (inga ännu)");
+ rader.push("Checks performed:");
+ if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
for (const k of b.utfordaKontroller)
rader.push(k.undantag ? ` ! ${k.text} — underlag saknas: ${k.undantag}` : ` ✓ ${k.text}${k.resultat ? ` — ${k.resultat}` : ""}`);
rader.push("");
- rader.push("Observationer:");
- if (b.observationer.length === 0) rader.push(" (inga ännu)");
+ rader.push("Observations:");
+ if (b.observationer.length === 0) rader.push(" (none yet)");
for (const o of b.observationer) rader.push(` • ${o}`);
if (b.hypoteser.length > 0) {
rader.push("");
- rader.push("Hypoteser (ej verifierade):");
+ rader.push("Hypotheses (unverified):");
for (const h of b.hypoteser) rader.push(` ? ${h.text}`);
}
rader.push("");
- rader.push("Ej kontrollerat:");
- if (b.ejKontrollerat.length === 0) rader.push(" (allt i metodiken utfört)");
+ rader.push("Not checked:");
+ if (b.ejKontrollerat.length === 0) rader.push(" (everything in the methodology is complete)");
for (const e of b.ejKontrollerat) rader.push(` – ${e}`);
rader.push("");
- rader.push("Rekommenderat nästa steg:");
+ rader.push("Recommended next step:");
b.rekommenderatNastaSteg.forEach((s, i) => rader.push(` ${i + 1}. ${s}`));
rader.push("");
rader.push(`Total arbetstid: ${b.totalArbetstid}`);
diff --git a/felsokning/app/src/felsokning/streckkod.ts b/felsokning/app/src/felsokning/streckkod.ts
index 69cc417..05ab08d 100644
--- a/felsokning/app/src/felsokning/streckkod.ts
+++ b/felsokning/app/src/felsokning/streckkod.ts
@@ -11,7 +11,7 @@
// Den avlästa koden klassificeras alltid innan den används, så att rätt
// identifieringsmetod sätts på fordonsobjektet.
-export type KodTyp = "VIN" | "Regnr" | "Serienummer";
+export type KodTyp = "VIN" | "Reg. no." | "Serial number";
export interface AvlastKod {
varde: string;
@@ -37,16 +37,16 @@ export function tolkaKod(ratext: string): AvlastKod | null {
const vinFalt = text.match(/(?:vin|chassi)[=:/\s]+([A-HJ-NPR-Z0-9]{17})/i);
if (vinFalt) return { varde: vinFalt[1].toUpperCase(), typ: "VIN" };
const regnrFalt = text.match(/(?:reg(?:nr)?|plate)[=:/\s]+([A-Za-z]{3}\s?[0-9]{2}[0-9A-Za-z])/i);
- if (regnrFalt) return { varde: regnrFalt[1].replace(/\s/g, "").toUpperCase(), typ: "Regnr" };
+ if (regnrFalt) return { varde: regnrFalt[1].replace(/\s/g, "").toUpperCase(), typ: "Reg. no." };
const rent = text.replace(/[\s-]/g, "").toUpperCase();
if (VIN_MONSTER.test(rent)) return { varde: rent, typ: "VIN" };
- if (REGNR_MONSTER.test(rent)) return { varde: rent, typ: "Regnr" };
+ if (REGNR_MONSTER.test(rent)) return { varde: rent, typ: "Reg. no." };
// Allt annat (maskin-/serienummer) tas som det är, men bara om det ser
// ut som en identifierare — inte som fritext eller en hel URL.
if (/^[A-Z0-9._/-]{4,40}$/.test(rent) && !/^HTTPS?:/.test(rent)) {
- return { varde: rent, typ: "Serienummer" };
+ return { varde: rent, typ: "Serial number" };
}
return null;
}
@@ -78,7 +78,7 @@ export async function startaSkanning(
): Promise {
const Detektor = detektorKlass();
if (!Detektor) {
- vidFel?.("Webbläsaren saknar streckkodsläsning — fotografera typskylten i stället.");
+ vidFel?.("The browser has no barcode reader — photograph the type plate instead.");
return { stoppa: () => {} };
}
@@ -104,7 +104,7 @@ export async function startaSkanning(
// starta. Detektorn får helt enkelt tomma rutor tills strömmen är i gång.
void video.play().catch(() => {});
} catch {
- vidFel?.("Kameran kunde inte startas — kontrollera behörigheten eller fotografera typskylten.");
+ vidFel?.("The camera could not be started — check the permission or photograph the type plate.");
return { stoppa };
}
diff --git a/felsokning/app/src/pages/felsokning/ArendeSida.tsx b/felsokning/app/src/pages/felsokning/ArendeSida.tsx
index 2bd2e9b..0816ac7 100644
--- a/felsokning/app/src/pages/felsokning/ArendeSida.tsx
+++ b/felsokning/app/src/pages/felsokning/ArendeSida.tsx
@@ -80,17 +80,17 @@ import { bilageFalt } from "@/felsokning/bilagor";
const FLIKAR = [
{ id: "guide", label: "Guide" },
- { id: "logg", label: "Logg" },
+ { id: "logg", label: "Log" },
{ id: "brief", label: "Brief" },
- { id: "rapport", label: "Rapport" },
+ { id: "rapport", label: "Report" },
] as const;
type Flik = (typeof FLIKAR)[number]["id"];
const SYNKSTATUS_LABEL: Record = {
- lokal: "Lokalt läge",
- synkar: "Synkar …",
- synkad: "Synkad",
+ lokal: "Local mode",
+ synkar: "Synchronizing …",
+ synkad: "Synchronized",
offline: "Offline",
};
@@ -144,8 +144,8 @@ export default function ArendeSida() {
if (!arende || !id) {
return (
-
- Ärendet finns inte på den här enheten.
+
+ The case does not exist on this device.
);
}
@@ -172,12 +172,12 @@ export default function ArendeSida() {
{total}
- {avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]}
+ {avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]}
}
@@ -254,16 +254,16 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
) : null;
return (
- {del("AO", idn.arbetsorder)}
+ {del("WO", idn.arbetsorder)}
{del("Claim", idn.claim)}
- {del("Skadenr", idn.skadenummer)}
+ {del("Claim no.", idn.skadenummer)}
{del("", idn.beskrivning)}
- {del("Regnr", idn.identifierare)}
+ {del("Reg. no.", idn.identifierare)}
{del("VIN", idn.vin)}
- {del("Miltal", idn.miltal)}
- {del("Ansvarig", idn.ansvarig)}
+ {del("Mileage", idn.miltal)}
+ {del("Responsible", idn.ansvarig)}
- Ärendetyp
+ Case type
Fotografera instrumentpanelen
)}
- {laser && Systemet läser av mätarställningen …
}
+ {laser && Reading the odometer …
}
{foto && !laser && (
{demo && (
@@ -334,8 +334,8 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
Demo-avläsning — bildtolkningen kräver inloggning. Kontrollera värdet.
)}
-
-
+
+
{
@@ -399,7 +399,7 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
Ärende #{rad.nummer} · {tidDatum(rad.skapad)} ·{" "}
- {rad.avslutat ? "avslutat" : "pågående"}
+ {rad.avslutat ? "closed" : "in progress"}
{rad.felbeskrivning && — ”{rad.felbeskrivning}” }
{rad.felorsaker.map((f, i) => (
@@ -437,7 +437,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
const harTidiga = arende.handelser.some((p) => p.handelse.typ === "observation" || p.handelse.typ === "foto");
return (
-
+
{rader.map((r) => (
{r.klar ? : "☐"} {r.rubrik}
@@ -456,16 +456,16 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
Ja — kontrollerad
setHistorikVal("nej")}>
- Nej
+ No
{historikVal && (
<>
- Dokumentera
+ Document
>
)}
@@ -541,26 +541,26 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
const [text, setText] = useState("");
const etikett =
status === "ja"
- ? "Hur reproducerades felet? (förhållanden, hastighet, temperatur, belastning …)"
+ ? "How was the fault reproduced? (conditions, speed, temperature, load …)"
: status === "delvis"
- ? "Vad kunde respektive kunde inte reproduceras?"
- : "Motivering (obligatorisk) — varför kunde felet inte reproduceras?";
+ ? "What could and could not be reproduced?"
+ : "Rationale (required) — why could the fault not be reproduced?";
return (
-
+
Har kundens fel kunnat reproduceras?
{(["ja", "delvis", "nej"] as const).map((val) => (
setStatus(val)}>
- {val === "ja" ? "Ja" : val === "delvis" ? "Delvis" : "Nej"}
+ {val === "ja" ? "Yes" : val === "delvis" ? "Partly" : "No"}
))}
{status && (
<>
+ platshallare={status === "ja" ? "e.g. Reproduced three times at 92–108 km/h on level road, engine warm" : ""} />
{
@@ -599,15 +599,15 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
const spara = () => {
const avvikelseFel = granskaAvvikelse(avvikelse);
if (avvikelseFel) return setFel(avvikelseFel);
- if (orsaker.length === 0) return setFel("Välj minst en orsakskategori.");
- if (underlag.length === 0) return setFel("Koppla minst en evidenskälla till bedömningen.");
+ if (orsaker.length === 0) return setFel("Select at least one cause category.");
+ if (underlag.length === 0) return setFel("Link at least one evidence source to the assessment.");
const saknat = underlag.find((k) => !underlagFinns(arende, k));
if (saknat) return setFel(`Underlaget ”${saknat}” finns inte i ärendets logg — dokumentera det först eller välj en annan källa.`);
- if (orsaker.includes("Okänd orsak") && !motivering.trim())
- return setFel("Okänd orsak kräver en motivering till varför orsaken inte kunnat fastställas.");
+ if (orsaker.includes("Unknown cause") && !motivering.trim())
+ return setFel("An unknown cause requires a reason why the cause could not be established.");
if (sakerhet !== "hog" && !ytterligare.trim())
- return setFel("Vid medel/låg säkerhet: ange vilka ytterligare kontroller som skulle stärka bedömningen.");
- if (!atgard.trim()) return setFel("Ange rekommenderad åtgärd.");
+ return setFel("At medium or low confidence: state which further checks would strengthen the assessment.");
+ if (!atgard.trim()) return setFel("State the recommended action.");
skicka({
typ: "felorsak",
avvikelse: avvikelse.trim(),
@@ -630,7 +630,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
};
return (
-
+
{dokumenterade.map((p) => {
const h = p.handelse;
if (h.typ !== "felorsak") return null;
@@ -651,14 +651,14 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
) : (
-
2. Mest sannolik orsak (en eller flera)
+
2. Most probable cause (one or more)
{aktivtRegelpaket().orsakskategorier.map((o) => (
))}
-
3. Underlag som stöder bedömningen
+
3. Evidence supporting the assessment
{aktivtRegelpaket().underlagskallor.map((k) => (
))}
-
4. Säkerhet i bedömningen
+
4. Confidence in the assessment
{(["hog", "medel", "lag"] as const).map((n) => (
{sakerhet !== "hog" && (
)}
- {orsaker.includes("Okänd orsak") && (
+ {orsaker.includes("Unknown cause") && (
)}
-
+
{fel && {fel}
}
setOppen(false)}>
Avbryt
- Spara felorsak
+ Save root cause
)}
@@ -762,7 +762,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
const [kkText, setKkText] = useState("");
return (
-
+
{poster.map((p) => {
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
@@ -770,7 +770,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{h.utford ? (
<>
- Utförd åtgärd: {h.beskrivning}
+ Action performed: {h.beskrivning}
{h.delar && · Delar: {h.delar} }
>
) : (
@@ -792,7 +792,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
if (h.typ !== "atgardsforslag") return null;
return (
- Åtgärdsförslag till kund: {h.beskrivning}
+ Proposed action for the customer: {h.beskrivning}
{h.uppskattadKostnad && · Uppskattad kostnad: {h.uppskattadKostnad} }
);
@@ -827,14 +827,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{forslagsLage && (
-
+
setForslagsLage(false)}>
Avbryt
@@ -878,7 +878,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{beslutsVal && (
<>
-
Hur lämnades beskedet?
+
How was the decision given?
{KUNDKANALER.map((k) => (
setLage("ingen")}>
- Ingen åtgärd utförd
+ No action performed
)}
@@ -937,14 +937,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{lage === "utford" && (
-
+
setLage("")}>
Avbryt
@@ -982,7 +982,7 @@ 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.
@@ -995,12 +995,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{
- skicka({ typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: orsak.trim() });
+ skicka({ typ: "atgard_utford", beskrivning: "No action performed", utford: false, motivering: orsak.trim() });
setLage("");
setOrsak("");
}}
>
- Dokumentera
+ Document
@@ -1034,16 +1034,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
-
- {rad("Objekt", b.objekt?.beskrivning)}
+
+ {rad("Object", b.objekt?.beskrivning)}
{rad("Ident", b.objekt?.identifierare)}
- {rad("Kund", b.objekt?.kund)}
- {rad("Ansvarig", b.ansvarig)}
- {rad("Status", b.avslutat ? "Avslutat" : "Pågår")}
- {rad("Synk", SYNKSTATUS_LABEL[synkStatus])}
- {rad("Arbetstid", b.totalArbetstid)}
+ {rad("Customer", b.objekt?.kund)}
+ {rad("Responsible", b.ansvarig)}
+ {rad("Status", b.avslutat ? "Closed" : "In progress")}
+ {rad("Sync", SYNKSTATUS_LABEL[synkStatus])}
+ {rad("Labour time", b.totalArbetstid)}
-
+
{b.tillforlitlighet.map((r, i) => (
{r.text}
@@ -1158,7 +1158,7 @@ function KontextPanel({
))}
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
-
+
{b.rekommenderatNastaSteg.map((s, i) => (
{i + 1}. {s}
@@ -1167,14 +1167,14 @@ function KontextPanel({
)}
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
-
+
{senasteAi.handelse.rader.map((r, i) => (
{AI_RADTYP_LABEL[r.typ]}: {r.text}
))}
- Nästa steg: {senasteAi.handelse.nastaSteg}
+ Next step: {senasteAi.handelse.nastaSteg}
Beslutsstöd · presenteras aldrig som konstaterat fel
@@ -1205,8 +1205,8 @@ function InaktivitetsBanner({
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
- Beskriv kort vad som gjorts under denna period.
-
+ Briefly describe what was done during this period.
+
{
@@ -1284,7 +1284,7 @@ function GuideFlik({
if (avslutat) {
return (
-
+
Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export.
@@ -1338,7 +1338,7 @@ function GuideFlik({
{steg.klart ? (
<>
- Samtliga steg i metodiken är dokumenterade.
+ 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.
@@ -1353,7 +1353,7 @@ function GuideFlik({
// hinder, inte en varning. Enda vägen framåt är att ändra
// förutsättningen, inte att klicka förbi.
-
Arbetet får inte fortsätta
+
Work must not continue
{steg.sparr.orsak}
{steg.sparr.atgard}
@@ -1373,12 +1373,12 @@ function GuideFlik({
rubrik={
senasteAiSvar?.handelse.typ === "ai_svar"
? `Handledning (${senasteAiSvar.handelse.modell})`
- : "Handledning"
+ : "Guidance"
}
>
- {aiStatus === "arbetar" &&
Systemet analyserar …
}
+ {aiStatus === "arbetar" &&
Analyzing …
}
{aiStatus === "fel" && (
-
Analysen kunde inte hämtas — försök igen. Metodiken fortsätter som vanligt.
+
The analysis could not be retrieved — try again. The methodology continues unaffected.
)}
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
<>
@@ -1388,7 +1388,7 @@ function GuideFlik({
))}
- Nästa steg: {" "}
+ Next step: {" "}
{senasteAiSvar.handelse.nastaSteg}
>
@@ -1414,7 +1414,7 @@ function GuideFlik({
{/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när
ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */}
{!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && (
-
+
)}
@@ -1498,8 +1498,8 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
{fraga.text}
{fraga.svarstyp === "janej" && (
- svara("Ja")}>Ja
- svara("Nej")}>Nej
+ svara("Yes")}>Yes
+ svara("No")}>No
)}
{fraga.svarstyp === "val" && (
@@ -1513,7 +1513,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
)}
{fraga.svarstyp === "text" && (
<>
-
+
svara(text.trim())}>
Spara svar
@@ -1576,29 +1576,29 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
utford();
}}
/>
-
- filRef.current?.click()}> Ta foto — verifierar kontrollen
- Denna kontroll verifieras med foto.
+
+ filRef.current?.click()}> Take a photograph — verifies the check
+ This check is verified with a photograph.
>
) : (
<>
{!kravUppfyllt && (
- Ingen {krav === "matvarde" ? "mätning" : "observation"} har registrerats — lägg till en kort{" "}
- {krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras.
+ No {krav === "matvarde" ? "measurement" : "observation"} has been recorded — add a short{" "}
+ {krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
)}
@@ -1656,7 +1656,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
))}
-
+
setOppen(false)}>
Avbryt
@@ -1671,12 +1671,12 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
const DOKTYPER = [
{ id: "observation", label: "Observation" },
- { id: "matvarde", label: "Mätvärde" },
- { id: "foto", label: "Foto" },
+ { id: "matvarde", label: "Measurement" },
+ { id: "foto", label: "Photo" },
{ id: "video", label: "Video" },
{ id: "instrument", label: "Instrument" },
- { id: "hypotes", label: "Hypotes" },
- { id: "kommentar", label: "Kommentar" },
+ { id: "hypotes", label: "Hypothesis" },
+ { id: "comment", label: "Comment" },
] as const;
// Fri dokumentation vid sidan av guiden. Hypoteser märks alltid som
@@ -1721,10 +1721,10 @@ function SnabbDokumentation({
} else if (!resultat) {
setAvlasning({ foto, tolkning: byggDemoInstrument(), demo: true });
} else {
- skicka({ typ: "foto", beskrivning: "Instrumentfoto — inga värden kunde läsas", dataUrl: foto });
+ skicka({ typ: "foto", beskrivning: "Instrument photograph — no values could be read", dataUrl: foto });
}
} catch {
- skicka({ typ: "foto", beskrivning: "Instrumentfoto — avläsningen kunde inte nås", dataUrl: foto });
+ skicka({ typ: "foto", beskrivning: "Instrument photograph — the reader could not be reached", dataUrl: foto });
}
setLaserAv(false);
};
@@ -1746,7 +1746,7 @@ function SnabbDokumentation({
beskrivning: v.beskrivning,
varde: v.varde,
enhet: v.enhet,
- kalla: "Instrumentavläsning (modelltolkad, bekräftad av tekniker)",
+ kalla: "Instrument reading (model-interpreted, confirmed by the technician)",
});
}
paSparad?.(
@@ -1763,7 +1763,7 @@ function SnabbDokumentation({
skicka({ typ: "observation", text: t });
paSparad?.(`Observation: ${t}`);
}
- if (typ === "kommentar" && t) {
+ if (typ === "comment" && t) {
skicka({ typ: "kommentar", text: t });
paSparad?.(t);
}
@@ -1779,7 +1779,7 @@ function SnabbDokumentation({
};
return (
-
+
{DOKTYPER.map((d) => (
@@ -1843,7 +1843,7 @@ function SnabbDokumentation({
try {
setVideo({ dataUrl: await lasVideo(fil), beskrivning: "" });
} catch (misslyckande) {
- setVideoFel(misslyckande instanceof Error ? misslyckande.message : "Videon kunde inte läsas.");
+ setVideoFel(misslyckande instanceof Error ? misslyckande.message : "The video could not be read.");
}
}}
/>
@@ -1852,10 +1852,10 @@ function SnabbDokumentation({
setVideo((v) => (v ? { ...v, beskrivning } : v))}
- platshallare="T.ex. Motorljud vid tomgång — tickande från topplocket"
+ platshallare="e.g. Engine noise at idle — ticking from the cylinder head"
rost
/>
@@ -1875,7 +1875,7 @@ function SnabbDokumentation({
)}
- {laserAv &&
Systemet läser av instrumentet …
}
+ {laserAv &&
Reading the instrument …
}
{avlasning && (
{avlasning.demo && (
@@ -1900,7 +1900,7 @@ function SnabbDokumentation({
setAvlasning(null)}>
Förkasta
- Spara foto + värden
+ Save photograph + values
)}
@@ -1912,13 +1912,13 @@ function SnabbDokumentation({
)}
d.id === typ)!.label}
+ label={typ === "matvarde" ? "What was measured?" : DOKTYPER.find((d) => d.id === typ)!.label}
varde={text}
satt={setText}
- platshallare={typ === "matvarde" ? "T.ex. Matningsspänning stift 30" : ""}
+ platshallare={typ === "matvarde" ? "e.g. Supply voltage at pin 30" : ""}
rost
/>
- {typ === "matvarde" && }
+ {typ === "matvarde" && }
Spara
@@ -1968,7 +1968,7 @@ function OverlamningDialog({
return (
-
Överlämningsrapport
+
Handover report
{text}
@@ -1977,9 +1977,9 @@ function OverlamningDialog({
Komplettera analysen: risker & osäkerheter
)}
- {aiLage === "arbetar" &&
Systemet sammanfattar …
}
+ {aiLage === "arbetar" &&
Summarizing …
}
{aiLage === "fel" && (
-
Kunde inte hämtas — kräver inloggning. Överlämningen fungerar ändå.
+
Could not be retrieved — requires sign-in. The handover works regardless.
)}
{aiLage === "klar" && aiRader.length > 0 && (
@@ -1990,7 +1990,7 @@ function OverlamningDialog({
))}
)}
-
+
Avbryt
@@ -2010,9 +2010,9 @@ function OverlamningDialog({
}
const NIVA_LABEL: Record = {
- kund: "Kund",
- partner: "Extern partner",
- intern: "Intern",
+ kund: "Customer",
+ partner: "External partner",
+ intern: "Internal",
};
// Live Share-behörighetsnivåer: skapa och återkalla delningslänkar.
@@ -2028,7 +2028,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
setDelningar(await hamtaDelningar(arende.id));
setFel("");
} catch {
- setFel("Kunde inte hämta delningar — kräver inloggning och att ärendet är synkat.");
+ setFel("Could not retrieve shares — requires sign-in and a synchronized case.");
}
};
@@ -2054,7 +2054,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{NIVA_LABEL[delning.niva]}{" "}
- {delning.aterkallad && (återkallad) }
+ {delning.aterkallad && (revoked) }
{!delning.aterkallad && (
@@ -2091,7 +2091,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
skicka({ typ: "kommentar", text: `Delningslänk skapad (${NIVA_LABEL[niva]}).` });
uppdatera();
} catch {
- setFel("Kunde inte skapa delning — kräver inloggning och att ärendet är synkat.");
+ setFel("Could not create a share — requires sign-in and a synchronized case.");
}
}}
>
@@ -2109,7 +2109,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
function LoggFlik({ arende }: { arende: Arende }) {
return (
-
+
{arende.handelser.map((post) => (
@@ -2162,14 +2162,14 @@ function BriefFlik({
return (
<>
{!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.
- {granskning === "arbetar" && Systemet granskar underlaget …
}
+ {granskning === "arbetar" && Reviewing the evidence …
}
{granskning === "fel" && (
- Granskningen kunde inte hämtas — kräver inloggning. Försök igen.
+ The review could not be retrieved — requires sign-in. Try again.
)}
{senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && (
@@ -2179,7 +2179,7 @@ function BriefFlik({
))}
- Nästa steg: {senasteAi.handelse.nastaSteg}
+ Next step: {senasteAi.handelse.nastaSteg}
Senaste analys ({senasteAi.handelse.modell})
@@ -2189,7 +2189,7 @@ function BriefFlik({
)}
-
+
{b.objekt ? (
<>
{b.objekt.beskrivning}
@@ -2198,14 +2198,14 @@ function BriefFlik({
{b.ansvarig && Ansvarig tekniker: {b.ansvarig}
}
>
) : (
- Ej identifierat
+ Not identified
)}
-
+
{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}
-
- {b.utfordaKontroller.length === 0 && Inga ännu.
}
+
+ {b.utfordaKontroller.length === 0 && None yet.
}
{b.utfordaKontroller.map((k, i) =>
k.undantag ? (
@@ -2219,14 +2219,14 @@ function BriefFlik({
),
)}
-
- {b.observationer.length === 0 && Inga ännu.
}
+
+ {b.observationer.length === 0 && None yet.
}
{b.observationer.map((o, i) => (
• {o}
))}
{b.hypoteser.length > 0 && (
-
+
{b.hypoteser.map((h, i) => (
{h.text}
@@ -2234,27 +2234,27 @@ function BriefFlik({
))}
)}
-
- {b.ejKontrollerat.length === 0 && Allt i metodiken är utfört.
}
+
+ {b.ejKontrollerat.length === 0 && Every step in the methodology is complete.
}
{b.ejKontrollerat.map((e, i) => (
– {e}
))}
{!b.avslutat && (
-
+
{b.rekommenderatNastaSteg.map((s, i) => (
{i + 1}. {s}
))}
)}
-
+
{b.tillforlitlighet.map((r, i) => (
{r.text}
))}
-
+
{b.totalArbetstid}
>
@@ -2338,7 +2338,7 @@ function RapportFlik({
{rad.ok ? : rad.kravs ? : }
{" "}
{rad.rubrik}
- {!rad.kravs &&
(rekommenderas) }
+ {!rad.kravs &&
(recommended) }
{rad.detalj &&
{rad.detalj}
}
@@ -2350,7 +2350,7 @@ function RapportFlik({
)}
-
+
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas — bilder kan
innehålla uppgifter om andra kunder.
@@ -2364,7 +2364,7 @@ function RapportFlik({
-
Öppna Live Share-vy
+
Open Live Share view
{plattformAktiv() ? (
@@ -2375,10 +2375,10 @@ function RapportFlik({
className="mt-2"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${arende.delningskod}`);
- skicka({ typ: "kommentar", text: "Delningslänk kopierad för extern mottagare." });
+ skicka({ typ: "kommentar", text: "Share link copied for an external recipient." });
}}
>
-
Kopiera delningslänk
+
Copy share link
)
)}
@@ -2390,9 +2390,9 @@ function RapportFlik({
rapporten — kund, handläggare, nästa tekniker — ska få bilden
på fem sekunder och sedan kunna gå djupare. Texten är härledd
ur loggen, så den kan inte säga något som inte står där. */}
-
+
{sammanfatta(arende).text}
- ALVA-PROC-0030 · härledd ur händelseloggen
+ ALVA-PROC-0030 · derived from the case log
{/* ALVA-RULE-200 · Teknikerns varför. Placerad före underlaget:
@@ -2402,12 +2402,12 @@ function RapportFlik({
if (!s || s.handelse.typ !== "slutsats") return null;
const h = s.handelse;
return (
-
+
{identitetsRader([
- [h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
- ["Uteslutna alternativ", h.uteslutet],
- ["Val av åtgärd", h.atgardsval ?? "—"],
- ["Kvarstående osäkerhet", h.kvarstaende],
+ [h.orsakFastställd === false ? "Reason the cause could not be established" : "Rationale", h.motivering],
+ ["Dismissed alternatives", h.uteslutet],
+ ["Choice of action", h.atgardsval ?? "—"],
+ ["Remaining uncertainty", h.kvarstaende],
])}
);
@@ -2415,43 +2415,43 @@ function RapportFlik({
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
återanvänd här automatiskt. */}
-
+
{identitetsRader([
- ["Ärende", `#${arende.nummer}`],
- ["Arbetsorder", idn.arbetsorder],
- ["Claim-/garantinr", idn.claim],
- ["Skadenummer", idn.skadenummer],
- ["Ärendetyp", typ],
- ["Ansvarig tekniker", idn.ansvarig],
+ ["Case", `#${arende.nummer}`],
+ ["Work order", idn.arbetsorder],
+ ["Claim / warranty no.", idn.claim],
+ ["Claim number", idn.skadenummer],
+ ["Case type", typ],
+ ["Responsible technician", idn.ansvarig],
])}
-
+
{identitetsRader([
- ["Personbil", idn.beskrivning],
- ["Regnr", idn.identifierare],
+ ["Passenger car", idn.beskrivning],
+ ["Reg. no.", idn.identifierare],
["VIN", idn.vin],
- ["Mätarställning in", matIn],
- ["Mätarställning ut", matUt],
- ["Kund", idn.kund],
+ ["Odometer in", matIn],
+ ["Odometer out", matUt],
+ ["Customer", idn.kund],
])}
{/* Beviskedjan: kundens upplevelse, teknikerns verifiering och den
tekniska slutsatsen hålls strikt åtskilda och spårbara. */}
-
+
{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}
-
+
{repro ? (
<>
{reproduceringsText(repro.status)}
{repro.beskrivning}
>
) : (
- Reproducering ej dokumenterad ännu.
+ Reproduction not documented yet.
)}
-
- {orsakerLista.length === 0 && Felorsaksanalys ej dokumenterad ännu.
}
+
+ {orsakerLista.length === 0 && Root cause analysis not documented yet.
}
{orsakerLista.map((p) => {
const h = p.handelse;
if (h.typ !== "felorsak") return null;
@@ -2459,42 +2459,42 @@ function RapportFlik({
{h.avvikelse}
{identitetsRader([
- ["Bedömd grundorsak", h.orsaker.join(", ")],
- ["Underlag för bedömningen", h.underlag.join(", ")],
- ["Säkerhetsnivå", TILLFORLITLIGHET_LABEL[h.sakerhet]],
- ["Rekommenderad åtgärd", h.atgard],
- ["Motivering", h.motivering],
- ["Stärkande kontroller", h.ytterligareKontroller],
+ ["Assessed root cause", h.orsaker.join(", ")],
+ ["Evidence for the assessment", h.underlag.join(", ")],
+ ["Confidence level", TILLFORLITLIGHET_LABEL[h.sakerhet]],
+ ["Recommended action", h.atgard],
+ ["Rationale", h.motivering],
+ ["Strengthening checks", h.ytterligareKontroller],
])}
);
})}
{(forslagRader.length > 0 || beslutRad) && (
-
+
{forslagRader.map((p) => {
const h = p.handelse;
if (h.typ !== "atgardsforslag") return null;
return (
{identitetsRader([
- ["Föreslagen åtgärd", h.beskrivning],
- ["Uppskattad kostnad", h.uppskattadKostnad],
- ["Uppskattad tid", h.uppskattadTid],
+ ["Proposed action", h.beskrivning],
+ ["Estimated cost", h.uppskattadKostnad],
+ ["Estimated time", h.uppskattadTid],
])}
);
})}
{beslutRad &&
identitetsRader([
- ["Kundens besked", KUNDBESLUT_LABEL[beslutRad.beslut]],
- ["Lämnat via", beslutRad.kanal],
- ["Kommentar", beslutRad.kommentar],
+ ["Customer decision", KUNDBESLUT_LABEL[beslutRad.beslut]],
+ ["Given via", beslutRad.kanal],
+ ["Comment", beslutRad.kommentar],
])}
)}
-
- {atgardsRader.length === 0 && Ingen åtgärd dokumenterad ännu.
}
+
+ {atgardsRader.length === 0 && No action documented yet.
}
{atgardsRader.map((p) => {
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
@@ -2502,8 +2502,8 @@ function RapportFlik({
{h.utford ? (
identitetsRader([
- ["Utförd åtgärd", h.beskrivning],
- ["Delar", h.delar],
+ ["Action performed", h.beskrivning],
+ ["Parts", h.delar],
])
) : (
Ingen åtgärd utförd — {h.motivering}
@@ -2513,12 +2513,12 @@ function RapportFlik({
})}
{kkRad &&
identitetsRader([
- ["Kvalitetskontroll", KVALITETSKONTROLL_LABEL[kkRad.resultat]],
- ["Verifiering", kkRad.beskrivning],
+ ["Quality check", KVALITETSKONTROLL_LABEL[kkRad.resultat]],
+ ["Verification", kkRad.beskrivning],
])}
-
- Total arbetstid: {b.totalArbetstid}
+
+ Total labour time: {b.totalArbetstid}
{fordelning.length > 0 && (
{fordelning.map((r) => (
@@ -2527,7 +2527,7 @@ function RapportFlik({
)}
-
+
{kundposter.map((post) => (
{tidKlockslag(post.tidpunkt)} {" "}
@@ -2536,7 +2536,7 @@ function RapportFlik({
))}
{klipp.length > 0 && (
-
+
{klipp.map((v, i) => (
@@ -2546,7 +2546,7 @@ function RapportFlik({
)}
{bilder.length > 0 && (
-
+
{bilder.map((bild, i) => (
@@ -2558,7 +2558,7 @@ function RapportFlik({
)}
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
-
+
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
{i + 1}. {s}
))}
@@ -2601,7 +2601,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
try {
const { anmalSupport, plattformAktiv } = await import("@/felsokning/plattform");
if (!plattformAktiv()) {
- setUtfall({ fel: "Ingen plattform är konfigurerad — anmälan kan inte skickas härifrån." });
+ setUtfall({ fel: "No platform is configured — the report cannot be sent from here." });
return;
}
const { beteckning } = await anmalSupport({
@@ -2633,42 +2633,42 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
onClick={() => setOppen(true)}
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline"
>
- Rapportera ett problem med systemet
+ Report a problem with the system
);
}
return (
-
+
Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden
skickas.
{[
- ["felanmalan", "Fel"],
- ["fraga", "Fråga"],
- ["forbattring", "Förbättring"],
+ ["felanmalan", "Fault"],
+ ["fraga", "Question"],
+ ["forbattring", "Improvement"],
].map(([id, etikett]) => (
setTyp(id)}>
{etikett}
))}
-
+
{utfall.ok && Anmäld som {utfall.ok}.
}
{utfall.fel && {utfall.fel}
}
- {skickar ? "Skickar" : "Skicka anmälan"}
+ {skickar ? "Sending" : "Send report"}
setOppen(false)}>
Stäng
@@ -2695,16 +2695,16 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
const paket = aktivtRegelpaket();
const avslutad = [...arende.handelser].reverse().find((p) => p.handelse.typ === "arende_avslutat");
const falt: [string, string][] = [
- ["Beteckning", arendebeteckning(arende.nummer)],
- ["Regelpaket", paket.version],
- ["Evidensmodell", `ECM ${ECM_VERSION}`],
+ ["Designation", arendebeteckning(arende.nummer)],
+ ["Rule package", paket.version],
+ ["Evidence model", `ECM ${ECM_VERSION}`],
// Versionen VID AVSLUTET, inte dagens. Stämpeln läste tidigare den
// aktuella konstanten, så ett ärende stängt under 1.0 visade 3.1 —
// vilket är exakt det stämpelns beskrivning ovan säger att den inte
// ska göra. Ett öppet ärende visar dagens, eftersom det är sant då.
- ["Plattform", avslutad ? versionVidAvslut(avslutad.handelse) : PLATTFORMSVERSION],
+ ["Platform", avslutad ? versionVidAvslut(avslutad.handelse) : PLATTFORMSVERSION],
["Status", avslutad ? "Closed" : "In progress"],
- ["Utgåva", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
+ ["Release", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
];
return (
diff --git a/felsokning/app/src/pages/felsokning/Arendelista.tsx b/felsokning/app/src/pages/felsokning/Arendelista.tsx
index 7f127d5..3258783 100644
--- a/felsokning/app/src/pages/felsokning/Arendelista.tsx
+++ b/felsokning/app/src/pages/felsokning/Arendelista.tsx
@@ -23,9 +23,9 @@ import { tidDatum, tidKlockslag } from "@/felsokning/format";
type Filter = "alla" | "pagaende" | "klara";
const ROLL_LABEL: Record = {
- tekniker: "Tekniker",
- arbetsledare: "Arbetsledare",
- admin: "Systemadministratör",
+ tekniker: "Technician",
+ arbetsledare: "Supervisor",
+ admin: "System administrator",
};
// Självhostat läge: inloggning mot plattformstjänsten i klustret.
@@ -43,7 +43,7 @@ function PlattformInloggning() {
if (konto) {
return (
<>
-
+
{konto.namn} · {ROLL_LABEL[konto.roll]} ·{" "}
@@ -61,12 +61,12 @@ function PlattformInloggning() {
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
- Organisationsöversikt
+ Organization overview
)}
{konto.roll === "admin" && (
- Inställningar för organisationen
+ Organization settings
)}
@@ -76,7 +76,7 @@ function PlattformInloggning() {
}
return (
-
+
{lage === "stangd" ? (
<>
@@ -85,9 +85,9 @@ function PlattformInloggning() {
setLage("registrera")}>
- Skapa organisation
+ Create organization
- setLage("loggaIn")}>Logga in
+ setLage("loggaIn")}>Sign in
>
) : (
@@ -102,18 +102,18 @@ function PlattformInloggning() {
: await registreraPlattform(epost, losenord, nyttNamn, organisation),
);
} catch (misslyckande) {
- setFel(misslyckande instanceof Error ? misslyckande.message : "Något gick fel.");
+ setFel(misslyckande instanceof Error ? misslyckande.message : "Something went wrong.");
}
}}
>
{lage === "registrera" && (
<>
-
-
+
+
>
)}
-
-
+
+
{fel && {fel}
}
setLage("stangd")}>
@@ -127,7 +127,7 @@ function PlattformInloggning() {
(lage === "registrera" && (!nyttNamn.trim() || !organisation.trim()))
}
>
- {lage === "loggaIn" ? "Logga in" : "Skapa organisation"}
+ {lage === "loggaIn" ? "Sign in" : "Create organization"}
@@ -151,13 +151,13 @@ function AnvandarAdmin() {
try {
setLista(await hamtaAnvandare());
} catch {
- setFel("Kunde inte hämta användare.");
+ setFel("Could not retrieve users.");
}
};
if (!oppen) {
return (
-
+
{
@@ -172,7 +172,7 @@ function AnvandarAdmin() {
}
return (
-
+
{lista.map((anv) => (
{" "}
· {ROLL_LABEL[anv.roll]} {anv.epost}
{anv.aktiv === false && (
- Avstängd
+ Disabled
)}
- {anv.aktiv === false ? "Öppna igen" : "Stäng av"}
+ {anv.aktiv === false ? "Reopen" : "Disable"}
))}
@@ -223,13 +223,13 @@ function AnvandarAdmin() {
setLosenord("");
await uppdatera();
} catch (misslyckande) {
- setFel(misslyckande instanceof Error ? misslyckande.message : "Något gick fel.");
+ setFel(misslyckande instanceof Error ? misslyckande.message : "Something went wrong.");
}
}}
>
-
-
-
+
+
+
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
-
+
+
Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja.
@@ -281,7 +281,7 @@ export default function Arendelista() {
if (namn.trim()) sattAnvandare(namn);
}}
>
-
+
Fortsätt
@@ -297,18 +297,18 @@ export default function Arendelista() {
const lista = filter === "pagaende" ? pagaende : filter === "klara" ? klara : alla;
const FILTER: { id: Filter; label: string; antal: number }[] = [
- { id: "alla", label: "Alla", antal: alla.length },
- { id: "pagaende", label: "Pågående", antal: pagaende.length },
- { id: "klara", label: "Klara", antal: klara.length },
+ { id: "alla", label: "All", antal: alla.length },
+ { id: "pagaende", label: "In progress", antal: pagaende.length },
+ { id: "klara", label: "Complete", antal: klara.length },
];
return (
{anvandare}}
>
- + Nytt ärende
+ + New case
{alla.length > 0 && (
@@ -328,7 +328,7 @@ 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.
@@ -340,18 +340,18 @@ export default function Arendelista() {
)}
{lista.length === 0 && alla.length > 0 && (
-
Inga ärenden i det här filtret.
+ No cases in this filter.
)}
{plattformAktiv() && }
{!plattformKonto() && (
- Inställningar
+ Settings
)}
-
+
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
@@ -379,10 +379,10 @@ export default function Arendelista() {
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>
- {avslutat ? "Avslutat" : "Pågående"}
+ {avslutat ? "Closed" : "In progress"}
- {o ? o.beskrivning : "Okänt objekt"}
+ {o ? o.beskrivning : "Unknown object"}
{o && {o.identifierare}{o.kund ? ` · ${o.kund}` : ""}
}
{fel && ”{fel}”
}
diff --git a/felsokning/app/src/pages/felsokning/DelatArende.tsx b/felsokning/app/src/pages/felsokning/DelatArende.tsx
index d148d7a..abdec8e 100644
--- a/felsokning/app/src/pages/felsokning/DelatArende.tsx
+++ b/felsokning/app/src/pages/felsokning/DelatArende.tsx
@@ -20,8 +20,8 @@ export default function DelatArende() {
if (!arende) {
return (
-
- Ärendet är inte tillgängligt.
+
+ The case is not available.
);
}
@@ -30,7 +30,7 @@ export default function DelatArende() {
);
}
diff --git a/felsokning/app/src/pages/felsokning/Installningar.tsx b/felsokning/app/src/pages/felsokning/Installningar.tsx
index 40dd4f6..c1d20c9 100644
--- a/felsokning/app/src/pages/felsokning/Installningar.tsx
+++ b/felsokning/app/src/pages/felsokning/Installningar.tsx
@@ -84,7 +84,7 @@ function Integrationer() {
if (leverantorer.length === 0) return null;
return (
-
+
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.
@@ -114,7 +114,7 @@ function Integrationer() {
)}
- {finns ? "Konfigurerad" : "Ej konfigurerad"}
+ {finns ? "Configured" : "Not configured"}
@@ -123,7 +123,7 @@ function Integrationer() {
{lev.falt.map((falt) => (
setVarden((f) => ({ ...f, [falt.nyckel]: v }))}
losenord={falt.hemlig}
@@ -145,7 +145,7 @@ function Integrationer() {
setSparat(lev.namn);
ladda();
} catch (misslyckande) {
- setFel(misslyckande instanceof Error ? misslyckande.message : "Kunde inte spara.");
+ setFel(misslyckande instanceof Error ? misslyckande.message : "Could not save.");
}
}}
>
@@ -172,7 +172,7 @@ function Integrationer() {
setVarden(start);
}}
>
- {finns ? "Uppdatera uppgifter" : "Lägg till uppgifter"}
+ {finns ? "Update details" : "Add details"}
{finns && (
+
Organisationens inställningar hanteras av er systemadministratör.
@@ -223,7 +223,7 @@ export default function Installningar() {
const spara = async () => {
if (inst.objekttyper.length === 0 || inst.identifieringsmetoder.length === 0) {
setStatus("fel");
- setFelText("Minst ett alternativ måste vara valt i varje lista.");
+ setFelText("At least one option must be selected in each list.");
return;
}
setStatus("sparar");
@@ -233,7 +233,7 @@ export default function Installningar() {
setStatus("sparat");
} catch (misslyckande) {
setStatus("fel");
- setFelText(misslyckande instanceof Error ? misslyckande.message : "Kunde inte spara.");
+ setFelText(misslyckande instanceof Error ? misslyckande.message : "Could not save.");
}
};
@@ -243,17 +243,17 @@ export default function Installningar() {
};
return (
-
+
Välj vad som visas när ett nytt ärende startas.{" "}
{inloggad
- ? "Valet gäller alla i organisationen."
- : "I lokalt läge gäller valet den här enheten; inloggad systemadministratör styr hela organisationen."}
+ ? "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."}
-
+
-
+
}
{status === "fel" && {felText}
}
- {status === "sparat" && ✓ Sparat
}
+ {status === "sparat" && ✓ Saved
}
- {status === "sparar" ? "Sparar …" : "Spara inställningar"}
+ {status === "sparar" ? "Saving …" : "Save settings"}
);
diff --git a/felsokning/app/src/pages/felsokning/NyttArende.tsx b/felsokning/app/src/pages/felsokning/NyttArende.tsx
index 8a7437a..e5a686e 100644
--- a/felsokning/app/src/pages/felsokning/NyttArende.tsx
+++ b/felsokning/app/src/pages/felsokning/NyttArende.tsx
@@ -87,10 +87,10 @@ export default function NyttArende() {
const kandidater = resultat?.tolkning.varden.map((v) => v.varde) ?? [];
const traff = kandidater.map((v) => tolkaKod(v)).find((k): k is AvlastKod => !!k);
if (traff) anvandKod(traff);
- else if (!resultat) setKodFel("Avläsning av typskylt kräver inloggning — ange identiteten manuellt.");
- else setKodFel("Ingen identifierare kunde läsas ur bilden — försök igen eller ange manuellt.");
+ else if (!resultat) setKodFel("Reading the type plate requires sign-in — enter the identity manually.");
+ else setKodFel("No identifier could be read from the image — try again or enter it manually.");
} catch {
- setKodFel("Avläsningen kunde inte nås — ange identiteten manuellt.");
+ setKodFel("The reader could not be reached — enter the identity manually.");
}
setLaserTypskylt(false);
};
@@ -110,7 +110,7 @@ export default function NyttArende() {
const resultat = await tolkaArbetsorder(foto);
if (resultat) {
if (resultat.falt.length === 0) {
- setSkanningsFel("Inga fält kunde läsas ur bilden — ta ett tydligare foto eller fyll i manuellt.");
+ setSkanningsFel("No fields could be read from the image — take a clearer photograph or fill in manually.");
} else {
setTolkning({ foto, falt: resultat.falt, modell: resultat.modell });
setSteg("granska");
@@ -120,7 +120,7 @@ export default function NyttArende() {
setSteg("granska");
}
} catch {
- setSkanningsFel("Dokumenttolkningen kunde inte nås — försök igen eller fyll i manuellt.");
+ setSkanningsFel("Document interpretation could not be reached — try again or fill in manually.");
}
setSkannar(false);
};
@@ -136,10 +136,10 @@ export default function NyttArende() {
const vin = v("fordon_vin");
const beskr = [v("fordon_marke"), v("fordon_modell"), v("fordon_arsmodell")].filter(Boolean).join(" ");
const objektet: Objekt = {
- typ: inst.objekttyper.includes("Personbil") ? "Personbil" : inst.objekttyper[0],
- identifierare: (regnr ?? vin ?? "OKÄND").toUpperCase(),
- identifieringsmetod: regnr ? "Regnr" : vin ? "VIN" : "Manuell inmatning",
- beskrivning: beskr || "Se arbetsorder",
+ typ: inst.objekttyper.includes("Passenger car") ? "Passenger car" : inst.objekttyper[0],
+ identifierare: (regnr ?? vin ?? "UNKNOWN").toUpperCase(),
+ identifieringsmetod: regnr ? "Reg. no." : vin ? "VIN" : "Manual entry",
+ beskrivning: beskr || "View work order",
kund: v("kund_namn"),
// Ärendeidentiteten: registreras en gång här, återanvänds i alla vyer.
vin: vin?.toUpperCase(),
@@ -148,7 +148,7 @@ export default function NyttArende() {
claim: v("ao_claim"),
skadenummer: v("ao_skadenummer"),
};
- const felText = v("felbeskrivning") ?? "Se skannad arbetsorder";
+ const felText = v("felbeskrivning") ?? "View the scanned work order";
let metodikId: string | undefined;
if (valjMetodik(felText).id === "generisk") {
metodikId = (await valjMetodikMedAi(felText)) ?? undefined;
@@ -174,8 +174,8 @@ export default function NyttArende() {
if (steg === "start") {
return (
-
-
+
+
Fota arbetsorderns framsida. Systemet läser kund-, fordons- och ärendeuppgifter automatiskt —
du granskar bara osäkra fält och trycker Starta diagnos.
@@ -193,7 +193,7 @@ export default function NyttArende() {
}}
/>
{skannar ? (
-
Tolkar dokumentet …
+ Interpreting the document …
) : (
skanRef.current?.click()}> Skanna arbetsorder
)}
@@ -212,8 +212,8 @@ export default function NyttArende() {
return (
{tolkning.demo && (
@@ -236,8 +236,8 @@ export default function NyttArende() {
if (steg === "identifiera") {
return (
-
-
+
+
{inst.objekttyper.map((t) => (
setTyp(t)}>
@@ -246,7 +246,7 @@ export default function NyttArende() {
))}
-
+
)}
@@ -301,10 +301,10 @@ export default function NyttArende() {
)}
{kodFel && {kodFel}
}
-
-
-
-
+
+
+
+
setSteg("bekrafta")}>
Fortsätt
@@ -315,7 +315,7 @@ export default function NyttArende() {
if (steg === "bekrafta") {
return (
-
+
{objektet.typ}
{objektet.beskrivning}
@@ -327,7 +327,7 @@ export default function NyttArende() {
setSteg("identifiera")}>
Ändra
- setSteg("felbeskrivning")}>Rätt objekt — fortsätt
+ setSteg("felbeskrivning")}>Correct object — continue
);
@@ -336,13 +336,13 @@ export default function NyttArende() {
const metodik = fel.trim() ? valjMetodik(fel) : undefined;
return (
-
+
@@ -365,7 +365,7 @@ export default function NyttArende() {
navigate(`/felsokning/arende/${id}`);
}}
>
- {startar ? "Startar …" : "Starta arbetslogg"}
+ {startar ? "Starting …" : "Start work log"}
@@ -396,16 +396,16 @@ function GranskaTolkning({
const obekraftade = rader.filter((r) => !r.bekraftad);
const vald = rader.find((r) => r.id === markerad);
- const grupper: ArbetsorderGrupp[] = ["Kund", "Personbil", "Verkstad", "Ärende"];
+ const grupper: ArbetsorderGrupp[] = ["Customer", "Vehicle", "Workshop", "Case"];
const uppdatera = (id: string, andring: Partial<(typeof rader)[number]>) =>
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
return (
-
+
-
+
{vald?.omrade && (
{vald?.omrade
? `Markerat: ${vald.etikett} (ungefärlig position)`
- : "Klicka på ett fält för att se var i dokumentet värdet hittades."}
+ : "Select a field to see where in the document the value was found."}
@@ -488,7 +488,7 @@ function GranskaTolkning({
Skanna om
0 || startar} onClick={() => vidStart(rader)}>
- {startar ? "Startar …" : "Starta diagnos"}
+ {startar ? "Starting …" : "Start diagnosis"}
diff --git a/felsokning/app/src/pages/felsokning/Oversikt.tsx b/felsokning/app/src/pages/felsokning/Oversikt.tsx
index 0d6a6eb..78e6bd1 100644
--- a/felsokning/app/src/pages/felsokning/Oversikt.tsx
+++ b/felsokning/app/src/pages/felsokning/Oversikt.tsx
@@ -46,7 +46,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
}
return (
-
Ny ansvarig tekniker
+
New responsible technician
{personer.map((person) => (
setFel("Kunde inte hämta översikten — kontrollera inloggningen."));
+ .catch(() => setFel("Could not retrieve the overview — check the sign-in."));
// Flottdata: återkommande felorsaker över organisationen.
hamtaFelorsaksstatistik().then(setFelorsaker).catch(() => setFelorsaker([]));
}, [behorig]);
if (!behorig) {
return (
-
+
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
@@ -134,7 +134,7 @@ export default function Oversikt() {
}
navigate(`/felsokning/arende/${rad.id}`);
} catch {
- setFel("Kunde inte hämta ärendet.");
+ setFel("Could not retrieve the case.");
}
};
@@ -151,28 +151,28 @@ export default function Oversikt() {
return (
{konto?.organisation}}
>
{fel && {fel}
}
- {!rader && !fel && Hämtar …
}
+ {!rader && !fel && Retrieving …
}
{rader && (
<>
-
+
{rader.length}
-
Totalt
+
Total
{pagaende.length}
-
Pågående
+
In progress
{avslutade.length}
-
Avslutade
+
Closed
{avslutade.length > 0 && (
@@ -183,7 +183,7 @@ export default function Oversikt() {
{felorsaker.length > 0 && (
-
+
{felorsaker.map((rad) => (
{rad.orsak}
@@ -200,7 +200,7 @@ export default function Oversikt() {
)}
- {rader.length === 0 &&
Inga ärenden i organisationen ännu.
}
+ {rader.length === 0 &&
No cases in the organization yet.
}
{rader.map((rad) => (
@@ -213,10 +213,10 @@ export default function Oversikt() {
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>
- {rad.avslutat ? "Avslutat" : "Pågående"}
+ {rad.avslutat ? "Closed" : "In progress"}
-
{rad.objekt ?? "Okänt objekt"}
+
{rad.objekt ?? "Unknown object"}
{rad.felbeskrivning &&
”{rad.felbeskrivning}”
}
Ansvarig: {rad.ansvarig ?? rad.skapare ?? "—"}
diff --git a/felsokning/app/src/pages/felsokning/PublikDelning.tsx b/felsokning/app/src/pages/felsokning/PublikDelning.tsx
index 2a82db5..f7d7803 100644
--- a/felsokning/app/src/pages/felsokning/PublikDelning.tsx
+++ b/felsokning/app/src/pages/felsokning/PublikDelning.tsx
@@ -16,10 +16,10 @@ interface DelatSvar {
}
const NIVA_NOTIS: Record = {
- kund: "Skrivskyddad livevy från verkstaden — sidan uppdateras automatiskt när ny information registreras.",
+ kund: "Read-only live view from the workshop — the page updates automatically as new information is recorded.",
partner:
- "Skrivskyddad livevy (partnernivå) — inkluderar tekniska hypoteser, tydligt märkta som ej verifierade. Uppdateras automatiskt.",
- intern: "Skrivskyddad livevy (intern nivå) — full insyn i ärendet. Uppdateras automatiskt.",
+ "Read-only live view (partner level) — includes technical hypotheses, clearly marked as unverified. Updates automatically.",
+ intern: "Read-only live view (internal level) — full visibility of the case. Updates automatically.",
};
async function hamtaDelat(kod: string): Promise<{ arende: Arende; niva: string } | undefined> {
@@ -87,12 +87,12 @@ export default function PublikDelning() {
if (status !== "klar" || !arende) {
return (
-
+
{status === "laddar"
- ? "Hämtar ärendet …"
- : "Ärendet är inte tillgängligt. Kontrollera länken med verkstaden — delningen kan ha stängts av."}
+ ? "Retrieving the case …"
+ : "The case is not available. Check the link with the workshop — the share may have been revoked."}
@@ -105,7 +105,7 @@ export default function PublikDelning() {
niva === "kund" && kod
? async (beslut: "godkant" | "avbojt", kommentar: string) => {
const { plattformAktiv, PLATTFORM_URL } = await import("@/felsokning/plattform");
- if (!plattformAktiv()) return "Beskedet kan inte lämnas här — kontakta verkstaden.";
+ if (!plattformAktiv()) return "The decision cannot be given here — contact the workshop.";
try {
const res = await fetch(`${PLATTFORM_URL}/api/delad/${kod}/beslut`, {
method: "POST",
@@ -114,14 +114,14 @@ export default function PublikDelning() {
});
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string };
- return data.error ?? "Beskedet kunde inte registreras — försök igen.";
+ return data.error ?? "The decision could not be recorded — try again.";
}
// Hämta om direkt så kvittensen syns.
const uppdaterat = await hamtaDelat(kod);
if (uppdaterat) setArende(uppdaterat.arende);
return null;
} catch {
- return "Beskedet kunde inte skickas — kontrollera anslutningen.";
+ return "The decision could not be sent — check the connection.";
}
}
: undefined;