Regelverk, ärendetyper och säkerhetssvar på engelska — med arv där data byter språk
ecm.ts bar regelverkets rådtexter: undantagsorsaker, orsakskategorier, underlagskällor, evidensgrader och de meningar som säger vad som fattas. Cirka hundra strängar, nu på engelska tillsammans med serverns ecm-regler.json — klienten och regelpaketet måste säga samma sak, annars är det inte samma regel. Tre av dem var inte text utan DATA, och där gjordes bytet med arv i stället för rakt av. ÄRENDETYPEN ligger i loggen och slår upp regelpaketets extra krav. Ett gammalt garantiärende vars typ heter "Garanti" hade inte matchat "Warranty", 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. ARENDETYP_ARV normaliserar typen vid uppslaget, aldrig i loggen. GALLRINGSTIDEN slås upp på samma nyckel, och det var värre. Ett garantiärende 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. Två tester låser det. SÄKERHETSSPÄRREN avgörs av en textsträng. 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. `arJakande` känner igen ett ja på alla språken. Att en säkerhetsspärr avgörs av fritext är i sig felet; rätt lösning är ett booleskt fält, och att arvet står synligt i koden är en påminnelse om det. Pre-diagnostikens kvitteringar känns igen på sin text och har fått samma behandling: gamla ärenden hade annars visat en ogjord kontroll för arbete som faktiskt utförts, och teknikern hade kvitterat en gång till — två kvitteringar för samma sak i en append-only logg. Genomgången drev gränssnittet på svenska knappnamn och gick sönder på rätt sätt: den hittade "+ Dokumentera felorsak", "Registrera besked" och kundkanalerna "Telefon"/"E-post", som alla missats av de tidigare svepen. Fyra av fyra ärenden avslutas igen. 609 tester, 186 integrationskontroller mot riktig Postgres, genomgång, portalspärr, typkontroll och lint gröna. Mutationsprövat: utan ARENDETYP_ARV faller två tester, utan `arJakande` ett. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -172,16 +172,16 @@ async function kor(sida, fall) {
|
||||
// ---- Starta ärendet ---------------------------------------------------
|
||||
await sida.goto(`${BAS}/#/felsokning/nytt`, { waitUntil: "networkidle" });
|
||||
await vanta(300);
|
||||
await klicka(btn(/Fyll i manuellt/i));
|
||||
await klicka(btn(/Fill in manually/i));
|
||||
const start = sida.locator('input:not([type="file"])');
|
||||
await fyll(start.nth(0), fall.regnr);
|
||||
await fyll(start.nth(1), fall.objekt);
|
||||
await klicka(btn(/^Fortsätt$/));
|
||||
await klicka(btn(/^Continue$/));
|
||||
await vanta(250);
|
||||
await klicka(btn(/Rätt objekt/i));
|
||||
await klicka(btn(/Correct object/i));
|
||||
await vanta(250);
|
||||
await fyll(sida.locator("textarea").first(), fall.fel);
|
||||
await klicka(btn(/Starta arbetslogg/i));
|
||||
await klicka(btn(/Start work log/i));
|
||||
await vanta(700);
|
||||
|
||||
if (!sida.url().includes("/arende/")) throw new Error(`${fall.id}: kom aldrig in i ärendet`);
|
||||
@@ -193,50 +193,50 @@ async function kor(sida, fall) {
|
||||
|
||||
for (let varv = 0; varv < 200; varv++) {
|
||||
const text = await sida.locator("body").innerText();
|
||||
if (!metodik) metodik = text.match(/METODIK:\s*([^\n·]+)/i)?.[1]?.trim() ?? "";
|
||||
if (text.includes("FELSÖKNINGEN ÄR AVSLUTAD")) break;
|
||||
if (!metodik) metodik = text.match(/METHODOLOGY:\s*([^\n·]+)/i)?.[1]?.trim() ?? "";
|
||||
if (text.includes("THE DIAGNOSIS IS CLOSED")) break;
|
||||
|
||||
const pre = panel(/Pre-diagnostik/i);
|
||||
const pre = panel(/Pre-diagnostics/i);
|
||||
if (await finns(pre)) {
|
||||
const ja = btn(/Ja — kontrollerad/i, pre);
|
||||
const ja = btn(/Yes — checked/i, pre);
|
||||
if (await finns(ja)) {
|
||||
// Historikkontrollen är en SPÄRR, inte en påminnelse: metodiken
|
||||
// får inte gå att nå förbi den. Det prövas här, i det ögonblick
|
||||
// frågan står obesvarad på skärmen — senare går det inte att
|
||||
// pröva, eftersom svaret då redan är avgivet.
|
||||
if (await finns(panel(/Metodik|Steg \d/i))) {
|
||||
if (await finns(panel(/Methodology|Step \d/i))) {
|
||||
throw new Error("metodiken var nåbar innan fordonets historik besvarats");
|
||||
}
|
||||
await klicka(ja);
|
||||
await klicka(btn(/^Dokumentera$/i, pre));
|
||||
await klicka(btn(/^Document$/i, pre));
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/Fotografera instrumentpanelen/i, pre))) {
|
||||
if (await finns(btn(/Photograph the instrument panel/i, pre))) {
|
||||
await foto(pre.locator('input[type="file"]'));
|
||||
const falt = pre.locator('input:not([type="file"])');
|
||||
if (await finns(falt)) {
|
||||
await fyll(falt, "84 320 km");
|
||||
await klicka(btn(/Registrera|Spara/i, pre));
|
||||
await klicka(btn(/Record|Save/i, pre));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/Stämmer — verifierad/i, pre))) {
|
||||
await klicka(btn(/Stämmer — verifierad/i, pre));
|
||||
if (await finns(btn(/Correct — verified/i, pre))) {
|
||||
await klicka(btn(/Correct — verified/i, pre));
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/Inga ytterligare observationer/i, pre))) {
|
||||
await klicka(btn(/Inga ytterligare observationer/i, pre));
|
||||
if (await finns(btn(/No further observations/i, pre))) {
|
||||
await klicka(btn(/No further observations/i, pre));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const guide = panel(/^Metodik:/i);
|
||||
const guide = panel(/^Methodology:/i);
|
||||
if (await finns(guide)) {
|
||||
if (await finns(btn(/Ta foto/i, guide))) {
|
||||
if (await finns(btn(/Take a photograph/i, guide))) {
|
||||
await foto(guide.locator('input[type="file"]'));
|
||||
continue;
|
||||
}
|
||||
const verifiera = btn(/Markera verifierad/i, guide);
|
||||
const verifiera = btn(/Mark as verified/i, guide);
|
||||
if (await finns(verifiera)) {
|
||||
const falt = guide.locator('input[type="text"], textarea').first();
|
||||
if (await finns(falt)) await fyll(falt, fall.matvarde);
|
||||
@@ -245,86 +245,86 @@ async function kor(sida, fall) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (await finns(btn(/Spara svar/i, guide))) {
|
||||
if (await finns(btn(/Save answer/i, guide))) {
|
||||
const falt = guide.locator('input[type="text"], textarea').first();
|
||||
if (await finns(falt)) await fyll(falt, `Enligt kundens uppgift: ${fall.fel}`);
|
||||
await klicka(btn(/Spara svar/i, guide));
|
||||
await klicka(btn(/Save answer/i, guide));
|
||||
continue;
|
||||
}
|
||||
// Säkerhetsfrågor besvaras jakande; spärren vid Nej provas i
|
||||
// enhetstesterna, inte här.
|
||||
if (await finns(btn(/^Ja$/, guide))) {
|
||||
await klicka(btn(/^Ja$/, guide));
|
||||
if (await finns(btn(/^Yes$/, guide))) {
|
||||
await klicka(btn(/^Yes$/, guide));
|
||||
continue;
|
||||
}
|
||||
const val = guide.locator("button").filter({ hasNot: sida.locator("svg") });
|
||||
if ((await val.count()) > 0 && !text.includes("Samtliga steg i metodiken")) {
|
||||
if ((await val.count()) > 0 && !text.includes("Every step in the methodology")) {
|
||||
await klicka(val.first());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const repro = panel(/reproducering/i);
|
||||
const repro = panel(/reproduction/i);
|
||||
if (await finns(repro)) {
|
||||
await klicka(btn(/^Ja$/, repro));
|
||||
await klicka(btn(/^Yes$/, repro));
|
||||
await fyll(repro.locator("textarea"), `${fall.fel} Återskapat vid provkörning under samma förhållanden.`);
|
||||
await klicka(btn(/Dokumentera reproducering/i, repro));
|
||||
await klicka(btn(/Document reproduction/i, repro));
|
||||
continue;
|
||||
}
|
||||
|
||||
const fo = panel(/Felorsaksanalys/i);
|
||||
if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Dokumentera felorsak/i, fo)))) {
|
||||
await klicka(btn(/Dokumentera felorsak/i, fo));
|
||||
const fo = panel(/Root cause analysis/i);
|
||||
if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Document root cause/i, fo)))) {
|
||||
await klicka(btn(/Document root cause/i, fo));
|
||||
await fyll(fo.locator("textarea").first(), fall.avvikelse);
|
||||
await klicka(btn(/^Normalt slitage$/, fo));
|
||||
await klicka(btn(/^Mätresultat$/, fo));
|
||||
await klicka(btn(/^Normal wear$/, fo));
|
||||
await klicka(btn(/^Measurement result$/, fo));
|
||||
await fyll(fo.locator('input[type="text"], textarea').last(), fall.atgard);
|
||||
await klicka(btn(/Spara felorsak/i, fo));
|
||||
await klicka(btn(/Save root cause/i, fo));
|
||||
// En metodik utan mätkontroller har inga mätresultat att luta sig
|
||||
// mot; systemet nekar då källan och teknikern byter.
|
||||
if (await finns(btn(/Spara felorsak/i, fo))) {
|
||||
await klicka(btn(/^Mätresultat$/, fo));
|
||||
await klicka(btn(/^Direkt observation$/, fo));
|
||||
await klicka(btn(/Spara felorsak/i, fo));
|
||||
if (await finns(btn(/Save root cause/i, fo))) {
|
||||
await klicka(btn(/^Measurement result$/, fo));
|
||||
await klicka(btn(/^Direct observation$/, fo));
|
||||
await klicka(btn(/Save root cause/i, fo));
|
||||
}
|
||||
felorsakGjord = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const at = panel(/Åtgärd och kvalitetskontroll/i);
|
||||
const at = panel(/Action and quality check/i);
|
||||
if (await finns(at)) {
|
||||
if (await finns(btn(/Lämna åtgärdsförslag till kund/i, at))) {
|
||||
await klicka(btn(/Lämna åtgärdsförslag till kund/i, at));
|
||||
await klicka(btn(/^Lämna förslag$/i, at));
|
||||
if (await finns(btn(/Give the customer a proposed action/i, at))) {
|
||||
await klicka(btn(/Give the customer a proposed action/i, at));
|
||||
await klicka(btn(/^Give the proposal$/i, at));
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/^Godkänt$/i, at))) {
|
||||
await klicka(btn(/^Godkänt$/i, at));
|
||||
await klicka(btn(/^Telefon$/i, at));
|
||||
await klicka(btn(/Registrera besked/i, at));
|
||||
if (await finns(btn(/^Approved$/i, at))) {
|
||||
await klicka(btn(/^Approved$/i, at));
|
||||
await klicka(btn(/^Telephone$/i, at));
|
||||
await klicka(btn(/Record the decision/i, at));
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/Dokumentera utförd åtgärd/i, at))) {
|
||||
await klicka(btn(/Dokumentera utförd åtgärd/i, at));
|
||||
if (await finns(btn(/Document the action performed/i, at))) {
|
||||
await klicka(btn(/Document the action performed/i, at));
|
||||
await fyll(at.locator("textarea").first(), `${fall.atgard} Utfört enligt den dokumenterade felorsaken.`);
|
||||
await klicka(btn(/Spara åtgärd/i, at));
|
||||
await klicka(btn(/Save action/i, at));
|
||||
continue;
|
||||
}
|
||||
if (await finns(btn(/Symptomet är borta/i, at))) {
|
||||
await klicka(btn(/Symptomet är borta/i, at));
|
||||
if (await finns(btn(/Symptom is gone/i, at))) {
|
||||
await klicka(btn(/Symptom is gone/i, at));
|
||||
await fyll(at.locator("textarea").first(), "Ny provkörning på samma vägsträcka — symptomet uppträder inte längre.");
|
||||
await klicka(btn(/Spara kvalitetskontroll/i, at));
|
||||
await klicka(btn(/Save quality check/i, at));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mat = panel(/Utgående mätarställning/i);
|
||||
const mat = panel(/Outgoing odometer reading/i);
|
||||
if (await finns(mat)) {
|
||||
await foto(mat.locator('input[type="file"]'));
|
||||
const falt = mat.locator('input:not([type="file"])');
|
||||
if (await finns(falt)) {
|
||||
await fyll(falt, "84 520 km");
|
||||
await klicka(btn(/Registrera|Spara/i, mat));
|
||||
await klicka(btn(/Record|Save/i, mat));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -336,7 +336,7 @@ async function kor(sida, fall) {
|
||||
const f = sida.locator(`#slutsats-${id}`);
|
||||
if (await finns(f)) await fyll(f, txt);
|
||||
}
|
||||
const reg = btn(/Registrera slutsats|Uppdatera slutsats/i);
|
||||
const reg = btn(/Record closing statement|Update closing statement/i);
|
||||
if ((await finns(reg)) && !(await reg.first().isDisabled())) {
|
||||
await klicka(reg);
|
||||
slutsatsfalt = synliga;
|
||||
@@ -345,7 +345,7 @@ async function kor(sida, fall) {
|
||||
}
|
||||
}
|
||||
|
||||
const avsluta = btn(/^Avsluta felsökning$/i);
|
||||
const avsluta = btn(/^Close diagnosis$/i);
|
||||
if ((await finns(avsluta)) && !(await avsluta.last().isDisabled())) {
|
||||
await klicka(avsluta.last());
|
||||
continue;
|
||||
@@ -374,7 +374,7 @@ async function kor(sida, fall) {
|
||||
|
||||
return {
|
||||
metodik,
|
||||
avslutat: slut.includes("FELSÖKNINGEN ÄR AVSLUTAD"),
|
||||
avslutat: slut.includes("THE DIAGNOSIS IS CLOSED"),
|
||||
interaktioner,
|
||||
handelser: handelser.length,
|
||||
avvisade: handelser.map((h) => [h?.typ, granskaHändelse(h)]).filter(([, fel]) => fel !== null),
|
||||
|
||||
@@ -30,14 +30,14 @@ function BeslutsKnappar({
|
||||
return (
|
||||
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Ditt besked till verkstaden
|
||||
Your decision to the workshop
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant={val === "godkant" ? "primar" : "sekundar"} onClick={() => setVal("godkant")}>
|
||||
Godkänn åtgärden
|
||||
Approve the action
|
||||
</StorKnapp>
|
||||
<StorKnapp variant={val === "avbojt" ? "fara" : "sekundar"} onClick={() => setVal("avbojt")}>
|
||||
Avböj
|
||||
Decline
|
||||
</StorKnapp>
|
||||
</div>
|
||||
{val && (
|
||||
@@ -67,7 +67,7 @@ function BeslutsKnappar({
|
||||
{skickar ? "Sending …" : "Send decision"}
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -226,7 +226,7 @@ export function DelatArendeVy({
|
||||
<BeslutsKnappar vidBeslut={vidBeslut} />
|
||||
) : (
|
||||
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[12px] text-[#707070]">
|
||||
Inväntar ditt besked — kontakta verkstaden för att godkänna eller avböja.
|
||||
Awaiting your decision — contact the workshop to approve or decline.
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
@@ -312,7 +312,7 @@ export function DelatArendeVy({
|
||||
</Panel>
|
||||
|
||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
||||
Ärende #{arende.nummer} · startat {tidDatum(arende.skapad)} · genererad ur ärendets händelselogg
|
||||
Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log
|
||||
</p>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
||||
</button>
|
||||
{lyssnar && (
|
||||
<span className="text-[12px] font-semibold text-[#8B1A1A]">
|
||||
● Inspelning pågår{interim ? ` — ”${interim}”` : "…"}
|
||||
● Recording{interim ? ` — ”${interim}”` : "…"}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -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:
|
||||
|
||||
<div className="px-4 py-2">
|
||||
<p className="mb-4 max-w-[62ch] text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
{/* 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:
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-2 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -26,7 +26,7 @@ function komplettLogg(extra: Record<string, unknown>[] = []) {
|
||||
{ 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
|
||||
@@ -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<EvidensNiva, string> = {
|
||||
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<string, string[]> = {
|
||||
[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<ComplianceKravTyp, (arende: Arende) => 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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
/>
|
||||
{!foto && !laser && (
|
||||
<StorKnapp variant="sekundar" onClick={() => filRef.current?.click()}>
|
||||
<IkonKamera /> Fotografera instrumentpanelen
|
||||
<IkonKamera /> Photograph the instrument panel
|
||||
</StorKnapp>
|
||||
)}
|
||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer …</p>}
|
||||
@@ -331,11 +331,11 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
<div className="mt-2">
|
||||
{demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
Demo-avläsning — bildtolkningen kräver inloggning. Kontrollera värdet.
|
||||
Demo reading — image interpretation requires sign-in. Check the value.
|
||||
</p>
|
||||
)}
|
||||
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
|
||||
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||
<TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||
<StorKnapp
|
||||
disabled={!varde.trim()}
|
||||
onClick={() => {
|
||||
@@ -344,7 +344,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
setVarde("");
|
||||
}}
|
||||
>
|
||||
Spara mätarställning
|
||||
Save odometer reading
|
||||
</StorKnapp>
|
||||
</div>
|
||||
)}
|
||||
@@ -393,12 +393,12 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
||||
return (
|
||||
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Tidigare ärenden på {identifierare} — granska innan historiken kvitteras
|
||||
Previous cases for {identifierare} — review before signing off the history
|
||||
</p>
|
||||
{rader.map((rad) => (
|
||||
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
|
||||
<p>
|
||||
<span className="font-semibold">Ärende #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||
{rad.avslutat ? "closed" : "in progress"}
|
||||
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
||||
</p>
|
||||
@@ -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
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -448,12 +448,12 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("historik").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Har fordonets historik kontrollerats? (tidigare arbeten, återkommande fel, TSB, kampanjer)
|
||||
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
|
||||
</p>
|
||||
<TidigareArenden arende={arende} skicka={skicka} />
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant={historikVal === "ja" ? "primar" : "sekundar"} onClick={() => setHistorikVal("ja")}>
|
||||
Ja — kontrollerad
|
||||
Yes — checked
|
||||
</StorKnapp>
|
||||
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
||||
No
|
||||
@@ -486,7 +486,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("matarstallning_in").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Ingående mätarställning — fotografera instrumentpanelen
|
||||
Incoming odometer reading — photograph the instrument panel
|
||||
</p>
|
||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||
</div>
|
||||
@@ -495,11 +495,11 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("felbeskrivning").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Är kundens felbeskrivning korrekt återgiven?
|
||||
Is the customer's fault description correctly recorded?
|
||||
</p>
|
||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||
Stämmer — verifierad
|
||||
Correct — verified
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
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 && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Något ytterligare vid mottagandet? (reparationsspår, modifieringar, skador, läckage, korrosion …)
|
||||
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}>
|
||||
Inga ytterligare observationer
|
||||
No further observations
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
variant="sekundar"
|
||||
disabled={!harTidiga}
|
||||
onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })}
|
||||
>
|
||||
Observationerna är dokumenterade
|
||||
The observations are documented
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Dokumentera med foto eller observation i panelen nedan — knappen låses upp när något loggats.
|
||||
Document with a photograph or an observation in the panel below — the button unlocks once something is logged.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -569,7 +569,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
Dokumentera reproducering
|
||||
Document reproduction
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -638,15 +638,15 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
<div key={p.id} className="mb-2 border-b border-[#EBEBEB] pb-2 text-[13px] last:border-0">
|
||||
<p className="font-semibold">{h.avvikelse}</p>
|
||||
<p className="text-[#4A5560]">
|
||||
Orsak: {h.orsaker.join(", ")} · Underlag: {h.underlag.join(", ")} · Säkerhet: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
</p>
|
||||
<p>Åtgärd: {h.atgard}</p>
|
||||
<p>Action: {h.atgard}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!oppen ? (
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(true)}>
|
||||
+ Dokumentera felorsak
|
||||
+ Document root cause
|
||||
</StorKnapp>
|
||||
) : (
|
||||
<div>
|
||||
@@ -721,7 +721,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={spara}>Save root cause</StorKnapp>
|
||||
</div>
|
||||
@@ -774,7 +774,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-[#9A6700]">Ingen åtgärd utförd — {h.motivering}</span>
|
||||
<span className="text-[#9A6700]">No action performed — {h.motivering}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
@@ -820,7 +820,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setForslagText(foreslagenAtgard);
|
||||
}}
|
||||
>
|
||||
Lämna åtgärdsförslag till kund
|
||||
Give the customer a proposed action
|
||||
</StorKnapp>
|
||||
)}
|
||||
|
||||
@@ -837,7 +837,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Estimated cost (optional)" varde={kostnad} satt={setKostnad} platshallare="e.g. 1,200 incl. VAT" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setForslagsLage(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={forslagText.trim().length < 10}
|
||||
@@ -852,7 +852,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setKostnad("");
|
||||
}}
|
||||
>
|
||||
Lämna förslag
|
||||
Give the proposal
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -861,7 +861,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{forslag.length > 0 && !beslut && (
|
||||
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
|
||||
Registrera kundens besked innan arbetet påbörjas
|
||||
Record the customer's decision before the work begins
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{(["godkant", "avbojt", "delvis"] as const).map((val) => (
|
||||
@@ -912,7 +912,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setBeslutsKommentar("");
|
||||
}}
|
||||
>
|
||||
Registrera besked
|
||||
Record the decision
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -926,7 +926,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
disabled={forslag.length > 0 && (!beslut || beslut.beslut === "avbojt")}
|
||||
onClick={() => setLage("utford")}
|
||||
>
|
||||
Dokumentera utförd åtgärd
|
||||
Document the action performed
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
||||
No action performed
|
||||
@@ -947,7 +947,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Parts (optional)" varde={delar} satt={setDelar} platshallare="e.g. Balance weights, part no. 30-1234" rost />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={beskrivning.trim().length < 10}
|
||||
@@ -958,7 +958,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setDelar("");
|
||||
}}
|
||||
>
|
||||
Spara åtgärd
|
||||
Save action
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -967,7 +967,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{lage === "ingen" && (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Orsak till att ingen åtgärd utfördes (obligatorisk)
|
||||
Reason no action was performed (required)
|
||||
</p>
|
||||
<div className="mb-2 grid gap-1">
|
||||
{INGEN_ATGARD_ORSAKER.map((val) => (
|
||||
@@ -985,12 +985,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||
{beslut?.beslut === "avbojt" && (
|
||||
<p className="mb-2 text-[11px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!orsak.trim()}
|
||||
@@ -1009,11 +1009,11 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{utford && !kk && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Kvalitetskontroll — är symptomet borta efter åtgärden?
|
||||
Quality check — is the symptom gone after the action?
|
||||
</p>
|
||||
{repro?.status === "ja" && (
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
@@ -1053,7 +1053,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setKkText("");
|
||||
}}
|
||||
>
|
||||
Spara kvalitetskontroll
|
||||
Save quality check
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1081,7 +1081,7 @@ function VyTrad({
|
||||
return (
|
||||
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
|
||||
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
||||
Ärendevyer
|
||||
Case views
|
||||
</p>
|
||||
{FLIKAR.map((f) => (
|
||||
<button
|
||||
@@ -1177,7 +1177,7 @@ function KontextPanel({
|
||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
|
||||
Beslutsstöd · presenteras aldrig som konstaterat fel
|
||||
Decision support · never presented as an established fault
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -1214,7 +1214,7 @@ function InaktivitetsBanner({
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
Lägg till i arbetsloggen
|
||||
Add to the work log
|
||||
</StorKnapp>
|
||||
</div>
|
||||
);
|
||||
@@ -1286,7 +1286,7 @@ function GuideFlik({
|
||||
return (
|
||||
<Panel rubrik="The diagnosis is closed">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Ä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.
|
||||
</p>
|
||||
</Panel>
|
||||
);
|
||||
@@ -1335,16 +1335,16 @@ function GuideFlik({
|
||||
först vid avslutsknappen vore att neka någon som redan är klar. */}
|
||||
{(steg.klart || underlagKlart) && <Slutsatspanel arende={arende} skicka={skicka} />}
|
||||
|
||||
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
||||
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
|
||||
{steg.klart ? (
|
||||
<>
|
||||
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||
<p className="mb-4 text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||
Avsluta felsökning
|
||||
Close diagnosis
|
||||
</StorKnapp>
|
||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||
</>
|
||||
@@ -1357,7 +1357,7 @@ function GuideFlik({
|
||||
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
||||
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
||||
<p className="text-[12px] text-[#666666]">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1422,10 +1422,10 @@ function GuideFlik({
|
||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||
Lämna över arbete
|
||||
Hand over the work
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||
Avsluta felsökning
|
||||
Close diagnosis
|
||||
</StorKnapp>
|
||||
</div>
|
||||
|
||||
@@ -1448,7 +1448,7 @@ function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detal
|
||||
return (
|
||||
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]">
|
||||
Ärendet kan inte avslutas ännu
|
||||
The case cannot be closed yet
|
||||
</p>
|
||||
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
|
||||
{hinder.map((h) => (
|
||||
@@ -1515,7 +1515,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
||||
<>
|
||||
<TextFalt label="Answer" varde={text} satt={setText} rost />
|
||||
<StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}>
|
||||
Spara svar
|
||||
Save answer
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1602,7 +1602,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
</p>
|
||||
)}
|
||||
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
||||
Markera verifierad
|
||||
Mark as verified
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1634,7 +1634,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
||||
onClick={() => setOppen(true)}
|
||||
className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
|
||||
>
|
||||
Underlag kan inte tas fram …
|
||||
Evidence cannot be produced …
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1659,10 +1659,10 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
||||
<TextFalt label="Or a cause of your own" varde={orsak && !UNDANTAGSORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="fara" disabled={!orsak.trim()} onClick={() => vidUndantag(orsak.trim())}>
|
||||
Dokumentera undantag
|
||||
Document an exemption
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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({
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVideo(null)}>
|
||||
Förkasta
|
||||
Discard
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!video.beskrivning.trim()}
|
||||
@@ -1870,7 +1870,7 @@ function SnabbDokumentation({
|
||||
setVideo(null);
|
||||
}}
|
||||
>
|
||||
Spara video
|
||||
Save video
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1880,11 +1880,11 @@ function SnabbDokumentation({
|
||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
||||
{avlasning.demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
Demo-avläsning — bildtolkningen kräver inloggning. Värdena är exempeldata.
|
||||
Demo reading — image interpretation requires sign-in. The values are sample data.
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Avläst: {avlasning.tolkning.instrumenttyp} — bekräfta innan något loggas
|
||||
Read: {avlasning.tolkning.instrumenttyp} — confirm before anything is logged
|
||||
</p>
|
||||
{avlasning.tolkning.varden.map((v, i) => (
|
||||
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
@@ -1898,7 +1898,7 @@ function SnabbDokumentation({
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
||||
Förkasta
|
||||
Discard
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={sparaAvlasning}>Save photograph + values</StorKnapp>
|
||||
</div>
|
||||
@@ -1908,7 +1908,7 @@ function SnabbDokumentation({
|
||||
<div className="mt-3">
|
||||
{typ === "hypotes" && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
<IkonPunkt farg="#8B1A1A" /> En hypotes är en möjlig felorsak som kräver verifiering — den loggas aldrig som ett konstaterat fel.
|
||||
<IkonPunkt farg="#8B1A1A" /> A hypothesis is a possible root cause that requires verification — it is never logged as an established fault.
|
||||
</p>
|
||||
)}
|
||||
<TextFalt
|
||||
@@ -1920,7 +1920,7 @@ function SnabbDokumentation({
|
||||
/>
|
||||
{typ === "matvarde" && <TextFalt label="Value (with unit)" varde={varde} satt={setVarde} platshallare="e.g. 13.9 V" />}
|
||||
<StorKnapp disabled={!text.trim() || (typ === "matvarde" && !varde.trim())} onClick={spara}>
|
||||
Spara
|
||||
Save
|
||||
</StorKnapp>
|
||||
</div>
|
||||
)}
|
||||
@@ -1974,7 +1974,7 @@ function OverlamningDialog({
|
||||
</pre>
|
||||
{aiLage === "vilar" && (
|
||||
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
||||
Komplettera analysen: risker & osäkerheter
|
||||
Extend the analysis: risks & uncertainties
|
||||
</StorKnapp>
|
||||
)}
|
||||
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing …</p>}
|
||||
@@ -1993,7 +1993,7 @@ function OverlamningDialog({
|
||||
<TextFalt label="Handed to (optional)" varde={till} satt={setTill} platshallare="e.g. Jonas" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={stang}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
onClick={() => {
|
||||
@@ -2001,7 +2001,7 @@ function OverlamningDialog({
|
||||
stang();
|
||||
}}
|
||||
>
|
||||
Bekräfta överlämning
|
||||
Confirm handover
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2042,7 +2042,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
<IkonLank /> Delningslänkar (kund/partner/intern)
|
||||
<IkonLank /> Share links (customer / partner / internal)
|
||||
</StorKnapp>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
</button>
|
||||
<button
|
||||
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
||||
onClick={async () => {
|
||||
await aterkallaDelning(delning.kod);
|
||||
skicka({ typ: "kommentar", text: `Delningslänk (${NIVA_LABEL[delning.niva]}) återkallad.` });
|
||||
skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` });
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
Återkalla
|
||||
Revoke
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
@@ -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
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -2164,8 +2164,8 @@ function BriefFlik({
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="Review of the evidence">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
||||
{granskning === "fel" && (
|
||||
@@ -2345,22 +2345,22 @@ function RapportFlik({
|
||||
))}
|
||||
{!godkand && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
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).
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Customer report">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" disabled={!godkand} onClick={() => window.print()}>
|
||||
Skriv ut / PDF
|
||||
Print / PDF
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={exporteraJson}>
|
||||
Exportera JSON
|
||||
Export JSON
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
||||
@@ -2383,7 +2383,7 @@ function RapportFlik({
|
||||
)
|
||||
)}
|
||||
<p className="mt-2 text-[12px] text-[#707070]">
|
||||
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).
|
||||
</p>
|
||||
</Panel>
|
||||
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
|
||||
@@ -2506,7 +2506,7 @@ function RapportFlik({
|
||||
["Parts", h.delar],
|
||||
])
|
||||
) : (
|
||||
<p className="text-[14px] text-[#9A6700]">Ingen åtgärd utförd — {h.motivering}</p>
|
||||
<p className="text-[14px] text-[#9A6700]">No action performed — {h.motivering}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2565,7 +2565,7 @@ function RapportFlik({
|
||||
</Panel>
|
||||
) : null}
|
||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
||||
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.
|
||||
</p>
|
||||
<Ritningsstampel arende={arende} />
|
||||
<Felanmalan arende={arende} metodik={metodik ?? null} />
|
||||
@@ -2642,8 +2642,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
return (
|
||||
<Panel rubrik="Report a problem with the system">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Ä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.
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
@@ -2664,14 +2663,14 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
platshallare="Whoever answers cannot see your screen."
|
||||
flerRad
|
||||
/>
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Anmäld som {utfall.ok}.</p>}
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Reported as {utfall.ok}.</p>}
|
||||
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||
{skickar ? "Sending" : "Send report"}
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Stäng
|
||||
Close
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
@@ -47,7 +47,7 @@ function PlattformInloggning() {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-[14px]">
|
||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||
{konto.organisation} — synk och beslutsstöd aktiva.
|
||||
{konto.organisation} — synchronization and decision support active.
|
||||
</p>
|
||||
<button
|
||||
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
|
||||
@@ -56,7 +56,7 @@ function PlattformInloggning() {
|
||||
setKonto(null);
|
||||
}}
|
||||
>
|
||||
Logga ut
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||
@@ -80,8 +80,7 @@ function PlattformInloggning() {
|
||||
{lage === "stangd" ? (
|
||||
<>
|
||||
<p className="mb-2 text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("registrera")}>
|
||||
@@ -117,7 +116,7 @@ function PlattformInloggning() {
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
type="submit"
|
||||
@@ -165,7 +164,7 @@ function AnvandarAdmin() {
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
Hantera användare i organisationen
|
||||
Manage users in the organization
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
);
|
||||
@@ -209,7 +208,7 @@ function AnvandarAdmin() {
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-1 text-[12px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
<form
|
||||
className="mt-3"
|
||||
@@ -248,7 +247,7 @@ function AnvandarAdmin() {
|
||||
</div>
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
||||
Skapa användare
|
||||
Create user
|
||||
</StorKnapp>
|
||||
</form>
|
||||
</Panel>
|
||||
@@ -273,7 +272,7 @@ export default function Arendelista() {
|
||||
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||
<Panel rubrik="Who is working?">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -283,7 +282,7 @@ export default function Arendelista() {
|
||||
>
|
||||
<TextFalt label="Name" varde={namn} satt={setNamn} platshallare="e.g. Anna" />
|
||||
<StorKnapp type="submit" disabled={!namn.trim()}>
|
||||
Fortsätt
|
||||
Continue
|
||||
</StorKnapp>
|
||||
</form>
|
||||
</Panel>
|
||||
@@ -330,11 +329,10 @@ export default function Arendelista() {
|
||||
{alla.length === 0 && (
|
||||
<Panel rubrik="Get started">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
||||
Skapa demoärende (Volvo XC60, vibration)
|
||||
Create demo case (Volvo XC60, vibration)
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -353,11 +351,7 @@ export default function Arendelista() {
|
||||
|
||||
<Panel rubrik="Decision support">
|
||||
<p className="text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
@@ -372,7 +366,7 @@ export default function Arendelista() {
|
||||
<div className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
Ärende #{arende.nummer}
|
||||
Case #{arende.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
|
||||
@@ -86,13 +86,11 @@ function Integrationer() {
|
||||
return (
|
||||
<Panel rubrik="Brand-specific connectors">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
{!krypteringKlar && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -132,7 +130,7 @@ function Integrationer() {
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => { setOppen(null); setFel(""); }}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!krypteringKlar}
|
||||
@@ -149,11 +147,11 @@ function Integrationer() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Spara uppgifter
|
||||
Save details
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -182,7 +180,7 @@ function Integrationer() {
|
||||
ladda();
|
||||
}}
|
||||
>
|
||||
Ta bort
|
||||
Delete
|
||||
</StorKnapp>
|
||||
)}
|
||||
</div>
|
||||
@@ -190,7 +188,7 @@ function Integrationer() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Uppgifter sparade för {sparat}.</p>}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Details saved for {sparat}.</p>}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -213,7 +211,7 @@ export default function Installningar() {
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Organisationens inställningar hanteras av er systemadministratör.
|
||||
Organization settings are managed by your system administrator.
|
||||
</p>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -246,7 +244,7 @@ export default function Installningar() {
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[#333333]">
|
||||
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."}
|
||||
|
||||
@@ -177,8 +177,7 @@ export default function NyttArende() {
|
||||
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel rubrik="Scan work order">
|
||||
<p className="mb-3 text-[#333333]">
|
||||
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.
|
||||
</p>
|
||||
<input
|
||||
ref={skanRef}
|
||||
@@ -201,7 +200,7 @@ export default function NyttArende() {
|
||||
</Panel>
|
||||
<Panel>
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
Fyll i manuellt
|
||||
Fill in manually
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -217,7 +216,7 @@ export default function NyttArende() {
|
||||
>
|
||||
{tolkning.demo && (
|
||||
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
<GranskaTolkning
|
||||
@@ -255,7 +254,7 @@ export default function NyttArende() {
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
</Panel>
|
||||
<Panel rubrik="Read the identity">
|
||||
@@ -275,17 +274,17 @@ export default function NyttArende() {
|
||||
<div>
|
||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Rikta kameran mot QR-koden, streckkoden eller VIN-etiketten.
|
||||
Point the camera at the QR code, barcode or VIN label.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
||||
Avbryt skanning
|
||||
Cancel scanning
|
||||
</StorKnapp>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{stodStreckkod() && (
|
||||
<StorKnapp variant="sekundar" onClick={oppnaSkanning}>
|
||||
Skanna QR/streckkod
|
||||
Scan QR or barcode
|
||||
</StorKnapp>
|
||||
)}
|
||||
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
||||
@@ -295,8 +294,8 @@ export default function NyttArende() {
|
||||
)}
|
||||
{avlastKod && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">
|
||||
Avläst {avlastKod.typ}: {avlastKod.varde}
|
||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — kontrollera värdet nedan.
|
||||
Read {avlastKod.typ}: {avlastKod.varde}
|
||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
||||
</p>
|
||||
)}
|
||||
{kodFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{kodFel}</p>}
|
||||
@@ -306,7 +305,7 @@ export default function NyttArende() {
|
||||
<TextFalt label="Description (make, model, year)" varde={beskrivning} satt={setBeskrivning} platshallare="T.ex. Volvo XC60 D4 2019" />
|
||||
<TextFalt label="Customer (optional)" varde={kund} satt={setKund} platshallare="e.g. Alex Meyer" />
|
||||
<StorKnapp disabled={!identifierare.trim()} onClick={() => setSteg("bekrafta")}>
|
||||
Fortsätt
|
||||
Continue
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -325,7 +324,7 @@ export default function NyttArende() {
|
||||
</Panel>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
Ändra
|
||||
Edit
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Correct object — continue</StorKnapp>
|
||||
</div>
|
||||
@@ -420,7 +419,7 @@ function GranskaTolkning({
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
{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."}
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -467,7 +466,7 @@ function GranskaTolkning({
|
||||
}}
|
||||
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
|
||||
>
|
||||
Bekräfta
|
||||
Confirm
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -480,12 +479,12 @@ function GranskaTolkning({
|
||||
|
||||
{obekraftade.length > 0 && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
{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.
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={avbryt}>
|
||||
Skanna om
|
||||
Scan again
|
||||
</StorKnapp>
|
||||
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
||||
{startar ? "Starting …" : "Start diagnosis"}
|
||||
|
||||
@@ -40,7 +40,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
hamtaAnvandare().then(setPersoner).catch(() => setPersoner([]));
|
||||
}}
|
||||
>
|
||||
Omfördela
|
||||
Reassign
|
||||
</StorKnapp>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
|
||||
onClick={() => setOppen(false)}
|
||||
>
|
||||
Avbryt
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@ export default function Oversikt() {
|
||||
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
|
||||
The overview requires supervisor or administrator rights on the platform.
|
||||
</p>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -177,7 +177,7 @@ export default function Oversikt() {
|
||||
</div>
|
||||
{avslutade.length > 0 && (
|
||||
<p className="mt-3 text-center text-[#333333]">
|
||||
Genomsnittlig ledtid för avslutade: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
@@ -195,7 +195,7 @@ export default function Oversikt() {
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
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.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -206,7 +206,7 @@ export default function Oversikt() {
|
||||
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
Ärende #{rad.nummer}
|
||||
Case #{rad.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
@@ -220,12 +220,12 @@ export default function Oversikt() {
|
||||
{rad.felbeskrivning && <p className="text-[14px] text-[#333333]">”{rad.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-[12px] text-[#707070]">
|
||||
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||
{` · ${rad.antal_handelser} händelser`}
|
||||
{` · ${rad.antal_handelser} events`}
|
||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => oppna(rad)}>
|
||||
Öppna ärendet
|
||||
Open the case
|
||||
</StorKnapp>
|
||||
{!rad.avslutat && <Omfordela rad={rad} vidKlar={() => hamtaOversikt().then(setRader)} />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user