Diagnostikgränssnittet på engelska
Portalen var engelsk, men själva arbetsverktyget — det tekniker faktiskt
använder — var svenskt rakt igenom. "Engelska som standard" gällde alltså
skyltfönstret och inte produkten.
Omkring 400 användarsynliga strängar är översatta: etiketter, knappar,
frågor, felmeddelanden, platshållare och de härledda projektionerna
(brief, tidrapport, kundrapport).
Bytet gjordes med en karta och ett skript som bara byter EXAKTA träffar
inom citattecken eller mellan JSX-taggar, aldrig delsträngar. Skriptet
rapporterar dessutom vilka poster som inte fick någon träff — fem
strängar stod som JSX-text efter ett element och syntes därför inte för
mönstret. Utan den rapporten hade de blivit kvar tysta.
Typsystemet fångade det som var farligt. Åtta anrop skickade händelsen
typ: "kommentar" och hade blivit typ: "comment" — en händelsetyp är DATA
i en append-only logg, inte text. Samma sak för `krav === "kommentar"`
och för ArbetsorderGrupp. Det är precis den sortens fel en
massöversättning gör, och precis därför den ska göras i ett projekt som
kompilerar.
Skillnaden mellan etikett och värde är genomgående kodad i domänmodellen
— nycklarna är `godkant`, `hog`, `symptomet_borta`, och det översatta är
_LABEL-posterna. Det var det som gjorde bytet möjligt att göra maskinellt
utan att röra loggen.
En regression från förra commiten är rättad: Slutsats.tsx sållade bort
hypotesbristerna med `text.includes("bemöts inte")`. När granskningen
började svara på engelska slutade jämförelsen tyst att matcha. Den går nu
på nyckeln, som inte byter språk.
Två saker att veta om befintliga installationer: en organisation som
sparat egna objekttyper eller identifieringsmetoder på svenska får
standardlistan tillbaka, eftersom okända värden filtreras bort, och
streckkodsläsarens `typ` matchar nu de engelska metodnamnen. Båda
degraderar till standard i stället för att gå sönder.
Datumen står kvar i ISO-format (sv-SE ger YYYY-MM-DD). Det är inte en
kvarlämnad svenskhet utan det entydiga formatet — en verkstadsrapport
ska inte kunna läsas som 03/04 åt två håll.
Kvar: ecm.ts (regelverkets rådtexter) och metodiker.mjs (metodikernas
steg och kontroller). De tas härnäst.
602 tester, typkontroll och lint rena.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -46,10 +46,10 @@ export function Bild({
|
|||||||
delningskod?: string;
|
delningskod?: string;
|
||||||
}) {
|
}) {
|
||||||
const kalla = useKalla(bilaga, delningskod);
|
const kalla = useKalla(bilaga, delningskod);
|
||||||
if (kalla === "laddar") return <Platshallare text="Hämtar bild …" />;
|
if (kalla === "laddar") return <Platshallare text="Retrieving image …" />;
|
||||||
// Ärligt om att bilden inte gick att hämta, i stället för en trasig
|
// Ärligt om att bilden inte gick att hämta, i stället för en trasig
|
||||||
// bildikon som lämnar teknikern i tvivel om vad som dokumenterats.
|
// bildikon som lämnar teknikern i tvivel om vad som dokumenterats.
|
||||||
if (!kalla) return <Platshallare text="Bilden kunde inte hämtas." />;
|
if (!kalla) return <Platshallare text="The image could not be retrieved." />;
|
||||||
return <img src={kalla} alt={alt} className={className} />;
|
return <img src={kalla} alt={alt} className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ export function Klipp({
|
|||||||
delningskod?: string;
|
delningskod?: string;
|
||||||
}) {
|
}) {
|
||||||
const kalla = useKalla(bilaga, delningskod);
|
const kalla = useKalla(bilaga, delningskod);
|
||||||
if (kalla === "laddar") return <Platshallare text="Hämtar videoklipp …" />;
|
if (kalla === "laddar") return <Platshallare text="Retrieving video …" />;
|
||||||
if (!kalla) return <Platshallare text="Videoklippet kunde inte hämtas." />;
|
if (!kalla) return <Platshallare text="The video could not be retrieved." />;
|
||||||
return <video src={kalla} controls className={className} />;
|
return <video src={kalla} controls className={className} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function BeslutsKnappar({
|
|||||||
<>
|
<>
|
||||||
<label className="mb-2 block">
|
<label className="mb-2 block">
|
||||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||||
{val === "avbojt" ? "Kort motivering (valfritt)" : "Kommentar (valfritt)"}
|
{val === "avbojt" ? "Short rationale (optional)" : "Comment (optional)"}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
value={kommentar}
|
value={kommentar}
|
||||||
@@ -64,7 +64,7 @@ function BeslutsKnappar({
|
|||||||
setSkickar(false);
|
setSkickar(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{skickar ? "Skickar …" : "Skicka besked"}
|
{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.
|
Beskedet registreras i ärendet och kan inte ändras här — kontakta verkstaden om du ändrar dig.
|
||||||
@@ -78,14 +78,14 @@ function BeslutsKnappar({
|
|||||||
function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boolean }) {
|
function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boolean }) {
|
||||||
const idn = arendeidentitet(arende);
|
const idn = arendeidentitet(arende);
|
||||||
const falt: [string, string | undefined][] = [
|
const falt: [string, string | undefined][] = [
|
||||||
["Arbetsorder", idn.arbetsorder],
|
["Work order", idn.arbetsorder],
|
||||||
["Claim", idn.claim],
|
["Claim", idn.claim],
|
||||||
["Skadenr", idn.skadenummer],
|
["Claim no.", idn.skadenummer],
|
||||||
["Regnr", idn.identifierare],
|
["Reg. no.", idn.identifierare],
|
||||||
["VIN", idn.vin],
|
["VIN", idn.vin],
|
||||||
["Miltal", idn.miltal],
|
["Mileage", idn.miltal],
|
||||||
["Ansvarig tekniker", idn.ansvarig],
|
["Responsible technician", idn.ansvarig],
|
||||||
["Status", avslutat ? "Avslutat" : "Felsökning pågår"],
|
["Status", avslutat ? "Closed" : "Diagnosis in progress"],
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-11 z-10 mb-3 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
|
<div className="sticky top-11 z-10 mb-3 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
|
||||||
@@ -142,14 +142,14 @@ export function DelatArendeVy({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
rubrik={b.objekt?.beskrivning ?? "Ärende"}
|
rubrik={b.objekt?.beskrivning ?? "Case"}
|
||||||
hoger={
|
hoger={
|
||||||
<span
|
<span
|
||||||
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
|
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
|
||||||
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
|
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{avslutat ? "Avslutat" : "Felsökning pågår"}
|
{avslutat ? "Closed" : "Diagnosis in progress"}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -163,7 +163,7 @@ export function DelatArendeVy({
|
|||||||
flottansvarig. De ska få bilden på fem sekunder och sedan kunna
|
flottansvarig. De ska få bilden på fem sekunder och sedan kunna
|
||||||
gå djupare — inte tvärtom. Texten härleds ur det nivåfiltrerade
|
gå djupare — inte tvärtom. Texten härleds ur det nivåfiltrerade
|
||||||
underlaget, så den kan aldrig avslöja något som nivån döljer. */}
|
underlaget, så den kan aldrig avslöja något som nivån döljer. */}
|
||||||
<Panel rubrik="Sammanfattning">
|
<Panel rubrik="Summary">
|
||||||
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
|
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
@@ -174,13 +174,13 @@ export function DelatArendeVy({
|
|||||||
if (!post || post.handelse.typ !== "slutsats") return null;
|
if (!post || post.handelse.typ !== "slutsats") return null;
|
||||||
const h = post.handelse;
|
const h = post.handelse;
|
||||||
const rader: [string, string][] = [
|
const rader: [string, string][] = [
|
||||||
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
|
[h.orsakFastställd === false ? "Reason the cause could not be established" : "Rationale", h.motivering],
|
||||||
["Uteslutna alternativ", h.uteslutet],
|
["Dismissed alternatives", h.uteslutet],
|
||||||
...(h.atgardsval ? ([["Val av åtgärd", h.atgardsval]] as [string, string][]) : []),
|
...(h.atgardsval ? ([["Choice of action", h.atgardsval]] as [string, string][]) : []),
|
||||||
["Kvarstående osäkerhet", h.kvarstaende],
|
["Remaining uncertainty", h.kvarstaende],
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Slutsats och motivering">
|
<Panel rubrik="Closing statement and rationale">
|
||||||
<dl className="text-[14px] leading-[22px]">
|
<dl className="text-[14px] leading-[22px]">
|
||||||
{rader.map(([etikett, text]) => (
|
{rader.map(([etikett, text]) => (
|
||||||
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
|
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
|
||||||
@@ -198,13 +198,13 @@ export function DelatArendeVy({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{b.felbeskrivning && (
|
{b.felbeskrivning && (
|
||||||
<Panel rubrik="Kundens felbeskrivning">
|
<Panel rubrik="Customer's fault description">
|
||||||
<p className="text-[14px]">”{b.felbeskrivning}”</p>
|
<p className="text-[14px]">”{b.felbeskrivning}”</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{forslag.length > 0 && (
|
{forslag.length > 0 && (
|
||||||
<Panel rubrik="Åtgärdsförslag">
|
<Panel rubrik="Proposed action">
|
||||||
{forslag.map((p) => {
|
{forslag.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgardsforslag") return null;
|
if (h.typ !== "atgardsforslag") return null;
|
||||||
@@ -232,7 +232,7 @@ export function DelatArendeVy({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel rubrik="Aktuell status">
|
<Panel rubrik="Current status">
|
||||||
{b.utfordaKontroller.map((k, i) => (
|
{b.utfordaKontroller.map((k, i) => (
|
||||||
<p key={`u${i}`} className="py-0.5 text-[14px]"><span className="text-[#1E6B34]"><IkonCheck /></span> {k.text}</p>
|
<p key={`u${i}`} className="py-0.5 text-[14px]"><span className="text-[#1E6B34]"><IkonCheck /></span> {k.text}</p>
|
||||||
))}
|
))}
|
||||||
@@ -253,7 +253,7 @@ export function DelatArendeVy({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{bilder.length > 0 && (
|
{bilder.length > 0 && (
|
||||||
<Panel rubrik="Bilder">
|
<Panel rubrik="Images">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{bilder.map((bild, i) => (
|
{bilder.map((bild, i) => (
|
||||||
<figure key={i}>
|
<figure key={i}>
|
||||||
@@ -266,7 +266,7 @@ export function DelatArendeVy({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{matvarden.length > 0 && (
|
{matvarden.length > 0 && (
|
||||||
<Panel rubrik="Mätvärden">
|
<Panel rubrik="Measurements">
|
||||||
<table className="w-full text-[14px]">
|
<table className="w-full text-[14px]">
|
||||||
<tbody>
|
<tbody>
|
||||||
{matvarden.map((p) => {
|
{matvarden.map((p) => {
|
||||||
@@ -287,7 +287,7 @@ export function DelatArendeVy({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel rubrik="Tidslinje">
|
<Panel rubrik="Timeline">
|
||||||
{kundposter.map((post) => (
|
{kundposter.map((post) => (
|
||||||
<p key={post.id} className="py-0.5 text-[13px]">
|
<p key={post.id} className="py-0.5 text-[13px]">
|
||||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||||
@@ -297,12 +297,12 @@ export function DelatArendeVy({
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{!avslutat && pagaende && (
|
{!avslutat && pagaende && (
|
||||||
<Panel rubrik="Rekommenderat nästa steg">
|
<Panel rubrik="Recommended next step">
|
||||||
<p className="text-[14px]">{pagaende}</p>
|
<p className="text-[14px]">{pagaende}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel rubrik="Arbetstid">
|
<Panel rubrik="Labour time">
|
||||||
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
||||||
{tidsfordelningsRader(arende, nu).map((r) => (
|
{tidsfordelningsRader(arende, nu).map((r) => (
|
||||||
<p key={r.label} className="text-[#333333]">
|
<p key={r.label} className="text-[#333333]">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => (lyssnar ? stoppa() : starta())}
|
onClick={() => (lyssnar ? stoppa() : starta())}
|
||||||
aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
|
aria-label={lyssnar ? "Stop recording" : "Start voice input"}
|
||||||
className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${
|
className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${
|
||||||
lyssnar
|
lyssnar
|
||||||
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
|
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
|
||||||
|
|||||||
@@ -25,35 +25,37 @@ import { FARG, Etikett, Knapp, Statusmärke } from "@/alva/komponenter";
|
|||||||
|
|
||||||
interface Brist {
|
interface Brist {
|
||||||
falt: string;
|
falt: string;
|
||||||
|
/** Katalognyckeln. Saknas för brister som bara har en färdig text. */
|
||||||
|
nyckel?: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FALT = [
|
const FALT = [
|
||||||
{
|
{
|
||||||
id: "motivering" as const,
|
id: "motivering" as const,
|
||||||
etikett: "Motivering",
|
etikett: "Rationale",
|
||||||
fraga: "Varför följer slutsatsen av underlaget?",
|
fraga: "Why does the conclusion follow from the evidence?",
|
||||||
hjalp: "Knyt mätvärden, foton och kontroller till slutsatsen. Vad i evidensen gör att just detta följer?",
|
hjalp: "Tie measurements, photographs and checks to the conclusion. What in the evidence makes this follow?",
|
||||||
rader: 4,
|
rader: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "uteslutet" as const,
|
id: "uteslutet" as const,
|
||||||
etikett: "Uteslutna alternativ",
|
etikett: "Dismissed alternatives",
|
||||||
fraga: "Vad övervägdes, och varför föll det bort?",
|
fraga: "What was considered, and why was it dismissed?",
|
||||||
hjalp: "En felsökning utan uteslutna alternativ är en gissning som råkade stämma.",
|
hjalp: "A diagnosis without dismissed alternatives is a guess that happened to be right.",
|
||||||
rader: 3,
|
rader: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "atgardsval" as const,
|
id: "atgardsval" as const,
|
||||||
etikett: "Val av åtgärd",
|
etikett: "Choice of action",
|
||||||
fraga: "Varför denna åtgärd och inte en annan?",
|
fraga: "Why this action and not another?",
|
||||||
hjalp: "Byte eller justering? Hela enheten eller en del? Skälet läses av den som granskar kostnaden.",
|
hjalp: "Replacement or adjustment? The whole unit or a part? The reason is read by whoever reviews the cost.",
|
||||||
rader: 2,
|
rader: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "kvarstaende" as const,
|
id: "kvarstaende" as const,
|
||||||
etikett: "Kvarstående osäkerhet",
|
etikett: "Remaining uncertainty",
|
||||||
fraga: "Vad är fortfarande osäkert?",
|
fraga: "What remains uncertain?",
|
||||||
hjalp: 'Får vara "inget" — men det måste sägas aktivt.',
|
hjalp: 'Får vara "inget" — men det måste sägas aktivt.',
|
||||||
rader: 2,
|
rader: 2,
|
||||||
},
|
},
|
||||||
@@ -82,7 +84,13 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
);
|
);
|
||||||
const obemötta: string[] = useMemo(() => obemottaHypoteser(handelser, förslag), [handelser, förslag]);
|
const obemötta: string[] = useMemo(() => obemottaHypoteser(handelser, förslag), [handelser, förslag]);
|
||||||
|
|
||||||
const brist = (falt: string) => brister.find((b) => b.falt === falt && !b.text.includes("bemöts inte"));
|
// Hypotesbristerna hör till fältet "uteslutet" men är inte en brist i
|
||||||
|
// fältet — de listas för sig nedan. De sållas därför bort här, på
|
||||||
|
// NYCKELN och inte på texten: texten är översatt och byter språk med
|
||||||
|
// organisationen, och en jämförelse mot en svensk fras slutade tyst
|
||||||
|
// att matcha när standardspråket blev engelska.
|
||||||
|
const brist = (falt: string) =>
|
||||||
|
brister.find((b) => b.falt === falt && b.nyckel !== "slutsats.hypotes_obemott");
|
||||||
const klar = brister.length === 0;
|
const klar = brister.length === 0;
|
||||||
|
|
||||||
const arbeteUtfört = handelser.some((h) => h.typ === "atgard_utford" && h.utford);
|
const arbeteUtfört = handelser.some((h) => h.typ === "atgard_utford" && h.utford);
|
||||||
@@ -115,8 +123,8 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
Men varför den inte kunde det är fortfarande ett varför. */}
|
Men varför den inte kunde det är fortfarande ett varför. */}
|
||||||
<div className="mb-4 flex flex-wrap gap-2">
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
{[
|
{[
|
||||||
{ värde: true, text: "Orsak fastställd" },
|
{ värde: true, text: "Cause established" },
|
||||||
{ värde: false, text: "Orsak kunde inte fastställas" },
|
{ värde: false, text: "Cause could not be established" },
|
||||||
].map((val) => (
|
].map((val) => (
|
||||||
<button
|
<button
|
||||||
key={String(val.värde)}
|
key={String(val.värde)}
|
||||||
@@ -137,7 +145,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
|
|
||||||
{obemötta.length > 0 && (
|
{obemötta.length > 0 && (
|
||||||
<div className="mb-4 border-2 px-4 py-2" style={{ borderColor: FARG.graphite }}>
|
<div className="mb-4 border-2 px-4 py-2" style={{ borderColor: FARG.graphite }}>
|
||||||
<Etikett ton="graphite">Hypoteser i loggen som behöver bemötas</Etikett>
|
<Etikett ton="graphite">Hypotheses in the log that need addressing</Etikett>
|
||||||
<ul className="mt-2 text-[13px] leading-[20px]" style={{ color: FARG.graphite }}>
|
<ul className="mt-2 text-[13px] leading-[20px]" style={{ color: FARG.graphite }}>
|
||||||
{obemötta.map((text) => (
|
{obemötta.map((text) => (
|
||||||
<li key={text} className="flex gap-2">
|
<li key={text} className="flex gap-2">
|
||||||
@@ -154,7 +162,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
|
|
||||||
{FALT.filter((f) => f.id !== "atgardsval" || arbeteUtfört).map((f) => {
|
{FALT.filter((f) => f.id !== "atgardsval" || arbeteUtfört).map((f) => {
|
||||||
const fel = brist(f.id);
|
const fel = brist(f.id);
|
||||||
const etikett = f.id === "motivering" && !fastställd ? "Skäl till att orsaken inte fastställts" : f.etikett;
|
const etikett = f.id === "motivering" && !fastställd ? "Reason the cause could not be established" : f.etikett;
|
||||||
return (
|
return (
|
||||||
<div key={f.id} className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
|
<div key={f.id} className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
|
||||||
<label
|
<label
|
||||||
@@ -191,7 +199,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-4">
|
<div className="mt-4 flex flex-wrap items-center gap-4">
|
||||||
<Knapp disabled={!klar} onClick={() => skicka(förslag)}>
|
<Knapp disabled={!klar} onClick={() => skicka(förslag)}>
|
||||||
{befintlig ? "Uppdatera slutsats" : "Registrera slutsats"}
|
{befintlig ? "Update closing statement" : "Record closing statement"}
|
||||||
</Knapp>
|
</Knapp>
|
||||||
{!klar && (
|
{!klar && (
|
||||||
<span className="text-[12px]" style={{ color: FARG.steel }}>
|
<span className="text-[12px]" style={{ color: FARG.steel }}>
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ describe("arbetsorderskanning", () => {
|
|||||||
expect(falt.map((f) => f.id)).toEqual(["fordon_regnr", "fordon_vin"]);
|
expect(falt.map((f) => f.id)).toEqual(["fordon_regnr", "fordon_vin"]);
|
||||||
expect(falt[0].konfidens).toBe(1);
|
expect(falt[0].konfidens).toBe(1);
|
||||||
expect(falt[0].varde).toBe("abc123");
|
expect(falt[0].varde).toBe("abc123");
|
||||||
expect(falt[0].grupp).toBe("Personbil");
|
expect(falt[0].grupp).toBe("Vehicle");
|
||||||
expect(falt[1].omrade?.x).toBe(0.5);
|
expect(falt[1].omrade?.x).toBe(0.5);
|
||||||
expect(() => normaliseraArbetsorder({})).toThrow();
|
expect(() => normaliseraArbetsorder({})).toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ describe("organisationsinställningar", () => {
|
|||||||
|
|
||||||
it("okända värden filtreras bort och ordningen följer standardlistan", () => {
|
it("okända värden filtreras bort och ordningen följer standardlistan", () => {
|
||||||
const inst = normalisera({
|
const inst = normalisera({
|
||||||
objekttyper: ["Hydraulik", "Påhittad typ", "Personbil"],
|
objekttyper: ["Hydraulics", "Påhittad typ", "Passenger car"],
|
||||||
identifieringsmetoder: ["VIN"],
|
identifieringsmetoder: ["VIN"],
|
||||||
});
|
});
|
||||||
expect(inst.objekttyper).toEqual(["Personbil", "Hydraulik"]);
|
expect(inst.objekttyper).toEqual(["Passenger car", "Hydraulics"]);
|
||||||
expect(inst.identifieringsmetoder).toEqual(["VIN"]);
|
expect(inst.identifieringsmetoder).toEqual(["VIN"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@ describe("organisationsinställningar", () => {
|
|||||||
it("sparade val på enheten läses tillbaka", () => {
|
it("sparade val på enheten läses tillbaka", () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"gf-installningar",
|
"gf-installningar",
|
||||||
JSON.stringify({ objekttyper: ["Elsystem"], identifieringsmetoder: ["Serienummer", "Manuell inmatning"] }),
|
JSON.stringify({ objekttyper: ["Electrical system"], identifieringsmetoder: ["Serial number", "Manual entry"] }),
|
||||||
);
|
);
|
||||||
const inst = lastaInstallningar();
|
const inst = lastaInstallningar();
|
||||||
expect(inst.objekttyper).toEqual(["Elsystem"]);
|
expect(inst.objekttyper).toEqual(["Electrical system"]);
|
||||||
expect(inst.identifieringsmetoder).toEqual(["Serienummer", "Manuell inmatning"]);
|
expect(inst.identifieringsmetoder).toEqual(["Serial number", "Manual entry"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function byggArende(poster: [string, Handelse][]): Arende {
|
|||||||
const OBJEKT: Handelse = {
|
const OBJEKT: Handelse = {
|
||||||
typ: "objekt_identifierat",
|
typ: "objekt_identifierat",
|
||||||
objekt: {
|
objekt: {
|
||||||
typ: "Personbil",
|
typ: "Passenger car",
|
||||||
identifierare: "ABC123",
|
identifierare: "ABC123",
|
||||||
identifieringsmetod: "Registreringsnummer",
|
identifieringsmetod: "Registreringsnummer",
|
||||||
beskrivning: "Volvo XC60 D4 2019",
|
beskrivning: "Volvo XC60 D4 2019",
|
||||||
@@ -192,7 +192,7 @@ describe("brief", () => {
|
|||||||
const b = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
const b = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
||||||
expect(b.hypoteser).toEqual([{ text: "Obalans i höger framhjul", niva: "lag" }]);
|
expect(b.hypoteser).toEqual([{ text: "Obalans i höger framhjul", niva: "lag" }]);
|
||||||
expect(b.observationer).not.toContain("Obalans i höger framhjul");
|
expect(b.observationer).not.toContain("Obalans i höger framhjul");
|
||||||
expect(b.tillforlitlighet.some((r) => r.niva === "medel" && /inte verifierad/.test(r.text))).toBe(true);
|
expect(b.tillforlitlighet.some((r) => r.niva === "medel" && /not yet verified/i.test(r.text))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("är regenererbar: samma logg ger alltid samma brief", () => {
|
it("är regenererbar: samma logg ger alltid samma brief", () => {
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ describe("avläst kod klassificeras innan den används", () => {
|
|||||||
// Mellanslag och bindestreck i streckkoder normaliseras bort.
|
// Mellanslag och bindestreck i streckkoder normaliseras bort.
|
||||||
expect(tolkaKod(" yv1dz8256f2123456 ")?.typ).toBe("VIN");
|
expect(tolkaKod(" yv1dz8256f2123456 ")?.typ).toBe("VIN");
|
||||||
// 16 tecken är inget VIN — men kan vara ett serienummer.
|
// 16 tecken är inget VIN — men kan vara ett serienummer.
|
||||||
expect(tolkaKod("YV1DZ8256F212345")?.typ).toBe("Serienummer");
|
expect(tolkaKod("YV1DZ8256F212345")?.typ).toBe("Serial number");
|
||||||
// I/O/Q förekommer aldrig i VIN → klassas som serienummer.
|
// I/O/Q förekommer aldrig i VIN → klassas som serienummer.
|
||||||
expect(tolkaKod("YV1DZ8256F212345O")?.typ).toBe("Serienummer");
|
expect(tolkaKod("YV1DZ8256F212345O")?.typ).toBe("Serial number");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("känner igen svenska registreringsnummer i båda serierna", () => {
|
it("känner igen svenska registreringsnummer i båda serierna", () => {
|
||||||
expect(tolkaKod("ABC123")).toEqual({ varde: "ABC123", typ: "Regnr" });
|
expect(tolkaKod("ABC123")).toEqual({ varde: "ABC123", typ: "Reg. no." });
|
||||||
expect(tolkaKod("abc12a")).toEqual({ varde: "ABC12A", typ: "Regnr" });
|
expect(tolkaKod("abc12a")).toEqual({ varde: "ABC12A", typ: "Reg. no." });
|
||||||
expect(tolkaKod("ABC 123")?.typ).toBe("Regnr");
|
expect(tolkaKod("ABC 123")?.typ).toBe("Reg. no.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("plockar ut identifieraren ur QR-innehåll med fält eller URL", () => {
|
it("plockar ut identifieraren ur QR-innehåll med fält eller URL", () => {
|
||||||
@@ -23,12 +23,12 @@ describe("avläst kod klassificeras innan den används", () => {
|
|||||||
varde: "YV1DZ8256F2123456",
|
varde: "YV1DZ8256F2123456",
|
||||||
typ: "VIN",
|
typ: "VIN",
|
||||||
});
|
});
|
||||||
expect(tolkaKod("REGNR: ABC123")).toEqual({ varde: "ABC123", typ: "Regnr" });
|
expect(tolkaKod("REGNR: ABC123")).toEqual({ varde: "ABC123", typ: "Reg. no." });
|
||||||
expect(tolkaKod("vin YV1DZ8256F2123456")?.typ).toBe("VIN");
|
expect(tolkaKod("vin YV1DZ8256F2123456")?.typ).toBe("VIN");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tar maskin-/serienummer som de är men avvisar fritext och nakna URL:er", () => {
|
it("tar maskin-/serienummer som de är men avvisar fritext och nakna URL:er", () => {
|
||||||
expect(tolkaKod("SN-4471/A")).toEqual({ varde: "SN4471/A", typ: "Serienummer" });
|
expect(tolkaKod("SN-4471/A")).toEqual({ varde: "SN4471/A", typ: "Serial number" });
|
||||||
expect(tolkaKod("https://exempel.se/nagot-helt-annat")).toBeNull();
|
expect(tolkaKod("https://exempel.se/nagot-helt-annat")).toBeNull();
|
||||||
expect(tolkaKod("Kontrollera oljenivån innan start")).toBeNull();
|
expect(tolkaKod("Kontrollera oljenivån innan start")).toBeNull();
|
||||||
expect(tolkaKod(" ")).toBeNull();
|
expect(tolkaKod(" ")).toBeNull();
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export type AiRadTyp = "observation" | "verifierat" | "hypotes" | "rekommendatio
|
|||||||
|
|
||||||
export const AI_RADTYP_LABEL: Record<AiRadTyp, string> = {
|
export const AI_RADTYP_LABEL: Record<AiRadTyp, string> = {
|
||||||
observation: "Observation",
|
observation: "Observation",
|
||||||
verifierat: "Verifierat",
|
verifierat: "Verified",
|
||||||
hypotes: "Hypotes (ej verifierad)",
|
hypotes: "Hypothesis (unverified)",
|
||||||
rekommendation: "Rekommendation",
|
rekommendation: "Recommendation",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AiRad {
|
export interface AiRad {
|
||||||
@@ -58,11 +58,11 @@ export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?
|
|||||||
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
||||||
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
|
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
|
||||||
if (brief.utfordaKontroller.length > 0) {
|
if (brief.utfordaKontroller.length > 0) {
|
||||||
rader.push("Utförda kontroller:");
|
rader.push("Checks performed:");
|
||||||
for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`);
|
for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`);
|
||||||
}
|
}
|
||||||
if (brief.observationer.length > 0) {
|
if (brief.observationer.length > 0) {
|
||||||
rader.push("Observationer och mätvärden:");
|
rader.push("Observations and measurements:");
|
||||||
for (const o of brief.observationer) rader.push(`- ${o}`);
|
for (const o of brief.observationer) rader.push(`- ${o}`);
|
||||||
}
|
}
|
||||||
if (brief.ejKontrollerat.length > 0) {
|
if (brief.ejKontrollerat.length > 0) {
|
||||||
@@ -87,7 +87,7 @@ export function byggGranskningsPrompt(arende: Arende, metodik: Metodik): string
|
|||||||
export function tolkaAiSvar(data: unknown): AiSvar {
|
export function tolkaAiSvar(data: unknown): AiSvar {
|
||||||
const svar = data as AiSvar;
|
const svar = data as AiSvar;
|
||||||
if (!svar || !Array.isArray(svar.rader) || typeof svar.nastaSteg !== "string") {
|
if (!svar || !Array.isArray(svar.rader) || typeof svar.nastaSteg !== "string") {
|
||||||
throw new Error("Oväntat AI-svarsformat");
|
throw new Error("Unexpected AI response format");
|
||||||
}
|
}
|
||||||
const giltiga: AiRadTyp[] = ["observation", "verifierat", "hypotes", "rekommendation"];
|
const giltiga: AiRadTyp[] = ["observation", "verifierat", "hypotes", "rekommendation"];
|
||||||
const rader = svar.rader.filter(
|
const rader = svar.rader.filter(
|
||||||
@@ -101,7 +101,7 @@ export function tolkaAiSvar(data: unknown): AiSvar {
|
|||||||
// (OCR + layoutförståelse) och returnerar strukturerade fält med
|
// (OCR + layoutförståelse) och returnerar strukturerade fält med
|
||||||
// konfidens per värde. Bara osäkra fält behöver granskas.
|
// konfidens per värde. Bara osäkra fält behöver granskas.
|
||||||
|
|
||||||
export type ArbetsorderGrupp = "Kund" | "Personbil" | "Verkstad" | "Ärende";
|
export type ArbetsorderGrupp = "Customer" | "Vehicle" | "Workshop" | "Case";
|
||||||
|
|
||||||
export interface TolkatFalt {
|
export interface TolkatFalt {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -114,36 +114,36 @@ export interface TolkatFalt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ARBETSORDER_FALT: { id: string; grupp: ArbetsorderGrupp; etikett: string }[] = [
|
export const ARBETSORDER_FALT: { id: string; grupp: ArbetsorderGrupp; etikett: string }[] = [
|
||||||
{ id: "kund_namn", grupp: "Kund", etikett: "Namn" },
|
{ id: "kund_namn", grupp: "Customer", etikett: "Name" },
|
||||||
{ id: "kund_foretag", grupp: "Kund", etikett: "Företag" },
|
{ id: "kund_foretag", grupp: "Customer", etikett: "Company" },
|
||||||
{ id: "kund_telefon", grupp: "Kund", etikett: "Telefon" },
|
{ id: "kund_telefon", grupp: "Customer", etikett: "Telephone" },
|
||||||
{ id: "kund_epost", grupp: "Kund", etikett: "E-post" },
|
{ id: "kund_epost", grupp: "Customer", etikett: "E-mail" },
|
||||||
{ id: "fordon_regnr", grupp: "Personbil", etikett: "Regnr" },
|
{ id: "fordon_regnr", grupp: "Vehicle", etikett: "Reg. no." },
|
||||||
{ id: "fordon_vin", grupp: "Personbil", etikett: "VIN" },
|
{ id: "fordon_vin", grupp: "Vehicle", etikett: "VIN" },
|
||||||
{ id: "fordon_marke", grupp: "Personbil", etikett: "Märke" },
|
{ id: "fordon_marke", grupp: "Vehicle", etikett: "Make" },
|
||||||
{ id: "fordon_modell", grupp: "Personbil", etikett: "Modell" },
|
{ id: "fordon_modell", grupp: "Vehicle", etikett: "Model" },
|
||||||
{ id: "fordon_motor", grupp: "Personbil", etikett: "Motor" },
|
{ id: "fordon_motor", grupp: "Vehicle", etikett: "Motor" },
|
||||||
{ id: "fordon_arsmodell", grupp: "Personbil", etikett: "Årsmodell" },
|
{ id: "fordon_arsmodell", grupp: "Vehicle", etikett: "Model year" },
|
||||||
{ id: "fordon_matarstallning", grupp: "Personbil", etikett: "Mätarställning" },
|
{ id: "fordon_matarstallning", grupp: "Vehicle", etikett: "Odometer" },
|
||||||
{ id: "fordon_motorkod", grupp: "Personbil", etikett: "Motorkod" },
|
{ id: "fordon_motorkod", grupp: "Vehicle", etikett: "Engine code" },
|
||||||
{ id: "fordon_vaxellada", grupp: "Personbil", etikett: "Växellåda" },
|
{ id: "fordon_vaxellada", grupp: "Vehicle", etikett: "Transmission" },
|
||||||
{ id: "ao_nummer", grupp: "Verkstad", etikett: "Arbetsordernr" },
|
{ id: "ao_nummer", grupp: "Workshop", etikett: "Work order no." },
|
||||||
{ id: "ao_claim", grupp: "Verkstad", etikett: "Claim-/garantinr" },
|
{ id: "ao_claim", grupp: "Workshop", etikett: "Claim / warranty no." },
|
||||||
{ id: "ao_skadenummer", grupp: "Verkstad", etikett: "Skadenummer" },
|
{ id: "ao_skadenummer", grupp: "Workshop", etikett: "Claim number" },
|
||||||
{ id: "ao_referens", grupp: "Verkstad", etikett: "Intern referens" },
|
{ id: "ao_referens", grupp: "Workshop", etikett: "Internal reference" },
|
||||||
{ id: "ao_serviceradgivare", grupp: "Verkstad", etikett: "Servicerådgivare" },
|
{ id: "ao_serviceradgivare", grupp: "Workshop", etikett: "Service advisor" },
|
||||||
{ id: "ao_bokningsdatum", grupp: "Verkstad", etikett: "Bokningsdatum" },
|
{ id: "ao_bokningsdatum", grupp: "Workshop", etikett: "Booking date" },
|
||||||
{ id: "felbeskrivning", grupp: "Ärende", etikett: "Felbeskrivning" },
|
{ id: "felbeskrivning", grupp: "Case", etikett: "Fault description" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ARBETSORDER_PROMPT =
|
const ARBETSORDER_PROMPT =
|
||||||
"Tolka den bifogade arbetsordern från en fordonsverkstad och extrahera fälten.";
|
"Interpret the attached work order from a vehicle workshop and extract the fields.";
|
||||||
|
|
||||||
// Rensar AI-svaret: okända fält-id filtreras, konfidens klipps till 0–1,
|
// Rensar AI-svaret: okända fält-id filtreras, konfidens klipps till 0–1,
|
||||||
// tomma värden tas bort och etikett/grupp slås upp ur katalogen.
|
// tomma värden tas bort och etikett/grupp slås upp ur katalogen.
|
||||||
export function normaliseraArbetsorder(data: unknown): TolkatFalt[] {
|
export function normaliseraArbetsorder(data: unknown): TolkatFalt[] {
|
||||||
const rader = (data as { falt?: unknown }).falt;
|
const rader = (data as { falt?: unknown }).falt;
|
||||||
if (!Array.isArray(rader)) throw new Error("Oväntat tolkningsformat");
|
if (!Array.isArray(rader)) throw new Error("Unexpected interpretation format");
|
||||||
const resultat: TolkatFalt[] = [];
|
const resultat: TolkatFalt[] = [];
|
||||||
for (const rad of rader as Partial<TolkatFalt>[]) {
|
for (const rad of rader as Partial<TolkatFalt>[]) {
|
||||||
const def = ARBETSORDER_FALT.find((f) => f.id === rad?.id);
|
const def = ARBETSORDER_FALT.find((f) => f.id === rad?.id);
|
||||||
@@ -188,7 +188,7 @@ export interface InstrumentTolkning {
|
|||||||
export function normaliseraInstrument(data: unknown): InstrumentTolkning {
|
export function normaliseraInstrument(data: unknown): InstrumentTolkning {
|
||||||
const d = data as Partial<InstrumentTolkning>;
|
const d = data as Partial<InstrumentTolkning>;
|
||||||
if (!d || typeof d.instrumenttyp !== "string" || !Array.isArray(d.varden)) {
|
if (!d || typeof d.instrumenttyp !== "string" || !Array.isArray(d.varden)) {
|
||||||
throw new Error("Oväntat avläsningsformat");
|
throw new Error("Unexpected reading format");
|
||||||
}
|
}
|
||||||
const varden: InstrumentVarde[] = [];
|
const varden: InstrumentVarde[] = [];
|
||||||
for (const rad of d.varden as Partial<InstrumentVarde>[]) {
|
for (const rad of d.varden as Partial<InstrumentVarde>[]) {
|
||||||
@@ -208,7 +208,7 @@ export async function lasAvInstrument(
|
|||||||
): Promise<{ tolkning: InstrumentTolkning; modell: string } | null> {
|
): Promise<{ tolkning: InstrumentTolkning; modell: string } | null> {
|
||||||
const resultat = await anropa(
|
const resultat = await anropa(
|
||||||
"instrumentavlasning",
|
"instrumentavlasning",
|
||||||
"Läs av instrumentet/skärmen i den bifogade bilden.",
|
"Read the instrument or display in the attached image.",
|
||||||
{ bild: dataUrl },
|
{ bild: dataUrl },
|
||||||
);
|
);
|
||||||
return resultat ? { tolkning: normaliseraInstrument(resultat.svar), modell: resultat.modell } : null;
|
return resultat ? { tolkning: normaliseraInstrument(resultat.svar), modell: resultat.modell } : null;
|
||||||
@@ -265,7 +265,7 @@ async function anropa(
|
|||||||
resultat = svar;
|
resultat = svar;
|
||||||
}
|
}
|
||||||
const { modell, svar } = resultat as { modell?: string; svar?: unknown };
|
const { modell, svar } = resultat as { modell?: string; svar?: unknown };
|
||||||
if (typeof modell !== "string") throw new Error("Oväntat svar från AI-endpointen");
|
if (typeof modell !== "string") throw new Error("Unexpected response from the AI endpoint");
|
||||||
return { modell, svar };
|
return { modell, svar };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,17 +188,17 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
|||||||
// fält → starta diagnos) kan provas ändå. Märks alltid som demo i UI:t.
|
// fält → starta diagnos) kan provas ändå. Märks alltid som demo i UI:t.
|
||||||
export function byggDemoTolkning(): TolkatFalt[] {
|
export function byggDemoTolkning(): TolkatFalt[] {
|
||||||
return [
|
return [
|
||||||
{ id: "kund_namn", etikett: "Namn", grupp: "Kund", varde: "Anders Svensson", konfidens: 0.98 },
|
{ id: "kund_namn", etikett: "Namn", grupp: "Customer", varde: "Anders Svensson", konfidens: 0.98 },
|
||||||
{ id: "kund_telefon", etikett: "Telefon", grupp: "Kund", varde: "070-123 45 67", konfidens: 0.91, omrade: { x: 0.08, y: 0.22, bredd: 0.3, hojd: 0.04 } },
|
{ id: "kund_telefon", etikett: "Telefon", grupp: "Customer", varde: "070-123 45 67", konfidens: 0.91, omrade: { x: 0.08, y: 0.22, bredd: 0.3, hojd: 0.04 } },
|
||||||
{ id: "fordon_regnr", etikett: "Regnr", grupp: "Personbil", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } },
|
{ id: "fordon_regnr", etikett: "Regnr", grupp: "Vehicle", varde: "ABC123", konfidens: 0.99, omrade: { x: 0.62, y: 0.12, bredd: 0.2, hojd: 0.05 } },
|
||||||
{ id: "fordon_vin", etikett: "VIN", grupp: "Personbil", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } },
|
{ id: "fordon_vin", etikett: "VIN", grupp: "Vehicle", varde: "YV1DZ8256F2123456", konfidens: 0.74, omrade: { x: 0.55, y: 0.19, bredd: 0.38, hojd: 0.04 } },
|
||||||
{ id: "fordon_marke", etikett: "Märke", grupp: "Personbil", varde: "Volvo", konfidens: 0.99 },
|
{ id: "fordon_marke", etikett: "Märke", grupp: "Vehicle", varde: "Volvo", konfidens: 0.99 },
|
||||||
{ id: "fordon_modell", etikett: "Modell", grupp: "Personbil", varde: "XC60 D4", konfidens: 0.97 },
|
{ id: "fordon_modell", etikett: "Modell", grupp: "Vehicle", varde: "XC60 D4", konfidens: 0.97 },
|
||||||
{ id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Personbil", varde: "2019", konfidens: 0.96 },
|
{ id: "fordon_arsmodell", etikett: "Årsmodell", grupp: "Vehicle", varde: "2019", konfidens: 0.96 },
|
||||||
{ id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Personbil", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } },
|
{ id: "fordon_matarstallning", etikett: "Mätarställning", grupp: "Vehicle", varde: "8 432 mil", konfidens: 0.87, omrade: { x: 0.62, y: 0.27, bredd: 0.18, hojd: 0.04 } },
|
||||||
{ id: "ao_nummer", etikett: "Arbetsordernr", grupp: "Verkstad", varde: "AO-2496", konfidens: 0.99 },
|
{ id: "ao_nummer", etikett: "Arbetsordernr", grupp: "Workshop", varde: "AO-2496", konfidens: 0.99 },
|
||||||
{ id: "ao_serviceradgivare", etikett: "Servicerådgivare", grupp: "Verkstad", varde: "M. Lindqvist", konfidens: 0.82 },
|
{ id: "ao_serviceradgivare", etikett: "Servicerådgivare", grupp: "Workshop", varde: "M. Lindqvist", konfidens: 0.82 },
|
||||||
{ id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Ärende", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } },
|
{ id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Case", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
export type Tillforlitlighet = "hog" | "medel" | "lag";
|
export type Tillforlitlighet = "hog" | "medel" | "lag";
|
||||||
|
|
||||||
export const TILLFORLITLIGHET_LABEL: Record<Tillforlitlighet, string> = {
|
export const TILLFORLITLIGHET_LABEL: Record<Tillforlitlighet, string> = {
|
||||||
hog: "Hög",
|
hog: "High",
|
||||||
medel: "Medel",
|
medel: "Medium",
|
||||||
lag: "Låg",
|
lag: "Low",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TidKategori =
|
export type TidKategori =
|
||||||
@@ -20,20 +20,20 @@ export type TidKategori =
|
|||||||
| "paus";
|
| "paus";
|
||||||
|
|
||||||
export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
|
export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
|
||||||
aktiv_felsokning: "Aktiv felsökning",
|
aktiv_felsokning: "Active diagnosis",
|
||||||
vantetid: "Väntetid",
|
vantetid: "Waiting time",
|
||||||
administration: "Administration",
|
administration: "Administration",
|
||||||
reservdelssokning: "Reservdelssökning",
|
reservdelssokning: "Parts search",
|
||||||
provkorning: "Provkörning",
|
provkorning: "Road test",
|
||||||
kundkontakt: "Kundkontakt",
|
kundkontakt: "Customer contact",
|
||||||
paus: "Paus",
|
paus: "Break",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kundens besked på ett lämnat åtgärdsförslag.
|
// Kundens besked på ett lämnat åtgärdsförslag.
|
||||||
export const KUNDBESLUT_LABEL: Record<"godkant" | "avbojt" | "delvis", string> = {
|
export const KUNDBESLUT_LABEL: Record<"godkant" | "avbojt" | "delvis", string> = {
|
||||||
godkant: "Godkänt",
|
godkant: "Approved",
|
||||||
avbojt: "Avböjt",
|
avbojt: "Declined",
|
||||||
delvis: "Delvis godkänt",
|
delvis: "Partly approved",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Kvalitetskontrollens utfall efter utförd åtgärd.
|
// Kvalitetskontrollens utfall efter utförd åtgärd.
|
||||||
@@ -41,10 +41,10 @@ export const KVALITETSKONTROLL_LABEL: Record<
|
|||||||
"symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar",
|
"symptomet_borta" | "kvarstar" | "delvis" | "ej_verifierbar",
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
symptomet_borta: "Symptomet är borta",
|
symptomet_borta: "Symptom is gone",
|
||||||
kvarstar: "Symptomet kvarstår",
|
kvarstar: "Symptom remains",
|
||||||
delvis: "Symptomet delvis kvar",
|
delvis: "Symptom partly remains",
|
||||||
ej_verifierbar: "Kunde inte verifieras",
|
ej_verifierbar: "Could not be verified",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fordonsobjektet är den röda tråden genom hela ärendet: identiteten
|
// Fordonsobjektet är den röda tråden genom hela ärendet: identiteten
|
||||||
@@ -52,9 +52,9 @@ export const KVALITETSKONTROLL_LABEL: Record<
|
|||||||
// och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet
|
// och export. Ärendereferenserna (AO/claim/skadenummer) hör till objektet
|
||||||
// så att de följer med i alla vyer utan att kunduppgifter läcker.
|
// så att de följer med i alla vyer utan att kunduppgifter läcker.
|
||||||
export interface Objekt {
|
export interface Objekt {
|
||||||
typ: string; // t.ex. "Personbil", "Industrimaskin"
|
typ: string; // t.ex. "Passenger car", "Industrial machine"
|
||||||
identifierare: string; // reg.nr, VIN, serienummer …
|
identifierare: string; // reg.nr, VIN, serienummer …
|
||||||
identifieringsmetod: string; // "Regnr", "VIN", "Serienummer", "Manuell inmatning"
|
identifieringsmetod: string; // "Reg. no.", "VIN", "Serial number", "Manual entry"
|
||||||
beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
|
beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
|
||||||
kund?: string;
|
kund?: string;
|
||||||
vin?: string;
|
vin?: string;
|
||||||
@@ -331,7 +331,7 @@ export function handelseRubrik(post: LoggPost): string {
|
|||||||
case "atgard_utford":
|
case "atgard_utford":
|
||||||
return h.utford
|
return h.utford
|
||||||
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
||||||
: `Ingen åtgärd utförd — ${h.motivering ?? "orsak saknas"}`;
|
: `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`;
|
||||||
case "kvalitetskontroll":
|
case "kvalitetskontroll":
|
||||||
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
|
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
|
||||||
case "export_skapad":
|
case "export_skapad":
|
||||||
@@ -341,6 +341,6 @@ export function handelseRubrik(post: LoggPost): string {
|
|||||||
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`;
|
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`;
|
||||||
}
|
}
|
||||||
case "arende_avslutat":
|
case "arende_avslutat":
|
||||||
return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Felsökning avslutad";
|
return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Diagnosis closed";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export function tidDatum(iso: string): string {
|
|||||||
export function skalaNerFoto(fil: File, maxSida = 1024): Promise<string> {
|
export function skalaNerFoto(fil: File, maxSida = 1024): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const lasare = new FileReader();
|
const lasare = new FileReader();
|
||||||
lasare.onerror = () => reject(new Error("Kunde inte läsa filen"));
|
lasare.onerror = () => reject(new Error("Could not read the file"));
|
||||||
lasare.onload = () => {
|
lasare.onload = () => {
|
||||||
const bild = new Image();
|
const bild = new Image();
|
||||||
bild.onerror = () => reject(new Error("Kunde inte tolka bilden"));
|
bild.onerror = () => reject(new Error("Could not interpret the image"));
|
||||||
bild.onload = () => {
|
bild.onload = () => {
|
||||||
const skala = Math.min(1, maxSida / Math.max(bild.width, bild.height));
|
const skala = Math.min(1, maxSida / Math.max(bild.width, bild.height));
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
@@ -51,12 +51,12 @@ export const MAX_VIDEO_BYTE = 2_500_000;
|
|||||||
export function lasVideo(fil: File): Promise<string> {
|
export function lasVideo(fil: File): Promise<string> {
|
||||||
return new Promise((los, avvisa) => {
|
return new Promise((los, avvisa) => {
|
||||||
if (fil.size > MAX_VIDEO_BYTE) {
|
if (fil.size > MAX_VIDEO_BYTE) {
|
||||||
avvisa(new Error("Videon är för stor — spela in ett kort klipp (ca 10–15 sekunder)."));
|
avvisa(new Error("The video is too large — record a short clip (about 10–15 seconds)."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lasare = new FileReader();
|
const lasare = new FileReader();
|
||||||
lasare.onload = () => los(lasare.result as string);
|
lasare.onload = () => los(lasare.result as string);
|
||||||
lasare.onerror = () => avvisa(new Error("Videon kunde inte läsas."));
|
lasare.onerror = () => avvisa(new Error("The video could not be read."));
|
||||||
lasare.readAsDataURL(fil);
|
lasare.readAsDataURL(fil);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,21 @@
|
|||||||
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
|
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
|
||||||
|
|
||||||
export const ALLA_OBJEKTTYPER = [
|
export const ALLA_OBJEKTTYPER = [
|
||||||
"Personbil",
|
"Passenger car",
|
||||||
"Lastbil/Buss",
|
"Truck / bus",
|
||||||
"Entreprenadmaskin",
|
"Construction machine",
|
||||||
"Industrimaskin",
|
"Industrial machine",
|
||||||
"Elsystem",
|
"Electrical system",
|
||||||
"Hydraulik",
|
"Hydraulics",
|
||||||
"Övrigt",
|
"Other",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ALLA_IDENTIFIERINGSMETODER = [
|
export const ALLA_IDENTIFIERINGSMETODER = [
|
||||||
"Regnr",
|
"Reg. no.",
|
||||||
"VIN",
|
"VIN",
|
||||||
"Serienummer",
|
"Serial number",
|
||||||
"Maskinnummer",
|
"Machine number",
|
||||||
"Manuell inmatning",
|
"Manual entry",
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface Installningar {
|
export interface Installningar {
|
||||||
|
|||||||
@@ -168,12 +168,12 @@ export interface NastaSteg {
|
|||||||
*/
|
*/
|
||||||
export const SPARRFRAGOR: Record<string, { orsak: string; atgard: string }> = {
|
export const SPARRFRAGOR: Record<string, { orsak: string; atgard: string }> = {
|
||||||
"hogvolt/sakerhet/behorighet": {
|
"hogvolt/sakerhet/behorighet": {
|
||||||
orsak: "Arbete på högvoltsystem kräver dokumenterad behörighet.",
|
orsak: "Work on high-voltage systems requires documented authorisation.",
|
||||||
atgard: "Lämna över ärendet till en behörig tekniker. Arbetet får inte påbörjas utan behörighet.",
|
atgard: "Hand the case to an authorised technician. Work must not begin without authorisation.",
|
||||||
},
|
},
|
||||||
"hogvolt/sakerhet/avstangt": {
|
"hogvolt/sakerhet/avstangt": {
|
||||||
orsak: "Fordonet är inte spänningslöst enligt tillverkarens rutin.",
|
orsak: "The vehicle is not de-energised per the manufacturer's procedure.",
|
||||||
atgard: "Gör fordonet spänningslöst enligt tillverkarens anvisning innan något ingrepp påbörjas.",
|
atgard: "De-energise the vehicle per the manufacturer's instruction before any work begins.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ export function nastaSteg(arende: Arende, metodik: Metodik): NastaSteg {
|
|||||||
// Säkerhetsspärr: ett nekande svar stoppar metodiken här i stället
|
// 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}`) !== "Ja") {
|
if (sparr && svar.get(`${steg.id}/${fraga.id}`) !== "Yes") {
|
||||||
return { steg, fraga, klart: false, sparr };
|
return { steg, fraga, klart: false, sparr };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,19 +262,19 @@ export interface TillforlitlighetsRad {
|
|||||||
|
|
||||||
export function tillforlitlighet(arende: Arende): TillforlitlighetsRad[] {
|
export function tillforlitlighet(arende: Arende): TillforlitlighetsRad[] {
|
||||||
const rader: TillforlitlighetsRad[] = [];
|
const rader: TillforlitlighetsRad[] = [];
|
||||||
if (objekt(arende)) rader.push({ niva: "hog", text: "Objekt identifierat" });
|
if (objekt(arende)) rader.push({ niva: "hog", text: "Object identified" });
|
||||||
if (foton(arende).length > 0) rader.push({ niva: "hog", text: "Bilder dokumenterade" });
|
if (foton(arende).length > 0) rader.push({ niva: "hog", text: "Images documented" });
|
||||||
if (arende.handelser.some((p) => p.handelse.typ === "matvarde")) {
|
if (arende.handelser.some((p) => p.handelse.typ === "matvarde")) {
|
||||||
rader.push({ niva: "hog", text: "Mätvärden registrerade" });
|
rader.push({ niva: "hog", text: "Measurements recorded" });
|
||||||
}
|
}
|
||||||
if (utfordaKontroller(arende).length > 0) {
|
if (utfordaKontroller(arende).length > 0) {
|
||||||
rader.push({ niva: "hog", text: "Kontroller dokumenterade" });
|
rader.push({ niva: "hog", text: "Checks documented" });
|
||||||
}
|
}
|
||||||
// Felorsaken är per definition inte verifierad förrän ärendet avslutats
|
// Felorsaken är per definition inte verifierad förrän ärendet avslutats
|
||||||
// med en verifierad slutsats — MVP:t markerar den alltid som ej verifierad
|
// med en verifierad slutsats — MVP:t markerar den alltid som ej verifierad
|
||||||
// så länge ärendet är öppet.
|
// så länge ärendet är öppet.
|
||||||
if (!arAvslutat(arende)) {
|
if (!arAvslutat(arende)) {
|
||||||
rader.push({ niva: "medel", text: "Felorsak ännu inte verifierad" });
|
rader.push({ niva: "medel", text: "Root cause not yet verified" });
|
||||||
}
|
}
|
||||||
return rader;
|
return rader;
|
||||||
}
|
}
|
||||||
@@ -306,7 +306,7 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief {
|
|||||||
if (!rekommenderat.includes(text)) rekommenderat.push(text);
|
if (!rekommenderat.includes(text)) rekommenderat.push(text);
|
||||||
}
|
}
|
||||||
if (rekommenderat.length === 0) {
|
if (rekommenderat.length === 0) {
|
||||||
rekommenderat.push("Samtliga kontroller i metodiken utförda. Verifiera slutsats eller utöka felsökningen.");
|
rekommenderat.push("Every check in the methodology is complete. Verify the conclusion or extend the diagnosis.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -332,25 +332,25 @@ export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string):
|
|||||||
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`);
|
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`);
|
||||||
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
|
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Utförda kontroller:");
|
rader.push("Checks performed:");
|
||||||
if (b.utfordaKontroller.length === 0) rader.push(" (inga ännu)");
|
if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
|
||||||
for (const k of b.utfordaKontroller)
|
for (const k of b.utfordaKontroller)
|
||||||
rader.push(k.undantag ? ` ! ${k.text} — underlag saknas: ${k.undantag}` : ` ✓ ${k.text}${k.resultat ? ` — ${k.resultat}` : ""}`);
|
rader.push(k.undantag ? ` ! ${k.text} — underlag saknas: ${k.undantag}` : ` ✓ ${k.text}${k.resultat ? ` — ${k.resultat}` : ""}`);
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Observationer:");
|
rader.push("Observations:");
|
||||||
if (b.observationer.length === 0) rader.push(" (inga ännu)");
|
if (b.observationer.length === 0) rader.push(" (none yet)");
|
||||||
for (const o of b.observationer) rader.push(` • ${o}`);
|
for (const o of b.observationer) rader.push(` • ${o}`);
|
||||||
if (b.hypoteser.length > 0) {
|
if (b.hypoteser.length > 0) {
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Hypoteser (ej verifierade):");
|
rader.push("Hypotheses (unverified):");
|
||||||
for (const h of b.hypoteser) rader.push(` ? ${h.text}`);
|
for (const h of b.hypoteser) rader.push(` ? ${h.text}`);
|
||||||
}
|
}
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Ej kontrollerat:");
|
rader.push("Not checked:");
|
||||||
if (b.ejKontrollerat.length === 0) rader.push(" (allt i metodiken utfört)");
|
if (b.ejKontrollerat.length === 0) rader.push(" (everything in the methodology is complete)");
|
||||||
for (const e of b.ejKontrollerat) rader.push(` – ${e}`);
|
for (const e of b.ejKontrollerat) rader.push(` – ${e}`);
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Rekommenderat nästa steg:");
|
rader.push("Recommended next step:");
|
||||||
b.rekommenderatNastaSteg.forEach((s, i) => rader.push(` ${i + 1}. ${s}`));
|
b.rekommenderatNastaSteg.forEach((s, i) => rader.push(` ${i + 1}. ${s}`));
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push(`Total arbetstid: ${b.totalArbetstid}`);
|
rader.push(`Total arbetstid: ${b.totalArbetstid}`);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
// Den avlästa koden klassificeras alltid innan den används, så att rätt
|
// Den avlästa koden klassificeras alltid innan den används, så att rätt
|
||||||
// identifieringsmetod sätts på fordonsobjektet.
|
// identifieringsmetod sätts på fordonsobjektet.
|
||||||
|
|
||||||
export type KodTyp = "VIN" | "Regnr" | "Serienummer";
|
export type KodTyp = "VIN" | "Reg. no." | "Serial number";
|
||||||
|
|
||||||
export interface AvlastKod {
|
export interface AvlastKod {
|
||||||
varde: string;
|
varde: string;
|
||||||
@@ -37,16 +37,16 @@ export function tolkaKod(ratext: string): AvlastKod | null {
|
|||||||
const vinFalt = text.match(/(?:vin|chassi)[=:/\s]+([A-HJ-NPR-Z0-9]{17})/i);
|
const vinFalt = text.match(/(?:vin|chassi)[=:/\s]+([A-HJ-NPR-Z0-9]{17})/i);
|
||||||
if (vinFalt) return { varde: vinFalt[1].toUpperCase(), typ: "VIN" };
|
if (vinFalt) return { varde: vinFalt[1].toUpperCase(), typ: "VIN" };
|
||||||
const regnrFalt = text.match(/(?:reg(?:nr)?|plate)[=:/\s]+([A-Za-z]{3}\s?[0-9]{2}[0-9A-Za-z])/i);
|
const regnrFalt = text.match(/(?:reg(?:nr)?|plate)[=:/\s]+([A-Za-z]{3}\s?[0-9]{2}[0-9A-Za-z])/i);
|
||||||
if (regnrFalt) return { varde: regnrFalt[1].replace(/\s/g, "").toUpperCase(), typ: "Regnr" };
|
if (regnrFalt) return { varde: regnrFalt[1].replace(/\s/g, "").toUpperCase(), typ: "Reg. no." };
|
||||||
|
|
||||||
const rent = text.replace(/[\s-]/g, "").toUpperCase();
|
const rent = text.replace(/[\s-]/g, "").toUpperCase();
|
||||||
if (VIN_MONSTER.test(rent)) return { varde: rent, typ: "VIN" };
|
if (VIN_MONSTER.test(rent)) return { varde: rent, typ: "VIN" };
|
||||||
if (REGNR_MONSTER.test(rent)) return { varde: rent, typ: "Regnr" };
|
if (REGNR_MONSTER.test(rent)) return { varde: rent, typ: "Reg. no." };
|
||||||
|
|
||||||
// Allt annat (maskin-/serienummer) tas som det är, men bara om det ser
|
// Allt annat (maskin-/serienummer) tas som det är, men bara om det ser
|
||||||
// ut som en identifierare — inte som fritext eller en hel URL.
|
// ut som en identifierare — inte som fritext eller en hel URL.
|
||||||
if (/^[A-Z0-9._/-]{4,40}$/.test(rent) && !/^HTTPS?:/.test(rent)) {
|
if (/^[A-Z0-9._/-]{4,40}$/.test(rent) && !/^HTTPS?:/.test(rent)) {
|
||||||
return { varde: rent, typ: "Serienummer" };
|
return { varde: rent, typ: "Serial number" };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export async function startaSkanning(
|
|||||||
): Promise<Skanning> {
|
): Promise<Skanning> {
|
||||||
const Detektor = detektorKlass();
|
const Detektor = detektorKlass();
|
||||||
if (!Detektor) {
|
if (!Detektor) {
|
||||||
vidFel?.("Webbläsaren saknar streckkodsläsning — fotografera typskylten i stället.");
|
vidFel?.("The browser has no barcode reader — photograph the type plate instead.");
|
||||||
return { stoppa: () => {} };
|
return { stoppa: () => {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export async function startaSkanning(
|
|||||||
// starta. Detektorn får helt enkelt tomma rutor tills strömmen är i gång.
|
// starta. Detektorn får helt enkelt tomma rutor tills strömmen är i gång.
|
||||||
void video.play().catch(() => {});
|
void video.play().catch(() => {});
|
||||||
} catch {
|
} catch {
|
||||||
vidFel?.("Kameran kunde inte startas — kontrollera behörigheten eller fotografera typskylten.");
|
vidFel?.("The camera could not be started — check the permission or photograph the type plate.");
|
||||||
return { stoppa };
|
return { stoppa };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,17 +80,17 @@ import { bilageFalt } from "@/felsokning/bilagor";
|
|||||||
|
|
||||||
const FLIKAR = [
|
const FLIKAR = [
|
||||||
{ id: "guide", label: "Guide" },
|
{ id: "guide", label: "Guide" },
|
||||||
{ id: "logg", label: "Logg" },
|
{ id: "logg", label: "Log" },
|
||||||
{ id: "brief", label: "Brief" },
|
{ id: "brief", label: "Brief" },
|
||||||
{ id: "rapport", label: "Rapport" },
|
{ id: "rapport", label: "Report" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type Flik = (typeof FLIKAR)[number]["id"];
|
type Flik = (typeof FLIKAR)[number]["id"];
|
||||||
|
|
||||||
const SYNKSTATUS_LABEL: Record<SynkStatus, string> = {
|
const SYNKSTATUS_LABEL: Record<SynkStatus, string> = {
|
||||||
lokal: "Lokalt läge",
|
lokal: "Local mode",
|
||||||
synkar: "Synkar …",
|
synkar: "Synchronizing …",
|
||||||
synkad: "Synkad",
|
synkad: "Synchronized",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,8 +144,8 @@ export default function ArendeSida() {
|
|||||||
|
|
||||||
if (!arende || !id) {
|
if (!arende || !id) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Ärendet hittades inte" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<FelsokningSkal rubrik="Case not found" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<p className="text-[14px] text-[#333333]">Ärendet finns inte på den här enheten.</p>
|
<p className="text-[14px] text-[#333333]">The case does not exist on this device.</p>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -172,12 +172,12 @@ export default function ArendeSida() {
|
|||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
bred
|
bred
|
||||||
rubrik={`#${arende.nummer} ${o?.beskrivning ?? ""}`}
|
rubrik={`#${arende.nummer} ${o?.beskrivning ?? ""}`}
|
||||||
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
|
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||||
hoger={
|
hoger={
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-[13px] font-semibold text-white">{total}</p>
|
<p className="text-[13px] font-semibold text-white">{total}</p>
|
||||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
|
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
|
||||||
{avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]}
|
{avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -254,16 +254,16 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
|
|||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
|
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
|
||||||
{del("AO", idn.arbetsorder)}
|
{del("WO", idn.arbetsorder)}
|
||||||
{del("Claim", idn.claim)}
|
{del("Claim", idn.claim)}
|
||||||
{del("Skadenr", idn.skadenummer)}
|
{del("Claim no.", idn.skadenummer)}
|
||||||
{del("", idn.beskrivning)}
|
{del("", idn.beskrivning)}
|
||||||
{del("Regnr", idn.identifierare)}
|
{del("Reg. no.", idn.identifierare)}
|
||||||
{del("VIN", idn.vin)}
|
{del("VIN", idn.vin)}
|
||||||
{del("Miltal", idn.miltal)}
|
{del("Mileage", idn.miltal)}
|
||||||
{del("Ansvarig", idn.ansvarig)}
|
{del("Responsible", idn.ansvarig)}
|
||||||
<label className="ml-auto flex items-center gap-1">
|
<label className="ml-auto flex items-center gap-1">
|
||||||
<span className="text-[#707070]">Ärendetyp</span>
|
<span className="text-[#707070]">Case type</span>
|
||||||
<select
|
<select
|
||||||
value={typ}
|
value={typ}
|
||||||
disabled={idn.avslutat}
|
disabled={idn.avslutat}
|
||||||
@@ -326,7 +326,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
|||||||
<IkonKamera /> Fotografera instrumentpanelen
|
<IkonKamera /> Fotografera instrumentpanelen
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av mätarställningen …</p>}
|
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer …</p>}
|
||||||
{foto && !laser && (
|
{foto && !laser && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{demo && (
|
{demo && (
|
||||||
@@ -335,7 +335,7 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
|||||||
</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" ? "ingående" : "utgående"})`} varde={varde} satt={setVarde} platshallare="T.ex. 84 320 km" />
|
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!varde.trim()}
|
disabled={!varde.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -399,7 +399,7 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
|||||||
<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">Ärende #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||||
{rad.avslutat ? "avslutat" : "pågående"}
|
{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>
|
||||||
{rad.felorsaker.map((f, i) => (
|
{rad.felorsaker.map((f, i) => (
|
||||||
@@ -437,7 +437,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
const harTidiga = arende.handelser.some((p) => p.handelse.typ === "observation" || p.handelse.typ === "foto");
|
const harTidiga = arende.handelser.some((p) => p.handelse.typ === "observation" || p.handelse.typ === "foto");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Pre-diagnostik — innan felsökningen börjar">
|
<Panel rubrik="Pre-diagnostics — before the work begins">
|
||||||
{rader.map((r) => (
|
{rader.map((r) => (
|
||||||
<p key={r.id} className="py-0.5 text-[13px]">
|
<p key={r.id} className="py-0.5 text-[13px]">
|
||||||
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
||||||
@@ -456,16 +456,16 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
Ja — kontrollerad
|
Ja — kontrollerad
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
<StorKnapp variant={historikVal === "nej" ? "fara" : "sekundar"} onClick={() => setHistorikVal("nej")}>
|
||||||
Nej
|
No
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
{historikVal && (
|
{historikVal && (
|
||||||
<>
|
<>
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label={historikVal === "ja" ? "Relevanta tidigare arbeten (valfritt — orsakskedja)" : "Orsak till att historiken inte kontrollerats (obligatoriskt)"}
|
label={historikVal === "ja" ? "Relevant previous work (optional — causal chain)" : "Reason the history was not checked (required)"}
|
||||||
varde={historikText}
|
varde={historikText}
|
||||||
satt={setHistorikText}
|
satt={setHistorikText}
|
||||||
platshallare={historikVal === "ja" ? "T.ex. Vattenpump bytt för 8 mån sedan — misstänkt läckage samma område" : ""}
|
platshallare={historikVal === "ja" ? "e.g. Water pump replaced 8 months ago — suspected leak in the same area" : ""}
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
@@ -476,7 +476,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
setHistorikText("");
|
setHistorikText("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Dokumentera
|
Document
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -541,26 +541,26 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
|||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const etikett =
|
const etikett =
|
||||||
status === "ja"
|
status === "ja"
|
||||||
? "Hur reproducerades felet? (förhållanden, hastighet, temperatur, belastning …)"
|
? "How was the fault reproduced? (conditions, speed, temperature, load …)"
|
||||||
: status === "delvis"
|
: status === "delvis"
|
||||||
? "Vad kunde respektive kunde inte reproduceras?"
|
? "What could and could not be reproduced?"
|
||||||
: "Motivering (obligatorisk) — varför kunde felet inte reproduceras?";
|
: "Rationale (required) — why could the fault not be reproduced?";
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Symptomverifiering — reproducering">
|
<Panel rubrik="Symptom verification — reproduction">
|
||||||
<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 kundens fel kunnat reproduceras?
|
Har kundens fel kunnat reproduceras?
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||||
{(["ja", "delvis", "nej"] as const).map((val) => (
|
{(["ja", "delvis", "nej"] as const).map((val) => (
|
||||||
<StorKnapp key={val} variant={status === val ? "primar" : "sekundar"} onClick={() => setStatus(val)}>
|
<StorKnapp key={val} variant={status === val ? "primar" : "sekundar"} onClick={() => setStatus(val)}>
|
||||||
{val === "ja" ? "Ja" : val === "delvis" ? "Delvis" : "Nej"}
|
{val === "ja" ? "Yes" : val === "delvis" ? "Partly" : "No"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{status && (
|
{status && (
|
||||||
<>
|
<>
|
||||||
<TextFalt label={etikett} varde={text} satt={setText} flerRad rost
|
<TextFalt label={etikett} varde={text} satt={setText} flerRad rost
|
||||||
platshallare={status === "ja" ? "T.ex. Reproducerad tre gånger vid 92–108 km/h på plan väg, varm motor" : ""} />
|
platshallare={status === "ja" ? "e.g. Reproduced three times at 92–108 km/h on level road, engine warm" : ""} />
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!text.trim()}
|
disabled={!text.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -599,15 +599,15 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
const spara = () => {
|
const spara = () => {
|
||||||
const avvikelseFel = granskaAvvikelse(avvikelse);
|
const avvikelseFel = granskaAvvikelse(avvikelse);
|
||||||
if (avvikelseFel) return setFel(avvikelseFel);
|
if (avvikelseFel) return setFel(avvikelseFel);
|
||||||
if (orsaker.length === 0) return setFel("Välj minst en orsakskategori.");
|
if (orsaker.length === 0) return setFel("Select at least one cause category.");
|
||||||
if (underlag.length === 0) return setFel("Koppla minst en evidenskälla till bedömningen.");
|
if (underlag.length === 0) return setFel("Link at least one evidence source to the assessment.");
|
||||||
const saknat = underlag.find((k) => !underlagFinns(arende, k));
|
const saknat = underlag.find((k) => !underlagFinns(arende, k));
|
||||||
if (saknat) return setFel(`Underlaget ”${saknat}” finns inte i ärendets logg — dokumentera det först eller välj en annan källa.`);
|
if (saknat) return setFel(`Underlaget ”${saknat}” finns inte i ärendets logg — dokumentera det först eller välj en annan källa.`);
|
||||||
if (orsaker.includes("Okänd orsak") && !motivering.trim())
|
if (orsaker.includes("Unknown cause") && !motivering.trim())
|
||||||
return setFel("Okänd orsak kräver en motivering till varför orsaken inte kunnat fastställas.");
|
return setFel("An unknown cause requires a reason why the cause could not be established.");
|
||||||
if (sakerhet !== "hog" && !ytterligare.trim())
|
if (sakerhet !== "hog" && !ytterligare.trim())
|
||||||
return setFel("Vid medel/låg säkerhet: ange vilka ytterligare kontroller som skulle stärka bedömningen.");
|
return setFel("At medium or low confidence: state which further checks would strengthen the assessment.");
|
||||||
if (!atgard.trim()) return setFel("Ange rekommenderad åtgärd.");
|
if (!atgard.trim()) return setFel("State the recommended action.");
|
||||||
skicka({
|
skicka({
|
||||||
typ: "felorsak",
|
typ: "felorsak",
|
||||||
avvikelse: avvikelse.trim(),
|
avvikelse: avvikelse.trim(),
|
||||||
@@ -630,7 +630,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Felorsaksanalys — obligatorisk före avslut">
|
<Panel rubrik="Root cause analysis — required before closing">
|
||||||
{dokumenterade.map((p) => {
|
{dokumenterade.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "felorsak") return null;
|
if (h.typ !== "felorsak") return null;
|
||||||
@@ -651,14 +651,14 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="1. Konstaterad avvikelse (inte bara komponenten)"
|
label="1. Established deviation (not just the component)"
|
||||||
varde={avvikelse}
|
varde={avvikelse}
|
||||||
satt={setAvvikelse}
|
satt={setAvvikelse}
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
platshallare="T.ex. Startmotorn aktiverar inte trots korrekt matningsspänning och god jordförbindelse."
|
platshallare="e.g. The starter motor does not engage despite correct supply voltage and a good earth connection."
|
||||||
/>
|
/>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">2. Mest sannolik orsak (en eller flera)</p>
|
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">2. Most probable cause (one or more)</p>
|
||||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
|
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
|
||||||
{aktivtRegelpaket().orsakskategorier.map((o) => (
|
{aktivtRegelpaket().orsakskategorier.map((o) => (
|
||||||
<button
|
<button
|
||||||
@@ -672,7 +672,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Underlag som stöder bedömningen</p>
|
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Evidence supporting the assessment</p>
|
||||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
|
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
|
||||||
{aktivtRegelpaket().underlagskallor.map((k) => (
|
{aktivtRegelpaket().underlagskallor.map((k) => (
|
||||||
<button
|
<button
|
||||||
@@ -686,7 +686,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Säkerhet i bedömningen</p>
|
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Confidence in the assessment</p>
|
||||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||||
{(["hog", "medel", "lag"] as const).map((n) => (
|
{(["hog", "medel", "lag"] as const).map((n) => (
|
||||||
<button
|
<button
|
||||||
@@ -702,28 +702,28 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
</div>
|
</div>
|
||||||
{sakerhet !== "hog" && (
|
{sakerhet !== "hog" && (
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Vilka ytterligare kontroller skulle stärka bedömningen?"
|
label="Which further checks would strengthen the assessment?"
|
||||||
varde={ytterligare}
|
varde={ytterligare}
|
||||||
satt={setYtterligare}
|
satt={setYtterligare}
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{orsaker.includes("Okänd orsak") && (
|
{orsaker.includes("Unknown cause") && (
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Motivering — varför kunde orsaken inte fastställas?"
|
label="Rationale — why could the cause not be established?"
|
||||||
varde={motivering}
|
varde={motivering}
|
||||||
satt={setMotivering}
|
satt={setMotivering}
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextFalt label="Rekommenderad åtgärd" varde={atgard} satt={setAtgard} rost platshallare="T.ex. Balansera framhjulen och genomför ny provkörning." />
|
<TextFalt label="Recommended action" varde={atgard} satt={setAtgard} rost platshallare="e.g. Balance the front wheels and perform a new road test." />
|
||||||
{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
|
Avbryt
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={spara}>Spara felorsak</StorKnapp>
|
<StorKnapp onClick={spara}>Save root cause</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -762,7 +762,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
const [kkText, setKkText] = useState("");
|
const [kkText, setKkText] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Åtgärd och kvalitetskontroll">
|
<Panel rubrik="Action and quality check">
|
||||||
{poster.map((p) => {
|
{poster.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgard_utford") return null;
|
if (h.typ !== "atgard_utford") return null;
|
||||||
@@ -770,7 +770,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||||
{h.utford ? (
|
{h.utford ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-semibold">Utförd åtgärd:</span> {h.beskrivning}
|
<span className="font-semibold">Action performed:</span> {h.beskrivning}
|
||||||
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -792,7 +792,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
if (h.typ !== "atgardsforslag") return null;
|
if (h.typ !== "atgardsforslag") return null;
|
||||||
return (
|
return (
|
||||||
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||||
<span className="font-semibold">Åtgärdsförslag till kund:</span> {h.beskrivning}
|
<span className="font-semibold">Proposed action for the customer:</span> {h.beskrivning}
|
||||||
{h.uppskattadKostnad && <span className="text-[#4A5560]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
|
{h.uppskattadKostnad && <span className="text-[#4A5560]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
@@ -827,14 +827,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{forslagsLage && (
|
{forslagsLage && (
|
||||||
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
|
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Föreslagen åtgärd (visas för kunden i delningsvyn)"
|
label="Proposed action (shown to the customer in the shared view)"
|
||||||
varde={forslagText}
|
varde={forslagText}
|
||||||
satt={setForslagText}
|
satt={setForslagText}
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
platshallare="T.ex. Balansera om höger framhjul och genomför ny provkörning."
|
platshallare="e.g. Rebalance the right front wheel and perform a new road test."
|
||||||
/>
|
/>
|
||||||
<TextFalt label="Uppskattad kostnad (valfritt)" varde={kostnad} satt={setKostnad} platshallare="T.ex. 1 200 kr inkl. moms" />
|
<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
|
Avbryt
|
||||||
@@ -878,7 +878,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
</div>
|
</div>
|
||||||
{beslutsVal && (
|
{beslutsVal && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">Hur lämnades beskedet?</p>
|
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">How was the decision given?</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-1">
|
<div className="mb-2 grid grid-cols-3 gap-1">
|
||||||
{KUNDKANALER.map((k) => (
|
{KUNDKANALER.map((k) => (
|
||||||
<button
|
<button
|
||||||
@@ -893,7 +893,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label={beslutsVal === "godkant" ? "Kommentar (valfritt)" : "Kundens motivering (obligatorisk)"}
|
label={beslutsVal === "godkant" ? "Comment (optional)" : "Customer's reason (required)"}
|
||||||
varde={beslutsKommentar}
|
varde={beslutsKommentar}
|
||||||
satt={setBeslutsKommentar}
|
satt={setBeslutsKommentar}
|
||||||
rost
|
rost
|
||||||
@@ -929,7 +929,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
Dokumentera utförd åtgärd
|
Dokumentera utförd åtgärd
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("ingen")}>
|
||||||
Ingen åtgärd utförd
|
No action performed
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -937,14 +937,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
{lage === "utford" && (
|
{lage === "utford" && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Vad gjordes? (kopplat till den dokumenterade felorsaken)"
|
label="What was done? (tied to the documented root cause)"
|
||||||
varde={beskrivning}
|
varde={beskrivning}
|
||||||
satt={setBeskrivning}
|
satt={setBeskrivning}
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
platshallare="T.ex. Balanserade om höger framhjul, ny balansvikt 40 g på insidan."
|
platshallare="e.g. Rebalanced the right front wheel, new 40 g weight on the inner rim."
|
||||||
/>
|
/>
|
||||||
<TextFalt label="Delar (valfritt)" varde={delar} satt={setDelar} platshallare="T.ex. Balansvikter, artikelnr 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
|
Avbryt
|
||||||
@@ -982,7 +982,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TextFalt label="Eller egen orsak" 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.
|
Kunden har avböjt åtgärden via {beslut.kanal} — beskedet finns registrerat i loggen.
|
||||||
@@ -995,12 +995,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!orsak.trim()}
|
disabled={!orsak.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
skicka({ typ: "atgard_utford", beskrivning: "Ingen åtgärd utförd", utford: false, motivering: orsak.trim() });
|
skicka({ typ: "atgard_utford", beskrivning: "No action performed", utford: false, motivering: orsak.trim() });
|
||||||
setLage("");
|
setLage("");
|
||||||
setOrsak("");
|
setOrsak("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Dokumentera
|
Document
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1034,16 +1034,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
<TextFalt
|
<TextFalt
|
||||||
label={
|
label={
|
||||||
kkResultat === "symptomet_borta"
|
kkResultat === "symptomet_borta"
|
||||||
? "Hur verifierades det? (förhållanden, hastighet, provkörning …)"
|
? "How was it verified? (conditions, speed, road test …)"
|
||||||
: kkResultat === "ej_verifierbar"
|
: kkResultat === "ej_verifierbar"
|
||||||
? "Varför kunde det inte verifieras? (obligatorisk)"
|
? "Why could it not be verified? (required)"
|
||||||
: "Vad kvarstår? (obligatorisk)"
|
: "What remains? (required)"
|
||||||
}
|
}
|
||||||
varde={kkText}
|
varde={kkText}
|
||||||
satt={setKkText}
|
satt={setKkText}
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
platshallare="T.ex. Ny provkörning 80–110 km/h på samma vägsträcka — ingen vibration."
|
platshallare="e.g. New road test at 80–110 km/h on the same stretch — no vibration."
|
||||||
/>
|
/>
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!kkText.trim()}
|
disabled={!kkText.trim()}
|
||||||
@@ -1141,16 +1141,16 @@ function KontextPanel({
|
|||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Panel rubrik="Teknisk information">
|
<Panel rubrik="Technical information">
|
||||||
{rad("Objekt", b.objekt?.beskrivning)}
|
{rad("Object", b.objekt?.beskrivning)}
|
||||||
{rad("Ident", b.objekt?.identifierare)}
|
{rad("Ident", b.objekt?.identifierare)}
|
||||||
{rad("Kund", b.objekt?.kund)}
|
{rad("Customer", b.objekt?.kund)}
|
||||||
{rad("Ansvarig", b.ansvarig)}
|
{rad("Responsible", b.ansvarig)}
|
||||||
{rad("Status", b.avslutat ? "Avslutat" : "Pågår")}
|
{rad("Status", b.avslutat ? "Closed" : "In progress")}
|
||||||
{rad("Synk", SYNKSTATUS_LABEL[synkStatus])}
|
{rad("Sync", SYNKSTATUS_LABEL[synkStatus])}
|
||||||
{rad("Arbetstid", b.totalArbetstid)}
|
{rad("Labour time", b.totalArbetstid)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Tillförlitlighet">
|
<Panel rubrik="Reliability">
|
||||||
{b.tillforlitlighet.map((r, i) => (
|
{b.tillforlitlighet.map((r, i) => (
|
||||||
<p key={i} className="py-0.5 text-[12px]">
|
<p key={i} className="py-0.5 text-[12px]">
|
||||||
<NivaBadge niva={r.niva} /> {r.text}
|
<NivaBadge niva={r.niva} /> {r.text}
|
||||||
@@ -1158,7 +1158,7 @@ function KontextPanel({
|
|||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
|
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
|
||||||
<Panel rubrik="Rekommenderat nästa steg">
|
<Panel rubrik="Recommended next step">
|
||||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
{b.rekommenderatNastaSteg.map((s, i) => (
|
||||||
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
|
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
|
||||||
{i + 1}. {s}
|
{i + 1}. {s}
|
||||||
@@ -1167,14 +1167,14 @@ function KontextPanel({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
|
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
|
||||||
<Panel rubrik="Teknisk rekommendation">
|
<Panel rubrik="Technical recommendation">
|
||||||
{senasteAi.handelse.rader.map((r, i) => (
|
{senasteAi.handelse.rader.map((r, i) => (
|
||||||
<p key={i} className="py-0.5 text-[12px]">
|
<p key={i} className="py-0.5 text-[12px]">
|
||||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
|
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-1 text-[12px]">
|
<p className="mt-1 text-[12px]">
|
||||||
<span className="font-semibold text-[#00437A]">Nästa steg:</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
|
Beslutsstöd · presenteras aldrig som konstaterat fel
|
||||||
@@ -1205,8 +1205,8 @@ function InaktivitetsBanner({
|
|||||||
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
|
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
|
||||||
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-3 text-[#333333]">Beskriv kort vad som gjorts under denna period.</p>
|
<p className="mb-3 text-[#333333]">Briefly describe what was done during this period.</p>
|
||||||
<TextFalt label="Vad har gjorts?" varde={text} satt={setText} platshallare="T.ex. Demonterade instrumentpanelen för att komma åt kabelstammen." rost />
|
<TextFalt label="What has been done?" varde={text} satt={setText} platshallare="e.g. Removed the instrument panel to reach the wiring harness." rost />
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!text.trim()}
|
disabled={!text.trim()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1284,7 +1284,7 @@ function GuideFlik({
|
|||||||
|
|
||||||
if (avslutat) {
|
if (avslutat) {
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Felsökningen är avslutad">
|
<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.
|
Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export.
|
||||||
</p>
|
</p>
|
||||||
@@ -1338,7 +1338,7 @@ function GuideFlik({
|
|||||||
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
||||||
{steg.klart ? (
|
{steg.klart ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-[15px] font-semibold">Samtliga steg i metodiken är dokumenterade.</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
|
Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet
|
||||||
med rekommenderade nästa steg.
|
med rekommenderade nästa steg.
|
||||||
@@ -1353,7 +1353,7 @@ function GuideFlik({
|
|||||||
// hinder, inte en varning. Enda vägen framåt är att ändra
|
// hinder, inte en varning. Enda vägen framåt är att ändra
|
||||||
// förutsättningen, inte att klicka förbi.
|
// förutsättningen, inte att klicka förbi.
|
||||||
<div className="border-2 border-[#B42318] bg-[#FEF3F2] p-4">
|
<div className="border-2 border-[#B42318] bg-[#FEF3F2] p-4">
|
||||||
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Arbetet får inte fortsätta</p>
|
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Work must not continue</p>
|
||||||
<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]">
|
||||||
@@ -1373,12 +1373,12 @@ function GuideFlik({
|
|||||||
rubrik={
|
rubrik={
|
||||||
senasteAiSvar?.handelse.typ === "ai_svar"
|
senasteAiSvar?.handelse.typ === "ai_svar"
|
||||||
? `Handledning (${senasteAiSvar.handelse.modell})`
|
? `Handledning (${senasteAiSvar.handelse.modell})`
|
||||||
: "Handledning"
|
: "Guidance"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Systemet analyserar …</p>}
|
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Analyzing …</p>}
|
||||||
{aiStatus === "fel" && (
|
{aiStatus === "fel" && (
|
||||||
<p className="mb-2 font-semibold text-[#8B1A1A]">Analysen kunde inte hämtas — försök igen. Metodiken fortsätter som vanligt.</p>
|
<p className="mb-2 font-semibold text-[#8B1A1A]">The analysis could not be retrieved — try again. The methodology continues unaffected.</p>
|
||||||
)}
|
)}
|
||||||
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
|
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
|
||||||
<>
|
<>
|
||||||
@@ -1388,7 +1388,7 @@ function GuideFlik({
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-2 text-[14px]">
|
<p className="mt-2 text-[14px]">
|
||||||
<span className="font-semibold text-[#00437A]">Nästa steg:</span>{" "}
|
<span className="font-semibold text-[#00437A]">Next step:</span>{" "}
|
||||||
{senasteAiSvar.handelse.nastaSteg}
|
{senasteAiSvar.handelse.nastaSteg}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -1414,7 +1414,7 @@ function GuideFlik({
|
|||||||
{/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när
|
{/* Utgående mätarställning: obligatorisk för kvalitetsgrinden när
|
||||||
ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */}
|
ärendet avslutas — erbjuds så fort metodiken är genomarbetad. */}
|
||||||
{!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && (
|
{!arende.handelser.some((p) => p.handelse.typ === "matarstallning" && p.handelse.lage === "utgaende") && (
|
||||||
<Panel rubrik="Utgående mätarställning — inför avslut">
|
<Panel rubrik="Outgoing odometer reading — before closing">
|
||||||
<MatarstallningSteg lage="utgaende" skicka={skicka} />
|
<MatarstallningSteg lage="utgaende" skicka={skicka} />
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
@@ -1498,8 +1498,8 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
|||||||
<p className="mb-4 text-[17px] font-semibold leading-snug">{fraga.text}</p>
|
<p className="mb-4 text-[17px] font-semibold leading-snug">{fraga.text}</p>
|
||||||
{fraga.svarstyp === "janej" && (
|
{fraga.svarstyp === "janej" && (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp onClick={() => svara("Ja")}>Ja</StorKnapp>
|
<StorKnapp onClick={() => svara("Yes")}>Yes</StorKnapp>
|
||||||
<StorKnapp onClick={() => svara("Nej")}>Nej</StorKnapp>
|
<StorKnapp onClick={() => svara("No")}>No</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{fraga.svarstyp === "val" && (
|
{fraga.svarstyp === "val" && (
|
||||||
@@ -1513,7 +1513,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
|||||||
)}
|
)}
|
||||||
{fraga.svarstyp === "text" && (
|
{fraga.svarstyp === "text" && (
|
||||||
<>
|
<>
|
||||||
<TextFalt label="Svar" 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
|
Spara svar
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
@@ -1576,29 +1576,29 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
|||||||
utford();
|
utford();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextFalt label="Observation (valfritt)" varde={resultat} satt={setResultat} rost />
|
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
|
||||||
<StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Ta foto — verifierar kontrollen</StorKnapp>
|
<StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Take a photograph — verifies the check</StorKnapp>
|
||||||
<p className="mt-2 text-[12px] text-[#707070]">Denna kontroll verifieras med foto.</p>
|
<p className="mt-2 text-[12px] text-[#707070]">This check is verified with a photograph.</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label={
|
label={
|
||||||
krav === "matvarde"
|
krav === "matvarde"
|
||||||
? "Uppmätt värde (krävs för verifiering)"
|
? "Measured value (required for verification)"
|
||||||
: krav === "kommentar"
|
: krav === "kommentar"
|
||||||
? "Vad observerades? (krävs för verifiering)"
|
? "What was observed? (required for verification)"
|
||||||
: "Resultat / avläst värde (valfritt)"
|
: "Result / value read (optional)"
|
||||||
}
|
}
|
||||||
varde={resultat}
|
varde={resultat}
|
||||||
satt={setResultat}
|
satt={setResultat}
|
||||||
platshallare={krav === "matvarde" ? "T.ex. 2,4 bar samtliga hjul" : "T.ex. Säkringen är hel, spänning på båda sidor"}
|
platshallare={krav === "matvarde" ? "e.g. 2.4 bar on all wheels" : "e.g. The fuse is intact, voltage on both sides"}
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
{!kravUppfyllt && (
|
{!kravUppfyllt && (
|
||||||
<p className="mb-3 text-[12px] font-semibold text-[#00437A]">
|
<p className="mb-3 text-[12px] font-semibold text-[#00437A]">
|
||||||
Ingen {krav === "matvarde" ? "mätning" : "observation"} har registrerats — lägg till en kort{" "}
|
No {krav === "matvarde" ? "measurement" : "observation"} has been recorded — add a short{" "}
|
||||||
{krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras.
|
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
<StorKnapp disabled={!kravUppfyllt} onClick={utford}>
|
||||||
@@ -1656,7 +1656,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TextFalt label="Eller egen orsak" 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
|
Avbryt
|
||||||
@@ -1671,12 +1671,12 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
|||||||
|
|
||||||
const DOKTYPER = [
|
const DOKTYPER = [
|
||||||
{ id: "observation", label: "Observation" },
|
{ id: "observation", label: "Observation" },
|
||||||
{ id: "matvarde", label: "Mätvärde" },
|
{ id: "matvarde", label: "Measurement" },
|
||||||
{ id: "foto", label: "Foto" },
|
{ id: "foto", label: "Photo" },
|
||||||
{ id: "video", label: "Video" },
|
{ id: "video", label: "Video" },
|
||||||
{ id: "instrument", label: "Instrument" },
|
{ id: "instrument", label: "Instrument" },
|
||||||
{ id: "hypotes", label: "Hypotes" },
|
{ id: "hypotes", label: "Hypothesis" },
|
||||||
{ id: "kommentar", label: "Kommentar" },
|
{ id: "comment", label: "Comment" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Fri dokumentation vid sidan av guiden. Hypoteser märks alltid som
|
// Fri dokumentation vid sidan av guiden. Hypoteser märks alltid som
|
||||||
@@ -1721,10 +1721,10 @@ function SnabbDokumentation({
|
|||||||
} else if (!resultat) {
|
} else if (!resultat) {
|
||||||
setAvlasning({ foto, tolkning: byggDemoInstrument(), demo: true });
|
setAvlasning({ foto, tolkning: byggDemoInstrument(), demo: true });
|
||||||
} else {
|
} else {
|
||||||
skicka({ typ: "foto", beskrivning: "Instrumentfoto — inga värden kunde läsas", dataUrl: foto });
|
skicka({ typ: "foto", beskrivning: "Instrument photograph — no values could be read", dataUrl: foto });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
skicka({ typ: "foto", beskrivning: "Instrumentfoto — avläsningen kunde inte nås", dataUrl: foto });
|
skicka({ typ: "foto", beskrivning: "Instrument photograph — the reader could not be reached", dataUrl: foto });
|
||||||
}
|
}
|
||||||
setLaserAv(false);
|
setLaserAv(false);
|
||||||
};
|
};
|
||||||
@@ -1746,7 +1746,7 @@ function SnabbDokumentation({
|
|||||||
beskrivning: v.beskrivning,
|
beskrivning: v.beskrivning,
|
||||||
varde: v.varde,
|
varde: v.varde,
|
||||||
enhet: v.enhet,
|
enhet: v.enhet,
|
||||||
kalla: "Instrumentavläsning (modelltolkad, bekräftad av tekniker)",
|
kalla: "Instrument reading (model-interpreted, confirmed by the technician)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
paSparad?.(
|
paSparad?.(
|
||||||
@@ -1763,7 +1763,7 @@ function SnabbDokumentation({
|
|||||||
skicka({ typ: "observation", text: t });
|
skicka({ typ: "observation", text: t });
|
||||||
paSparad?.(`Observation: ${t}`);
|
paSparad?.(`Observation: ${t}`);
|
||||||
}
|
}
|
||||||
if (typ === "kommentar" && t) {
|
if (typ === "comment" && t) {
|
||||||
skicka({ typ: "kommentar", text: t });
|
skicka({ typ: "kommentar", text: t });
|
||||||
paSparad?.(t);
|
paSparad?.(t);
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1779,7 @@ function SnabbDokumentation({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Dokumentera">
|
<Panel rubrik="Document">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{DOKTYPER.map((d) => (
|
{DOKTYPER.map((d) => (
|
||||||
<button
|
<button
|
||||||
@@ -1813,7 +1813,7 @@ function SnabbDokumentation({
|
|||||||
const fil = e.target.files?.[0];
|
const fil = e.target.files?.[0];
|
||||||
if (!fil) return;
|
if (!fil) return;
|
||||||
const dataUrl = await skalaNerFoto(fil);
|
const dataUrl = await skalaNerFoto(fil);
|
||||||
skicka({ typ: "foto", beskrivning: "Foto", dataUrl });
|
skicka({ typ: "foto", beskrivning: "Photo", dataUrl });
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1843,7 +1843,7 @@ function SnabbDokumentation({
|
|||||||
try {
|
try {
|
||||||
setVideo({ dataUrl: await lasVideo(fil), beskrivning: "" });
|
setVideo({ dataUrl: await lasVideo(fil), beskrivning: "" });
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setVideoFel(misslyckande instanceof Error ? misslyckande.message : "Videon kunde inte läsas.");
|
setVideoFel(misslyckande instanceof Error ? misslyckande.message : "The video could not be read.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -1852,10 +1852,10 @@ function SnabbDokumentation({
|
|||||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
||||||
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#C6C6C6]" />
|
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#C6C6C6]" />
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Vad visar videon? (obligatoriskt — det som låter eller rör sig)"
|
label="What does the video show? (required — what makes the noise or moves)"
|
||||||
varde={video.beskrivning}
|
varde={video.beskrivning}
|
||||||
satt={(beskrivning) => setVideo((v) => (v ? { ...v, beskrivning } : v))}
|
satt={(beskrivning) => setVideo((v) => (v ? { ...v, beskrivning } : v))}
|
||||||
platshallare="T.ex. Motorljud vid tomgång — tickande från topplocket"
|
platshallare="e.g. Engine noise at idle — ticking from the cylinder head"
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
@@ -1875,7 +1875,7 @@ function SnabbDokumentation({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av instrumentet …</p>}
|
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Reading the instrument …</p>}
|
||||||
{avlasning && (
|
{avlasning && (
|
||||||
<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 && (
|
||||||
@@ -1900,7 +1900,7 @@ function SnabbDokumentation({
|
|||||||
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
|
||||||
Förkasta
|
Förkasta
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={sparaAvlasning}>Spara foto + värden</StorKnapp>
|
<StorKnapp onClick={sparaAvlasning}>Save photograph + values</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1912,13 +1912,13 @@ function SnabbDokumentation({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label={typ === "matvarde" ? "Vad mättes?" : DOKTYPER.find((d) => d.id === typ)!.label}
|
label={typ === "matvarde" ? "What was measured?" : DOKTYPER.find((d) => d.id === typ)!.label}
|
||||||
varde={text}
|
varde={text}
|
||||||
satt={setText}
|
satt={setText}
|
||||||
platshallare={typ === "matvarde" ? "T.ex. Matningsspänning stift 30" : ""}
|
platshallare={typ === "matvarde" ? "e.g. Supply voltage at pin 30" : ""}
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
{typ === "matvarde" && <TextFalt label="Värde (med enhet)" varde={varde} satt={setVarde} platshallare="T.ex. 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
|
Spara
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
@@ -1968,7 +1968,7 @@ function OverlamningDialog({
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
|
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
|
||||||
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#ADADAD] bg-[#F7F7F7] p-4">
|
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#ADADAD] bg-[#F7F7F7] p-4">
|
||||||
<h2 className="mb-3 text-[15px] font-semibold">Överlämningsrapport</h2>
|
<h2 className="mb-3 text-[15px] font-semibold">Handover report</h2>
|
||||||
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
|
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
|
||||||
{text}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -1977,9 +1977,9 @@ function OverlamningDialog({
|
|||||||
Komplettera analysen: risker & osäkerheter
|
Komplettera analysen: risker & osäkerheter
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Systemet sammanfattar …</p>}
|
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing …</p>}
|
||||||
{aiLage === "fel" && (
|
{aiLage === "fel" && (
|
||||||
<p className="mb-3 font-semibold text-[#8B1A1A]">Kunde inte hämtas — kräver inloggning. Överlämningen fungerar ändå.</p>
|
<p className="mb-3 font-semibold text-[#8B1A1A]">Could not be retrieved — requires sign-in. The handover works regardless.</p>
|
||||||
)}
|
)}
|
||||||
{aiLage === "klar" && aiRader.length > 0 && (
|
{aiLage === "klar" && aiRader.length > 0 && (
|
||||||
<div className="mb-3 bg-white p-4">
|
<div className="mb-3 bg-white p-4">
|
||||||
@@ -1990,7 +1990,7 @@ function OverlamningDialog({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<TextFalt label="Lämnas till (valfritt)" varde={till} satt={setTill} platshallare="T.ex. Johan" />
|
<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
|
Avbryt
|
||||||
@@ -2010,9 +2010,9 @@ function OverlamningDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NIVA_LABEL: Record<DelningsNiva, string> = {
|
const NIVA_LABEL: Record<DelningsNiva, string> = {
|
||||||
kund: "Kund",
|
kund: "Customer",
|
||||||
partner: "Extern partner",
|
partner: "External partner",
|
||||||
intern: "Intern",
|
intern: "Internal",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Live Share-behörighetsnivåer: skapa och återkalla delningslänkar.
|
// Live Share-behörighetsnivåer: skapa och återkalla delningslänkar.
|
||||||
@@ -2028,7 +2028,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
setDelningar(await hamtaDelningar(arende.id));
|
setDelningar(await hamtaDelningar(arende.id));
|
||||||
setFel("");
|
setFel("");
|
||||||
} catch {
|
} catch {
|
||||||
setFel("Kunde inte hämta delningar — kräver inloggning och att ärendet är synkat.");
|
setFel("Could not retrieve shares — requires sign-in and a synchronized case.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2054,7 +2054,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#DDDDDD] py-2 last:border-0">
|
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#DDDDDD] py-2 last:border-0">
|
||||||
<span className="text-[14px]">
|
<span className="text-[14px]">
|
||||||
{NIVA_LABEL[delning.niva]}{" "}
|
{NIVA_LABEL[delning.niva]}{" "}
|
||||||
{delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(återkallad)</span>}
|
{delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(revoked)</span>}
|
||||||
</span>
|
</span>
|
||||||
{!delning.aterkallad && (
|
{!delning.aterkallad && (
|
||||||
<span className="flex gap-2">
|
<span className="flex gap-2">
|
||||||
@@ -2091,7 +2091,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
skicka({ typ: "kommentar", text: `Delningslänk skapad (${NIVA_LABEL[niva]}).` });
|
skicka({ typ: "kommentar", text: `Delningslänk skapad (${NIVA_LABEL[niva]}).` });
|
||||||
uppdatera();
|
uppdatera();
|
||||||
} catch {
|
} catch {
|
||||||
setFel("Kunde inte skapa delning — kräver inloggning och att ärendet är synkat.");
|
setFel("Could not create a share — requires sign-in and a synchronized case.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2109,7 +2109,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
|
|
||||||
function LoggFlik({ arende }: { arende: Arende }) {
|
function LoggFlik({ arende }: { arende: Arende }) {
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Arbetslogg — append-only, ingenting skrivs över">
|
<Panel rubrik="Work log — append-only, nothing is overwritten">
|
||||||
<ol>
|
<ol>
|
||||||
{arende.handelser.map((post) => (
|
{arende.handelser.map((post) => (
|
||||||
<li key={post.id} className="flex gap-3 border-b border-[#DDDDDD] py-2 last:border-0">
|
<li key={post.id} className="flex gap-3 border-b border-[#DDDDDD] py-2 last:border-0">
|
||||||
@@ -2162,14 +2162,14 @@ function BriefFlik({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!b.avslutat && (
|
{!b.avslutat && (
|
||||||
<Panel rubrik="Granskning av underlaget">
|
<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
|
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.
|
observationer, luckor i dokumentationen och slutsatser som saknar stöd.
|
||||||
</p>
|
</p>
|
||||||
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Systemet granskar underlaget …</p>}
|
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
||||||
{granskning === "fel" && (
|
{granskning === "fel" && (
|
||||||
<p className="mb-2 font-semibold text-[#8B1A1A]">Granskningen kunde inte hämtas — kräver inloggning. Försök igen.</p>
|
<p className="mb-2 font-semibold text-[#8B1A1A]">The review could not be retrieved — requires sign-in. Try again.</p>
|
||||||
)}
|
)}
|
||||||
{senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && (
|
{senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
@@ -2179,7 +2179,7 @@ function BriefFlik({
|
|||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-1 text-[14px]">
|
<p className="mt-1 text-[14px]">
|
||||||
<span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
|
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">Senaste analys ({senasteAi.handelse.modell})</p>
|
<p className="mt-1 text-[11px] text-[#707070]">Senaste analys ({senasteAi.handelse.modell})</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -2189,7 +2189,7 @@ function BriefFlik({
|
|||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Objekt">
|
<Panel rubrik="Object">
|
||||||
{b.objekt ? (
|
{b.objekt ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
|
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
|
||||||
@@ -2198,14 +2198,14 @@ function BriefFlik({
|
|||||||
{b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</p>}
|
{b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</p>}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[#4A5560]">Ej identifierat</p>
|
<p className="text-[#4A5560]">Not identified</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Kundens beskrivning">
|
<Panel rubrik="Customer's description">
|
||||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Utförda kontroller">
|
<Panel rubrik="Checks performed">
|
||||||
{b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
|
{b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">None yet.</p>}
|
||||||
{b.utfordaKontroller.map((k, i) =>
|
{b.utfordaKontroller.map((k, i) =>
|
||||||
k.undantag ? (
|
k.undantag ? (
|
||||||
<p key={i} className="py-0.5 text-[14px] text-[#9A6700]">
|
<p key={i} className="py-0.5 text-[14px] text-[#9A6700]">
|
||||||
@@ -2219,14 +2219,14 @@ function BriefFlik({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Observationer">
|
<Panel rubrik="Observations">
|
||||||
{b.observationer.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
|
{b.observationer.length === 0 && <p className="text-[#4A5560]">None yet.</p>}
|
||||||
{b.observationer.map((o, i) => (
|
{b.observationer.map((o, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">• {o}</p>
|
<p key={i} className="py-0.5 text-[14px]">• {o}</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
{b.hypoteser.length > 0 && (
|
{b.hypoteser.length > 0 && (
|
||||||
<Panel rubrik="Hypoteser — kräver verifiering">
|
<Panel rubrik="Hypotheses — require verification">
|
||||||
{b.hypoteser.map((h, i) => (
|
{b.hypoteser.map((h, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">
|
<p key={i} className="py-0.5 text-[14px]">
|
||||||
<NivaBadge niva={h.niva} /> {h.text}
|
<NivaBadge niva={h.niva} /> {h.text}
|
||||||
@@ -2234,27 +2234,27 @@ function BriefFlik({
|
|||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Ej kontrollerat">
|
<Panel rubrik="Not checked">
|
||||||
{b.ejKontrollerat.length === 0 && <p className="text-[#4A5560]">Allt i metodiken är utfört.</p>}
|
{b.ejKontrollerat.length === 0 && <p className="text-[#4A5560]">Every step in the methodology is complete.</p>}
|
||||||
{b.ejKontrollerat.map((e, i) => (
|
{b.ejKontrollerat.map((e, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px] text-[#333333]">– {e}</p>
|
<p key={i} className="py-0.5 text-[14px] text-[#333333]">– {e}</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
{!b.avslutat && (
|
{!b.avslutat && (
|
||||||
<Panel rubrik="Rekommenderat nästa steg">
|
<Panel rubrik="Recommended next step">
|
||||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
{b.rekommenderatNastaSteg.map((s, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Tillförlitlighet">
|
<Panel rubrik="Reliability">
|
||||||
{b.tillforlitlighet.map((r, i) => (
|
{b.tillforlitlighet.map((r, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">
|
<p key={i} className="py-0.5 text-[14px]">
|
||||||
<NivaBadge niva={r.niva} /> {r.text}
|
<NivaBadge niva={r.niva} /> {r.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Total arbetstid">
|
<Panel rubrik="Total labour time">
|
||||||
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
@@ -2338,7 +2338,7 @@ function RapportFlik({
|
|||||||
{rad.ok ? <IkonCheck /> : rad.kravs ? <IkonKryss /> : <IkonVarning />}
|
{rad.ok ? <IkonCheck /> : rad.kravs ? <IkonKryss /> : <IkonVarning />}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{rad.rubrik}
|
{rad.rubrik}
|
||||||
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (rekommenderas)</span>}
|
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (recommended)</span>}
|
||||||
</p>
|
</p>
|
||||||
{rad.detalj && <p className="pl-6 text-[12px] text-[#707070]">{rad.detalj}</p>}
|
{rad.detalj && <p className="pl-6 text-[12px] text-[#707070]">{rad.detalj}</p>}
|
||||||
</div>
|
</div>
|
||||||
@@ -2350,7 +2350,7 @@ function RapportFlik({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Kundrapport">
|
<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
|
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas — bilder kan
|
||||||
innehålla uppgifter om andra kunder.
|
innehålla uppgifter om andra kunder.
|
||||||
@@ -2364,7 +2364,7 @@ function RapportFlik({
|
|||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
||||||
<StorKnapp variant="sekundar"><IkonPunkt farg="#1E6B34" /> Öppna Live Share-vy</StorKnapp>
|
<StorKnapp variant="sekundar"><IkonPunkt farg="#1E6B34" /> Open Live Share view</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
{plattformAktiv() ? (
|
{plattformAktiv() ? (
|
||||||
<DelningsHanterare arende={arende} skicka={skicka} />
|
<DelningsHanterare arende={arende} skicka={skicka} />
|
||||||
@@ -2375,10 +2375,10 @@ function RapportFlik({
|
|||||||
className="mt-2"
|
className="mt-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${arende.delningskod}`);
|
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${arende.delningskod}`);
|
||||||
skicka({ typ: "kommentar", text: "Delningslänk kopierad för extern mottagare." });
|
skicka({ typ: "kommentar", text: "Share link copied for an external recipient." });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IkonLank /> Kopiera delningslänk
|
<IkonLank /> Copy share link
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -2390,9 +2390,9 @@ function RapportFlik({
|
|||||||
rapporten — kund, handläggare, nästa tekniker — ska få bilden
|
rapporten — kund, handläggare, nästa tekniker — ska få bilden
|
||||||
på fem sekunder och sedan kunna gå djupare. Texten är härledd
|
på fem sekunder och sedan kunna gå djupare. Texten är härledd
|
||||||
ur loggen, så den kan inte säga något som inte står där. */}
|
ur loggen, så den kan inte säga något som inte står där. */}
|
||||||
<Panel rubrik="Sammanfattning">
|
<Panel rubrik="Summary">
|
||||||
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
|
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
|
||||||
<p className="mt-2 font-mono text-[11px] text-[#4D5662]">ALVA-PROC-0030 · härledd ur händelseloggen</p>
|
<p className="mt-2 font-mono text-[11px] text-[#4D5662]">ALVA-PROC-0030 · derived from the case log</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{/* ALVA-RULE-200 · Teknikerns varför. Placerad före underlaget:
|
{/* ALVA-RULE-200 · Teknikerns varför. Placerad före underlaget:
|
||||||
@@ -2402,12 +2402,12 @@ function RapportFlik({
|
|||||||
if (!s || s.handelse.typ !== "slutsats") return null;
|
if (!s || s.handelse.typ !== "slutsats") return null;
|
||||||
const h = s.handelse;
|
const h = s.handelse;
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Slutsats och motivering">
|
<Panel rubrik="Closing statement and rationale">
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
|
[h.orsakFastställd === false ? "Reason the cause could not be established" : "Rationale", h.motivering],
|
||||||
["Uteslutna alternativ", h.uteslutet],
|
["Dismissed alternatives", h.uteslutet],
|
||||||
["Val av åtgärd", h.atgardsval ?? "—"],
|
["Choice of action", h.atgardsval ?? "—"],
|
||||||
["Kvarstående osäkerhet", h.kvarstaende],
|
["Remaining uncertainty", h.kvarstaende],
|
||||||
])}
|
])}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
@@ -2415,43 +2415,43 @@ function RapportFlik({
|
|||||||
|
|
||||||
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
|
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
|
||||||
återanvänd här automatiskt. */}
|
återanvänd här automatiskt. */}
|
||||||
<Panel rubrik="Ärendeinformation">
|
<Panel rubrik="Case information">
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
["Ärende", `#${arende.nummer}`],
|
["Case", `#${arende.nummer}`],
|
||||||
["Arbetsorder", idn.arbetsorder],
|
["Work order", idn.arbetsorder],
|
||||||
["Claim-/garantinr", idn.claim],
|
["Claim / warranty no.", idn.claim],
|
||||||
["Skadenummer", idn.skadenummer],
|
["Claim number", idn.skadenummer],
|
||||||
["Ärendetyp", typ],
|
["Case type", typ],
|
||||||
["Ansvarig tekniker", idn.ansvarig],
|
["Responsible technician", idn.ansvarig],
|
||||||
])}
|
])}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Fordonsinformation">
|
<Panel rubrik="Vehicle information">
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
["Personbil", idn.beskrivning],
|
["Passenger car", idn.beskrivning],
|
||||||
["Regnr", idn.identifierare],
|
["Reg. no.", idn.identifierare],
|
||||||
["VIN", idn.vin],
|
["VIN", idn.vin],
|
||||||
["Mätarställning in", matIn],
|
["Odometer in", matIn],
|
||||||
["Mätarställning ut", matUt],
|
["Odometer out", matUt],
|
||||||
["Kund", idn.kund],
|
["Customer", idn.kund],
|
||||||
])}
|
])}
|
||||||
</Panel>
|
</Panel>
|
||||||
{/* Beviskedjan: kundens upplevelse, teknikerns verifiering och den
|
{/* Beviskedjan: kundens upplevelse, teknikerns verifiering och den
|
||||||
tekniska slutsatsen hålls strikt åtskilda och spårbara. */}
|
tekniska slutsatsen hålls strikt åtskilda och spårbara. */}
|
||||||
<Panel rubrik="Kundens beskrivning">
|
<Panel rubrik="Customer's description">
|
||||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Verifierad observation">
|
<Panel rubrik="Verified observation">
|
||||||
{repro ? (
|
{repro ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-[14px] font-semibold">{reproduceringsText(repro.status)}</p>
|
<p className="text-[14px] font-semibold">{reproduceringsText(repro.status)}</p>
|
||||||
<p className="text-[13px] text-[#4A5560]">{repro.beskrivning}</p>
|
<p className="text-[13px] text-[#4A5560]">{repro.beskrivning}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[14px] text-[#4A5560]">Reproducering ej dokumenterad ännu.</p>
|
<p className="text-[14px] text-[#4A5560]">Reproduction not documented yet.</p>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Felorsaksanalys">
|
<Panel rubrik="Root cause analysis">
|
||||||
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4A5560]">Felorsaksanalys ej dokumenterad ännu.</p>}
|
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4A5560]">Root cause analysis not documented yet.</p>}
|
||||||
{orsakerLista.map((p) => {
|
{orsakerLista.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "felorsak") return null;
|
if (h.typ !== "felorsak") return null;
|
||||||
@@ -2459,42 +2459,42 @@ function RapportFlik({
|
|||||||
<div key={p.id} className="mb-3 border-b border-[#EBEBEB] pb-2 last:mb-0 last:border-0">
|
<div key={p.id} className="mb-3 border-b border-[#EBEBEB] pb-2 last:mb-0 last:border-0">
|
||||||
<p className="text-[14px] font-semibold">{h.avvikelse}</p>
|
<p className="text-[14px] font-semibold">{h.avvikelse}</p>
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
["Bedömd grundorsak", h.orsaker.join(", ")],
|
["Assessed root cause", h.orsaker.join(", ")],
|
||||||
["Underlag för bedömningen", h.underlag.join(", ")],
|
["Evidence for the assessment", h.underlag.join(", ")],
|
||||||
["Säkerhetsnivå", TILLFORLITLIGHET_LABEL[h.sakerhet]],
|
["Confidence level", TILLFORLITLIGHET_LABEL[h.sakerhet]],
|
||||||
["Rekommenderad åtgärd", h.atgard],
|
["Recommended action", h.atgard],
|
||||||
["Motivering", h.motivering],
|
["Rationale", h.motivering],
|
||||||
["Stärkande kontroller", h.ytterligareKontroller],
|
["Strengthening checks", h.ytterligareKontroller],
|
||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Panel>
|
</Panel>
|
||||||
{(forslagRader.length > 0 || beslutRad) && (
|
{(forslagRader.length > 0 || beslutRad) && (
|
||||||
<Panel rubrik="Åtgärdsförslag och kundens besked">
|
<Panel rubrik="Proposed action and customer decision">
|
||||||
{forslagRader.map((p) => {
|
{forslagRader.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgardsforslag") return null;
|
if (h.typ !== "atgardsforslag") return null;
|
||||||
return (
|
return (
|
||||||
<div key={p.id}>
|
<div key={p.id}>
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
["Föreslagen åtgärd", h.beskrivning],
|
["Proposed action", h.beskrivning],
|
||||||
["Uppskattad kostnad", h.uppskattadKostnad],
|
["Estimated cost", h.uppskattadKostnad],
|
||||||
["Uppskattad tid", h.uppskattadTid],
|
["Estimated time", h.uppskattadTid],
|
||||||
])}
|
])}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{beslutRad &&
|
{beslutRad &&
|
||||||
identitetsRader([
|
identitetsRader([
|
||||||
["Kundens besked", KUNDBESLUT_LABEL[beslutRad.beslut]],
|
["Customer decision", KUNDBESLUT_LABEL[beslutRad.beslut]],
|
||||||
["Lämnat via", beslutRad.kanal],
|
["Given via", beslutRad.kanal],
|
||||||
["Kommentar", beslutRad.kommentar],
|
["Comment", beslutRad.kommentar],
|
||||||
])}
|
])}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Utförd åtgärd och verifiering">
|
<Panel rubrik="Action performed and verification">
|
||||||
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4A5560]">Ingen åtgärd dokumenterad ännu.</p>}
|
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4A5560]">No action documented yet.</p>}
|
||||||
{atgardsRader.map((p) => {
|
{atgardsRader.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgard_utford") return null;
|
if (h.typ !== "atgard_utford") return null;
|
||||||
@@ -2502,8 +2502,8 @@ function RapportFlik({
|
|||||||
<div key={p.id} className="mb-2">
|
<div key={p.id} className="mb-2">
|
||||||
{h.utford ? (
|
{h.utford ? (
|
||||||
identitetsRader([
|
identitetsRader([
|
||||||
["Utförd åtgärd", h.beskrivning],
|
["Action performed", h.beskrivning],
|
||||||
["Delar", 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]">Ingen åtgärd utförd — {h.motivering}</p>
|
||||||
@@ -2513,12 +2513,12 @@ function RapportFlik({
|
|||||||
})}
|
})}
|
||||||
{kkRad &&
|
{kkRad &&
|
||||||
identitetsRader([
|
identitetsRader([
|
||||||
["Kvalitetskontroll", KVALITETSKONTROLL_LABEL[kkRad.resultat]],
|
["Quality check", KVALITETSKONTROLL_LABEL[kkRad.resultat]],
|
||||||
["Verifiering", kkRad.beskrivning],
|
["Verification", kkRad.beskrivning],
|
||||||
])}
|
])}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Sammanfattning">
|
<Panel rubrik="Summary">
|
||||||
<p className="text-[14px]">Total arbetstid: <span className="font-semibold">{b.totalArbetstid}</span></p>
|
<p className="text-[14px]">Total labour time: <span className="font-semibold">{b.totalArbetstid}</span></p>
|
||||||
{fordelning.length > 0 && (
|
{fordelning.length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{fordelning.map((r) => (
|
{fordelning.map((r) => (
|
||||||
@@ -2527,7 +2527,7 @@ function RapportFlik({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Tidslinje">
|
<Panel rubrik="Timeline">
|
||||||
{kundposter.map((post) => (
|
{kundposter.map((post) => (
|
||||||
<p key={post.id} className="py-0.5 text-[13px]">
|
<p key={post.id} className="py-0.5 text-[13px]">
|
||||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||||
@@ -2536,7 +2536,7 @@ function RapportFlik({
|
|||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
{klipp.length > 0 && (
|
{klipp.length > 0 && (
|
||||||
<Panel rubrik="Dokumenterad video">
|
<Panel rubrik="Documented video">
|
||||||
{klipp.map((v, i) => (
|
{klipp.map((v, i) => (
|
||||||
<figure key={i} className="mb-2">
|
<figure key={i} className="mb-2">
|
||||||
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
|
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
|
||||||
@@ -2546,7 +2546,7 @@ function RapportFlik({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{bilder.length > 0 && (
|
{bilder.length > 0 && (
|
||||||
<Panel rubrik="Dokumenterade bilder">
|
<Panel rubrik="Documented images">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{bilder.map((bild, i) => (
|
{bilder.map((bild, i) => (
|
||||||
<figure key={i}>
|
<figure key={i}>
|
||||||
@@ -2558,7 +2558,7 @@ function RapportFlik({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
||||||
<Panel rubrik="Rekommenderade nästa steg">
|
<Panel rubrik="Recommended next steps">
|
||||||
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
|
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
||||||
))}
|
))}
|
||||||
@@ -2601,7 +2601,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
|||||||
try {
|
try {
|
||||||
const { anmalSupport, plattformAktiv } = await import("@/felsokning/plattform");
|
const { anmalSupport, plattformAktiv } = await import("@/felsokning/plattform");
|
||||||
if (!plattformAktiv()) {
|
if (!plattformAktiv()) {
|
||||||
setUtfall({ fel: "Ingen plattform är konfigurerad — anmälan kan inte skickas härifrån." });
|
setUtfall({ fel: "No platform is configured — the report cannot be sent from here." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { beteckning } = await anmalSupport({
|
const { beteckning } = await anmalSupport({
|
||||||
@@ -2633,42 +2633,42 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
|||||||
onClick={() => setOppen(true)}
|
onClick={() => setOppen(true)}
|
||||||
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline"
|
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline"
|
||||||
>
|
>
|
||||||
Rapportera ett problem med systemet
|
Report a problem with the system
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Rapportera ett problem med systemet">
|
<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
|
Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden
|
||||||
skickas.
|
skickas.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||||
{[
|
{[
|
||||||
["felanmalan", "Fel"],
|
["felanmalan", "Fault"],
|
||||||
["fraga", "Fråga"],
|
["fraga", "Question"],
|
||||||
["forbattring", "Förbättring"],
|
["forbattring", "Improvement"],
|
||||||
].map(([id, etikett]) => (
|
].map(([id, etikett]) => (
|
||||||
<StorKnapp key={id} variant={typ === id ? "primar" : "sekundar"} onClick={() => setTyp(id)}>
|
<StorKnapp key={id} variant={typ === id ? "primar" : "sekundar"} onClick={() => setTyp(id)}>
|
||||||
{etikett}
|
{etikett}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TextFalt label="Vad gäller det?" varde={rubrik} satt={setRubrik} platshallare="Kort rubrik" />
|
<TextFalt label="What is it about?" varde={rubrik} satt={setRubrik} platshallare="Short title" />
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Vad hände, och vad väntade du dig?"
|
label="What happened, and what did you expect?"
|
||||||
varde={text}
|
varde={text}
|
||||||
satt={setText}
|
satt={setText}
|
||||||
platshallare="Den som svarar ser inte din skärm."
|
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]">Anmäld som {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 ? "Skickar" : "Skicka anmälan"}
|
{skickar ? "Sending" : "Send report"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||||
Stäng
|
Stäng
|
||||||
@@ -2695,16 +2695,16 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
|
|||||||
const paket = aktivtRegelpaket();
|
const paket = aktivtRegelpaket();
|
||||||
const avslutad = [...arende.handelser].reverse().find((p) => p.handelse.typ === "arende_avslutat");
|
const avslutad = [...arende.handelser].reverse().find((p) => p.handelse.typ === "arende_avslutat");
|
||||||
const falt: [string, string][] = [
|
const falt: [string, string][] = [
|
||||||
["Beteckning", arendebeteckning(arende.nummer)],
|
["Designation", arendebeteckning(arende.nummer)],
|
||||||
["Regelpaket", paket.version],
|
["Rule package", paket.version],
|
||||||
["Evidensmodell", `ECM ${ECM_VERSION}`],
|
["Evidence model", `ECM ${ECM_VERSION}`],
|
||||||
// Versionen VID AVSLUTET, inte dagens. Stämpeln läste tidigare den
|
// Versionen VID AVSLUTET, inte dagens. Stämpeln läste tidigare den
|
||||||
// aktuella konstanten, så ett ärende stängt under 1.0 visade 3.1 —
|
// aktuella konstanten, så ett ärende stängt under 1.0 visade 3.1 —
|
||||||
// vilket är exakt det stämpelns beskrivning ovan säger att den inte
|
// vilket är exakt det stämpelns beskrivning ovan säger att den inte
|
||||||
// ska göra. Ett öppet ärende visar dagens, eftersom det är sant då.
|
// ska göra. Ett öppet ärende visar dagens, eftersom det är sant då.
|
||||||
["Plattform", avslutad ? versionVidAvslut(avslutad.handelse) : PLATTFORMSVERSION],
|
["Platform", avslutad ? versionVidAvslut(avslutad.handelse) : PLATTFORMSVERSION],
|
||||||
["Status", avslutad ? "Closed" : "In progress"],
|
["Status", avslutad ? "Closed" : "In progress"],
|
||||||
["Utgåva", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
|
["Release", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import { tidDatum, tidKlockslag } from "@/felsokning/format";
|
|||||||
type Filter = "alla" | "pagaende" | "klara";
|
type Filter = "alla" | "pagaende" | "klara";
|
||||||
|
|
||||||
const ROLL_LABEL: Record<PlattformRoll, string> = {
|
const ROLL_LABEL: Record<PlattformRoll, string> = {
|
||||||
tekniker: "Tekniker",
|
tekniker: "Technician",
|
||||||
arbetsledare: "Arbetsledare",
|
arbetsledare: "Supervisor",
|
||||||
admin: "Systemadministratör",
|
admin: "System administrator",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Självhostat läge: inloggning mot plattformstjänsten i klustret.
|
// Självhostat läge: inloggning mot plattformstjänsten i klustret.
|
||||||
@@ -43,7 +43,7 @@ function PlattformInloggning() {
|
|||||||
if (konto) {
|
if (konto) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel rubrik="Plattformskonto">
|
<Panel rubrik="Platform account">
|
||||||
<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]} ·{" "}
|
||||||
@@ -61,12 +61,12 @@ function PlattformInloggning() {
|
|||||||
</div>
|
</div>
|
||||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||||
<Link to="/felsokning/oversikt" className="mt-3 block">
|
<Link to="/felsokning/oversikt" className="mt-3 block">
|
||||||
<StorKnapp variant="sekundar"><IkonDiagram /> Organisationsöversikt</StorKnapp>
|
<StorKnapp variant="sekundar"><IkonDiagram /> Organization overview</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{konto.roll === "admin" && (
|
{konto.roll === "admin" && (
|
||||||
<Link to="/felsokning/installningar" className="mt-2 block">
|
<Link to="/felsokning/installningar" className="mt-2 block">
|
||||||
<StorKnapp variant="sekundar"><IkonKugghjul /> Inställningar för organisationen</StorKnapp>
|
<StorKnapp variant="sekundar"><IkonKugghjul /> Organization settings</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -76,7 +76,7 @@ function PlattformInloggning() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Plattformskonto">
|
<Panel rubrik="Platform account">
|
||||||
{lage === "stangd" ? (
|
{lage === "stangd" ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-2 text-[#333333]">
|
<p className="mb-2 text-[#333333]">
|
||||||
@@ -85,9 +85,9 @@ function PlattformInloggning() {
|
|||||||
</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")}>
|
||||||
Skapa organisation
|
Create organization
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={() => setLage("loggaIn")}>Logga in</StorKnapp>
|
<StorKnapp onClick={() => setLage("loggaIn")}>Sign in</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -102,18 +102,18 @@ function PlattformInloggning() {
|
|||||||
: await registreraPlattform(epost, losenord, nyttNamn, organisation),
|
: await registreraPlattform(epost, losenord, nyttNamn, organisation),
|
||||||
);
|
);
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setFel(misslyckande instanceof Error ? misslyckande.message : "Något gick fel.");
|
setFel(misslyckande instanceof Error ? misslyckande.message : "Something went wrong.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{lage === "registrera" && (
|
{lage === "registrera" && (
|
||||||
<>
|
<>
|
||||||
<TextFalt label="Organisation" varde={organisation} satt={setOrganisation} platshallare="T.ex. Verkstad Syd AB" />
|
<TextFalt label="Organisation" varde={organisation} satt={setOrganisation} platshallare="e.g. Southside Motors Ltd" />
|
||||||
<TextFalt label="Ditt namn" varde={nyttNamn} satt={setNyttNamn} platshallare="T.ex. Anna" />
|
<TextFalt label="Your name" varde={nyttNamn} satt={setNyttNamn} platshallare="e.g. Anna" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<TextFalt label="E-post" varde={epost} satt={setEpost} platshallare="anna@verkstaden.se" />
|
<TextFalt label="E-mail" varde={epost} satt={setEpost} platshallare="anna@workshop.example" />
|
||||||
<TextFalt label="Lösenord (minst 8 tecken)" varde={losenord} satt={setLosenord} losenord />
|
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
||||||
{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")}>
|
||||||
@@ -127,7 +127,7 @@ function PlattformInloggning() {
|
|||||||
(lage === "registrera" && (!nyttNamn.trim() || !organisation.trim()))
|
(lage === "registrera" && (!nyttNamn.trim() || !organisation.trim()))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{lage === "loggaIn" ? "Logga in" : "Skapa organisation"}
|
{lage === "loggaIn" ? "Sign in" : "Create organization"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -151,13 +151,13 @@ function AnvandarAdmin() {
|
|||||||
try {
|
try {
|
||||||
setLista(await hamtaAnvandare());
|
setLista(await hamtaAnvandare());
|
||||||
} catch {
|
} catch {
|
||||||
setFel("Kunde inte hämta användare.");
|
setFel("Could not retrieve users.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!oppen) {
|
if (!oppen) {
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Användare">
|
<Panel rubrik="Users">
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
variant="sekundar"
|
variant="sekundar"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -172,7 +172,7 @@ function AnvandarAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Användare">
|
<Panel rubrik="Users">
|
||||||
{lista.map((anv) => (
|
{lista.map((anv) => (
|
||||||
<div
|
<div
|
||||||
key={anv.id}
|
key={anv.id}
|
||||||
@@ -184,7 +184,7 @@ function AnvandarAdmin() {
|
|||||||
</span>{" "}
|
</span>{" "}
|
||||||
· {ROLL_LABEL[anv.roll]} <span className="text-[#707070]">{anv.epost}</span>
|
· {ROLL_LABEL[anv.roll]} <span className="text-[#707070]">{anv.epost}</span>
|
||||||
{anv.aktiv === false && (
|
{anv.aktiv === false && (
|
||||||
<span className="ml-1 text-[12px] font-semibold text-[#8B1A1A]">Avstängd</span>
|
<span className="ml-1 text-[12px] font-semibold text-[#8B1A1A]">Disabled</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
@@ -200,11 +200,11 @@ function AnvandarAdmin() {
|
|||||||
await sattKontoAktiv(anv.id, anv.aktiv === false);
|
await sattKontoAktiv(anv.id, anv.aktiv === false);
|
||||||
await uppdatera();
|
await uppdatera();
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setFel(misslyckande instanceof Error ? misslyckande.message : "Något gick fel.");
|
setFel(misslyckande instanceof Error ? misslyckande.message : "Something went wrong.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{anv.aktiv === false ? "Öppna igen" : "Stäng av"}
|
{anv.aktiv === false ? "Reopen" : "Disable"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -223,13 +223,13 @@ function AnvandarAdmin() {
|
|||||||
setLosenord("");
|
setLosenord("");
|
||||||
await uppdatera();
|
await uppdatera();
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setFel(misslyckande instanceof Error ? misslyckande.message : "Något gick fel.");
|
setFel(misslyckande instanceof Error ? misslyckande.message : "Something went wrong.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextFalt label="Namn" varde={namn} satt={setNamn} />
|
<TextFalt label="Name" varde={namn} satt={setNamn} />
|
||||||
<TextFalt label="E-post" varde={epost} satt={setEpost} />
|
<TextFalt label="E-mail" varde={epost} satt={setEpost} />
|
||||||
<TextFalt label="Lösenord (minst 8 tecken)" varde={losenord} satt={setLosenord} losenord />
|
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
||||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||||
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
|
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
|
||||||
<button
|
<button
|
||||||
@@ -270,8 +270,8 @@ export default function Arendelista() {
|
|||||||
|
|
||||||
if (!anvandare) {
|
if (!anvandare) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Guidad Felsökning">
|
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||||
<Panel rubrik="Vem arbetar?">
|
<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.
|
Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja.
|
||||||
</p>
|
</p>
|
||||||
@@ -281,7 +281,7 @@ export default function Arendelista() {
|
|||||||
if (namn.trim()) sattAnvandare(namn);
|
if (namn.trim()) sattAnvandare(namn);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextFalt label="Namn" varde={namn} satt={setNamn} platshallare="T.ex. 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
|
Fortsätt
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
@@ -297,18 +297,18 @@ export default function Arendelista() {
|
|||||||
const lista = filter === "pagaende" ? pagaende : filter === "klara" ? klara : alla;
|
const lista = filter === "pagaende" ? pagaende : filter === "klara" ? klara : alla;
|
||||||
|
|
||||||
const FILTER: { id: Filter; label: string; antal: number }[] = [
|
const FILTER: { id: Filter; label: string; antal: number }[] = [
|
||||||
{ id: "alla", label: "Alla", antal: alla.length },
|
{ id: "alla", label: "All", antal: alla.length },
|
||||||
{ id: "pagaende", label: "Pågående", antal: pagaende.length },
|
{ id: "pagaende", label: "In progress", antal: pagaende.length },
|
||||||
{ id: "klara", label: "Klara", antal: klara.length },
|
{ id: "klara", label: "Complete", antal: klara.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
rubrik="Guidad Felsökning"
|
rubrik="Guided Diagnostics"
|
||||||
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{anvandare}</span>}
|
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{anvandare}</span>}
|
||||||
>
|
>
|
||||||
<Link to="/felsokning/nytt">
|
<Link to="/felsokning/nytt">
|
||||||
<StorKnapp className="mb-4">+ Nytt ärende</StorKnapp>
|
<StorKnapp className="mb-4">+ New case</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{alla.length > 0 && (
|
{alla.length > 0 && (
|
||||||
@@ -328,7 +328,7 @@ export default function Arendelista() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{alla.length === 0 && (
|
{alla.length === 0 && (
|
||||||
<Panel rubrik="Kom igång">
|
<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
|
Inga ärenden ännu. Starta med att identifiera ett objekt — eller utforska ett färdigt demoärende
|
||||||
med komplett arbetslogg, brief och kundrapport.
|
med komplett arbetslogg, brief och kundrapport.
|
||||||
@@ -340,18 +340,18 @@ export default function Arendelista() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{lista.length === 0 && alla.length > 0 && (
|
{lista.length === 0 && alla.length > 0 && (
|
||||||
<p className="text-center text-[14px] text-[#4A5560]">Inga ärenden i det här filtret.</p>
|
<p className="text-center text-[14px] text-[#4A5560]">No cases in this filter.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plattformAktiv() && <PlattformInloggning />}
|
{plattformAktiv() && <PlattformInloggning />}
|
||||||
|
|
||||||
{!plattformKonto() && (
|
{!plattformKonto() && (
|
||||||
<Link to="/felsokning/installningar" className="mb-4 block">
|
<Link to="/felsokning/installningar" className="mb-4 block">
|
||||||
<StorKnapp variant="sekundar"><IkonKugghjul /> Inställningar</StorKnapp>
|
<StorKnapp variant="sekundar"><IkonKugghjul /> Settings</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel rubrik="Beslutsstöd">
|
<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
|
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
|
dokumenterar, granskar hela ärendet på begäran och skriver utkast till överlämningen. Den
|
||||||
@@ -379,10 +379,10 @@ export default function Arendelista() {
|
|||||||
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{avslutat ? "Avslutat" : "Pågående"}
|
{avslutat ? "Closed" : "In progress"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Okänt objekt"}</p>
|
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</p>
|
||||||
{o && <p className="text-[12px] font-semibold text-[#4A5560]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
|
{o && <p className="text-[12px] font-semibold text-[#4A5560]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
|
||||||
{fel && <p className="mt-2 text-[14px] text-[#333333]">”{fel}”</p>}
|
{fel && <p className="mt-2 text-[14px] text-[#333333]">”{fel}”</p>}
|
||||||
<p className="mt-2 text-[12px] text-[#707070]">
|
<p className="mt-2 text-[12px] text-[#707070]">
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export default function DelatArende() {
|
|||||||
|
|
||||||
if (!arende) {
|
if (!arende) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Delat ärende">
|
<FelsokningSkal rubrik="Shared case">
|
||||||
<p className="text-[14px] text-[#333333]">Ärendet är inte tillgängligt.</p>
|
<p className="text-[14px] text-[#333333]">The case is not available.</p>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export default function DelatArende() {
|
|||||||
<DelatArendeVy
|
<DelatArendeVy
|
||||||
arende={arende}
|
arende={arende}
|
||||||
nu={nu}
|
nu={nu}
|
||||||
notis="Skrivskyddad livevy — uppdateras automatiskt när verkstaden registrerar ny information. Detta är förhandsvisningen; den externa delningslänken visar samma vy."
|
notis="Read-only live view — updates automatically as the workshop records new information. This is the preview; the external share link shows the same view."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function Integrationer() {
|
|||||||
if (leverantorer.length === 0) return null;
|
if (leverantorer.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Märkesspecifika kopplingar">
|
<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å
|
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.
|
plattformen och används bara av servern — de skickas aldrig till teknikernas enheter.
|
||||||
@@ -114,7 +114,7 @@ function Integrationer() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className={`shrink-0 text-[11px] font-semibold ${finns ? "text-[#1E6B34]" : "text-[#707070]"}`}>
|
<span className={`shrink-0 text-[11px] font-semibold ${finns ? "text-[#1E6B34]" : "text-[#707070]"}`}>
|
||||||
{finns ? "Konfigurerad" : "Ej konfigurerad"}
|
{finns ? "Configured" : "Not configured"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ function Integrationer() {
|
|||||||
{lev.falt.map((falt) => (
|
{lev.falt.map((falt) => (
|
||||||
<TextFalt
|
<TextFalt
|
||||||
key={falt.nyckel}
|
key={falt.nyckel}
|
||||||
label={`${falt.etikett}${falt.hemlig ? " (lagras krypterat)" : ""}`}
|
label={`${falt.etikett}${falt.hemlig ? " (stored encrypted)" : ""}`}
|
||||||
varde={varden[falt.nyckel] ?? ""}
|
varde={varden[falt.nyckel] ?? ""}
|
||||||
satt={(v) => setVarden((f) => ({ ...f, [falt.nyckel]: v }))}
|
satt={(v) => setVarden((f) => ({ ...f, [falt.nyckel]: v }))}
|
||||||
losenord={falt.hemlig}
|
losenord={falt.hemlig}
|
||||||
@@ -145,7 +145,7 @@ function Integrationer() {
|
|||||||
setSparat(lev.namn);
|
setSparat(lev.namn);
|
||||||
ladda();
|
ladda();
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setFel(misslyckande instanceof Error ? misslyckande.message : "Kunde inte spara.");
|
setFel(misslyckande instanceof Error ? misslyckande.message : "Could not save.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -172,7 +172,7 @@ function Integrationer() {
|
|||||||
setVarden(start);
|
setVarden(start);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{finns ? "Uppdatera uppgifter" : "Lägg till uppgifter"}
|
{finns ? "Update details" : "Add details"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
{finns && (
|
{finns && (
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
@@ -210,7 +210,7 @@ export default function Installningar() {
|
|||||||
|
|
||||||
if (!farAndra) {
|
if (!farAndra) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<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.
|
Organisationens inställningar hanteras av er systemadministratör.
|
||||||
@@ -223,7 +223,7 @@ export default function Installningar() {
|
|||||||
const spara = async () => {
|
const spara = async () => {
|
||||||
if (inst.objekttyper.length === 0 || inst.identifieringsmetoder.length === 0) {
|
if (inst.objekttyper.length === 0 || inst.identifieringsmetoder.length === 0) {
|
||||||
setStatus("fel");
|
setStatus("fel");
|
||||||
setFelText("Minst ett alternativ måste vara valt i varje lista.");
|
setFelText("At least one option must be selected in each list.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus("sparar");
|
setStatus("sparar");
|
||||||
@@ -233,7 +233,7 @@ export default function Installningar() {
|
|||||||
setStatus("sparat");
|
setStatus("sparat");
|
||||||
} catch (misslyckande) {
|
} catch (misslyckande) {
|
||||||
setStatus("fel");
|
setStatus("fel");
|
||||||
setFelText(misslyckande instanceof Error ? misslyckande.message : "Kunde inte spara.");
|
setFelText(misslyckande instanceof Error ? misslyckande.message : "Could not save.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,17 +243,17 @@ export default function Installningar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<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.{" "}
|
Välj vad som visas när ett nytt ärende startas.{" "}
|
||||||
{inloggad
|
{inloggad
|
||||||
? "Valet gäller alla i organisationen."
|
? "The setting applies to everyone in the organization."
|
||||||
: "I lokalt läge gäller valet den här enheten; inloggad systemadministratör styr hela organisationen."}
|
: "In local mode the setting applies to this device; a signed-in system administrator governs the whole organization."}
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel rubrik="Objekttyper">
|
<Panel rubrik="Object types">
|
||||||
<Vallista
|
<Vallista
|
||||||
alla={ALLA_OBJEKTTYPER}
|
alla={ALLA_OBJEKTTYPER}
|
||||||
valda={inst.objekttyper}
|
valda={inst.objekttyper}
|
||||||
@@ -261,7 +261,7 @@ export default function Installningar() {
|
|||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel rubrik="Identifieringsmetoder">
|
<Panel rubrik="Identification methods">
|
||||||
<Vallista
|
<Vallista
|
||||||
alla={ALLA_IDENTIFIERINGSMETODER}
|
alla={ALLA_IDENTIFIERINGSMETODER}
|
||||||
valda={inst.identifieringsmetoder}
|
valda={inst.identifieringsmetoder}
|
||||||
@@ -272,9 +272,9 @@ export default function Installningar() {
|
|||||||
{inloggad && <Integrationer />}
|
{inloggad && <Integrationer />}
|
||||||
|
|
||||||
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
|
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
|
||||||
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]">✓ Sparat</p>}
|
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]">✓ Saved</p>}
|
||||||
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
||||||
{status === "sparar" ? "Sparar …" : "Spara inställningar"}
|
{status === "sparar" ? "Saving …" : "Save settings"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ export default function NyttArende() {
|
|||||||
const kandidater = resultat?.tolkning.varden.map((v) => v.varde) ?? [];
|
const kandidater = resultat?.tolkning.varden.map((v) => v.varde) ?? [];
|
||||||
const traff = kandidater.map((v) => tolkaKod(v)).find((k): k is AvlastKod => !!k);
|
const traff = kandidater.map((v) => tolkaKod(v)).find((k): k is AvlastKod => !!k);
|
||||||
if (traff) anvandKod(traff);
|
if (traff) anvandKod(traff);
|
||||||
else if (!resultat) setKodFel("Avläsning av typskylt kräver inloggning — ange identiteten manuellt.");
|
else if (!resultat) setKodFel("Reading the type plate requires sign-in — enter the identity manually.");
|
||||||
else setKodFel("Ingen identifierare kunde läsas ur bilden — försök igen eller ange manuellt.");
|
else setKodFel("No identifier could be read from the image — try again or enter it manually.");
|
||||||
} catch {
|
} catch {
|
||||||
setKodFel("Avläsningen kunde inte nås — ange identiteten manuellt.");
|
setKodFel("The reader could not be reached — enter the identity manually.");
|
||||||
}
|
}
|
||||||
setLaserTypskylt(false);
|
setLaserTypskylt(false);
|
||||||
};
|
};
|
||||||
@@ -110,7 +110,7 @@ export default function NyttArende() {
|
|||||||
const resultat = await tolkaArbetsorder(foto);
|
const resultat = await tolkaArbetsorder(foto);
|
||||||
if (resultat) {
|
if (resultat) {
|
||||||
if (resultat.falt.length === 0) {
|
if (resultat.falt.length === 0) {
|
||||||
setSkanningsFel("Inga fält kunde läsas ur bilden — ta ett tydligare foto eller fyll i manuellt.");
|
setSkanningsFel("No fields could be read from the image — take a clearer photograph or fill in manually.");
|
||||||
} else {
|
} else {
|
||||||
setTolkning({ foto, falt: resultat.falt, modell: resultat.modell });
|
setTolkning({ foto, falt: resultat.falt, modell: resultat.modell });
|
||||||
setSteg("granska");
|
setSteg("granska");
|
||||||
@@ -120,7 +120,7 @@ export default function NyttArende() {
|
|||||||
setSteg("granska");
|
setSteg("granska");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setSkanningsFel("Dokumenttolkningen kunde inte nås — försök igen eller fyll i manuellt.");
|
setSkanningsFel("Document interpretation could not be reached — try again or fill in manually.");
|
||||||
}
|
}
|
||||||
setSkannar(false);
|
setSkannar(false);
|
||||||
};
|
};
|
||||||
@@ -136,10 +136,10 @@ export default function NyttArende() {
|
|||||||
const vin = v("fordon_vin");
|
const vin = v("fordon_vin");
|
||||||
const beskr = [v("fordon_marke"), v("fordon_modell"), v("fordon_arsmodell")].filter(Boolean).join(" ");
|
const beskr = [v("fordon_marke"), v("fordon_modell"), v("fordon_arsmodell")].filter(Boolean).join(" ");
|
||||||
const objektet: Objekt = {
|
const objektet: Objekt = {
|
||||||
typ: inst.objekttyper.includes("Personbil") ? "Personbil" : inst.objekttyper[0],
|
typ: inst.objekttyper.includes("Passenger car") ? "Passenger car" : inst.objekttyper[0],
|
||||||
identifierare: (regnr ?? vin ?? "OKÄND").toUpperCase(),
|
identifierare: (regnr ?? vin ?? "UNKNOWN").toUpperCase(),
|
||||||
identifieringsmetod: regnr ? "Regnr" : vin ? "VIN" : "Manuell inmatning",
|
identifieringsmetod: regnr ? "Reg. no." : vin ? "VIN" : "Manual entry",
|
||||||
beskrivning: beskr || "Se arbetsorder",
|
beskrivning: beskr || "View work order",
|
||||||
kund: v("kund_namn"),
|
kund: v("kund_namn"),
|
||||||
// Ärendeidentiteten: registreras en gång här, återanvänds i alla vyer.
|
// Ärendeidentiteten: registreras en gång här, återanvänds i alla vyer.
|
||||||
vin: vin?.toUpperCase(),
|
vin: vin?.toUpperCase(),
|
||||||
@@ -148,7 +148,7 @@ export default function NyttArende() {
|
|||||||
claim: v("ao_claim"),
|
claim: v("ao_claim"),
|
||||||
skadenummer: v("ao_skadenummer"),
|
skadenummer: v("ao_skadenummer"),
|
||||||
};
|
};
|
||||||
const felText = v("felbeskrivning") ?? "Se skannad arbetsorder";
|
const felText = v("felbeskrivning") ?? "View the scanned work order";
|
||||||
let metodikId: string | undefined;
|
let metodikId: string | undefined;
|
||||||
if (valjMetodik(felText).id === "generisk") {
|
if (valjMetodik(felText).id === "generisk") {
|
||||||
metodikId = (await valjMetodikMedAi(felText)) ?? undefined;
|
metodikId = (await valjMetodikMedAi(felText)) ?? undefined;
|
||||||
@@ -174,8 +174,8 @@ export default function NyttArende() {
|
|||||||
|
|
||||||
if (steg === "start") {
|
if (steg === "start") {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Starta ärende" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel rubrik="Skanna arbetsorder">
|
<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 —
|
Fota arbetsorderns framsida. Systemet läser kund-, fordons- och ärendeuppgifter automatiskt —
|
||||||
du granskar bara osäkra fält och trycker Starta diagnos.
|
du granskar bara osäkra fält och trycker Starta diagnos.
|
||||||
@@ -193,7 +193,7 @@ export default function NyttArende() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{skannar ? (
|
{skannar ? (
|
||||||
<p className="animate-pulse py-2 text-center font-semibold text-[#00437A]">Tolkar dokumentet …</p>
|
<p className="animate-pulse py-2 text-center font-semibold text-[#00437A]">Interpreting the document …</p>
|
||||||
) : (
|
) : (
|
||||||
<StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp>
|
<StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp>
|
||||||
)}
|
)}
|
||||||
@@ -212,8 +212,8 @@ export default function NyttArende() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
bred
|
bred
|
||||||
rubrik="Granska tolkad arbetsorder"
|
rubrik="Review the interpreted work order"
|
||||||
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
|
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||||
>
|
>
|
||||||
{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]">
|
||||||
@@ -236,8 +236,8 @@ export default function NyttArende() {
|
|||||||
|
|
||||||
if (steg === "identifiera") {
|
if (steg === "identifiera") {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Identifiera objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<FelsokningSkal rubrik="Identify object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel rubrik="Objekttyp">
|
<Panel rubrik="Object type">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{inst.objekttyper.map((t) => (
|
{inst.objekttyper.map((t) => (
|
||||||
<StorKnapp key={t} variant={t === typ ? "primar" : "sekundar"} onClick={() => setTyp(t)}>
|
<StorKnapp key={t} variant={t === typ ? "primar" : "sekundar"} onClick={() => setTyp(t)}>
|
||||||
@@ -246,7 +246,7 @@ export default function NyttArende() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Identifieringsmetod">
|
<Panel rubrik="Identification method">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{inst.identifieringsmetoder.map((m) => (
|
{inst.identifieringsmetoder.map((m) => (
|
||||||
<StorKnapp key={m} variant={m === metod ? "primar" : "sekundar"} onClick={() => setMetod(m)}>
|
<StorKnapp key={m} variant={m === metod ? "primar" : "sekundar"} onClick={() => setMetod(m)}>
|
||||||
@@ -258,7 +258,7 @@ export default function NyttArende() {
|
|||||||
QR-kod, streckkod och typskylt kan läsas av i nästa steg — eller ange identiteten manuellt.
|
QR-kod, streckkod och typskylt kan läsas av i nästa steg — eller ange identiteten manuellt.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Läs av identiteten">
|
<Panel rubrik="Read the identity">
|
||||||
<input
|
<input
|
||||||
ref={typskyltRef}
|
ref={typskyltRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -289,7 +289,7 @@ export default function NyttArende() {
|
|||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
)}
|
)}
|
||||||
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
<StorKnapp variant="sekundar" disabled={laserTypskylt} onClick={() => typskyltRef.current?.click()}>
|
||||||
<IkonKamera /> {laserTypskylt ? "Läser av …" : "Fotografera typskylt"}
|
<IkonKamera /> {laserTypskylt ? "Reading …" : "Photograph the type plate"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -301,10 +301,10 @@ export default function NyttArende() {
|
|||||||
)}
|
)}
|
||||||
{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>}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Objektets identitet">
|
<Panel rubrik="Object identity">
|
||||||
<TextFalt label={metod} varde={identifierare} satt={setIdentifierare} platshallare="T.ex. ABC123" />
|
<TextFalt label={metod} varde={identifierare} satt={setIdentifierare} platshallare="e.g. ABC123" />
|
||||||
<TextFalt label="Beskrivning (märke, modell, år)" 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="Kund (valfritt)" varde={kund} satt={setKund} platshallare="T.ex. Anders Svensson" />
|
<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
|
Fortsätt
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
@@ -315,7 +315,7 @@ export default function NyttArende() {
|
|||||||
|
|
||||||
if (steg === "bekrafta") {
|
if (steg === "bekrafta") {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Bekräfta objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
|
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
|
||||||
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
||||||
@@ -327,7 +327,7 @@ export default function NyttArende() {
|
|||||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||||
Ändra
|
Ändra
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Rätt objekt — fortsätt</StorKnapp>
|
<StorKnapp onClick={() => setSteg("felbeskrivning")}>Correct object — continue</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
@@ -336,13 +336,13 @@ export default function NyttArende() {
|
|||||||
const metodik = fel.trim() ? valjMetodik(fel) : undefined;
|
const metodik = fel.trim() ? valjMetodik(fel) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Beskriv felet" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<FelsokningSkal rubrik="Describe the fault" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel rubrik={`${objektet.beskrivning} · ${objektet.identifierare}`}>
|
<Panel rubrik={`${objektet.beskrivning} · ${objektet.identifierare}`}>
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Kundens/användarens felbeskrivning"
|
label="Customer's or user's fault description"
|
||||||
varde={fel}
|
varde={fel}
|
||||||
satt={setFel}
|
satt={setFel}
|
||||||
platshallare="T.ex. Bilen vibrerar runt 88 km/h"
|
platshallare="e.g. The car vibrates at around 88 km/h"
|
||||||
flerRad
|
flerRad
|
||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
@@ -365,7 +365,7 @@ export default function NyttArende() {
|
|||||||
navigate(`/felsokning/arende/${id}`);
|
navigate(`/felsokning/arende/${id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{startar ? "Startar …" : "Starta arbetslogg"}
|
{startar ? "Starting …" : "Start work log"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -396,16 +396,16 @@ function GranskaTolkning({
|
|||||||
|
|
||||||
const obekraftade = rader.filter((r) => !r.bekraftad);
|
const obekraftade = rader.filter((r) => !r.bekraftad);
|
||||||
const vald = rader.find((r) => r.id === markerad);
|
const vald = rader.find((r) => r.id === markerad);
|
||||||
const grupper: ArbetsorderGrupp[] = ["Kund", "Personbil", "Verkstad", "Ärende"];
|
const grupper: ArbetsorderGrupp[] = ["Customer", "Vehicle", "Workshop", "Case"];
|
||||||
|
|
||||||
const uppdatera = (id: string, andring: Partial<(typeof rader)[number]>) =>
|
const uppdatera = (id: string, andring: Partial<(typeof rader)[number]>) =>
|
||||||
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
|
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-3">
|
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-3">
|
||||||
<Panel rubrik="Skannad arbetsorder">
|
<Panel rubrik="Scanned work order">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img src={foto} alt="Skannad arbetsorder" className="w-full border border-[#C6C6C6]" />
|
<img src={foto} alt="Scanned work order" className="w-full border border-[#C6C6C6]" />
|
||||||
{vald?.omrade && (
|
{vald?.omrade && (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute border-2 border-[#00437A] bg-[#00437A]/15"
|
className="pointer-events-none absolute border-2 border-[#00437A] bg-[#00437A]/15"
|
||||||
@@ -421,7 +421,7 @@ function GranskaTolkning({
|
|||||||
<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} (ungefärlig position)`
|
||||||
: "Klicka på ett fält för att se var i dokumentet värdet hittades."}
|
: "Select a field to see where in the document the value was found."}
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
@@ -488,7 +488,7 @@ function GranskaTolkning({
|
|||||||
Skanna om
|
Skanna om
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
<StorKnapp disabled={obekraftade.length > 0 || startar} onClick={() => vidStart(rader)}>
|
||||||
{startar ? "Startar …" : "Starta diagnos"}
|
{startar ? "Starting …" : "Start diagnosis"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
|
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">Ny ansvarig tekniker</p>
|
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">New responsible technician</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{personer.map((person) => (
|
{personer.map((person) => (
|
||||||
<button
|
<button
|
||||||
@@ -92,14 +92,14 @@ export default function Oversikt() {
|
|||||||
if (!behorig) return;
|
if (!behorig) return;
|
||||||
hamtaOversikt()
|
hamtaOversikt()
|
||||||
.then(setRader)
|
.then(setRader)
|
||||||
.catch(() => setFel("Kunde inte hämta översikten — kontrollera inloggningen."));
|
.catch(() => setFel("Could not retrieve the overview — check the sign-in."));
|
||||||
// Flottdata: återkommande felorsaker över organisationen.
|
// Flottdata: återkommande felorsaker över organisationen.
|
||||||
hamtaFelorsaksstatistik().then(setFelorsaker).catch(() => setFelorsaker([]));
|
hamtaFelorsaksstatistik().then(setFelorsaker).catch(() => setFelorsaker([]));
|
||||||
}, [behorig]);
|
}, [behorig]);
|
||||||
|
|
||||||
if (!behorig) {
|
if (!behorig) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Organisationsöversikt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
<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.
|
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
|
||||||
@@ -134,7 +134,7 @@ export default function Oversikt() {
|
|||||||
}
|
}
|
||||||
navigate(`/felsokning/arende/${rad.id}`);
|
navigate(`/felsokning/arende/${rad.id}`);
|
||||||
} catch {
|
} catch {
|
||||||
setFel("Kunde inte hämta ärendet.");
|
setFel("Could not retrieve the case.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,28 +151,28 @@ export default function Oversikt() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
rubrik="Organisationsöversikt"
|
rubrik="Organization overview"
|
||||||
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
|
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||||
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{konto?.organisation}</span>}
|
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{konto?.organisation}</span>}
|
||||||
>
|
>
|
||||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Hämtar …</p>}
|
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Retrieving …</p>}
|
||||||
|
|
||||||
{rader && (
|
{rader && (
|
||||||
<>
|
<>
|
||||||
<Panel rubrik="Statistik">
|
<Panel rubrik="Statistics">
|
||||||
<div className="grid grid-cols-3 gap-3 text-center">
|
<div className="grid grid-cols-3 gap-3 text-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold">{rader.length}</p>
|
<p className="text-[20px] font-semibold">{rader.length}</p>
|
||||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Totalt</p>
|
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Total</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
|
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
|
||||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Pågående</p>
|
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">In progress</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold">{avslutade.length}</p>
|
<p className="text-[20px] font-semibold">{avslutade.length}</p>
|
||||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Avslutade</p>
|
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Closed</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{avslutade.length > 0 && (
|
{avslutade.length > 0 && (
|
||||||
@@ -183,7 +183,7 @@ export default function Oversikt() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{felorsaker.length > 0 && (
|
{felorsaker.length > 0 && (
|
||||||
<Panel rubrik="Felorsaksstatistik — återkommande fel i organisationen">
|
<Panel rubrik="Root cause statistics — recurring faults in the organization">
|
||||||
{felorsaker.map((rad) => (
|
{felorsaker.map((rad) => (
|
||||||
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
|
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
|
||||||
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
|
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
|
||||||
@@ -200,7 +200,7 @@ export default function Oversikt() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">Inga ärenden i organisationen ännu.</p>}
|
{rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">No cases in the organization yet.</p>}
|
||||||
|
|
||||||
{rader.map((rad) => (
|
{rader.map((rad) => (
|
||||||
<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">
|
||||||
@@ -213,10 +213,10 @@ export default function Oversikt() {
|
|||||||
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{rad.avslutat ? "Avslutat" : "Pågående"}
|
{rad.avslutat ? "Closed" : "In progress"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Okänt objekt"}</p>
|
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
|
||||||
{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>
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ interface DelatSvar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NIVA_NOTIS: Record<string, string> = {
|
const NIVA_NOTIS: Record<string, string> = {
|
||||||
kund: "Skrivskyddad livevy från verkstaden — sidan uppdateras automatiskt när ny information registreras.",
|
kund: "Read-only live view from the workshop — the page updates automatically as new information is recorded.",
|
||||||
partner:
|
partner:
|
||||||
"Skrivskyddad livevy (partnernivå) — inkluderar tekniska hypoteser, tydligt märkta som ej verifierade. Uppdateras automatiskt.",
|
"Read-only live view (partner level) — includes technical hypotheses, clearly marked as unverified. Updates automatically.",
|
||||||
intern: "Skrivskyddad livevy (intern nivå) — full insyn i ärendet. Uppdateras automatiskt.",
|
intern: "Read-only live view (internal level) — full visibility of the case. Updates automatically.",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function hamtaDelat(kod: string): Promise<{ arende: Arende; niva: string } | undefined> {
|
async function hamtaDelat(kod: string): Promise<{ arende: Arende; niva: string } | undefined> {
|
||||||
@@ -87,12 +87,12 @@ export default function PublikDelning() {
|
|||||||
|
|
||||||
if (status !== "klar" || !arende) {
|
if (status !== "klar" || !arende) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Delat ärende">
|
<FelsokningSkal rubrik="Shared case">
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<p className="text-[14px] text-[#333333]">
|
||||||
{status === "laddar"
|
{status === "laddar"
|
||||||
? "Hämtar ärendet …"
|
? "Retrieving the case …"
|
||||||
: "Ärendet är inte tillgängligt. Kontrollera länken med verkstaden — delningen kan ha stängts av."}
|
: "The case is not available. Check the link with the workshop — the share may have been revoked."}
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
@@ -105,7 +105,7 @@ export default function PublikDelning() {
|
|||||||
niva === "kund" && kod
|
niva === "kund" && kod
|
||||||
? async (beslut: "godkant" | "avbojt", kommentar: string) => {
|
? async (beslut: "godkant" | "avbojt", kommentar: string) => {
|
||||||
const { plattformAktiv, PLATTFORM_URL } = await import("@/felsokning/plattform");
|
const { plattformAktiv, PLATTFORM_URL } = await import("@/felsokning/plattform");
|
||||||
if (!plattformAktiv()) return "Beskedet kan inte lämnas här — kontakta verkstaden.";
|
if (!plattformAktiv()) return "The decision cannot be given here — contact the workshop.";
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${PLATTFORM_URL}/api/delad/${kod}/beslut`, {
|
const res = await fetch(`${PLATTFORM_URL}/api/delad/${kod}/beslut`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -114,14 +114,14 @@ export default function PublikDelning() {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||||
return data.error ?? "Beskedet kunde inte registreras — försök igen.";
|
return data.error ?? "The decision could not be recorded — try again.";
|
||||||
}
|
}
|
||||||
// Hämta om direkt så kvittensen syns.
|
// Hämta om direkt så kvittensen syns.
|
||||||
const uppdaterat = await hamtaDelat(kod);
|
const uppdaterat = await hamtaDelat(kod);
|
||||||
if (uppdaterat) setArende(uppdaterat.arende);
|
if (uppdaterat) setArende(uppdaterat.arende);
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
} catch {
|
||||||
return "Beskedet kunde inte skickas — kontrollera anslutningen.";
|
return "The decision could not be sent — check the connection.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user