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:
@@ -323,7 +323,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
/>
|
||||
{!foto && !laser && (
|
||||
<StorKnapp variant="sekundar" onClick={() => filRef.current?.click()}>
|
||||
<IkonKamera /> Fotografera instrumentpanelen
|
||||
<IkonKamera /> Photograph the instrument panel
|
||||
</StorKnapp>
|
||||
)}
|
||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer …</p>}
|
||||
@@ -331,11 +331,11 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
<div className="mt-2">
|
||||
{demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
Demo-avläsning — bildtolkningen kräver inloggning. Kontrollera värdet.
|
||||
Demo reading — image interpretation requires sign-in. Check the value.
|
||||
</p>
|
||||
)}
|
||||
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
|
||||
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||
<TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||
<StorKnapp
|
||||
disabled={!varde.trim()}
|
||||
onClick={() => {
|
||||
@@ -344,7 +344,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
setVarde("");
|
||||
}}
|
||||
>
|
||||
Spara mätarställning
|
||||
Save odometer reading
|
||||
</StorKnapp>
|
||||
</div>
|
||||
)}
|
||||
@@ -393,12 +393,12 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
||||
return (
|
||||
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Tidigare ärenden på {identifierare} — granska innan historiken kvitteras
|
||||
Previous cases for {identifierare} — review before signing off the history
|
||||
</p>
|
||||
{rader.map((rad) => (
|
||||
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
|
||||
<p>
|
||||
<span className="font-semibold">Ärende #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||
{rad.avslutat ? "closed" : "in progress"}
|
||||
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
||||
</p>
|
||||
@@ -412,13 +412,13 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
||||
onClick={() =>
|
||||
skicka({
|
||||
typ: "kommentar",
|
||||
text: `Orsakskedja: kopplat till tidigare ärende #${rad.nummer}${
|
||||
text: `Causal chain: linked to previous case #${rad.nummer}${
|
||||
rad.felorsaker[0] ? ` — ${rad.felorsaker[0].avvikelse}` : rad.felbeskrivning ? ` — ${rad.felbeskrivning}` : ""
|
||||
}`,
|
||||
})
|
||||
}
|
||||
>
|
||||
+ Koppla till orsakskedjan
|
||||
+ Link to the causal chain
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -448,12 +448,12 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("historik").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Har fordonets historik kontrollerats? (tidigare arbeten, återkommande fel, TSB, kampanjer)
|
||||
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
|
||||
</p>
|
||||
<TidigareArenden arende={arende} skicka={skicka} />
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant={historikVal === "ja" ? "primar" : "sekundar"} onClick={() => setHistorikVal("ja")}>
|
||||
Ja — kontrollerad
|
||||
Yes — checked
|
||||
</StorKnapp>
|
||||
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
||||
No
|
||||
@@ -486,7 +486,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("matarstallning_in").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Ingående mätarställning — fotografera instrumentpanelen
|
||||
Incoming odometer reading — photograph the instrument panel
|
||||
</p>
|
||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||
</div>
|
||||
@@ -495,11 +495,11 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("felbeskrivning").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Är kundens felbeskrivning korrekt återgiven?
|
||||
Is the customer's fault description correctly recorded?
|
||||
</p>
|
||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||
Stämmer — verifierad
|
||||
Correct — verified
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Ytterligare symptom dokumenteras som separata observationer — blanda dem inte med kundens beskrivning.
|
||||
@@ -510,22 +510,22 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
{!rad("tidiga_observationer").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Något ytterligare vid mottagandet? (reparationsspår, modifieringar, skador, läckage, korrosion …)
|
||||
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_INGA_TIDIGA_OBSERVATIONER}.` })}>
|
||||
Inga ytterligare observationer
|
||||
No further observations
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
variant="sekundar"
|
||||
disabled={!harTidiga}
|
||||
onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_TIDIGA_OBSERVATIONER_KLARA}.` })}
|
||||
>
|
||||
Observationerna är dokumenterade
|
||||
The observations are documented
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Dokumentera med foto eller observation i panelen nedan — knappen låses upp när något loggats.
|
||||
Document with a photograph or an observation in the panel below — the button unlocks once something is logged.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -569,7 +569,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
Dokumentera reproducering
|
||||
Document reproduction
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -638,15 +638,15 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
<div key={p.id} className="mb-2 border-b border-[#EBEBEB] pb-2 text-[13px] last:border-0">
|
||||
<p className="font-semibold">{h.avvikelse}</p>
|
||||
<p className="text-[#4A5560]">
|
||||
Orsak: {h.orsaker.join(", ")} · Underlag: {h.underlag.join(", ")} · Säkerhet: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
</p>
|
||||
<p>Åtgärd: {h.atgard}</p>
|
||||
<p>Action: {h.atgard}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{!oppen ? (
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(true)}>
|
||||
+ Dokumentera felorsak
|
||||
+ Document root cause
|
||||
</StorKnapp>
|
||||
) : (
|
||||
<div>
|
||||
@@ -721,7 +721,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={spara}>Save root cause</StorKnapp>
|
||||
</div>
|
||||
@@ -774,7 +774,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-[#9A6700]">Ingen åtgärd utförd — {h.motivering}</span>
|
||||
<span className="text-[#9A6700]">No action performed — {h.motivering}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
@@ -820,7 +820,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setForslagText(foreslagenAtgard);
|
||||
}}
|
||||
>
|
||||
Lämna åtgärdsförslag till kund
|
||||
Give the customer a proposed action
|
||||
</StorKnapp>
|
||||
)}
|
||||
|
||||
@@ -837,7 +837,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Estimated cost (optional)" varde={kostnad} satt={setKostnad} platshallare="e.g. 1,200 incl. VAT" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setForslagsLage(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={forslagText.trim().length < 10}
|
||||
@@ -852,7 +852,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setKostnad("");
|
||||
}}
|
||||
>
|
||||
Lämna förslag
|
||||
Give the proposal
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -861,7 +861,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{forslag.length > 0 && !beslut && (
|
||||
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
|
||||
Registrera kundens besked innan arbetet påbörjas
|
||||
Record the customer's decision before the work begins
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{(["godkant", "avbojt", "delvis"] as const).map((val) => (
|
||||
@@ -912,7 +912,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setBeslutsKommentar("");
|
||||
}}
|
||||
>
|
||||
Registrera besked
|
||||
Record the decision
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -926,7 +926,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
disabled={forslag.length > 0 && (!beslut || beslut.beslut === "avbojt")}
|
||||
onClick={() => setLage("utford")}
|
||||
>
|
||||
Dokumentera utförd åtgärd
|
||||
Document the action performed
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
||||
No action performed
|
||||
@@ -947,7 +947,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Parts (optional)" varde={delar} satt={setDelar} platshallare="e.g. Balance weights, part no. 30-1234" rost />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={beskrivning.trim().length < 10}
|
||||
@@ -958,7 +958,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setDelar("");
|
||||
}}
|
||||
>
|
||||
Spara åtgärd
|
||||
Save action
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -967,7 +967,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{lage === "ingen" && (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Orsak till att ingen åtgärd utfördes (obligatorisk)
|
||||
Reason no action was performed (required)
|
||||
</p>
|
||||
<div className="mb-2 grid gap-1">
|
||||
{INGEN_ATGARD_ORSAKER.map((val) => (
|
||||
@@ -985,12 +985,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||
{beslut?.beslut === "avbojt" && (
|
||||
<p className="mb-2 text-[11px] text-[#707070]">
|
||||
Kunden har avböjt åtgärden via {beslut.kanal} — beskedet finns registrerat i loggen.
|
||||
The customer has declined the action via {beslut.kanal} — the decision is recorded in the log.
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!orsak.trim()}
|
||||
@@ -1009,11 +1009,11 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
{utford && !kk && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Kvalitetskontroll — är symptomet borta efter åtgärden?
|
||||
Quality check — is the symptom gone after the action?
|
||||
</p>
|
||||
{repro?.status === "ja" && (
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Symptomet reproducerades vid undersökningen — återskapa samma förhållanden för att verifiera.
|
||||
The symptom was reproduced during the examination — recreate the same conditions to verify.
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
@@ -1053,7 +1053,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
setKkText("");
|
||||
}}
|
||||
>
|
||||
Spara kvalitetskontroll
|
||||
Save quality check
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1081,7 +1081,7 @@ function VyTrad({
|
||||
return (
|
||||
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
|
||||
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
||||
Ärendevyer
|
||||
Case views
|
||||
</p>
|
||||
{FLIKAR.map((f) => (
|
||||
<button
|
||||
@@ -1177,7 +1177,7 @@ function KontextPanel({
|
||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
|
||||
Beslutsstöd · presenteras aldrig som konstaterat fel
|
||||
Decision support · never presented as an established fault
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -1214,7 +1214,7 @@ function InaktivitetsBanner({
|
||||
setText("");
|
||||
}}
|
||||
>
|
||||
Lägg till i arbetsloggen
|
||||
Add to the work log
|
||||
</StorKnapp>
|
||||
</div>
|
||||
);
|
||||
@@ -1286,7 +1286,7 @@ function GuideFlik({
|
||||
return (
|
||||
<Panel rubrik="The diagnosis is closed">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export.
|
||||
The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export.
|
||||
</p>
|
||||
</Panel>
|
||||
);
|
||||
@@ -1335,16 +1335,16 @@ function GuideFlik({
|
||||
först vid avslutsknappen vore att neka någon som redan är klar. */}
|
||||
{(steg.klart || underlagKlart) && <Slutsatspanel arende={arende} skicka={skicka} />}
|
||||
|
||||
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
||||
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
|
||||
{steg.klart ? (
|
||||
<>
|
||||
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||
<p className="mb-4 text-[#333333]">
|
||||
Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet
|
||||
med rekommenderade nästa steg.
|
||||
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
|
||||
with recommended next steps.
|
||||
</p>
|
||||
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||
Avsluta felsökning
|
||||
Close diagnosis
|
||||
</StorKnapp>
|
||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||
</>
|
||||
@@ -1357,7 +1357,7 @@ function GuideFlik({
|
||||
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
||||
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
||||
<p className="text-[12px] text-[#666666]">
|
||||
Svaret är dokumenterat i loggen. Metodiken öppnas när förutsättningen är uppfylld och frågan besvaras
|
||||
The answer is documented in the log. The methodology opens when the precondition is met and the question is answered
|
||||
jakande.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1422,10 +1422,10 @@ function GuideFlik({
|
||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||
Lämna över arbete
|
||||
Hand over the work
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare, plattformsversion: PLATTFORMSVERSION })}>
|
||||
Avsluta felsökning
|
||||
Close diagnosis
|
||||
</StorKnapp>
|
||||
</div>
|
||||
|
||||
@@ -1448,7 +1448,7 @@ function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detal
|
||||
return (
|
||||
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]">
|
||||
Ärendet kan inte avslutas ännu
|
||||
The case cannot be closed yet
|
||||
</p>
|
||||
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
|
||||
{hinder.map((h) => (
|
||||
@@ -1515,7 +1515,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
||||
<>
|
||||
<TextFalt label="Answer" varde={text} satt={setText} rost />
|
||||
<StorKnapp disabled={!text.trim()} onClick={() => svara(text.trim())}>
|
||||
Spara svar
|
||||
Save answer
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1602,7 +1602,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
</p>
|
||||
)}
|
||||
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
||||
Markera verifierad
|
||||
Mark as verified
|
||||
</StorKnapp>
|
||||
</>
|
||||
)}
|
||||
@@ -1634,7 +1634,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
||||
onClick={() => setOppen(true)}
|
||||
className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
|
||||
>
|
||||
Underlag kan inte tas fram …
|
||||
Evidence cannot be produced …
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1659,10 +1659,10 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
||||
<TextFalt label="Or a cause of your own" varde={orsak && !UNDANTAGSORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="fara" disabled={!orsak.trim()} onClick={() => vidUndantag(orsak.trim())}>
|
||||
Dokumentera undantag
|
||||
Document an exemption
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1733,7 +1733,7 @@ function SnabbDokumentation({
|
||||
if (!avlasning) return;
|
||||
// Originalet först, sedan varje bekräftat värde som eget mätvärde —
|
||||
// strukturerad data ersätter aldrig originalevidensen.
|
||||
skicka({ typ: "foto", beskrivning: `Instrumentavläsning (${avlasning.tolkning.instrumenttyp})`, dataUrl: avlasning.foto });
|
||||
skicka({ typ: "foto", beskrivning: `Instrument reading (${avlasning.tolkning.instrumenttyp})`, dataUrl: avlasning.foto });
|
||||
for (const v of avlasning.tolkning.varden) {
|
||||
// `kalla` sätts därför att värdet AVLÄSTES av en modell ur fotot
|
||||
// (TÜV T-5). Teknikern bekräftar innan något skrivs, vilket täcker
|
||||
@@ -1750,7 +1750,7 @@ function SnabbDokumentation({
|
||||
});
|
||||
}
|
||||
paSparad?.(
|
||||
`Instrumentavläsning (${avlasning.tolkning.instrumenttyp}): ${avlasning.tolkning.varden
|
||||
`Instrument reading (${avlasning.tolkning.instrumenttyp}): ${avlasning.tolkning.varden
|
||||
.map((v) => `${v.beskrivning} = ${v.varde}${v.enhet ? ` ${v.enhet}` : ""}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
@@ -1773,7 +1773,7 @@ function SnabbDokumentation({
|
||||
}
|
||||
if (typ === "matvarde" && t && varde.trim()) {
|
||||
skicka({ typ: "matvarde", beskrivning: t, varde: varde.trim() });
|
||||
paSparad?.(`Mätvärde: ${t} = ${varde.trim()}`);
|
||||
paSparad?.(`Measurement: ${t} = ${varde.trim()}`);
|
||||
}
|
||||
aterstall();
|
||||
};
|
||||
@@ -1860,7 +1860,7 @@ function SnabbDokumentation({
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVideo(null)}>
|
||||
Förkasta
|
||||
Discard
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!video.beskrivning.trim()}
|
||||
@@ -1870,7 +1870,7 @@ function SnabbDokumentation({
|
||||
setVideo(null);
|
||||
}}
|
||||
>
|
||||
Spara video
|
||||
Save video
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1880,11 +1880,11 @@ function SnabbDokumentation({
|
||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
||||
{avlasning.demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
Demo-avläsning — bildtolkningen kräver inloggning. Värdena är exempeldata.
|
||||
Demo reading — image interpretation requires sign-in. The values are sample data.
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
Avläst: {avlasning.tolkning.instrumenttyp} — bekräfta innan något loggas
|
||||
Read: {avlasning.tolkning.instrumenttyp} — confirm before anything is logged
|
||||
</p>
|
||||
{avlasning.tolkning.varden.map((v, i) => (
|
||||
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
@@ -1898,7 +1898,7 @@ function SnabbDokumentation({
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
||||
Förkasta
|
||||
Discard
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={sparaAvlasning}>Save photograph + values</StorKnapp>
|
||||
</div>
|
||||
@@ -1908,7 +1908,7 @@ function SnabbDokumentation({
|
||||
<div className="mt-3">
|
||||
{typ === "hypotes" && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
<IkonPunkt farg="#8B1A1A" /> En hypotes är en möjlig felorsak som kräver verifiering — den loggas aldrig som ett konstaterat fel.
|
||||
<IkonPunkt farg="#8B1A1A" /> A hypothesis is a possible root cause that requires verification — it is never logged as an established fault.
|
||||
</p>
|
||||
)}
|
||||
<TextFalt
|
||||
@@ -1920,7 +1920,7 @@ function SnabbDokumentation({
|
||||
/>
|
||||
{typ === "matvarde" && <TextFalt label="Value (with unit)" varde={varde} satt={setVarde} platshallare="e.g. 13.9 V" />}
|
||||
<StorKnapp disabled={!text.trim() || (typ === "matvarde" && !varde.trim())} onClick={spara}>
|
||||
Spara
|
||||
Save
|
||||
</StorKnapp>
|
||||
</div>
|
||||
)}
|
||||
@@ -1974,7 +1974,7 @@ function OverlamningDialog({
|
||||
</pre>
|
||||
{aiLage === "vilar" && (
|
||||
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
||||
Komplettera analysen: risker & osäkerheter
|
||||
Extend the analysis: risks & uncertainties
|
||||
</StorKnapp>
|
||||
)}
|
||||
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing …</p>}
|
||||
@@ -1993,7 +1993,7 @@ function OverlamningDialog({
|
||||
<TextFalt label="Handed to (optional)" varde={till} satt={setTill} platshallare="e.g. Jonas" />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={stang}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
onClick={() => {
|
||||
@@ -2001,7 +2001,7 @@ function OverlamningDialog({
|
||||
stang();
|
||||
}}
|
||||
>
|
||||
Bekräfta överlämning
|
||||
Confirm handover
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2042,7 +2042,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
<IkonLank /> Delningslänkar (kund/partner/intern)
|
||||
<IkonLank /> Share links (customer / partner / internal)
|
||||
</StorKnapp>
|
||||
);
|
||||
}
|
||||
@@ -2064,17 +2064,17 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
||||
}
|
||||
>
|
||||
Kopiera
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
||||
onClick={async () => {
|
||||
await aterkallaDelning(delning.kod);
|
||||
skicka({ typ: "kommentar", text: `Delningslänk (${NIVA_LABEL[delning.niva]}) återkallad.` });
|
||||
skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` });
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
Återkalla
|
||||
Revoke
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
@@ -2088,7 +2088,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
onClick={async () => {
|
||||
try {
|
||||
await skapaDelning(arende.id, niva);
|
||||
skicka({ typ: "kommentar", text: `Delningslänk skapad (${NIVA_LABEL[niva]}).` });
|
||||
skicka({ typ: "kommentar", text: `Share link created (${NIVA_LABEL[niva]}).` });
|
||||
uppdatera();
|
||||
} catch {
|
||||
setFel("Could not create a share — requires sign-in and a synchronized case.");
|
||||
@@ -2100,8 +2100,8 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
Kund: det kunddelbara · Extern partner: även hypoteser (märkta ej verifierade) · Intern: full insyn.
|
||||
Filtreringen sker på servern.
|
||||
Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) ·
|
||||
Internal: full visibility. Filtering happens on the server.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -2164,8 +2164,8 @@ function BriefFlik({
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="Review of the evidence">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
Låt systemet gå igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
|
||||
observationer, luckor i dokumentationen och slutsatser som saknar stöd.
|
||||
Have the system review the whole record before you hand it on: contradictions between
|
||||
observations, gaps in the documentation, and conclusions that lack support.
|
||||
</p>
|
||||
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
||||
{granskning === "fel" && (
|
||||
@@ -2345,22 +2345,22 @@ function RapportFlik({
|
||||
))}
|
||||
{!godkand && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
Rapporten kan inte genereras förrän varje underkänd rad har evidens eller ett dokumenterat undantag
|
||||
The report cannot be generated until every failing row has evidence or a documented exemption
|
||||
(”Underlag kan inte tas fram” i guiden).
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Customer report">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas — bilder kan
|
||||
innehålla uppgifter om andra kunder.
|
||||
A shareable summary of the work performed. Review the content before the report is shared — images may
|
||||
contain details about other customers.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" disabled={!godkand} onClick={() => window.print()}>
|
||||
Skriv ut / PDF
|
||||
Print / PDF
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={exporteraJson}>
|
||||
Exportera JSON
|
||||
Export JSON
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
||||
@@ -2383,7 +2383,7 @@ function RapportFlik({
|
||||
)
|
||||
)}
|
||||
<p className="mt-2 text-[12px] text-[#707070]">
|
||||
Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare).
|
||||
The share link requires the case to be synchronized (signed-in user).
|
||||
</p>
|
||||
</Panel>
|
||||
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
|
||||
@@ -2506,7 +2506,7 @@ function RapportFlik({
|
||||
["Parts", h.delar],
|
||||
])
|
||||
) : (
|
||||
<p className="text-[14px] text-[#9A6700]">Ingen åtgärd utförd — {h.motivering}</p>
|
||||
<p className="text-[14px] text-[#9A6700]">No action performed — {h.motivering}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2565,7 +2565,7 @@ function RapportFlik({
|
||||
</Panel>
|
||||
) : null}
|
||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
||||
Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd.
|
||||
Generated from the case log · Observations and measurements are reported without conclusions that lack support.
|
||||
</p>
|
||||
<Ritningsstampel arende={arende} />
|
||||
<Felanmalan arende={arende} metodik={metodik ?? null} />
|
||||
@@ -2642,8 +2642,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
return (
|
||||
<Panel rubrik="Report a problem with the system">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden
|
||||
skickas.
|
||||
Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
@@ -2664,14 +2663,14 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
platshallare="Whoever answers cannot see your screen."
|
||||
flerRad
|
||||
/>
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Anmäld som {utfall.ok}.</p>}
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Reported as {utfall.ok}.</p>}
|
||||
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||
{skickar ? "Sending" : "Send report"}
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Stäng
|
||||
Close
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
@@ -47,7 +47,7 @@ function PlattformInloggning() {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-[14px]">
|
||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||
{konto.organisation} — synk och beslutsstöd aktiva.
|
||||
{konto.organisation} — synchronization and decision support active.
|
||||
</p>
|
||||
<button
|
||||
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
|
||||
@@ -56,7 +56,7 @@ function PlattformInloggning() {
|
||||
setKonto(null);
|
||||
}}
|
||||
>
|
||||
Logga ut
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||
@@ -80,8 +80,7 @@ function PlattformInloggning() {
|
||||
{lage === "stangd" ? (
|
||||
<>
|
||||
<p className="mb-2 text-[#333333]">
|
||||
Logga in för synk mellan enheter, samarbete i ärenden och beslutsstöd. Utan inloggning
|
||||
arbetar appen i lokalt läge. Ny organisation? Skapa konto — du blir systemadministratör.
|
||||
Sign in for synchronization across devices, collaboration on cases and decision support. Without sign-in the app works in local mode. New organization? Create an account — you become the system administrator.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("registrera")}>
|
||||
@@ -117,7 +116,7 @@ function PlattformInloggning() {
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
type="submit"
|
||||
@@ -165,7 +164,7 @@ function AnvandarAdmin() {
|
||||
uppdatera();
|
||||
}}
|
||||
>
|
||||
Hantera användare i organisationen
|
||||
Manage users in the organization
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
);
|
||||
@@ -209,7 +208,7 @@ function AnvandarAdmin() {
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-1 text-[12px] text-[#707070]">
|
||||
En avstängning gäller omedelbart — pågående sessioner på personens enheter upphör direkt.
|
||||
Disabling takes effect immediately — active sessions on that person's devices end at once.
|
||||
</p>
|
||||
<form
|
||||
className="mt-3"
|
||||
@@ -248,7 +247,7 @@ function AnvandarAdmin() {
|
||||
</div>
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
||||
Skapa användare
|
||||
Create user
|
||||
</StorKnapp>
|
||||
</form>
|
||||
</Panel>
|
||||
@@ -273,7 +272,7 @@ export default function Arendelista() {
|
||||
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||
<Panel rubrik="Who is working?">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja.
|
||||
All work is logged with user and timestamp. Enter your name to begin.
|
||||
</p>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -283,7 +282,7 @@ export default function Arendelista() {
|
||||
>
|
||||
<TextFalt label="Name" varde={namn} satt={setNamn} platshallare="e.g. Anna" />
|
||||
<StorKnapp type="submit" disabled={!namn.trim()}>
|
||||
Fortsätt
|
||||
Continue
|
||||
</StorKnapp>
|
||||
</form>
|
||||
</Panel>
|
||||
@@ -330,11 +329,10 @@ export default function Arendelista() {
|
||||
{alla.length === 0 && (
|
||||
<Panel rubrik="Get started">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
Inga ärenden ännu. Starta med att identifiera ett objekt — eller utforska ett färdigt demoärende
|
||||
med komplett arbetslogg, brief och kundrapport.
|
||||
No cases yet. Start by identifying an object — or explore a finished demo case with a complete work log, brief and customer report.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
||||
Skapa demoärende (Volvo XC60, vibration)
|
||||
Create demo case (Volvo XC60, vibration)
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -353,11 +351,7 @@ export default function Arendelista() {
|
||||
|
||||
<Panel rubrik="Decision support">
|
||||
<p className="text-[#333333]">
|
||||
När du är inloggad hjälper systemet dig medan du arbetar: föreslår nästa steg utifrån det du
|
||||
dokumenterar, granskar hela ärendet på begäran och skriver utkast till överlämningen. Den
|
||||
skiljer alltid på verifierat och hypotes — och slår aldrig fast en felorsak som inte är
|
||||
bekräftad. Inget att installera eller ställa in; allt ingår i tjänsten. Utan inloggning
|
||||
guidar checklistan dig steg för steg.
|
||||
When you are signed in the system helps while you work: it proposes the next step from what you document, reviews the whole case on request and drafts the handover. It always separates verified from hypothesis — and never states a root cause that is not confirmed. Nothing to install or configure; it is part of the service. Without sign-in the checklist guides you step by step.
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
@@ -372,7 +366,7 @@ export default function Arendelista() {
|
||||
<div className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
Ärende #{arende.nummer}
|
||||
Case #{arende.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
|
||||
@@ -86,13 +86,11 @@ function Integrationer() {
|
||||
return (
|
||||
<Panel rubrik="Brand-specific connectors">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Organisationens egna avtal hos tillverkare och dataleverantörer. Uppgifterna lagras krypterat på
|
||||
plattformen och används bara av servern — de skickas aldrig till teknikernas enheter.
|
||||
The organization's own agreements with manufacturers and data providers. The details are stored encrypted on the platform and used only by the server — they are never sent to technicians' devices.
|
||||
</p>
|
||||
{!krypteringKlar && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
Kryptering är inte konfigurerad i driften (INTEGRATION_NYCKEL) — uppgifter kan inte sparas förrän
|
||||
nyckeln finns.
|
||||
Encryption is not configured in this deployment (INTEGRATION_NYCKEL) — details cannot be saved until the key exists.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -132,7 +130,7 @@ function Integrationer() {
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => { setOppen(null); setFel(""); }}>
|
||||
Avbryt
|
||||
Cancel
|
||||
</StorKnapp>
|
||||
<StorKnapp
|
||||
disabled={!krypteringKlar}
|
||||
@@ -149,11 +147,11 @@ function Integrationer() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Spara uppgifter
|
||||
Save details
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
Hemliga fält visas aldrig igen efter sparande — fyll i på nytt för att byta värde.
|
||||
Secret fields are never shown again after saving — enter a new value to replace one.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -182,7 +180,7 @@ function Integrationer() {
|
||||
ladda();
|
||||
}}
|
||||
>
|
||||
Ta bort
|
||||
Delete
|
||||
</StorKnapp>
|
||||
)}
|
||||
</div>
|
||||
@@ -190,7 +188,7 @@ function Integrationer() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Uppgifter sparade för {sparat}.</p>}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Details saved for {sparat}.</p>}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -213,7 +211,7 @@ export default function Installningar() {
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Organisationens inställningar hanteras av er systemadministratör.
|
||||
Organization settings are managed by your system administrator.
|
||||
</p>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -246,7 +244,7 @@ export default function Installningar() {
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[#333333]">
|
||||
Välj vad som visas när ett nytt ärende startas.{" "}
|
||||
Choose what is shown when a new case is started.{" "}
|
||||
{inloggad
|
||||
? "The setting applies to everyone in the organization."
|
||||
: "In local mode the setting applies to this device; a signed-in system administrator governs the whole organization."}
|
||||
|
||||
@@ -177,8 +177,7 @@ export default function NyttArende() {
|
||||
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel rubrik="Scan work order">
|
||||
<p className="mb-3 text-[#333333]">
|
||||
Fota arbetsorderns framsida. Systemet läser kund-, fordons- och ärendeuppgifter automatiskt —
|
||||
du granskar bara osäkra fält och trycker Starta diagnos.
|
||||
Photograph the front of the work order. The system reads customer, vehicle and case details automatically — you review only the uncertain fields and press Start diagnosis.
|
||||
</p>
|
||||
<input
|
||||
ref={skanRef}
|
||||
@@ -201,7 +200,7 @@ export default function NyttArende() {
|
||||
</Panel>
|
||||
<Panel>
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
Fyll i manuellt
|
||||
Fill in manually
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -217,7 +216,7 @@ export default function NyttArende() {
|
||||
>
|
||||
{tolkning.demo && (
|
||||
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
Demo-tolkning — plattformens dokumenttolkning kräver inloggning. Värdena nedan är exempeldata.
|
||||
Demo interpretation — the platform's document interpretation requires sign-in. The values below are sample data.
|
||||
</p>
|
||||
)}
|
||||
<GranskaTolkning
|
||||
@@ -255,7 +254,7 @@ export default function NyttArende() {
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] text-[#707070]">
|
||||
QR-kod, streckkod och typskylt kan läsas av i nästa steg — eller ange identiteten manuellt.
|
||||
QR code, barcode and type plate can be read in the next step — or enter the identity manually.
|
||||
</p>
|
||||
</Panel>
|
||||
<Panel rubrik="Read the identity">
|
||||
@@ -275,17 +274,17 @@ export default function NyttArende() {
|
||||
<div>
|
||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Rikta kameran mot QR-koden, streckkoden eller VIN-etiketten.
|
||||
Point the camera at the QR code, barcode or VIN label.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
||||
Avbryt skanning
|
||||
Cancel scanning
|
||||
</StorKnapp>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{stodStreckkod() && (
|
||||
<StorKnapp variant="sekundar" onClick={oppnaSkanning}>
|
||||
Skanna QR/streckkod
|
||||
Scan QR or barcode
|
||||
</StorKnapp>
|
||||
)}
|
||||
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
||||
@@ -295,8 +294,8 @@ export default function NyttArende() {
|
||||
)}
|
||||
{avlastKod && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">
|
||||
Avläst {avlastKod.typ}: {avlastKod.varde}
|
||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — kontrollera värdet nedan.
|
||||
Read {avlastKod.typ}: {avlastKod.varde}
|
||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
||||
</p>
|
||||
)}
|
||||
{kodFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{kodFel}</p>}
|
||||
@@ -306,7 +305,7 @@ export default function NyttArende() {
|
||||
<TextFalt label="Description (make, model, year)" varde={beskrivning} satt={setBeskrivning} platshallare="T.ex. Volvo XC60 D4 2019" />
|
||||
<TextFalt label="Customer (optional)" varde={kund} satt={setKund} platshallare="e.g. Alex Meyer" />
|
||||
<StorKnapp disabled={!identifierare.trim()} onClick={() => setSteg("bekrafta")}>
|
||||
Fortsätt
|
||||
Continue
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -325,7 +324,7 @@ export default function NyttArende() {
|
||||
</Panel>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
Ändra
|
||||
Edit
|
||||
</StorKnapp>
|
||||
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Correct object — continue</StorKnapp>
|
||||
</div>
|
||||
@@ -420,7 +419,7 @@ function GranskaTolkning({
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
{vald?.omrade
|
||||
? `Markerat: ${vald.etikett} (ungefärlig position)`
|
||||
? `Markerat: ${vald.etikett} (approximate position)`
|
||||
: "Select a field to see where in the document the value was found."}
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -467,7 +466,7 @@ function GranskaTolkning({
|
||||
}}
|
||||
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
|
||||
>
|
||||
Bekräfta
|
||||
Confirm
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -480,12 +479,12 @@ function GranskaTolkning({
|
||||
|
||||
{obekraftade.length > 0 && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
{obekraftade.length} osäkra fält kräver bekräftelse eller korrigering innan diagnosen startar.
|
||||
{obekraftade.length} uncertain fields require confirmation or correction before the diagnosis starts.
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={avbryt}>
|
||||
Skanna om
|
||||
Scan again
|
||||
</StorKnapp>
|
||||
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
||||
{startar ? "Starting …" : "Start diagnosis"}
|
||||
|
||||
@@ -40,7 +40,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
hamtaAnvandare().then(setPersoner).catch(() => setPersoner([]));
|
||||
}}
|
||||
>
|
||||
Omfördela
|
||||
Reassign
|
||||
</StorKnapp>
|
||||
);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
|
||||
onClick={() => setOppen(false)}
|
||||
>
|
||||
Avbryt
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@ export default function Oversikt() {
|
||||
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
|
||||
The overview requires supervisor or administrator rights on the platform.
|
||||
</p>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
@@ -177,7 +177,7 @@ export default function Oversikt() {
|
||||
</div>
|
||||
{avslutade.length > 0 && (
|
||||
<p className="mt-3 text-center text-[#333333]">
|
||||
Genomsnittlig ledtid för avslutade: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
@@ -195,7 +195,7 @@ export default function Oversikt() {
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
Ur dokumenterade felorsaksanalyser — visar mönster som slitage, tidigare reparationer eller möjliga konstruktionsproblem.
|
||||
From documented root cause analyses — shows patterns such as wear, previous repairs or possible design problems.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
@@ -206,7 +206,7 @@ export default function Oversikt() {
|
||||
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
Ärende #{rad.nummer}
|
||||
Case #{rad.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
@@ -220,12 +220,12 @@ export default function Oversikt() {
|
||||
{rad.felbeskrivning && <p className="text-[14px] text-[#333333]">”{rad.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-[12px] text-[#707070]">
|
||||
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||
{` · ${rad.antal_handelser} händelser`}
|
||||
{` · ${rad.antal_handelser} events`}
|
||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => oppna(rad)}>
|
||||
Öppna ärendet
|
||||
Open the case
|
||||
</StorKnapp>
|
||||
{!rad.avslutat && <Omfordela rad={rad} vidKlar={() => hamtaOversikt().then(setRader)} />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user