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 ---------------------------------------------------
|
// ---- Starta ärendet ---------------------------------------------------
|
||||||
await sida.goto(`${BAS}/#/felsokning/nytt`, { waitUntil: "networkidle" });
|
await sida.goto(`${BAS}/#/felsokning/nytt`, { waitUntil: "networkidle" });
|
||||||
await vanta(300);
|
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"])');
|
const start = sida.locator('input:not([type="file"])');
|
||||||
await fyll(start.nth(0), fall.regnr);
|
await fyll(start.nth(0), fall.regnr);
|
||||||
await fyll(start.nth(1), fall.objekt);
|
await fyll(start.nth(1), fall.objekt);
|
||||||
await klicka(btn(/^Fortsätt$/));
|
await klicka(btn(/^Continue$/));
|
||||||
await vanta(250);
|
await vanta(250);
|
||||||
await klicka(btn(/Rätt objekt/i));
|
await klicka(btn(/Correct object/i));
|
||||||
await vanta(250);
|
await vanta(250);
|
||||||
await fyll(sida.locator("textarea").first(), fall.fel);
|
await fyll(sida.locator("textarea").first(), fall.fel);
|
||||||
await klicka(btn(/Starta arbetslogg/i));
|
await klicka(btn(/Start work log/i));
|
||||||
await vanta(700);
|
await vanta(700);
|
||||||
|
|
||||||
if (!sida.url().includes("/arende/")) throw new Error(`${fall.id}: kom aldrig in i ärendet`);
|
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++) {
|
for (let varv = 0; varv < 200; varv++) {
|
||||||
const text = await sida.locator("body").innerText();
|
const text = await sida.locator("body").innerText();
|
||||||
if (!metodik) metodik = text.match(/METODIK:\s*([^\n·]+)/i)?.[1]?.trim() ?? "";
|
if (!metodik) metodik = text.match(/METHODOLOGY:\s*([^\n·]+)/i)?.[1]?.trim() ?? "";
|
||||||
if (text.includes("FELSÖKNINGEN ÄR AVSLUTAD")) break;
|
if (text.includes("THE DIAGNOSIS IS CLOSED")) break;
|
||||||
|
|
||||||
const pre = panel(/Pre-diagnostik/i);
|
const pre = panel(/Pre-diagnostics/i);
|
||||||
if (await finns(pre)) {
|
if (await finns(pre)) {
|
||||||
const ja = btn(/Ja — kontrollerad/i, pre);
|
const ja = btn(/Yes — checked/i, pre);
|
||||||
if (await finns(ja)) {
|
if (await finns(ja)) {
|
||||||
// Historikkontrollen är en SPÄRR, inte en påminnelse: metodiken
|
// 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
|
// 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
|
// frågan står obesvarad på skärmen — senare går det inte att
|
||||||
// pröva, eftersom svaret då redan är avgivet.
|
// 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");
|
throw new Error("metodiken var nåbar innan fordonets historik besvarats");
|
||||||
}
|
}
|
||||||
await klicka(ja);
|
await klicka(ja);
|
||||||
await klicka(btn(/^Dokumentera$/i, pre));
|
await klicka(btn(/^Document$/i, pre));
|
||||||
continue;
|
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"]'));
|
await foto(pre.locator('input[type="file"]'));
|
||||||
const falt = pre.locator('input:not([type="file"])');
|
const falt = pre.locator('input:not([type="file"])');
|
||||||
if (await finns(falt)) {
|
if (await finns(falt)) {
|
||||||
await fyll(falt, "84 320 km");
|
await fyll(falt, "84 320 km");
|
||||||
await klicka(btn(/Registrera|Spara/i, pre));
|
await klicka(btn(/Record|Save/i, pre));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await finns(btn(/Stämmer — verifierad/i, pre))) {
|
if (await finns(btn(/Correct — verified/i, pre))) {
|
||||||
await klicka(btn(/Stämmer — verifierad/i, pre));
|
await klicka(btn(/Correct — verified/i, pre));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await finns(btn(/Inga ytterligare observationer/i, pre))) {
|
if (await finns(btn(/No further observations/i, pre))) {
|
||||||
await klicka(btn(/Inga ytterligare observationer/i, pre));
|
await klicka(btn(/No further observations/i, pre));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const guide = panel(/^Metodik:/i);
|
const guide = panel(/^Methodology:/i);
|
||||||
if (await finns(guide)) {
|
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"]'));
|
await foto(guide.locator('input[type="file"]'));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const verifiera = btn(/Markera verifierad/i, guide);
|
const verifiera = btn(/Mark as verified/i, guide);
|
||||||
if (await finns(verifiera)) {
|
if (await finns(verifiera)) {
|
||||||
const falt = guide.locator('input[type="text"], textarea').first();
|
const falt = guide.locator('input[type="text"], textarea').first();
|
||||||
if (await finns(falt)) await fyll(falt, fall.matvarde);
|
if (await finns(falt)) await fyll(falt, fall.matvarde);
|
||||||
@@ -245,86 +245,86 @@ async function kor(sida, fall) {
|
|||||||
continue;
|
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();
|
const falt = guide.locator('input[type="text"], textarea').first();
|
||||||
if (await finns(falt)) await fyll(falt, `Enligt kundens uppgift: ${fall.fel}`);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
// Säkerhetsfrågor besvaras jakande; spärren vid Nej provas i
|
// Säkerhetsfrågor besvaras jakande; spärren vid Nej provas i
|
||||||
// enhetstesterna, inte här.
|
// enhetstesterna, inte här.
|
||||||
if (await finns(btn(/^Ja$/, guide))) {
|
if (await finns(btn(/^Yes$/, guide))) {
|
||||||
await klicka(btn(/^Ja$/, guide));
|
await klicka(btn(/^Yes$/, guide));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const val = guide.locator("button").filter({ hasNot: sida.locator("svg") });
|
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());
|
await klicka(val.first());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const repro = panel(/reproducering/i);
|
const repro = panel(/reproduction/i);
|
||||||
if (await finns(repro)) {
|
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 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fo = panel(/Felorsaksanalys/i);
|
const fo = panel(/Root cause analysis/i);
|
||||||
if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Dokumentera felorsak/i, fo)))) {
|
if (!felorsakGjord && (await finns(fo)) && (await finns(btn(/Document root cause/i, fo)))) {
|
||||||
await klicka(btn(/Dokumentera felorsak/i, fo));
|
await klicka(btn(/Document root cause/i, fo));
|
||||||
await fyll(fo.locator("textarea").first(), fall.avvikelse);
|
await fyll(fo.locator("textarea").first(), fall.avvikelse);
|
||||||
await klicka(btn(/^Normalt slitage$/, fo));
|
await klicka(btn(/^Normal wear$/, fo));
|
||||||
await klicka(btn(/^Mätresultat$/, fo));
|
await klicka(btn(/^Measurement result$/, fo));
|
||||||
await fyll(fo.locator('input[type="text"], textarea').last(), fall.atgard);
|
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
|
// En metodik utan mätkontroller har inga mätresultat att luta sig
|
||||||
// mot; systemet nekar då källan och teknikern byter.
|
// mot; systemet nekar då källan och teknikern byter.
|
||||||
if (await finns(btn(/Spara felorsak/i, fo))) {
|
if (await finns(btn(/Save root cause/i, fo))) {
|
||||||
await klicka(btn(/^Mätresultat$/, fo));
|
await klicka(btn(/^Measurement result$/, fo));
|
||||||
await klicka(btn(/^Direkt observation$/, fo));
|
await klicka(btn(/^Direct observation$/, fo));
|
||||||
await klicka(btn(/Spara felorsak/i, fo));
|
await klicka(btn(/Save root cause/i, fo));
|
||||||
}
|
}
|
||||||
felorsakGjord = true;
|
felorsakGjord = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const at = panel(/Åtgärd och kvalitetskontroll/i);
|
const at = panel(/Action and quality check/i);
|
||||||
if (await finns(at)) {
|
if (await finns(at)) {
|
||||||
if (await finns(btn(/Lämna åtgärdsförslag till kund/i, at))) {
|
if (await finns(btn(/Give the customer a proposed action/i, at))) {
|
||||||
await klicka(btn(/Lämna åtgärdsförslag till kund/i, at));
|
await klicka(btn(/Give the customer a proposed action/i, at));
|
||||||
await klicka(btn(/^Lämna förslag$/i, at));
|
await klicka(btn(/^Give the proposal$/i, at));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await finns(btn(/^Godkänt$/i, at))) {
|
if (await finns(btn(/^Approved$/i, at))) {
|
||||||
await klicka(btn(/^Godkänt$/i, at));
|
await klicka(btn(/^Approved$/i, at));
|
||||||
await klicka(btn(/^Telefon$/i, at));
|
await klicka(btn(/^Telephone$/i, at));
|
||||||
await klicka(btn(/Registrera besked/i, at));
|
await klicka(btn(/Record the decision/i, at));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (await finns(btn(/Dokumentera utförd åtgärd/i, at))) {
|
if (await finns(btn(/Document the action performed/i, at))) {
|
||||||
await klicka(btn(/Dokumentera utförd åtgärd/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 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;
|
continue;
|
||||||
}
|
}
|
||||||
if (await finns(btn(/Symptomet är borta/i, at))) {
|
if (await finns(btn(/Symptom is gone/i, at))) {
|
||||||
await klicka(btn(/Symptomet är borta/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 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mat = panel(/Utgående mätarställning/i);
|
const mat = panel(/Outgoing odometer reading/i);
|
||||||
if (await finns(mat)) {
|
if (await finns(mat)) {
|
||||||
await foto(mat.locator('input[type="file"]'));
|
await foto(mat.locator('input[type="file"]'));
|
||||||
const falt = mat.locator('input:not([type="file"])');
|
const falt = mat.locator('input:not([type="file"])');
|
||||||
if (await finns(falt)) {
|
if (await finns(falt)) {
|
||||||
await fyll(falt, "84 520 km");
|
await fyll(falt, "84 520 km");
|
||||||
await klicka(btn(/Registrera|Spara/i, mat));
|
await klicka(btn(/Record|Save/i, mat));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,7 @@ async function kor(sida, fall) {
|
|||||||
const f = sida.locator(`#slutsats-${id}`);
|
const f = sida.locator(`#slutsats-${id}`);
|
||||||
if (await finns(f)) await fyll(f, txt);
|
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())) {
|
if ((await finns(reg)) && !(await reg.first().isDisabled())) {
|
||||||
await klicka(reg);
|
await klicka(reg);
|
||||||
slutsatsfalt = synliga;
|
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())) {
|
if ((await finns(avsluta)) && !(await avsluta.last().isDisabled())) {
|
||||||
await klicka(avsluta.last());
|
await klicka(avsluta.last());
|
||||||
continue;
|
continue;
|
||||||
@@ -374,7 +374,7 @@ async function kor(sida, fall) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
metodik,
|
metodik,
|
||||||
avslutat: slut.includes("FELSÖKNINGEN ÄR AVSLUTAD"),
|
avslutat: slut.includes("THE DIAGNOSIS IS CLOSED"),
|
||||||
interaktioner,
|
interaktioner,
|
||||||
handelser: handelser.length,
|
handelser: handelser.length,
|
||||||
avvisade: handelser.map((h) => [h?.typ, granskaHändelse(h)]).filter(([, fel]) => fel !== null),
|
avvisade: handelser.map((h) => [h?.typ, granskaHändelse(h)]).filter(([, fel]) => fel !== null),
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ function BeslutsKnappar({
|
|||||||
return (
|
return (
|
||||||
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
|
<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]">
|
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||||
Ditt besked till verkstaden
|
Your decision to the workshop
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant={val === "godkant" ? "primar" : "sekundar"} onClick={() => setVal("godkant")}>
|
<StorKnapp variant={val === "godkant" ? "primar" : "sekundar"} onClick={() => setVal("godkant")}>
|
||||||
Godkänn åtgärden
|
Approve the action
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant={val === "avbojt" ? "fara" : "sekundar"} onClick={() => setVal("avbojt")}>
|
<StorKnapp variant={val === "avbojt" ? "fara" : "sekundar"} onClick={() => setVal("avbojt")}>
|
||||||
Avböj
|
Decline
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
{val && (
|
{val && (
|
||||||
@@ -67,7 +67,7 @@ function BeslutsKnappar({
|
|||||||
{skickar ? "Sending …" : "Send decision"}
|
{skickar ? "Sending …" : "Send decision"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -226,7 +226,7 @@ export function DelatArendeVy({
|
|||||||
<BeslutsKnappar vidBeslut={vidBeslut} />
|
<BeslutsKnappar vidBeslut={vidBeslut} />
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -312,7 +312,7 @@ export function DelatArendeVy({
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
<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>
|
</p>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
|||||||
</button>
|
</button>
|
||||||
{lyssnar && (
|
{lyssnar && (
|
||||||
<span className="text-[12px] font-semibold text-[#8B1A1A]">
|
<span className="text-[12px] font-semibold text-[#8B1A1A]">
|
||||||
● Inspelning pågår{interim ? ` — ”${interim}”` : "…"}
|
● Recording{interim ? ` — ”${interim}”` : "…"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const FALT = [
|
|||||||
id: "kvarstaende" as const,
|
id: "kvarstaende" as const,
|
||||||
etikett: "Remaining uncertainty",
|
etikett: "Remaining uncertainty",
|
||||||
fraga: "What remains uncertain?",
|
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,
|
rader: 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -114,8 +114,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
|
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<p className="mb-4 max-w-[62ch] text-[13px] leading-[20px]" style={{ color: FARG.steel }}>
|
<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
|
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.
|
||||||
ena följer av det andra — och den enda en garantihandläggare eller försäkringsbedömare behöver granska.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Den ärliga vägen. Att orsaken inte kunnat fastställas är ett
|
{/* 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>
|
</ul>
|
||||||
<p className="mt-2 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
<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>
|
</p>
|
||||||
</div>
|
</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", () => {
|
it("gallringstiden följer ärendetypen, och okänd typ får standardtiden", () => {
|
||||||
const avslut = "2026-01-15T10:00:00.000Z";
|
const avslut = "2026-01-15T10:00:00.000Z";
|
||||||
const garanti = gallringsdatum("Garanti", avslut);
|
expect(gallringsdatum("Warranty", avslut).getFullYear()).toBe(2036);
|
||||||
const okand = gallringsdatum("Hittepå", avslut);
|
expect(gallringsdatum("Hittepå", avslut).getFullYear()).toBe(2029);
|
||||||
expect(garanti.getFullYear()).toBe(2036);
|
// Complaint följer konsumentköplagens reklamationsrätt.
|
||||||
expect(okand.getFullYear()).toBe(2029);
|
expect(GALLRING_MANADER.Complaint).toBe(36);
|
||||||
// Reklamation följer konsumentköplagens reklamationsrätt.
|
});
|
||||||
expect(GALLRING_MANADER.Reklamation).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,
|
stegId: steg.id,
|
||||||
kontrollId: kontroll.id,
|
kontrollId: kontroll.id,
|
||||||
text: kontroll.text,
|
text: kontroll.text,
|
||||||
undantag: "Kunden avböjde demontering",
|
undantag: "The customer declined disassembly",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,17 +160,17 @@ describe("Completion Engine (kvalitetsgrind)", () => {
|
|||||||
describe("Compliance Engine", () => {
|
describe("Compliance Engine", () => {
|
||||||
it("ärendetypen styr dokumentationskraven", () => {
|
it("ärendetypen styr dokumentationskraven", () => {
|
||||||
const privat = byggArende([OBJEKT]);
|
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);
|
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);
|
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_claim")?.ok).toBe(true); // claim finns i objektet
|
||||||
expect(rader.find((r) => r.id === "garanti_historik")?.ok).toBe(false); // historik ej kontrollerad
|
expect(rader.find((r) => r.id === "garanti_historik")?.ok).toBe(false); // historik ej kontrollerad
|
||||||
});
|
});
|
||||||
|
|
||||||
it("försäkringsärenden kräver skadenummer och bildbevis", () => {
|
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);
|
const rader = kvalitetsgrind(utanBild, VIBRATION_METODIK);
|
||||||
expect(rader.find((r) => r.id === "forsakring_skadenummer")?.ok).toBe(true);
|
expect(rader.find((r) => r.id === "forsakring_skadenummer")?.ok).toBe(true);
|
||||||
expect(rader.find((r) => r.id === "forsakring_bildbevis")?.ok).toBe(false);
|
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 arende = byggArende([OBJEKT, { typ: "foto", beskrivning: "x", dataUrl: "data:" }]);
|
||||||
const paket = sparbarhetspaket(arende, VIBRATION_METODIK);
|
const paket = sparbarhetspaket(arende, VIBRATION_METODIK);
|
||||||
expect(paket.ecmVersion).toBe("2.0");
|
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.evidensposter).toHaveLength(1);
|
||||||
expect(paket.kvalitetsgrind.some((r) => r.id === "objekt" && r.ok)).toBe(true);
|
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 { underlagFinns } = await import("../ecm");
|
||||||
const utanFoto = byggArende([OBJEKT, { typ: "matvarde", beskrivning: "U", varde: "12" }]);
|
const utanFoto = byggArende([OBJEKT, { typ: "matvarde", beskrivning: "U", varde: "12" }]);
|
||||||
expect(underlagFinns(utanFoto, "Foto")).toBe(false);
|
expect(underlagFinns(utanFoto, "Foto")).toBe(false);
|
||||||
expect(underlagFinns(utanFoto, "Mätresultat")).toBe(true);
|
expect(underlagFinns(utanFoto, "Measurement result")).toBe(true);
|
||||||
expect(underlagFinns(utanFoto, "Servicehistorik")).toBe(false);
|
expect(underlagFinns(utanFoto, "Service history")).toBe(false);
|
||||||
const medHistorik = byggArende([{ typ: "historik_kontrollerad", kontrollerad: true }]);
|
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", () => {
|
it("avslut kräver reproducering och felorsak — grinden blir obligatorisk vid stängning", () => {
|
||||||
@@ -241,8 +241,8 @@ describe("Felorsaksanalys (Root Cause Analysis)", () => {
|
|||||||
{
|
{
|
||||||
typ: "felorsak",
|
typ: "felorsak",
|
||||||
avvikelse: "Ingen avvikelse kunde konstateras vid undersökningen trots systematisk genomgång.",
|
avvikelse: "Ingen avvikelse kunde konstateras vid undersökningen trots systematisk genomgång.",
|
||||||
orsaker: ["Okänd orsak"],
|
orsaker: ["Unknown cause"],
|
||||||
underlag: ["Direkt observation"],
|
underlag: ["Direct observation"],
|
||||||
sakerhet: "lag",
|
sakerhet: "lag",
|
||||||
atgard: "Återkom vid kall motor så att symptomet kan reproduceras.",
|
atgard: "Återkom vid kall motor så att symptomet kan reproduceras.",
|
||||||
motivering: "Symptomet kunde inte reproduceras under rådande förhållanden.",
|
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);
|
const rader2 = kvalitetsgrind(stangdMed, VIBRATION_METODIK);
|
||||||
expect(rader2.find((r) => r.id === "svp")?.ok).toBe(true);
|
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);
|
expect(rader2.find((r) => r.id === "felorsak")?.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rapportformuleringen skiljer reproducerat från ej reproducerat", async () => {
|
it("rapportformuleringen skiljer reproducerat från ej reproducerat", async () => {
|
||||||
const { reproduceringsText } = await import("../ecm");
|
const { reproduceringsText } = await import("../ecm");
|
||||||
expect(reproduceringsText("ja")).toContain("reproducerades");
|
expect(reproduceringsText("ja")).toContain("was reproduced");
|
||||||
expect(reproduceringsText("nej")).toContain("kunde inte reproduceras under de förhållanden");
|
expect(reproduceringsText("nej")).toContain("could not be reproduced under the conditions");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("demoärendet bär hela beviskedjan: reproducering + felorsak med underlag", () => {
|
it("demoärendet bär hela beviskedjan: reproducering + felorsak med underlag", () => {
|
||||||
@@ -306,7 +306,7 @@ describe("ECM Knowledge Library (regelpaket)", () => {
|
|||||||
normaliseraRegelpaket({
|
normaliseraRegelpaket({
|
||||||
version: "2.1-test",
|
version: "2.1-test",
|
||||||
arendetypRegler: {
|
arendetypRegler: {
|
||||||
"Privat kund": [
|
"Private customer": [
|
||||||
{ id: "test_foto", rubrik: "Foto krävs för alla privatkunder", krav: "foto", detaljVidBrist: "Testregel." },
|
{ 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 utanFoto = byggArende([OBJEKT]);
|
||||||
const rad = kvalitetsgrind(utanFoto, VIBRATION_METODIK).find((r) => r.id === "test_foto");
|
const rad = kvalitetsgrind(utanFoto, VIBRATION_METODIK).find((r) => r.id === "test_foto");
|
||||||
expect(rad?.ok).toBe(false);
|
expect(rad?.ok).toBe(false);
|
||||||
expect(rad?.rubrik).toContain("Privat kund");
|
expect(rad?.rubrik).toContain("Private customer");
|
||||||
// Spårbarheten bär paketversionen.
|
// Spårbarheten bär paketversionen.
|
||||||
expect(sparbarhetspaket(utanFoto, VIBRATION_METODIK).regelpaketVersion).toBe("2.1-test");
|
expect(sparbarhetspaket(utanFoto, VIBRATION_METODIK).regelpaketVersion).toBe("2.1-test");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -334,7 +334,7 @@ describe("Fordonshistorik (lokal projektion)", () => {
|
|||||||
{
|
{
|
||||||
typ: "felorsak",
|
typ: "felorsak",
|
||||||
avvikelse: "Vattenpumpen läcker vid axeltätningen.",
|
avvikelse: "Vattenpumpen läcker vid axeltätningen.",
|
||||||
orsaker: ["Normalt slitage"],
|
orsaker: ["Normal wear"],
|
||||||
underlag: ["Foto"],
|
underlag: ["Foto"],
|
||||||
sakerhet: "hog",
|
sakerhet: "hog",
|
||||||
atgard: "Byt vattenpump.",
|
atgard: "Byt vattenpump.",
|
||||||
@@ -346,7 +346,7 @@ describe("Fordonshistorik (lokal projektion)", () => {
|
|||||||
const historik = lokalFordonshistorik({ [tidigare.id]: tidigare, [aktuellt.id]: aktuellt }, "abc123", aktuellt.id);
|
const historik = lokalFordonshistorik({ [tidigare.id]: tidigare, [aktuellt.id]: aktuellt }, "abc123", aktuellt.id);
|
||||||
expect(historik).toHaveLength(1);
|
expect(historik).toHaveLength(1);
|
||||||
expect(historik[0].avslutat).toBe(true);
|
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.
|
// Annat fordon → ingen träff.
|
||||||
expect(lokalFordonshistorik({ [tidigare.id]: tidigare }, "ZZZ111")).toHaveLength(0);
|
expect(lokalFordonshistorik({ [tidigare.id]: tidigare }, "ZZZ111")).toHaveLength(0);
|
||||||
});
|
});
|
||||||
@@ -398,8 +398,8 @@ describe("Åtgärdsfasen och kvalitetskontroll", () => {
|
|||||||
{
|
{
|
||||||
typ: "felorsak",
|
typ: "felorsak",
|
||||||
avvikelse: "Framhjulen uppvisar obalans om 38 g på höger sida.",
|
avvikelse: "Framhjulen uppvisar obalans om 38 g på höger sida.",
|
||||||
orsaker: ["Yttre påverkan"],
|
orsaker: ["External influence"],
|
||||||
underlag: ["Direkt observation"],
|
underlag: ["Direct observation"],
|
||||||
sakerhet: "hog",
|
sakerhet: "hog",
|
||||||
atgard: "Balansera om hjulet.",
|
atgard: "Balansera om hjulet.",
|
||||||
},
|
},
|
||||||
@@ -433,19 +433,19 @@ describe("Åtgärdsfasen och kvalitetskontroll", () => {
|
|||||||
expect(kvalitetskontroll(komplett)?.resultat).toBe("symptomet_borta");
|
expect(kvalitetskontroll(komplett)?.resultat).toBe("symptomet_borta");
|
||||||
const rader = kvalitetsgrind(komplett, VIBRATION_METODIK);
|
const rader = kvalitetsgrind(komplett, VIBRATION_METODIK);
|
||||||
expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true);
|
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", () => {
|
it("motiverat uteblivande godtas utan kvalitetskontroll", () => {
|
||||||
const ingenAtgard = byggArende([
|
const ingenAtgard = byggArende([
|
||||||
OBJEKT,
|
OBJEKT,
|
||||||
...PREDIAG,
|
...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" },
|
{ typ: "arende_avslutat", signatur: "Erik" },
|
||||||
]);
|
]);
|
||||||
const rader = kvalitetsgrind(ingenAtgard, VIBRATION_METODIK);
|
const rader = kvalitetsgrind(ingenAtgard, VIBRATION_METODIK);
|
||||||
expect(rader.find((r) => r.id === "atgard")?.ok).toBe(true);
|
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.
|
// Inget att verifiera → kvalitetskontrollen är inte obligatorisk.
|
||||||
expect(rader.find((r) => r.id === "kvalitetskontroll")?.kravs).toBe(false);
|
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." },
|
{ typ: "kvalitetskontroll", resultat: "kvarstar", beskrivning: "Missljudet finns kvar vid 60 km/h." },
|
||||||
]);
|
]);
|
||||||
const rad = kvalitetsgrind(kvarstar, VIBRATION_METODIK).find((r) => r.id === "kvalitetskontroll");
|
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", () => {
|
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");
|
const rad = kvalitetsgrind(utanBesked, VIBRATION_METODIK).find((r) => r.id === "kundgodkannande");
|
||||||
expect(rad?.kravs).toBe(true);
|
expect(rad?.kravs).toBe(true);
|
||||||
expect(rad?.ok).toBe(false);
|
expect(rad?.ok).toBe(false);
|
||||||
expect(rad?.detalj).toContain("registrera kundens besked");
|
expect(rad?.detalj).toContain("record the customer's decision");
|
||||||
|
|
||||||
const medBesked = byggArende([
|
const medBesked = byggArende([
|
||||||
OBJEKT,
|
OBJEKT,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function komplettLogg(extra: Record<string, unknown>[] = []) {
|
|||||||
{ typ: "matarstallning", lage: "ingaende", varde: "14 200", bilagaId: "bil-in" },
|
{ typ: "matarstallning", lage: "ingaende", varde: "14 200", bilagaId: "bil-in" },
|
||||||
{ typ: "matarstallning", lage: "utgaende", varde: "14 205", bilagaId: "bil-ut" },
|
{ typ: "matarstallning", lage: "utgaende", varde: "14 205", bilagaId: "bil-ut" },
|
||||||
{ typ: "reproducering", status: "ja", beskrivning: "Reproducerat vid 88 km/h." },
|
{ 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: "atgard_utford", beskrivning: "Balanserade hjulen.", utford: true },
|
||||||
{ typ: "kundbeslut", beslut: "godkant", kanal: "Telefon" },
|
{ typ: "kundbeslut", beslut: "godkant", kanal: "Telefon" },
|
||||||
{ typ: "kvalitetskontroll", resultat: "symptomet_borta", beskrivning: "Provkört, symptomet borta." },
|
{ 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", () => {
|
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", () => {
|
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([
|
expect(grindaArendetyp(utan, paket).map((h) => h.id)).toEqual([
|
||||||
"arendetyp_miltal",
|
"arendetyp_miltal",
|
||||||
"arendetyp_historik",
|
"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", () => {
|
it("en okänd kravtyp spärrar i stället för att tolkas som uppfylld", () => {
|
||||||
const trasigt = { arendetyper: { Garanti: { krav: [{ typ: "hittepa" }] } } };
|
const trasigt = { arendetyper: { Warranty: { krav: [{ typ: "hittepa" }] } } };
|
||||||
const logg = [{ typ: "arendetyp_satt", arendetyp: "Garanti" }];
|
const logg = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }];
|
||||||
expect(grindaArendetyp(logg, trasigt)[0].nyckel).toBe("grind.arendetyp.okant");
|
expect(grindaArendetyp(logg, trasigt)[0].nyckel).toBe("grind.arendetyp.okant");
|
||||||
expect(grindaArendetyp(logg, trasigt)[0].rubrik).toContain("hittepa");
|
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
|
// Bakgrund: en kontroll vars krav är "matvarde" loggade bara
|
||||||
// kontroll_utford. Följden var att 56 av metodikernas 153 kontroller —
|
// kontroll_utford. Följden var att 56 av metodikernas 153 kontroller —
|
||||||
// drygt en tredjedel — mätte utan att det syntes någonstans:
|
// 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
|
// teknikern just hade mätt, och evidensprofilen i analysvyn underskattade
|
||||||
// systematiskt hur mycket som faktiskt mäts i verkstaden.
|
// 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/);
|
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 { underlagFinns } = await import("../ecm");
|
||||||
const arende = {
|
const arende = {
|
||||||
handelser: [{ id: "1", tidpunkt: "", anvandare: "A", handelse: { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4 bar" } }],
|
handelser: [{ id: "1", tidpunkt: "", anvandare: "A", handelse: { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4 bar" } }],
|
||||||
} as never;
|
} 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",
|
typ: "felorsak",
|
||||||
avvikelse: "Höger framhjul har 38 g obalans mot gränsen 10 g.",
|
avvikelse: "Höger framhjul har 38 g obalans mot gränsen 10 g.",
|
||||||
orsaker: ["Normalt slitage"],
|
orsaker: ["Normal wear"],
|
||||||
underlag: ["Mätresultat"],
|
underlag: ["Measurement result"],
|
||||||
sakerhet: "hog",
|
sakerhet: "hog",
|
||||||
atgard: "Balansera om höger framhjul.",
|
atgard: "Balansera om höger framhjul.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import type { Arende, LoggPost } from "./domain";
|
import type { Arende, LoggPost } from "./domain";
|
||||||
import type { Metodik } from "./metodik";
|
import type { Metodik } from "./metodik";
|
||||||
|
import { arendetypNu } from "../../../services/gemensam/sprak/index.mjs";
|
||||||
|
|
||||||
export const ECM_VERSION = "2.0";
|
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 type EvidensNiva = "E0" | "E1" | "E2" | "E3" | "E4" | "E5" | "E6";
|
||||||
|
|
||||||
export const EVIDENS_LABEL: Record<EvidensNiva, string> = {
|
export const EVIDENS_LABEL: Record<EvidensNiva, string> = {
|
||||||
E0: "E0 · Inget underlag",
|
E0: "E0 · No evidence",
|
||||||
E1: "E1 · Teknikerns observation",
|
E1: "E1 · Technician's observation",
|
||||||
E2: "E2 · Foto",
|
E2: "E2 · Photograph",
|
||||||
E3: "E3 · Video",
|
E3: "E3 · Video",
|
||||||
E4: "E4 · Mätvärde",
|
E4: "E4 · Measurement",
|
||||||
E5: "E5 · Diagnosdata/dokument",
|
E5: "E5 · Diagnostic data or document",
|
||||||
E6: "E6 · Flera oberoende källor",
|
E6: "E6 · Several independent sources",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Evidenspost {
|
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
|
// Godkända orsaker när underlag inte kan tas fram (fri text tillåts
|
||||||
// också — men en orsak är alltid obligatorisk).
|
// också — men en orsak är alltid obligatorisk).
|
||||||
export const UNDANTAGSORSAKER = [
|
export const UNDANTAGSORSAKER = [
|
||||||
"Komponenten är oåtkomlig",
|
"The component is inaccessible",
|
||||||
"Fordonet kan inte lyftas säkert",
|
"The vehicle cannot be lifted safely",
|
||||||
"Kunden avböjde demontering",
|
"The customer declined disassembly",
|
||||||
"Dålig sikt/åtkomst",
|
"Poor visibility or access",
|
||||||
"Utrustning saknas",
|
"Equipment not available",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Markörtexter för pre-diagnostikens kvitteringar (loggas som kommentar).
|
// Markörtexter för pre-diagnostikens kvitteringar (loggas som kommentar).
|
||||||
export const MARKOR_FELBESKRIVNING_VERIFIERAD = "Kundens felbeskrivning verifierad vid mottagandet";
|
export const MARKOR_FELBESKRIVNING_VERIFIERAD = "Customer's fault description verified on receipt";
|
||||||
export const MARKOR_INGA_TIDIGA_OBSERVATIONER = "Inga ytterligare observationer vid mottagandet";
|
export const MARKOR_INGA_TIDIGA_OBSERVATIONER = "No further observations on receipt";
|
||||||
export const MARKOR_TIDIGA_OBSERVATIONER_KLARA = "Tidiga observationer vid mottagandet dokumenterade";
|
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) --
|
// -- Felorsaksanalys (Root Cause Analysis) --
|
||||||
// Ett ärende avslutas aldrig med enbart "komponent trasig, byt komponent":
|
// 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.
|
// säkerhetsnivå — eller en motivering till varför orsaken inte fastställts.
|
||||||
|
|
||||||
export const ORSAKSKATEGORIER = [
|
export const ORSAKSKATEGORIER = [
|
||||||
"Normalt slitage",
|
"Normal wear",
|
||||||
"Ålder",
|
"Age",
|
||||||
"Körsträcka",
|
"Distance travelled",
|
||||||
"Materialutmattning",
|
"Material fatigue",
|
||||||
"Tillverkningsfel",
|
"Manufacturing defect",
|
||||||
"Bristande underhåll",
|
"Inadequate maintenance",
|
||||||
"Felaktig tidigare reparation",
|
"Faulty previous repair",
|
||||||
"Yttre påverkan",
|
"External influence",
|
||||||
"Korrosion",
|
"Corrosion",
|
||||||
"Överhettning",
|
"Overheating",
|
||||||
"Förorening",
|
"Contamination",
|
||||||
"Felaktig användning",
|
"Improper use",
|
||||||
"Modifiering",
|
"Modification",
|
||||||
"Olycka eller skada",
|
"Accident or damage",
|
||||||
"Okänd orsak",
|
"Unknown cause",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const UNDERLAGSKALLOR = [
|
export const UNDERLAGSKALLOR = [
|
||||||
"Foto",
|
"Foto",
|
||||||
"Video",
|
"Video",
|
||||||
"Mätresultat",
|
"Measurement result",
|
||||||
"Diagnosutläsning",
|
"Diagnostic readout",
|
||||||
"Tidigare historik",
|
"Previous history",
|
||||||
"Servicehistorik",
|
"Service history",
|
||||||
"Teknisk dokumentation",
|
"Technical documentation",
|
||||||
"Direkt observation",
|
"Direct observation",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Kvalitetsregeln: generella formuleringar utan förklaring avvisas.
|
// 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");
|
return h.some((x) => x.typ === "foto");
|
||||||
case "Video":
|
case "Video":
|
||||||
return h.some((x) => x.typ === "video");
|
return h.some((x) => x.typ === "video");
|
||||||
case "Mätresultat":
|
case "Measurement result":
|
||||||
return h.some((x) => x.typ === "matvarde");
|
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));
|
return h.some((x) => x.typ === "foto" && /instrument|diagnos/i.test(x.beskrivning));
|
||||||
case "Tidigare historik":
|
case "Previous history":
|
||||||
case "Servicehistorik":
|
case "Service history":
|
||||||
return h.some((x) => x.typ === "historik_kontrollerad" && x.kontrollerad);
|
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");
|
return h.some((x) => x.typ === "observation" || x.typ === "kontroll_utford");
|
||||||
default:
|
default:
|
||||||
// Teknisk dokumentation: kan ännu inte verifieras maskinellt.
|
// Teknisk dokumentation: kan ännu inte verifieras maskinellt.
|
||||||
@@ -256,16 +278,16 @@ export function felorsaker(arende: Arende) {
|
|||||||
// kontrollen visar om symptomet faktiskt är borta.
|
// kontrollen visar om symptomet faktiskt är borta.
|
||||||
|
|
||||||
export const INGEN_ATGARD_ORSAKER = [
|
export const INGEN_ATGARD_ORSAKER = [
|
||||||
"Kunden avböjde åtgärd",
|
"The customer declined the action",
|
||||||
"Väntar på reservdel",
|
"Awaiting a spare part",
|
||||||
"Åtgärd utförs av annan verkstad",
|
"The action is performed by another workshop",
|
||||||
"Endast utredning beställd",
|
"Investigation only was ordered",
|
||||||
"Kostnadsförslag lämnat, inväntar besked",
|
"Estimate given, awaiting a decision",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Kanaler för kundens besked — beskedet ska alltid gå att härleda till
|
// Kanaler för kundens besked — beskedet ska alltid gå att härleda till
|
||||||
// ett faktiskt samtal, besök eller meddelande.
|
// 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) {
|
export function atgardsforslag(arende: Arende) {
|
||||||
return arende.handelser.filter((p) => p.handelse.typ === "atgardsforslag");
|
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
|
// Rapportformuleringen styrs av verifieringsläget — aldrig "felet
|
||||||
// konstaterat" utan reproducering eller annan dokumenterad verifiering.
|
// konstaterat" utan reproducering eller annan dokumenterad verifiering.
|
||||||
export function reproduceringsText(status: "ja" | "delvis" | "nej"): string {
|
export function reproduceringsText(status: "ja" | "delvis" | "nej"): string {
|
||||||
if (status === "ja") return "Symptomet reproducerades vid undersökningen.";
|
if (status === "ja") return "The symptom was reproduced during the examination.";
|
||||||
if (status === "delvis") return "Symptomet kunde delvis reproduceras vid undersökningen.";
|
if (status === "delvis") return "The symptom could be partly reproduced during the examination.";
|
||||||
return "Kundens beskrivning kunde inte reproduceras under de förhållanden som rådde vid undersökningen.";
|
return "The customer's description could not be reproduced under the conditions present during the examination.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 3. Compliance Engine ---------------------------------------------
|
// ---- 3. Compliance Engine ---------------------------------------------
|
||||||
|
|
||||||
// Ärendetypen styr vilka dokumentationskrav som gäller utöver metodiken.
|
// Ärendetypen styr vilka dokumentationskrav som gäller utöver metodiken.
|
||||||
export const ARENDETYPER = [
|
export const ARENDETYPER = [
|
||||||
"Privat kund",
|
"Private customer",
|
||||||
"Företagskund",
|
"Business customer",
|
||||||
"Garanti",
|
"Warranty",
|
||||||
"Goodwill",
|
"Goodwill",
|
||||||
"Försäkring",
|
"Insurance",
|
||||||
"Reklamation",
|
"Complaint",
|
||||||
"Begagnatgaranti",
|
"Used-vehicle warranty",
|
||||||
"Intern kvalitetskontroll",
|
"Internal quality check",
|
||||||
"Teknisk utredning",
|
"Technical investigation",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Arendetyp = (typeof ARENDETYPER)[number];
|
export type Arendetyp = (typeof ARENDETYPER)[number];
|
||||||
|
|
||||||
export function arendetyp(arende: Arende): Arendetyp {
|
export function arendetyp(arende: Arende): Arendetyp {
|
||||||
let typ: Arendetyp = "Privat kund";
|
let typ: Arendetyp = "Private customer";
|
||||||
for (const post of arende.handelser) {
|
for (const post of arende.handelser) {
|
||||||
const h = post.handelse;
|
const h = post.handelse;
|
||||||
if (h.typ === "arendetyp_satt" && (ARENDETYPER as readonly string[]).includes(h.arendetyp)) {
|
if (h.typ !== "arendetyp_satt") continue;
|
||||||
typ = h.arendetyp as Arendetyp;
|
// 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;
|
return typ;
|
||||||
}
|
}
|
||||||
@@ -388,24 +412,24 @@ const KRAV_KONTROLLER: Record<ComplianceKravTyp, (arende: Arende) => boolean> =
|
|||||||
export const STANDARD_REGELPAKET: RegelPaket = {
|
export const STANDARD_REGELPAKET: RegelPaket = {
|
||||||
version: ECM_VERSION,
|
version: ECM_VERSION,
|
||||||
arendetypRegler: {
|
arendetypRegler: {
|
||||||
Garanti: [
|
"Warranty": [
|
||||||
{ id: "garanti_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Garantiärenden kräver dokumenterad mätarställning." },
|
{ id: "garanti_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Warranty cases require a documented odometer reading." },
|
||||||
{ id: "garanti_historik", rubrik: "Servicehistorik kontrollerad", krav: "historik", detaljVidBrist: "Garantiärenden kräver kontrollerad servicehistorik." },
|
{ id: "garanti_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Warranty cases require a checked service history." },
|
||||||
{ id: "garanti_claim", rubrik: "Claim-/garantinummer registrerat", krav: "claim", detaljVidBrist: "Ange claim-/garantinummer (läses ur arbetsordern)." },
|
{ id: "garanti_claim", rubrik: "Claim or warranty number recorded", krav: "claim", detaljVidBrist: "State the claim or warranty number (read from the work order)." },
|
||||||
],
|
],
|
||||||
Goodwill: [
|
Goodwill: [
|
||||||
{ id: "goodwill_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Goodwillärenden kräver dokumenterad mätarställning." },
|
{ id: "goodwill_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Goodwill cases require a documented odometer reading." },
|
||||||
{ id: "goodwill_historik", rubrik: "Servicehistorik kontrollerad", krav: "historik", detaljVidBrist: "Goodwillärenden kräver kontrollerad servicehistorik." },
|
{ id: "goodwill_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Goodwill cases require a checked service history." },
|
||||||
],
|
],
|
||||||
Försäkring: [
|
"Insurance": [
|
||||||
{ id: "forsakring_skadenummer", rubrik: "Skadenummer registrerat", krav: "skadenummer", detaljVidBrist: "Försäkringsärenden kräver skadenummer (läses ur arbetsordern)." },
|
{ 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: "Bildbevis finns", krav: "foto", detaljVidBrist: "Försäkringsärenden kräver bilddokumentation." },
|
{ id: "forsakring_bildbevis", rubrik: "Photographic evidence present", krav: "foto", detaljVidBrist: "Insurance cases require photographic documentation." },
|
||||||
],
|
],
|
||||||
Reklamation: [
|
"Complaint": [
|
||||||
{ id: "reklamation_historik", rubrik: "Historik och tidigare försök kontrollerade", krav: "historik", detaljVidBrist: "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." },
|
{ id: "reklamation_historik", rubrik: "History and previous attempts checked", krav: "historik", detaljVidBrist: "Complaint cases require a checked history (previous repairs and attempts)." },
|
||||||
],
|
],
|
||||||
Begagnatgaranti: [
|
"Used-vehicle warranty": [
|
||||||
{ id: "begagnat_miltal", rubrik: "Miltal dokumenterat", krav: "miltal", detaljVidBrist: "Begagnatgaranti kräver dokumenterad mätarställning." },
|
{ id: "begagnat_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Used-vehicle warranty cases require a documented odometer reading." },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
undantagsorsaker: UNDANTAGSORSAKER,
|
undantagsorsaker: UNDANTAGSORSAKER,
|
||||||
@@ -494,7 +518,7 @@ export interface PreDiagRad {
|
|||||||
// Ingen felsökning påbörjas förrän grundkontrollerna är genomförda —
|
// Ingen felsökning påbörjas förrän grundkontrollerna är genomförda —
|
||||||
// eller dokumenterat motiverade. Allt härleds ur loggen.
|
// eller dokumenterat motiverade. Allt härleds ur loggen.
|
||||||
export function preDiagnostik(arende: Arende): PreDiagRad[] {
|
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 matarstallning = false;
|
||||||
let felbeskrivningVerifierad = false;
|
let felbeskrivningVerifierad = false;
|
||||||
let tidiga = false;
|
let tidiga = false;
|
||||||
@@ -503,25 +527,25 @@ export function preDiagnostik(arende: Arende): PreDiagRad[] {
|
|||||||
if (h.typ === "historik_kontrollerad") {
|
if (h.typ === "historik_kontrollerad") {
|
||||||
historik = {
|
historik = {
|
||||||
id: "historik",
|
id: "historik",
|
||||||
rubrik: "Fordonshistorik kontrollerad",
|
rubrik: "Vehicle history checked",
|
||||||
klar: true,
|
klar: true,
|
||||||
varning: h.kontrollerad ? undefined : `Ej kontrollerad — orsak: ${h.kommentar ?? "saknas"}`,
|
varning: h.kontrollerad ? undefined : `Ej kontrollerad — orsak: ${h.kommentar ?? "saknas"}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (h.typ === "matarstallning" && h.lage === "ingaende") matarstallning = true;
|
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 (
|
if (
|
||||||
h.typ === "kommentar" &&
|
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;
|
tidiga = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
historik,
|
historik,
|
||||||
{ id: "matarstallning_in", rubrik: "Ingående mätarställning dokumenterad", klar: matarstallning },
|
{ id: "matarstallning_in", rubrik: "Incoming odometer reading documented", klar: matarstallning },
|
||||||
{ id: "felbeskrivning", rubrik: "Kundens felbeskrivning verifierad", klar: felbeskrivningVerifierad },
|
{ id: "felbeskrivning", rubrik: "Customer's fault description verified", klar: felbeskrivningVerifierad },
|
||||||
{ id: "tidiga_observationer", rubrik: "Tidiga observationer hanterade", klar: tidiga },
|
{ 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");
|
const objektFinns = handelser.some((h) => h.typ === "objekt_identifierat");
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "objekt",
|
id: "objekt",
|
||||||
rubrik: "Fordons-/objektidentifiering verifierad",
|
rubrik: "Vehicle or object identification verified",
|
||||||
ok: objektFinns,
|
ok: objektFinns,
|
||||||
kravs: true,
|
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");
|
const arbetsorder = handelser.some((h) => h.typ === "arbetsorder_skannad") || !!objektFalt(arende, "arbetsorder");
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "arbetsorder",
|
id: "arbetsorder",
|
||||||
rubrik: "Arbetsorder inläst",
|
rubrik: "Work order read",
|
||||||
ok: arbetsorder,
|
ok: arbetsorder,
|
||||||
kravs: false,
|
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
|
// 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")!;
|
const historik = pre.find((r) => r.id === "historik")!;
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "historik",
|
id: "historik",
|
||||||
rubrik: "Fordonshistorik kontrollerad eller motiverad",
|
rubrik: "Vehicle history checked or justified",
|
||||||
ok: historik.klar,
|
ok: historik.klar,
|
||||||
kravs: true,
|
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")!;
|
const matIn = pre.find((r) => r.id === "matarstallning_in")!;
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "matarstallning_in",
|
id: "matarstallning_in",
|
||||||
rubrik: "Ingående mätarställning dokumenterad",
|
rubrik: "Incoming odometer reading documented",
|
||||||
ok: matIn.klar,
|
ok: matIn.klar,
|
||||||
kravs: true,
|
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")!;
|
const felb = pre.find((r) => r.id === "felbeskrivning")!;
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "felbeskrivning_verifierad",
|
id: "felbeskrivning_verifierad",
|
||||||
rubrik: "Kundens felbeskrivning verifierad",
|
rubrik: "Customer's fault description verified",
|
||||||
ok: felb.klar,
|
ok: felb.klar,
|
||||||
kravs: false,
|
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 —
|
// SVP: symptomet reproducerat eller dokumenterat ej reproducerbart —
|
||||||
@@ -599,12 +623,12 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
|||||||
const repro = reproducering(arende);
|
const repro = reproducering(arende);
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "svp",
|
id: "svp",
|
||||||
rubrik: "Symptomverifiering: reproducerat eller dokumenterat ej reproducerbart",
|
rubrik: "Symptom verification: reproduced, or documented as not reproducible",
|
||||||
ok: !!repro,
|
ok: !!repro,
|
||||||
kravs: avslutat,
|
kravs: avslutat,
|
||||||
detalj: repro
|
detalj: repro
|
||||||
? reproduceringsText(repro.status)
|
? 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
|
// Felorsaksanalys: minst en dokumenterad felorsak (eller motiverad
|
||||||
@@ -612,13 +636,13 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
|||||||
const orsaker = felorsaker(arende);
|
const orsaker = felorsaker(arende);
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "felorsak",
|
id: "felorsak",
|
||||||
rubrik: "Felorsaksanalys dokumenterad",
|
rubrik: "Root cause analysis documented",
|
||||||
ok: orsaker.length > 0,
|
ok: orsaker.length > 0,
|
||||||
kravs: avslutat,
|
kravs: avslutat,
|
||||||
detalj:
|
detalj:
|
||||||
orsaker.length > 0
|
orsaker.length > 0
|
||||||
? undefined
|
? 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
|
// Å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);
|
const utfordAtgard = atgardsposter.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford);
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "atgard",
|
id: "atgard",
|
||||||
rubrik: "Åtgärd dokumenterad eller motiverad",
|
rubrik: "Corrective action documented or justified",
|
||||||
ok: atgardsposter.length > 0,
|
ok: atgardsposter.length > 0,
|
||||||
kravs: avslutat,
|
kravs: avslutat,
|
||||||
detalj:
|
detalj:
|
||||||
atgardsposter.length === 0
|
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
|
: utfordAtgard
|
||||||
? undefined
|
? 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
|
// 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) {
|
if (forslag.length > 0 || beslut) {
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "kundgodkannande",
|
id: "kundgodkannande",
|
||||||
rubrik: "Kundens besked på åtgärdsförslaget registrerat",
|
rubrik: "Customer decision on the proposed action recorded",
|
||||||
ok: !!beslut,
|
ok: !!beslut,
|
||||||
kravs: utfordAtgard,
|
kravs: utfordAtgard,
|
||||||
detalj: beslut
|
detalj: beslut
|
||||||
? `${beslut.beslut === "godkant" ? "Godkänt" : beslut.beslut === "avbojt" ? "Avböjt" : "Delvis godkänt"} via ${beslut.kanal}.`
|
? `${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) {
|
if (beslut?.beslut === "avbojt" && utfordAtgard) {
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "godkannande_konflikt",
|
id: "godkannande_konflikt",
|
||||||
rubrik: "Utfört arbete trots avböjt åtgärdsförslag",
|
rubrik: "Work performed despite a declined proposal",
|
||||||
ok: false,
|
ok: false,
|
||||||
kravs: true,
|
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);
|
const kk = kvalitetskontroll(arende);
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "kvalitetskontroll",
|
id: "kvalitetskontroll",
|
||||||
rubrik: "Kvalitetskontroll genomförd — symptomet verifierat",
|
rubrik: "Quality check performed — symptom verified",
|
||||||
ok: !!kk,
|
ok: !!kk,
|
||||||
kravs: avslutat && utfordAtgard,
|
kravs: avslutat && utfordAtgard,
|
||||||
detalj: kk
|
detalj: kk
|
||||||
? kk.resultat === "symptomet_borta"
|
? kk.resultat === "symptomet_borta"
|
||||||
? "Symptomet är verifierat borta efter åtgärd."
|
? "The symptom is verified gone after the action."
|
||||||
: kk.resultat === "ej_verifierbar"
|
: kk.resultat === "ej_verifierbar"
|
||||||
? `Kunde inte verifieras: ${kk.beskrivning}`
|
? `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
|
: utfordAtgard
|
||||||
? "Verifiera att symptomet är borta efter åtgärden (provkörning/återtest)."
|
? "Verify that the symptom is gone after the action (road test or retest)."
|
||||||
: "Ingen åtgärd utförd — inget att verifiera.",
|
: "No action performed — nothing to verify.",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Utgående mätarställning: obligatorisk först när ärendet avslutas.
|
// Utgående mätarställning: obligatorisk först när ärendet avslutas.
|
||||||
const matUt = handelser.some((h) => h.typ === "matarstallning" && h.lage === "utgaende");
|
const matUt = handelser.some((h) => h.typ === "matarstallning" && h.lage === "utgaende");
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "matarstallning_ut",
|
id: "matarstallning_ut",
|
||||||
rubrik: "Utgående mätarställning dokumenterad",
|
rubrik: "Outgoing odometer reading documented",
|
||||||
ok: matUt,
|
ok: matUt,
|
||||||
kravs: avslutat,
|
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.
|
// Metodikens kontroller: evidens eller dokumenterat undantag per kontroll.
|
||||||
@@ -711,7 +735,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
|||||||
}
|
}
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "kontroller",
|
id: "kontroller",
|
||||||
rubrik: "Metodikens kontroller: evidens eller dokumenterat undantag",
|
rubrik: "Methodology checks: evidence, or a documented exemption",
|
||||||
ok: saknade.length === 0,
|
ok: saknade.length === 0,
|
||||||
kravs: true,
|
kravs: true,
|
||||||
detalj:
|
detalj:
|
||||||
@@ -724,7 +748,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
|||||||
|
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "fotokrav",
|
id: "fotokrav",
|
||||||
rubrik: "Foton finns för fotokrävande kontroller",
|
rubrik: "Photographs present for checks that require them",
|
||||||
ok: fotoKravUtanFoto === 0,
|
ok: fotoKravUtanFoto === 0,
|
||||||
kravs: true,
|
kravs: true,
|
||||||
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined,
|
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");
|
const avslutEvent = handelser.find((h) => h.typ === "arende_avslutat");
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "signering",
|
id: "signering",
|
||||||
rubrik: "Teknikerns slutsats signerad",
|
rubrik: "The technician's closing statement is signed",
|
||||||
ok: !avslutat || (avslutEvent?.typ === "arende_avslutat" && !!avslutEvent.signatur),
|
ok: !avslutat || (avslutEvent?.typ === "arende_avslutat" && !!avslutEvent.signatur),
|
||||||
kravs: false,
|
kravs: false,
|
||||||
detalj:
|
detalj:
|
||||||
avslutat && avslutEvent?.typ === "arende_avslutat" && avslutEvent.signatur
|
avslutat && avslutEvent?.typ === "arende_avslutat" && avslutEvent.signatur
|
||||||
? `Signerad av ${avslutEvent.signatur}.`
|
? `Signerad av ${avslutEvent.signatur}.`
|
||||||
: avslutat
|
: avslutat
|
||||||
? "Avslutet saknar signatur (äldre ärende)."
|
? "The closing has no signature (older case)."
|
||||||
: "Signeras automatiskt när ärendet avslutas.",
|
: "Signed automatically when the case is closed.",
|
||||||
});
|
});
|
||||||
|
|
||||||
const hypoteser = handelser.filter((h) => h.typ === "hypotes").length;
|
const hypoteser = handelser.filter((h) => h.typ === "hypotes").length;
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "hypoteser",
|
id: "hypoteser",
|
||||||
rubrik: "Hypoteser redovisas som ej verifierade",
|
rubrik: "Hypotheses are reported as unverified",
|
||||||
ok: true,
|
ok: true,
|
||||||
kravs: false,
|
kravs: false,
|
||||||
detalj: hypoteser > 0 ? `${hypoteser} hypotes(er) markeras som ej verifierade i rapporten — aldrig som konstaterade fel.` : undefined,
|
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)]}`,
|
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
|
||||||
ok: evidensNiva(arende) !== "E0",
|
ok: evidensNiva(arende) !== "E0",
|
||||||
kravs: true,
|
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;
|
return rader;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import type { Arende } from "./domain";
|
import type { Arende } from "./domain";
|
||||||
import { ALLA_METODIKER, ELSYSTEM, GENERISK, VIBRATION } from "./metodiker";
|
import { ALLA_METODIKER, ELSYSTEM, GENERISK, VIBRATION } from "./metodiker";
|
||||||
|
import { arJakande } from "../../../services/gemensam/sprak/index.mjs";
|
||||||
|
|
||||||
export interface Fraga {
|
export interface Fraga {
|
||||||
id: string;
|
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
|
// 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.
|
// för att räknas som besvarat och släppa fram nästa steg.
|
||||||
const sparr = SPARRFRAGOR[`${metodik.id}/${steg.id}/${fraga.id}`];
|
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 };
|
return { steg, fraga, klart: false, sparr };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
|||||||
/>
|
/>
|
||||||
{!foto && !laser && (
|
{!foto && !laser && (
|
||||||
<StorKnapp variant="sekundar" onClick={() => filRef.current?.click()}>
|
<StorKnapp variant="sekundar" onClick={() => filRef.current?.click()}>
|
||||||
<IkonKamera /> Fotografera instrumentpanelen
|
<IkonKamera /> Photograph the instrument panel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer …</p>}
|
{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">
|
<div className="mt-2">
|
||||||
{demo && (
|
{demo && (
|
||||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
|
<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
|
<StorKnapp
|
||||||
disabled={!varde.trim()}
|
disabled={!varde.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -344,7 +344,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
|||||||
setVarde("");
|
setVarde("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spara mätarställning
|
Save odometer reading
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -393,12 +393,12 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
{rader.map((rad) => (
|
{rader.map((rad) => (
|
||||||
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
|
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
|
||||||
<p>
|
<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.avslutat ? "closed" : "in progress"}
|
||||||
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -412,13 +412,13 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
skicka({
|
skicka({
|
||||||
typ: "kommentar",
|
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}` : ""
|
rad.felorsaker[0] ? ` — ${rad.felorsaker[0].avvikelse}` : rad.felbeskrivning ? ` — ${rad.felbeskrivning}` : ""
|
||||||
}`,
|
}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
+ Koppla till orsakskedjan
|
+ Link to the causal chain
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -448,12 +448,12 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
{!rad("historik").klar && (
|
{!rad("historik").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
<TidigareArenden arende={arende} skicka={skicka} />
|
<TidigareArenden arende={arende} skicka={skicka} />
|
||||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant={historikVal === "ja" ? "primar" : "sekundar"} onClick={() => setHistorikVal("ja")}>
|
<StorKnapp variant={historikVal === "ja" ? "primar" : "sekundar"} onClick={() => setHistorikVal("ja")}>
|
||||||
Ja — kontrollerad
|
Yes — checked
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
||||||
No
|
No
|
||||||
@@ -486,7 +486,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
{!rad("matarstallning_in").klar && (
|
{!rad("matarstallning_in").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||||
</div>
|
</div>
|
||||||
@@ -495,11 +495,11 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
{!rad("felbeskrivning").klar && (
|
{!rad("felbeskrivning").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||||
Stämmer — verifierad
|
Correct — verified
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<p className="mt-1 text-[11px] text-[#707070]">
|
||||||
Ytterligare symptom dokumenteras som separata observationer — blanda dem inte med kundens beskrivning.
|
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 && (
|
{!rad("tidiga_observationer").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}>
|
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}>
|
||||||
Inga ytterligare observationer
|
No further observations
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
variant="sekundar"
|
variant="sekundar"
|
||||||
disabled={!harTidiga}
|
disabled={!harTidiga}
|
||||||
onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })}
|
onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })}
|
||||||
>
|
>
|
||||||
Observationerna är dokumenterade
|
The observations are documented
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -569,7 +569,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
|||||||
setText("");
|
setText("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Dokumentera reproducering
|
Document reproduction
|
||||||
</StorKnapp>
|
</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">
|
<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="font-semibold">{h.avvikelse}</p>
|
||||||
<p className="text-[#4A5560]">
|
<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>
|
||||||
<p>Åtgärd: {h.atgard}</p>
|
<p>Action: {h.atgard}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!oppen ? (
|
{!oppen ? (
|
||||||
<StorKnapp variant="sekundar" onClick={() => setOppen(true)}>
|
<StorKnapp variant="sekundar" onClick={() => setOppen(true)}>
|
||||||
+ Dokumentera felorsak
|
+ Document root cause
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<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>}
|
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={spara}>Save root cause</StorKnapp>
|
<StorKnapp onClick={spara}>Save root cause</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
@@ -774,7 +774,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
{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>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -820,7 +820,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
setForslagText(foreslagenAtgard);
|
setForslagText(foreslagenAtgard);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Lämna åtgärdsförslag till kund
|
Give the customer a proposed action
|
||||||
</StorKnapp>
|
</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" />
|
<TextFalt label="Estimated cost (optional)" varde={kostnad} satt={setKostnad} platshallare="e.g. 1,200 incl. VAT" />
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setForslagsLage(false)}>
|
<StorKnapp variant="sekundar" onClick={() => setForslagsLage(false)}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={forslagText.trim().length < 10}
|
disabled={forslagText.trim().length < 10}
|
||||||
@@ -852,7 +852,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
setKostnad("");
|
setKostnad("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Lämna förslag
|
Give the proposal
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -861,7 +861,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{forslag.length > 0 && !beslut && (
|
{forslag.length > 0 && !beslut && (
|
||||||
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
|
<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>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||||
{(["godkant", "avbojt", "delvis"] as const).map((val) => (
|
{(["godkant", "avbojt", "delvis"] as const).map((val) => (
|
||||||
@@ -912,7 +912,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
setBeslutsKommentar("");
|
setBeslutsKommentar("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Registrera besked
|
Record the decision
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -926,7 +926,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
disabled={forslag.length > 0 && (!beslut || beslut.beslut === "avbojt")}
|
disabled={forslag.length > 0 && (!beslut || beslut.beslut === "avbojt")}
|
||||||
onClick={() => setLage("utford")}
|
onClick={() => setLage("utford")}
|
||||||
>
|
>
|
||||||
Dokumentera utförd åtgärd
|
Document the action performed
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
||||||
No action performed
|
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 />
|
<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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={beskrivning.trim().length < 10}
|
disabled={beskrivning.trim().length < 10}
|
||||||
@@ -958,7 +958,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
setDelar("");
|
setDelar("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spara åtgärd
|
Save action
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -967,7 +967,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{lage === "ingen" && (
|
{lage === "ingen" && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
<div className="mb-2 grid gap-1">
|
<div className="mb-2 grid gap-1">
|
||||||
{INGEN_ATGARD_ORSAKER.map((val) => (
|
{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 />
|
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||||
{beslut?.beslut === "avbojt" && (
|
{beslut?.beslut === "avbojt" && (
|
||||||
<p className="mb-2 text-[11px] text-[#707070]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!orsak.trim()}
|
disabled={!orsak.trim()}
|
||||||
@@ -1009,11 +1009,11 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{utford && !kk && (
|
{utford && !kk && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
{repro?.status === "ja" && (
|
{repro?.status === "ja" && (
|
||||||
<p className="mb-2 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||||
@@ -1053,7 +1053,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
setKkText("");
|
setKkText("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spara kvalitetskontroll
|
Save quality check
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1081,7 +1081,7 @@ function VyTrad({
|
|||||||
return (
|
return (
|
||||||
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
|
<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]">
|
<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>
|
</p>
|
||||||
{FLIKAR.map((f) => (
|
{FLIKAR.map((f) => (
|
||||||
<button
|
<button
|
||||||
@@ -1177,7 +1177,7 @@ function KontextPanel({
|
|||||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
@@ -1214,7 +1214,7 @@ function InaktivitetsBanner({
|
|||||||
setText("");
|
setText("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Lägg till i arbetsloggen
|
Add to the work log
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1286,7 +1286,7 @@ function GuideFlik({
|
|||||||
return (
|
return (
|
||||||
<Panel rubrik="The diagnosis is closed">
|
<Panel rubrik="The diagnosis is closed">
|
||||||
<p className="text-[14px] text-[#333333]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
@@ -1335,16 +1335,16 @@ function GuideFlik({
|
|||||||
först vid avslutsknappen vore att neka någon som redan är klar. */}
|
först vid avslutsknappen vore att neka någon som redan är klar. */}
|
||||||
{(steg.klart || underlagKlart) && <Slutsatspanel arende={arende} skicka={skicka} />}
|
{(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 ? (
|
{steg.klart ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||||
<p className="mb-4 text-[#333333]">
|
<p className="mb-4 text-[#333333]">
|
||||||
Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet
|
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
|
||||||
med rekommenderade nästa steg.
|
with recommended next steps.
|
||||||
</p>
|
</p>
|
||||||
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||||
Avsluta felsökning
|
Close diagnosis
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||||
</>
|
</>
|
||||||
@@ -1357,7 +1357,7 @@ function GuideFlik({
|
|||||||
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
||||||
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
||||||
<p className="text-[12px] text-[#666666]">
|
<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.
|
jakande.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1422,10 +1422,10 @@ function GuideFlik({
|
|||||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||||
Lämna över arbete
|
Hand over the work
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
<StorKnapp variant="sekundar" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||||
Avsluta felsökning
|
Close diagnosis
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1448,7 +1448,7 @@ function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detal
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
|
<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]">
|
<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>
|
</p>
|
||||||
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
|
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
|
||||||
{hinder.map((h) => (
|
{hinder.map((h) => (
|
||||||
@@ -1515,7 +1515,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
|||||||
<>
|
<>
|
||||||
<TextFalt label="Answer" varde={text} satt={setText} rost />
|
<TextFalt label="Answer" varde={text} satt={setText} rost />
|
||||||
<StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}>
|
<StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}>
|
||||||
Spara svar
|
Save answer
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1602,7 +1602,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
||||||
Markera verifierad
|
Mark as verified
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1634,7 +1634,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
|||||||
onClick={() => setOppen(true)}
|
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"
|
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>
|
</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 />
|
<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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="fara" disabled={!orsak.trim()} onClick={() => vidUndantag(orsak.trim())}>
|
<StorKnapp variant="fara" disabled={!orsak.trim()} onClick={() => vidUndantag(orsak.trim())}>
|
||||||
Dokumentera undantag
|
Document an exemption
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1733,7 +1733,7 @@ function SnabbDokumentation({
|
|||||||
if (!avlasning) return;
|
if (!avlasning) return;
|
||||||
// Originalet först, sedan varje bekräftat värde som eget mätvärde —
|
// Originalet först, sedan varje bekräftat värde som eget mätvärde —
|
||||||
// strukturerad data ersätter aldrig originalevidensen.
|
// 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) {
|
for (const v of avlasning.tolkning.varden) {
|
||||||
// `kalla` sätts därför att värdet AVLÄSTES av en modell ur fotot
|
// `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
|
// (TÜV T-5). Teknikern bekräftar innan något skrivs, vilket täcker
|
||||||
@@ -1750,7 +1750,7 @@ function SnabbDokumentation({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
paSparad?.(
|
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}` : ""}`)
|
.map((v) => `${v.beskrivning} = ${v.varde}${v.enhet ? ` ${v.enhet}` : ""}`)
|
||||||
.join("; ")}`,
|
.join("; ")}`,
|
||||||
);
|
);
|
||||||
@@ -1773,7 +1773,7 @@ function SnabbDokumentation({
|
|||||||
}
|
}
|
||||||
if (typ === "matvarde" && t && varde.trim()) {
|
if (typ === "matvarde" && t && varde.trim()) {
|
||||||
skicka({ typ: "matvarde", beskrivning: t, varde: varde.trim() });
|
skicka({ typ: "matvarde", beskrivning: t, varde: varde.trim() });
|
||||||
paSparad?.(`Mätvärde: ${t} = ${varde.trim()}`);
|
paSparad?.(`Measurement: ${t} = ${varde.trim()}`);
|
||||||
}
|
}
|
||||||
aterstall();
|
aterstall();
|
||||||
};
|
};
|
||||||
@@ -1860,7 +1860,7 @@ function SnabbDokumentation({
|
|||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setVideo(null)}>
|
<StorKnapp variant="sekundar" onClick={() => setVideo(null)}>
|
||||||
Förkasta
|
Discard
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!video.beskrivning.trim()}
|
disabled={!video.beskrivning.trim()}
|
||||||
@@ -1870,7 +1870,7 @@ function SnabbDokumentation({
|
|||||||
setVideo(null);
|
setVideo(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spara video
|
Save video
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1880,11 +1880,11 @@ function SnabbDokumentation({
|
|||||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
||||||
{avlasning.demo && (
|
{avlasning.demo && (
|
||||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
<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>
|
||||||
)}
|
)}
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<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>
|
</p>
|
||||||
{avlasning.tolkning.varden.map((v, i) => (
|
{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">
|
<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">
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
||||||
Förkasta
|
Discard
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={sparaAvlasning}>Save photograph + values</StorKnapp>
|
<StorKnapp onClick={sparaAvlasning}>Save photograph + values</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
@@ -1908,7 +1908,7 @@ function SnabbDokumentation({
|
|||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
{typ === "hypotes" && (
|
{typ === "hypotes" && (
|
||||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<TextFalt
|
<TextFalt
|
||||||
@@ -1920,7 +1920,7 @@ function SnabbDokumentation({
|
|||||||
/>
|
/>
|
||||||
{typ === "matvarde" && <TextFalt label="Value (with unit)" varde={varde} satt={setVarde} platshallare="e.g. 13.9 V" />}
|
{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}>
|
<StorKnapp disabled={!text.trim() || (typ === "matvarde" && !varde.trim())} onClick={spara}>
|
||||||
Spara
|
Save
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1974,7 +1974,7 @@ function OverlamningDialog({
|
|||||||
</pre>
|
</pre>
|
||||||
{aiLage === "vilar" && (
|
{aiLage === "vilar" && (
|
||||||
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
||||||
Komplettera analysen: risker & osäkerheter
|
Extend the analysis: risks & uncertainties
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing …</p>}
|
{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" />
|
<TextFalt label="Handed to (optional)" varde={till} satt={setTill} platshallare="e.g. Jonas" />
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={stang}>
|
<StorKnapp variant="sekundar" onClick={stang}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -2001,7 +2001,7 @@ function OverlamningDialog({
|
|||||||
stang();
|
stang();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Bekräfta överlämning
|
Confirm handover
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2042,7 +2042,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
uppdatera();
|
uppdatera();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IkonLank /> Delningslänkar (kund/partner/intern)
|
<IkonLank /> Share links (customer / partner / internal)
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2064,17 +2064,17 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Kopiera
|
Copy
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await aterkallaDelning(delning.kod);
|
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();
|
uppdatera();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Återkalla
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -2088,7 +2088,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await skapaDelning(arende.id, niva);
|
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();
|
uppdatera();
|
||||||
} catch {
|
} catch {
|
||||||
setFel("Could not create a share — requires sign-in and a synchronized case.");
|
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>
|
</div>
|
||||||
<p className="mt-2 text-[11px] text-[#707070]">
|
<p className="mt-2 text-[11px] text-[#707070]">
|
||||||
Kund: det kunddelbara · Extern partner: även hypoteser (märkta ej verifierade) · Intern: full insyn.
|
Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) ·
|
||||||
Filtreringen sker på servern.
|
Internal: full visibility. Filtering happens on the server.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -2164,8 +2164,8 @@ function BriefFlik({
|
|||||||
{!b.avslutat && (
|
{!b.avslutat && (
|
||||||
<Panel rubrik="Review of the evidence">
|
<Panel rubrik="Review of the evidence">
|
||||||
<p className="mb-2 text-[#333333]">
|
<p className="mb-2 text-[#333333]">
|
||||||
Låt systemet gå igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
|
Have the system review the whole record before you hand it on: contradictions between
|
||||||
observationer, luckor i dokumentationen och slutsatser som saknar stöd.
|
observations, gaps in the documentation, and conclusions that lack support.
|
||||||
</p>
|
</p>
|
||||||
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
||||||
{granskning === "fel" && (
|
{granskning === "fel" && (
|
||||||
@@ -2345,22 +2345,22 @@ function RapportFlik({
|
|||||||
))}
|
))}
|
||||||
{!godkand && (
|
{!godkand && (
|
||||||
<p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">
|
<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).
|
(”Underlag kan inte tas fram” i guiden).
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Customer report">
|
<Panel rubrik="Customer report">
|
||||||
<p className="mb-2 text-[#333333]">
|
<p className="mb-2 text-[#333333]">
|
||||||
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas — bilder kan
|
A shareable summary of the work performed. Review the content before the report is shared — images may
|
||||||
innehålla uppgifter om andra kunder.
|
contain details about other customers.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" disabled={!godkand} onClick={() => window.print()}>
|
<StorKnapp variant="sekundar" disabled={!godkand} onClick={() => window.print()}>
|
||||||
Skriv ut / PDF
|
Print / PDF
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" onClick={exporteraJson}>
|
<StorKnapp variant="sekundar" onClick={exporteraJson}>
|
||||||
Exportera JSON
|
Export JSON
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
||||||
@@ -2383,7 +2383,7 @@ function RapportFlik({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<p className="mt-2 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
|
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
|
||||||
@@ -2506,7 +2506,7 @@ function RapportFlik({
|
|||||||
["Parts", h.delar],
|
["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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -2565,7 +2565,7 @@ function RapportFlik({
|
|||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
<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>
|
</p>
|
||||||
<Ritningsstampel arende={arende} />
|
<Ritningsstampel arende={arende} />
|
||||||
<Felanmalan arende={arende} metodik={metodik ?? null} />
|
<Felanmalan arende={arende} metodik={metodik ?? null} />
|
||||||
@@ -2642,8 +2642,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
|||||||
return (
|
return (
|
||||||
<Panel rubrik="Report a problem with the system">
|
<Panel rubrik="Report a problem with the system">
|
||||||
<p className="mb-2 text-[12px] text-[#707070]">
|
<p className="mb-2 text-[12px] text-[#707070]">
|
||||||
Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden
|
Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.
|
||||||
skickas.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<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."
|
platshallare="Whoever answers cannot see your screen."
|
||||||
flerRad
|
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>}
|
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||||
{skickar ? "Sending" : "Send report"}
|
{skickar ? "Sending" : "Send report"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||||
Stäng
|
Close
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function PlattformInloggning() {
|
|||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<p className="text-[14px]">
|
<p className="text-[14px]">
|
||||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
<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>
|
</p>
|
||||||
<button
|
<button
|
||||||
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
|
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);
|
setKonto(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logga ut
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||||
@@ -80,8 +80,7 @@ function PlattformInloggning() {
|
|||||||
{lage === "stangd" ? (
|
{lage === "stangd" ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-2 text-[#333333]">
|
<p className="mb-2 text-[#333333]">
|
||||||
Logga in för synk mellan enheter, samarbete i ärenden och beslutsstöd. Utan inloggning
|
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.
|
||||||
arbetar appen i lokalt läge. Ny organisation? Skapa konto — du blir systemadministratör.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("registrera")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("registrera")}>
|
||||||
@@ -117,7 +116,7 @@ function PlattformInloggning() {
|
|||||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -165,7 +164,7 @@ function AnvandarAdmin() {
|
|||||||
uppdatera();
|
uppdatera();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hantera användare i organisationen
|
Manage users in the organization
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
@@ -209,7 +208,7 @@ function AnvandarAdmin() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<p className="mt-1 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
<form
|
<form
|
||||||
className="mt-3"
|
className="mt-3"
|
||||||
@@ -248,7 +247,7 @@ function AnvandarAdmin() {
|
|||||||
</div>
|
</div>
|
||||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
||||||
Skapa användare
|
Create user
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</form>
|
</form>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -273,7 +272,7 @@ export default function Arendelista() {
|
|||||||
<FelsokningSkal rubrik="Guided Diagnostics">
|
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||||
<Panel rubrik="Who is working?">
|
<Panel rubrik="Who is working?">
|
||||||
<p className="mb-3 text-[14px] text-[#333333]">
|
<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>
|
</p>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -283,7 +282,7 @@ export default function Arendelista() {
|
|||||||
>
|
>
|
||||||
<TextFalt label="Name" varde={namn} satt={setNamn} platshallare="e.g. Anna" />
|
<TextFalt label="Name" varde={namn} satt={setNamn} platshallare="e.g. Anna" />
|
||||||
<StorKnapp type="submit" disabled={!namn.trim()}>
|
<StorKnapp type="submit" disabled={!namn.trim()}>
|
||||||
Fortsätt
|
Continue
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</form>
|
</form>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -330,11 +329,10 @@ export default function Arendelista() {
|
|||||||
{alla.length === 0 && (
|
{alla.length === 0 && (
|
||||||
<Panel rubrik="Get started">
|
<Panel rubrik="Get started">
|
||||||
<p className="mb-3 text-[14px] text-[#333333]">
|
<p className="mb-3 text-[14px] text-[#333333]">
|
||||||
Inga ärenden ännu. Starta med att identifiera ett objekt — eller utforska ett färdigt demoärende
|
No cases yet. Start by identifying an object — or explore a finished demo case with a complete work log, brief and customer report.
|
||||||
med komplett arbetslogg, brief och kundrapport.
|
|
||||||
</p>
|
</p>
|
||||||
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
||||||
Skapa demoärende (Volvo XC60, vibration)
|
Create demo case (Volvo XC60, vibration)
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
@@ -353,11 +351,7 @@ export default function Arendelista() {
|
|||||||
|
|
||||||
<Panel rubrik="Decision support">
|
<Panel rubrik="Decision support">
|
||||||
<p className="text-[#333333]">
|
<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
|
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.
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</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="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||||
Ärende #{arende.nummer}
|
Case #{arende.nummer}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||||
|
|||||||
@@ -86,13 +86,11 @@ function Integrationer() {
|
|||||||
return (
|
return (
|
||||||
<Panel rubrik="Brand-specific connectors">
|
<Panel rubrik="Brand-specific connectors">
|
||||||
<p className="mb-2 text-[12px] text-[#707070]">
|
<p className="mb-2 text-[12px] text-[#707070]">
|
||||||
Organisationens egna avtal hos tillverkare och dataleverantörer. Uppgifterna lagras krypterat på
|
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.
|
||||||
plattformen och används bara av servern — de skickas aldrig till teknikernas enheter.
|
|
||||||
</p>
|
</p>
|
||||||
{!krypteringKlar && (
|
{!krypteringKlar && (
|
||||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
<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
|
Encryption is not configured in this deployment (INTEGRATION_NYCKEL) — details cannot be saved until the key exists.
|
||||||
nyckeln finns.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ function Integrationer() {
|
|||||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => { setOppen(null); setFel(""); }}>
|
<StorKnapp variant="sekundar" onClick={() => { setOppen(null); setFel(""); }}>
|
||||||
Avbryt
|
Cancel
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!krypteringKlar}
|
disabled={!krypteringKlar}
|
||||||
@@ -149,11 +147,11 @@ function Integrationer() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Spara uppgifter
|
Save details
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -182,7 +180,7 @@ function Integrationer() {
|
|||||||
ladda();
|
ladda();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ta bort
|
Delete
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +188,7 @@ function Integrationer() {
|
|||||||
</div>
|
</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>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -213,7 +211,7 @@ export default function Installningar() {
|
|||||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<p className="text-[14px] text-[#333333]">
|
||||||
Organisationens inställningar hanteras av er systemadministratör.
|
Organization settings are managed by your system administrator.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -246,7 +244,7 @@ export default function Installningar() {
|
|||||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[#333333]">
|
<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
|
{inloggad
|
||||||
? "The setting applies to everyone in the organization."
|
? "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."}
|
: "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" }}>
|
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel rubrik="Scan work order">
|
<Panel rubrik="Scan work order">
|
||||||
<p className="mb-3 text-[#333333]">
|
<p className="mb-3 text-[#333333]">
|
||||||
Fota arbetsorderns framsida. Systemet läser kund-, fordons- och ärendeuppgifter automatiskt —
|
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.
|
||||||
du granskar bara osäkra fält och trycker Starta diagnos.
|
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
ref={skanRef}
|
ref={skanRef}
|
||||||
@@ -201,7 +200,7 @@ export default function NyttArende() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||||
Fyll i manuellt
|
Fill in manually
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -217,7 +216,7 @@ export default function NyttArende() {
|
|||||||
>
|
>
|
||||||
{tolkning.demo && (
|
{tolkning.demo && (
|
||||||
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<GranskaTolkning
|
<GranskaTolkning
|
||||||
@@ -255,7 +254,7 @@ export default function NyttArende() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Read the identity">
|
<Panel rubrik="Read the identity">
|
||||||
@@ -275,17 +274,17 @@ export default function NyttArende() {
|
|||||||
<div>
|
<div>
|
||||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
||||||
<p className="mb-2 text-[12px] text-[#707070]">
|
<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>
|
</p>
|
||||||
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
||||||
Avbryt skanning
|
Cancel scanning
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{stodStreckkod() && (
|
{stodStreckkod() && (
|
||||||
<StorKnapp variant="sekundar" onClick={oppnaSkanning}>
|
<StorKnapp variant="sekundar" onClick={oppnaSkanning}>
|
||||||
Skanna QR/streckkod
|
Scan QR or barcode
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
||||||
@@ -295,8 +294,8 @@ export default function NyttArende() {
|
|||||||
)}
|
)}
|
||||||
{avlastKod && (
|
{avlastKod && (
|
||||||
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">
|
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">
|
||||||
Avläst {avlastKod.typ}: {avlastKod.varde}
|
Read {avlastKod.typ}: {avlastKod.varde}
|
||||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — kontrollera värdet nedan.
|
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{kodFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{kodFel}</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="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" />
|
<TextFalt label="Customer (optional)" varde={kund} satt={setKund} platshallare="e.g. Alex Meyer" />
|
||||||
<StorKnapp disabled={!identifierare.trim()} onClick={() => setSteg("bekrafta")}>
|
<StorKnapp disabled={!identifierare.trim()} onClick={() => setSteg("bekrafta")}>
|
||||||
Fortsätt
|
Continue
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -325,7 +324,7 @@ export default function NyttArende() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||||
Ändra
|
Edit
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Correct object — continue</StorKnapp>
|
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Correct object — continue</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,7 +419,7 @@ function GranskaTolkning({
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<p className="mt-1 text-[11px] text-[#707070]">
|
||||||
{vald?.omrade
|
{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."}
|
: "Select a field to see where in the document the value was found."}
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</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"
|
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
|
||||||
>
|
>
|
||||||
Bekräfta
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -480,12 +479,12 @@ function GranskaTolkning({
|
|||||||
|
|
||||||
{obekraftade.length > 0 && (
|
{obekraftade.length > 0 && (
|
||||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={avbryt}>
|
<StorKnapp variant="sekundar" onClick={avbryt}>
|
||||||
Skanna om
|
Scan again
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
||||||
{startar ? "Starting …" : "Start diagnosis"}
|
{startar ? "Starting …" : "Start diagnosis"}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
|||||||
hamtaAnvandare().then(setPersoner).catch(() => setPersoner([]));
|
hamtaAnvandare().then(setPersoner).catch(() => setPersoner([]));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Omfördela
|
Reassign
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
|||||||
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
|
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
|
||||||
onClick={() => setOppen(false)}
|
onClick={() => setOppen(false)}
|
||||||
>
|
>
|
||||||
Avbryt
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +102,7 @@ export default function Oversikt() {
|
|||||||
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -177,7 +177,7 @@ export default function Oversikt() {
|
|||||||
</div>
|
</div>
|
||||||
{avslutade.length > 0 && (
|
{avslutade.length > 0 && (
|
||||||
<p className="mt-3 text-center text-[#333333]">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -195,7 +195,7 @@ export default function Oversikt() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<p className="mt-2 text-[11px] text-[#707070]">
|
<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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
@@ -206,7 +206,7 @@ export default function Oversikt() {
|
|||||||
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
|
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||||
Ärende #{rad.nummer}
|
Case #{rad.nummer}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
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>}
|
{rad.felbeskrivning && <p className="text-[14px] text-[#333333]">”{rad.felbeskrivning}”</p>}
|
||||||
<p className="mt-1 text-[12px] text-[#707070]">
|
<p className="mt-1 text-[12px] text-[#707070]">
|
||||||
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
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)}` : ""}
|
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => oppna(rad)}>
|
<StorKnapp variant="sekundar" onClick={() => oppna(rad)}>
|
||||||
Öppna ärendet
|
Open the case
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
{!rad.avslutat && <Omfordela rad={rad} vidKlar={() => hamtaOversikt().then(setRader)} />}
|
{!rad.avslutat && <Omfordela rad={rad} vidKlar={() => hamtaOversikt().then(setRader)} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
// samma utfall, vilket är vad som gör en spärr granskbar.
|
// samma utfall, vilket är vad som gör en spärr granskbar.
|
||||||
|
|
||||||
import { granskaSlutsats } from "./motivering.mjs";
|
import { granskaSlutsats } from "./motivering.mjs";
|
||||||
import { STANDARD, t } from "./sprak/index.mjs";
|
import { STANDARD, arJakande, arendetypNu, t } from "./sprak/index.mjs";
|
||||||
|
|
||||||
const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0;
|
const UNDANTAG_MOTIVERAT = (h) => typeof h.undantag === "string" && h.undantag.trim().length > 0;
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ export function grinda(handelser, metodik, sprak = STANDARD) {
|
|||||||
const svaret = av(handelser, "fraga_besvarad").findLast(
|
const svaret = av(handelser, "fraga_besvarad").findLast(
|
||||||
(h) => h.stegId === stegId && h.frageId === frageId,
|
(h) => h.stegId === stegId && h.frageId === frageId,
|
||||||
);
|
);
|
||||||
krav(`sparr_${frageId}`, nyckel, svaret?.svar === "Ja", "grind.sparr.ej_uppfyllt");
|
krav(`sparr_${frageId}`, nyckel, arJakande(svaret?.svar), "grind.sparr.ej_uppfyllt");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan
|
// Evidensnivån måste överstiga E0 — annars avslutas ett ärende utan
|
||||||
@@ -210,7 +210,10 @@ export function grinda(handelser, metodik, sprak = STANDARD) {
|
|||||||
* (ECM Knowledge Library) och är alltså data, inte kod.
|
* (ECM Knowledge Library) och är alltså data, inte kod.
|
||||||
*/
|
*/
|
||||||
export function grindaArendetyp(handelser, regelpaket, sprak = STANDARD) {
|
export function grindaArendetyp(handelser, regelpaket, sprak = STANDARD) {
|
||||||
const typ = av(handelser, "arendetyp_satt").at(-1)?.arendetyp;
|
// Ärendetypen slås upp i sin nuvarande form. Ett ärende som öppnades
|
||||||
|
// när typerna hette "Garanti" ska få garantins extra krav även efter
|
||||||
|
// att de bytt språk — se ARENDETYP_ARV.
|
||||||
|
const typ = arendetypNu(av(handelser, "arendetyp_satt").at(-1)?.arendetyp);
|
||||||
const regler = regelpaket?.arendetyper?.[typ]?.krav ?? [];
|
const regler = regelpaket?.arendetyper?.[typ]?.krav ?? [];
|
||||||
const hinder = [];
|
const hinder = [];
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
// sämre i praktiken — då raderas beviset tillsammans med personuppgiften.
|
// sämre i praktiken — då raderas beviset tillsammans med personuppgiften.
|
||||||
|
|
||||||
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||||
|
import { arendetypNu } from "./sprak/index.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fält som räknas som identifierande och därför krypteras.
|
* Fält som räknas som identifierande och därför krypteras.
|
||||||
@@ -134,16 +135,27 @@ export function öppnaHändelse(handelse, nycklar) {
|
|||||||
* den som inte gör ett aktivt val får det försiktigaste.
|
* den som inte gör ett aktivt val får det försiktigaste.
|
||||||
*/
|
*/
|
||||||
export const GALLRING_MANADER = {
|
export const GALLRING_MANADER = {
|
||||||
Garanti: 120, // Följer den längsta rimliga garantitiden på en drivlina.
|
Warranty: 120, // Följer den längsta rimliga garantitiden på en drivlina.
|
||||||
Goodwill: 120,
|
Goodwill: 120,
|
||||||
Försäkring: 120, // Preskription för försäkringskrav.
|
Insurance: 120, // Preskription för försäkringskrav.
|
||||||
Reklamation: 36, // Konsumentköplagens reklamationsrätt.
|
Complaint: 36, // Konsumentköplagens reklamationsrätt.
|
||||||
Begagnatgaranti: 60,
|
"Used-vehicle warranty": 60,
|
||||||
standard: 36,
|
standard: 36,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gallringsdatum för ett avslutat ärende.
|
||||||
|
*
|
||||||
|
* Ärendetypen normaliseras genom ARENDETYP_ARV innan den slås upp. Det
|
||||||
|
* är inte en artighet mot gamla data utan en spärr: när typerna bytte
|
||||||
|
* språk hade ett garantiärende vars logg säger "Garanti" fallit ur
|
||||||
|
* tabellen och tagit standardvärdet 36 månader i stället för 120 —
|
||||||
|
* alltså raderat garantiunderlaget sju år för tidigt, tyst, och
|
||||||
|
* oåterkalleligt. Ett uppslag som missar ska ge det FÖRSIKTIGASTE
|
||||||
|
* utfallet; här gav det det farligaste.
|
||||||
|
*/
|
||||||
export function gallringsdatum(arendetyp, avslutat, manader = GALLRING_MANADER) {
|
export function gallringsdatum(arendetyp, avslutat, manader = GALLRING_MANADER) {
|
||||||
const antal = manader[arendetyp] ?? manader.standard;
|
const antal = manader[arendetypNu(arendetyp)] ?? manader.standard;
|
||||||
const d = new Date(avslutat);
|
const d = new Date(avslutat);
|
||||||
d.setMonth(d.getMonth() + antal);
|
d.setMonth(d.getMonth() + antal);
|
||||||
return d;
|
return d;
|
||||||
|
|||||||
@@ -187,3 +187,51 @@ export function metodikvarning(sprak) {
|
|||||||
if (!s || s.granskat) return null;
|
if (!s || s.granskat) return null;
|
||||||
return t(sprak, "metodik.ogranskad", { sprak: s.egetNamn });
|
return t(sprak, "metodik.ogranskad", { sprak: s.egetNamn });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ärendetyper som skrevs på svenska innan engelska blev källspråket.
|
||||||
|
*
|
||||||
|
* Ärendetypen är DATA: den ligger i händelseloggen och slår upp
|
||||||
|
* regelpaketets extra krav. Ett byte av språk får därför inte göras tyst
|
||||||
|
* — ett gammalt garantiärende vars typ heter "Garanti" hade annars inte
|
||||||
|
* matchat "Warranty", fått noll extra krav, och släppts igenom grinden
|
||||||
|
* med FÄRRE krav än när det öppnades. Ingenting i gränssnittet hade
|
||||||
|
* visat det.
|
||||||
|
*
|
||||||
|
* Tabellen är därför en del av regeln, inte en bekvämlighet, och den tas
|
||||||
|
* bort först när ingen logg innehåller de gamla värdena.
|
||||||
|
*/
|
||||||
|
export const ARENDETYP_ARV = {
|
||||||
|
"Privat kund": "Private customer",
|
||||||
|
Företagskund: "Business customer",
|
||||||
|
Garanti: "Warranty",
|
||||||
|
Försäkring: "Insurance",
|
||||||
|
Reklamation: "Complaint",
|
||||||
|
Begagnatgaranti: "Used-vehicle warranty",
|
||||||
|
"Intern kvalitetskontroll": "Internal quality check",
|
||||||
|
"Teknisk utredning": "Technical investigation",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ärendetypen i sin nuvarande form, oavsett vilket språk den skrevs i. */
|
||||||
|
export const arendetypNu = (typ) => ARENDETYP_ARV[typ] ?? typ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jakande svar på en säkerhetsspärr.
|
||||||
|
*
|
||||||
|
* Svaret på en ja/nej-fråga ligger i loggen som text, och spärrfrågorna
|
||||||
|
* släpper bara igenom ett jakande svar. När knappen bytte från "Ja" till
|
||||||
|
* "Yes" gick de två isär: klienten skrev "Yes", servern jämförde mot
|
||||||
|
* "Ja". Utfallet blev fail-closed — högvoltsärenden gick inte att
|
||||||
|
* avsluta alls — vilket är rätt riktning för ett fel att falla åt, men
|
||||||
|
* det var en slump och inte en konstruktion.
|
||||||
|
*
|
||||||
|
* Det verkliga felet är att en säkerhetsspärr avgörs av en textsträng.
|
||||||
|
* Rätt lösning är ett booleskt fält i händelsen; tills det finns står
|
||||||
|
* båda formerna här, och de gäller alla språk samtidigt av samma skäl
|
||||||
|
* som ordlistorna i ord.mjs gör det: den som svarar ska inte kunna
|
||||||
|
* missas för att organisationens språkinställning var en annan.
|
||||||
|
*/
|
||||||
|
export const JAKANDE = new Set(["Yes", "Ja", "Oui", "Sí", "Si", "Sì", "Tak", "Da", "Sim", "Ja."]);
|
||||||
|
|
||||||
|
/** Är svaret jakande, oavsett vilket språk knappen stod på? */
|
||||||
|
export const arJakande = (svar) => JAKANDE.has(String(svar ?? "").trim());
|
||||||
|
|||||||
@@ -2,58 +2,103 @@
|
|||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"beskrivning": "ECM Knowledge Library — serverdistribuerat regelpaket. Uppdateras utan appändring: redigera filen (eller montera en ConfigMap över den i klustret) och starta om tjänsten. Klienten hämtar paketet vid inloggning och faller tillbaka till sitt inbyggda standardpaket offline.",
|
"beskrivning": "ECM Knowledge Library — serverdistribuerat regelpaket. Uppdateras utan appändring: redigera filen (eller montera en ConfigMap över den i klustret) och starta om tjänsten. Klienten hämtar paketet vid inloggning och faller tillbaka till sitt inbyggda standardpaket offline.",
|
||||||
"arendetypRegler": {
|
"arendetypRegler": {
|
||||||
"Garanti": [
|
"Warranty": [
|
||||||
{ "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_miltal",
|
||||||
{ "id": "garanti_claim", "rubrik": "Claim-/garantinummer registrerat", "krav": "claim", "detaljVidBrist": "Ange claim-/garantinummer (läses ur arbetsordern)." }
|
"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": [
|
"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": [
|
"Insurance": [
|
||||||
{ "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." }
|
"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": [
|
"Complaint": [
|
||||||
{ "id": "reklamation_historik", "rubrik": "Historik och tidigare försök kontrollerade", "krav": "historik", "detaljVidBrist": "Reklamationer kräver kontrollerad historik (tidigare reparationer/försök)." }
|
{
|
||||||
|
"id": "reklamation_historik",
|
||||||
|
"rubrik": "History and previous attempts checked",
|
||||||
|
"krav": "historik",
|
||||||
|
"detaljVidBrist": "Complaint cases require a checked history (previous repairs and attempts)."
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"Begagnatgaranti": [
|
"Used-vehicle warranty": [
|
||||||
{ "id": "begagnat_miltal", "rubrik": "Miltal dokumenterat", "krav": "miltal", "detaljVidBrist": "Begagnatgaranti kräver dokumenterad mätarställning." }
|
{
|
||||||
|
"id": "begagnat_miltal",
|
||||||
|
"rubrik": "Mileage documented",
|
||||||
|
"krav": "miltal",
|
||||||
|
"detaljVidBrist": "Used-vehicle warranty cases require a documented odometer reading."
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"undantagsorsaker": [
|
"undantagsorsaker": [
|
||||||
"Komponenten är oåtkomlig",
|
"The component is inaccessible",
|
||||||
"Fordonet kan inte lyftas säkert",
|
"The vehicle cannot be lifted safely",
|
||||||
"Kunden avböjde demontering",
|
"The customer declined disassembly",
|
||||||
"Dålig sikt/åtkomst",
|
"Poor visibility or access",
|
||||||
"Utrustning saknas"
|
"Equipment not available"
|
||||||
],
|
],
|
||||||
"orsakskategorier": [
|
"orsakskategorier": [
|
||||||
"Normalt slitage",
|
"Normal wear",
|
||||||
"Ålder",
|
"Age",
|
||||||
"Körsträcka",
|
"Distance travelled",
|
||||||
"Materialutmattning",
|
"Material fatigue",
|
||||||
"Tillverkningsfel",
|
"Manufacturing defect",
|
||||||
"Bristande underhåll",
|
"Inadequate maintenance",
|
||||||
"Felaktig tidigare reparation",
|
"Faulty previous repair",
|
||||||
"Yttre påverkan",
|
"External influence",
|
||||||
"Korrosion",
|
"Corrosion",
|
||||||
"Överhettning",
|
"Overheating",
|
||||||
"Förorening",
|
"Contamination",
|
||||||
"Felaktig användning",
|
"Improper use",
|
||||||
"Modifiering",
|
"Modification",
|
||||||
"Olycka eller skada",
|
"Accident or damage",
|
||||||
"Okänd orsak"
|
"Unknown cause"
|
||||||
],
|
],
|
||||||
"underlagskallor": [
|
"underlagskallor": [
|
||||||
"Foto",
|
"Foto",
|
||||||
"Video",
|
"Video",
|
||||||
"Mätresultat",
|
"Measurement result",
|
||||||
"Diagnosutläsning",
|
"Diagnostic readout",
|
||||||
"Tidigare historik",
|
"Previous history",
|
||||||
"Servicehistorik",
|
"Service history",
|
||||||
"Teknisk dokumentation",
|
"Technical documentation",
|
||||||
"Direkt observation"
|
"Direct observation"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ kontroll "statistiken är organisationsknuten data" "$(echo "$STAT" | falt '.ors
|
|||||||
# 10b. ECM Knowledge Library: regelpaketet serveras till inloggade klienter
|
# 10b. ECM Knowledge Library: regelpaketet serveras till inloggade klienter
|
||||||
REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J")
|
REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J")
|
||||||
kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0"
|
kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0"
|
||||||
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Garanti.length')" "3"
|
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Warranty.length')" "3"
|
||||||
|
|
||||||
# 10c. Publikt kundgodkännande — den enda skrivande publika vägen
|
# 10c. Publikt kundgodkännande — den enda skrivande publika vägen
|
||||||
# Ärendets ursprungliga delningskod saknar registrerad nivå och får
|
# Ärendets ursprungliga delningskod saknar registrerad nivå och får
|
||||||
|
|||||||
Reference in New Issue
Block a user