Ett designsystem, inte två

Portalen höll åtta färger. Arbetsvyn höll fyrtiosju. Det var inte ett
slarv utan två system: ui.tsx bar en egen designfilosofi i sitt filhuvud
— "industriellt verkstadsverktyg i ETKA-tradition", egen palett, eget
snitt, egen rytm — vid sidan av ALVA-SPEC-001. Två beskrivningar av hur
en produkt ska se ut blir aldrig lika.

Värst syntes det i typsnittet. Arbetsvyn låg utanför `.alva-yta` och
ärvde därför värdapplikationens antikva — exakt samma fynd som en gång
gjordes om portalen, fast här hade det stått kvar. En tekniker som
klickade från portalen in i ett ärende bytte både färg och bokstavsform.

De fyrtiosju färgerna avbildas nu på ALVA:s åtta. Tonade fyllningar
avskaffas helt: allvarlighet bärs i linje och text, aldrig i en fylld
yta. En gul varningsruta drar mer uppmärksamhet än sitt innehåll, och
när tre ligger på samma skärm bär ingen av dem något. Grönt finns inte i
paletten — blått bär "verifierat", vilket STATUSFARG redan sa.

Skuggor och övergångar bort. Panelen är samma form som portalens Block,
verktygsraden samma vita rad med en linje under, och ordmärket står
först i den precis som i portalens sidhuvud. 176 avståndsklasser snäpptes
till 8 px-rutnätet.

Knapparna är gemener nu, i BÅDA ytorna. Etiketter och rubriker är versala
i ALVA — de är uppmärkning. En knapp är en uppmaning, och i arbetsvyn är
uppmaningen ofta en hel mening: "Photograph the instrument panel". Versal
sats läses långsammare och bryts sämre, och just den knappen trycks med
handskar bredvid ett fordon. Att låta portalen behålla versaler hade gett
två knappspråk i samma produkt, vilket var det som skulle bort.

Designtestet omfattar nu hela ytan — marknadswebb, portal OCH arbetsvy.
Att det bara gällde halva produkten var själva orsaken till att de kunde
glida isär: ett designsystem som gäller halva ytan är en stilguide.
Mutationsprövat: en enda återinförd blå utanför paletten fäller det.

Två fel som bara en skärmdump från en telefon kunde hitta, båda samma
sak — text som var vit eller ljusgrå mot den gamla mörkblå raden och
blev osynlig mot den vita: tiden i ärendehuvudet och användarnamnet i
listan.

Tjugo svenska strängar till, alla i mall-literaler och därför osynliga
för de tidigare sveparna: loggens radtexter, överlämningsrapporten,
AI-promptens fältnamn och "1 tim 37 min".

750 tester, 186 integrationskontroller, genomgång, portalspärr,
typkontroll och lint gröna. Artefakten mätt på 390 och 1280 px över
femton vyer: inget spill, inga externa begäranden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 18:39:56 +00:00
parent 15328c6380
commit b72048e40b
22 changed files with 551 additions and 428 deletions
+13 -1
View File
@@ -341,7 +341,19 @@ export function Knapp({
// `h-full` och inte bara `w-full`: lika breda knappar där den ena // `h-full` och inte bara `w-full`: lika breda knappar där den ena
// texten bryts till två rader blir olika HÖGA, vilket är samma // texten bryts till två rader blir olika HÖGA, vilket är samma
// obalans en rad ned. Bägge fyller sin cell. // obalans en rad ned. Bägge fyller sin cell.
className={`border px-4 py-2 text-[13px] font-semibold uppercase tracking-[0.08em] disabled:opacity-40${ // Gemener, inte versaler.
//
// Etiketter och rubriker är versala i ALVA — de är uppmärkning, och
// ett tekniskt dokument märker upp sina fält så. En knapp är inte
// uppmärkning utan en uppmaning, och i arbetsvyn är uppmaningen ofta
// en hel mening: "Photograph the instrument panel". Versal sats är
// långsammare att läsa och bryts sämre, och just den knappen trycks
// med handskar bredvid ett fordon.
//
// Regeln gäller båda ytorna. Att låta portalen behålla versaler hade
// gett två knappspråk i samma produkt, vilket är precis det som
// skulle bort.
className={`border px-4 py-2 text-[13px] font-semibold tracking-[0.02em] disabled:opacity-40${
bred ? " h-full w-full" : "" bred ? " h-full w-full" : ""
}`} }`}
style={{ style={{
@@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar
function Platshallare({ text }: { text: string }) { function Platshallare({ text }: { text: string }) {
return ( return (
<p className="border border-dashed border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#707070]"> <p className="border border-dashed border-[#D7DCE2] bg-[#F6F7F8] p-4 text-[12px] text-[#4D5662]">
{text} {text}
</p> </p>
); );
+29 -29
View File
@@ -28,8 +28,8 @@ function BeslutsKnappar({
const [fel, setFel] = useState(""); const [fel, setFel] = useState("");
return ( return (
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden"> <div className="mt-2 border-t border-[#D7DCE2] pt-2 print:hidden">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Your decision to the workshop Your decision to the workshop
</p> </p>
<div className="mb-2 grid grid-cols-2 gap-2"> <div className="mb-2 grid grid-cols-2 gap-2">
@@ -43,14 +43,14 @@ function BeslutsKnappar({
{val && ( {val && (
<> <>
<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-2 block text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
{val === "avbojt" ? "Short rationale (optional)" : "Comment (optional)"} {val === "avbojt" ? "Short rationale (optional)" : "Comment (optional)"}
</span> </span>
<input <input
value={kommentar} value={kommentar}
maxLength={500} maxLength={500}
onChange={(e) => setKommentar(e.target.value)} onChange={(e) => setKommentar(e.target.value)}
className="w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] focus:border-[#00437A] focus:outline-none" className="w-full border border-[#D7DCE2] bg-white px-2 py-2 text-[13px] focus:border-[#005CA9] focus:outline-none"
/> />
</label> </label>
{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>}
@@ -66,7 +66,7 @@ function BeslutsKnappar({
> >
{skickar ? "Sending …" : "Send decision"} {skickar ? "Sending …" : "Send decision"}
</StorKnapp> </StorKnapp>
<p className="mt-1 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
The decision is recorded in the case and cannot be changed here contact the workshop if you change your mind. The decision is recorded in the case and cannot be changed here contact the workshop if you change your mind.
</p> </p>
</> </>
@@ -88,14 +88,14 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole
["Status", avslutat ? "Closed" : "Diagnosis in progress"], ["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-12 z-10 mb-4 border border-[#D7DCE2] bg-[#F6F7F8] px-4 py-2 print:static">
{idn.beskrivning && <p className="text-[13px] font-semibold">{idn.beskrivning}</p>} {idn.beskrivning && <p className="text-[13px] font-semibold">{idn.beskrivning}</p>}
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]"> <p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
{falt {falt
.filter(([, varde]) => varde) .filter(([, varde]) => varde)
.map(([etikett, varde]) => ( .map(([etikett, varde]) => (
<span key={etikett} className="whitespace-nowrap"> <span key={etikett} className="whitespace-nowrap">
<span className="text-[#707070]">{etikett}:</span> <span className="font-medium">{varde}</span> <span className="text-[#4D5662]">{etikett}:</span> <span className="font-medium">{varde}</span>
</span> </span>
))} ))}
</p> </p>
@@ -145,8 +145,8 @@ export function DelatArendeVy({
rubrik={b.objekt?.beskrivning ?? "Case"} rubrik={b.objekt?.beskrivning ?? "Case"}
hoger={ hoger={
<span <span
className={`px-3 py-1 text-[12px] font-semibold uppercase ${ className={`px-4 py-2 text-[12px] font-semibold uppercase ${
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white" avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`} }`}
> >
{avslutat ? "Closed" : "Diagnosis in progress"} {avslutat ? "Closed" : "Diagnosis in progress"}
@@ -185,7 +185,7 @@ export function DelatArendeVy({
{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">
<dt className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#4D5662]">{etikett}</dt> <dt className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#4D5662]">{etikett}</dt>
<dd className="mt-1 text-[#1B1E22]">{text}</dd> <dd className="mt-2 text-[#1B1E22]">{text}</dd>
</div> </div>
))} ))}
</dl> </dl>
@@ -193,7 +193,7 @@ export function DelatArendeVy({
); );
})()} })()}
<p className="mb-4 border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden"> <p className="mb-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4 text-[12px] text-[#4D5662] print:hidden">
{notis} {notis}
</p> </p>
@@ -212,20 +212,20 @@ export function DelatArendeVy({
<div key={p.id} className="mb-2 last:mb-0"> <div key={p.id} className="mb-2 last:mb-0">
<p className="text-[14px]">{h.beskrivning}</p> <p className="text-[14px]">{h.beskrivning}</p>
{h.uppskattadKostnad && ( {h.uppskattadKostnad && (
<p className="text-[13px] text-[#4A5560]">Uppskattad kostnad: {h.uppskattadKostnad}</p> <p className="text-[13px] text-[#4D5662]">Uppskattad kostnad: {h.uppskattadKostnad}</p>
)} )}
</div> </div>
); );
})} })}
{beslut?.handelse.typ === "kundbeslut" ? ( {beslut?.handelse.typ === "kundbeslut" ? (
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[13px] font-semibold"> <p className="mt-2 border-t border-[#D7DCE2] pt-2 text-[13px] font-semibold">
Ditt besked: {KUNDBESLUT_LABEL[beslut.handelse.beslut]}{" "} Ditt besked: {KUNDBESLUT_LABEL[beslut.handelse.beslut]}{" "}
<span className="font-normal text-[#4A5560]">(registrerat via {beslut.handelse.kanal})</span> <span className="font-normal text-[#4D5662]">(registrerat via {beslut.handelse.kanal})</span>
</p> </p>
) : vidBeslut ? ( ) : vidBeslut ? (
<BeslutsKnappar vidBeslut={vidBeslut} /> <BeslutsKnappar vidBeslut={vidBeslut} />
) : ( ) : (
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[12px] text-[#707070]"> <p className="mt-2 border-t border-[#D7DCE2] pt-2 text-[12px] text-[#4D5662]">
Awaiting your decision contact the workshop to approve or decline. Awaiting your decision contact the workshop to approve or decline.
</p> </p>
)} )}
@@ -234,11 +234,11 @@ export function DelatArendeVy({
<Panel rubrik="Current 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 text-[14px]"><span className="text-[#005CA9]"><IkonCheck /></span> {k.text}</p>
))} ))}
{!avslutat && pagaende && <p className="py-0.5 text-[14px] text-[#00437A]"><IkonUppdatera /> {pagaende}</p>} {!avslutat && pagaende && <p className="py-0 text-[14px] text-[#005CA9]"><IkonUppdatera /> {pagaende}</p>}
{b.ejKontrollerat.slice(avslutat ? 0 : 1).map((e, i) => ( {b.ejKontrollerat.slice(avslutat ? 0 : 1).map((e, i) => (
<p key={`e${i}`} className="py-0.5 text-[14px] text-[#4A5560]"><IkonKlocka /> {e}</p> <p key={`e${i}`} className="py-0 text-[14px] text-[#4D5662]"><IkonKlocka /> {e}</p>
))} ))}
</Panel> </Panel>
@@ -246,8 +246,8 @@ export function DelatArendeVy({
<Panel rubrik="Video"> <Panel rubrik="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} delningskod={delningskod} className="w-full border border-[#C6C6C6]" /> <Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#D7DCE2]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption> <figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</Panel> </Panel>
@@ -257,8 +257,8 @@ export function DelatArendeVy({
<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}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#C6C6C6]" /> <Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#D7DCE2]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption> <figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</div> </div>
@@ -273,9 +273,9 @@ export function DelatArendeVy({
const h = p.handelse; const h = p.handelse;
if (h.typ !== "matvarde") return null; if (h.typ !== "matvarde") return null;
return ( return (
<tr key={p.id} className="border-b border-[#DDDDDD] last:border-0"> <tr key={p.id} className="border-b border-[#D7DCE2] last:border-0">
<td className="py-1 pr-3 text-[#333333]">{h.beskrivning}</td> <td className="py-2 pr-4 text-[#1B1E22]">{h.beskrivning}</td>
<td className="py-1 font-semibold"> <td className="py-2 font-semibold">
{h.varde} {h.varde}
{h.enhet ? ` ${h.enhet}` : ""} {h.enhet ? ` ${h.enhet}` : ""}
</td> </td>
@@ -289,8 +289,8 @@ export function DelatArendeVy({
<Panel rubrik="Timeline"> <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 text-[13px]">
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "} <span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)} {handelseRubrik(post)}
</p> </p>
))} ))}
@@ -305,13 +305,13 @@ export function DelatArendeVy({
<Panel rubrik="Labour time"> <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-[#1B1E22]">
{r.label}: {r.tid} {r.label}: {r.tid}
</p> </p>
))} ))}
</Panel> </Panel>
<p className="text-center text-[11px] text-[#8A8A8A]"> <p className="text-center text-[11px] text-[#4D5662]">
Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log
</p> </p>
</FelsokningSkal> </FelsokningSkal>
+3 -3
View File
@@ -48,10 +48,10 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
type="button" type="button"
onClick={() => (lyssnar ? stoppa() : starta())} onClick={() => (lyssnar ? stoppa() : starta())}
aria-label={lyssnar ? "Stop recording" : "Start voice input"} 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-4 text-[14px] font-semibold ${
lyssnar lyssnar
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white" ? "border-[#8B1A1A] bg-[#8B1A1A] text-white"
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]" : "border-[#D7DCE2] bg-white text-[#1B1E22] hover:border-[#005CA9]"
}`} }`}
> >
{lyssnar ? "■" : <IkonMik />} {lyssnar ? "■" : <IkonMik />}
+2 -2
View File
@@ -171,7 +171,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
> >
{etikett} {etikett}
</label> </label>
<p className="mt-1 text-[13px]" style={{ color: FARG.graphite }}> <p className="mt-2 text-[13px]" style={{ color: FARG.graphite }}>
{f.fraga} {f.fraga}
</p> </p>
<textarea <textarea
@@ -189,7 +189,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
color: FARG.graphite, color: FARG.graphite,
}} }}
/> />
<p id={`slutsats-${f.id}-hjalp`} className="mt-1 text-[12px] leading-[16px]" style={{ color: FARG.steel }}> <p id={`slutsats-${f.id}-hjalp`} className="mt-2 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
{fel ? fel.text : f.hjalp} {fel ? fel.text : f.hjalp}
</p> </p>
</div> </div>
@@ -147,7 +147,7 @@ describe("AI-orkestern", () => {
"Reläet klickar inte.", "Reläet klickar inte.",
); );
expect(prompt).toContain("Volvo XC60 D4 2019"); expect(prompt).toContain("Volvo XC60 D4 2019");
expect(prompt).toContain("Felbeskrivning:"); expect(prompt).toContain("Fault description:");
expect(prompt).toContain("Kontrollera lufttryck"); expect(prompt).toContain("Kontrollera lufttryck");
expect(prompt).toContain("Ej kontrollerat enligt metodiken:"); expect(prompt).toContain("Ej kontrollerat enligt metodiken:");
expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte."); expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte.");
@@ -156,8 +156,8 @@ describe("AI-orkestern", () => {
it("granskningsprompten innehåller hela arbetsloggen men ingen ny inmatning", () => { it("granskningsprompten innehåller hela arbetsloggen men ingen ny inmatning", () => {
const arende = byggDemoArende(1); const arende = byggDemoArende(1);
const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK); const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK);
expect(prompt).toContain("Fullständig arbetslogg:"); expect(prompt).toContain("Full work log:");
expect(prompt).toContain("Arbete överlämnat från Anna till Johan"); expect(prompt).toContain("Work handed over from Anna to Johan");
expect(prompt).not.toContain("Teknikerns nya inmatning"); expect(prompt).not.toContain("Teknikerns nya inmatning");
}); });
@@ -14,7 +14,16 @@ import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { FORBJUDNA_ORD } from "@/alva/sprak"; import { FORBJUDNA_ORD } from "@/alva/sprak";
const KATALOGER = ["src/alva", "src/pages/alva"]; // ---- Vad testet omfattar ------------------------------------------------
//
// Arbetsvyn låg länge utanför. Följden var förutsägbar och blev verklig:
// portalen höll åtta färger, arbetsvyn fyrtiosju, och de två delarna av
// samma produkt såg ut som två produkter. Ett designsystem som bara gäller
// halva ytan är en stilguide, inte ett system.
//
// Katalogerna nedan är därför hela ALVA-ytan — marknadswebb, portal OCH
// det gränssnitt teknikern faktiskt arbetar i.
const KATALOGER = ["src/alva", "src/pages/alva", "src/felsokning", "src/pages/felsokning"];
const filer = KATALOGER.flatMap((katalog) => const filer = KATALOGER.flatMap((katalog) =>
readdirSync(katalog) readdirSync(katalog)
@@ -28,7 +37,17 @@ const utanKommentarer = (kod: string) =>
describe("ALVA-ytan följer designsystemet", () => { describe("ALVA-ytan följer designsystemet", () => {
it("hittar filerna att granska", () => { it("hittar filerna att granska", () => {
expect(filer.length).toBeGreaterThanOrEqual(7); // Höjt från 7 när arbetsvyn kom med. Talet är motvikten mot en
// katalog som råkar tömmas: ett test som granskar noll filer är
// grönt av fel skäl.
expect(filer.length).toBeGreaterThanOrEqual(25);
});
it("granskar både portalen och arbetsvyn", () => {
// Det som gick fel var att halva produkten stod utanför regeln.
expect(filer.some(([n]) => n.startsWith("src/pages/alva/"))).toBe(true);
expect(filer.some(([n]) => n.startsWith("src/pages/felsokning/"))).toBe(true);
expect(filer.some(([n]) => n === "src/felsokning/ui.tsx")).toBe(true);
}); });
it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => { it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => {
@@ -25,7 +25,7 @@ describe("demoärendet", () => {
it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => { it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => {
const b = brief(arende, VIBRATION_METODIK); const b = brief(arende, VIBRATION_METODIK);
expect(b.totalArbetstid).toBe("1 tim 35 min"); expect(b.totalArbetstid).toBe("1 h 35 min");
expect(arende.handelser.some((p) => p.handelse.typ === "overlamning")).toBe(true); expect(arende.handelser.some((p) => p.handelse.typ === "overlamning")).toBe(true);
expect(new Set(arende.handelser.map((p) => p.anvandare)).size).toBeGreaterThanOrEqual(2); expect(new Set(arende.handelser.map((p) => p.anvandare)).size).toBeGreaterThanOrEqual(2);
}); });
@@ -380,7 +380,7 @@ describe("Videoevidens (E3) och signerat avslut", () => {
it("avslutet signeras och grinden redovisar signaturen", async () => { it("avslutet signeras och grinden redovisar signaturen", async () => {
const { handelseRubrik, nyLoggPost: ny } = await import("../domain"); const { handelseRubrik, nyLoggPost: ny } = await import("../domain");
expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signerad av Erik"); expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signed by Erik");
const stangd = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat", signatur: "Erik" }]); const stangd = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat", signatur: "Erik" }]);
const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering"); const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering");
expect(rad?.ok).toBe(true); expect(rad?.ok).toBe(true);
+4 -4
View File
@@ -54,9 +54,9 @@ export interface AiSvarMedModell extends AiSvar {
export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string { export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string {
const rader: string[] = []; const rader: string[] = [];
rader.push(`Metodik: ${metodikNamn}`); rader.push(`Methodology: ${metodikNamn}`);
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`); if (brief.objekt) rader.push(`Object: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`); if (brief.felbeskrivning) rader.push(`Fault description: ${brief.felbeskrivning}`);
if (brief.utfordaKontroller.length > 0) { if (brief.utfordaKontroller.length > 0) {
rader.push("Checks performed:"); 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}` : ""}`);
@@ -81,7 +81,7 @@ export function byggGranskningsPrompt(arende: Arende, metodik: Metodik): string
const logg = arende.handelser const logg = arende.handelser
.map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`) .map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`)
.join("\n"); .join("\n");
return `${kontext}\n\nFullständig arbetslogg:\n${logg}`; return `${kontext}\n\nFull work log:\n${logg}`;
} }
export function tolkaAiSvar(data: unknown): AiSvar { export function tolkaAiSvar(data: unknown): AiSvar {
+5 -5
View File
@@ -16,14 +16,14 @@ function demoFoto(text: string): string {
canvas.height = 360; canvas.height = 360;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("canvas saknas"); if (!ctx) throw new Error("canvas saknas");
ctx.fillStyle = "#27272a"; ctx.fillStyle = "#1B1E22";
ctx.fillRect(0, 0, 480, 360); ctx.fillRect(0, 0, 480, 360);
ctx.strokeStyle = "#52525b"; ctx.strokeStyle = "#4D5662";
ctx.lineWidth = 22; ctx.lineWidth = 22;
ctx.beginPath(); ctx.beginPath();
ctx.arc(240, 170, 110, 0, Math.PI * 2); ctx.arc(240, 170, 110, 0, Math.PI * 2);
ctx.stroke(); ctx.stroke();
ctx.strokeStyle = "#a1a1aa"; ctx.strokeStyle = "#4D5662";
ctx.lineWidth = 4; ctx.lineWidth = 4;
ctx.beginPath(); ctx.beginPath();
ctx.arc(240, 170, 58, 0, Math.PI * 2); ctx.arc(240, 170, 58, 0, Math.PI * 2);
@@ -35,11 +35,11 @@ function demoFoto(text: string): string {
ctx.lineTo(240 + Math.cos(v) * 58, 170 + Math.sin(v) * 58); ctx.lineTo(240 + Math.cos(v) * 58, 170 + Math.sin(v) * 58);
ctx.stroke(); ctx.stroke();
} }
ctx.fillStyle = "#fbbf24"; ctx.fillStyle = "#8A5A00";
ctx.font = "bold 22px sans-serif"; ctx.font = "bold 22px sans-serif";
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.fillText(text, 240, 330); ctx.fillText(text, 240, 330);
ctx.fillStyle = "#71717a"; ctx.fillStyle = "#4D5662";
ctx.font = "14px sans-serif"; ctx.font = "14px sans-serif";
ctx.fillText("Demobild", 240, 26); ctx.fillText("Demobild", 240, 26);
return canvas.toDataURL("image/jpeg", 0.7); return canvas.toDataURL("image/jpeg", 0.7);
+12 -12
View File
@@ -279,18 +279,18 @@ export function handelseRubrik(post: LoggPost): string {
case "objekt_identifierat": case "objekt_identifierat":
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`; return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
case "arbetsorder_skannad": case "arbetsorder_skannad":
return `Arbetsorder skannad — ${h.falt.length} fält tolkade`; return `Work order scanned — ${h.falt.length} fields interpreted`;
case "felbeskrivning": case "felbeskrivning":
return `Felbeskrivning registrerad`; return `Felbeskrivning registrerad`;
case "fraga_besvarad": case "fraga_besvarad":
return `${h.fraga}${h.svar}`; return `${h.fraga}${h.svar}`;
case "kontroll_utford": case "kontroll_utford":
if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`; if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`;
return h.resultat ? `${h.text}${h.resultat}` : `${h.text}utförd`; return h.resultat ? `${h.text}${h.resultat}` : `${h.text}performed`;
case "observation": case "observation":
return `Observation: ${h.text}`; return `Observation: ${h.text}`;
case "matvarde": case "matvarde":
return `Mätvärde: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`; return `Measurement: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`;
case "hypotes": case "hypotes":
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`; return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
case "foto": case "foto":
@@ -304,18 +304,18 @@ export function handelseRubrik(post: LoggPost): string {
case "inaktivitet_forklarad": case "inaktivitet_forklarad":
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`; return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
case "overlamning": case "overlamning":
return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`; return h.till ? `Work handed over from ${h.fran} to ${h.till}` : `Work handed over by ${h.fran}`;
case "ansvarig_satt": case "ansvarig_satt":
return `Ansvarig tekniker: ${h.ansvarig}`; return `Ansvarig tekniker: ${h.ansvarig}`;
case "arendetyp_satt": case "arendetyp_satt":
return `Ärendetyp: ${h.arendetyp}`; return `Case type: ${h.arendetyp}`;
case "historik_kontrollerad": case "historik_kontrollerad":
return h.kontrollerad return h.kontrollerad
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}` ? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`; : `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
case "matarstallning": case "matarstallning":
if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`; if (h.undantag) return `Odometer reading (${h.lage === "ingaende" ? "in" : "out"}) could not be documented: ${h.undantag}`;
return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`; return `Odometer ${h.lage === "ingaende" ? "in" : "out"}: ${h.varde}`;
case "reproducering": case "reproducering":
return h.status === "ja" return h.status === "ja"
? `Symptomet reproducerat: ${h.beskrivning}` ? `Symptomet reproducerat: ${h.beskrivning}`
@@ -325,22 +325,22 @@ export function handelseRubrik(post: LoggPost): string {
case "felorsak": case "felorsak":
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse}${h.orsaker.join(", ")}`; return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse}${h.orsaker.join(", ")}`;
case "atgardsforslag": case "atgardsforslag":
return `Åtgärdsförslag till kund: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`; return `Proposed action for the customer: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`;
case "kundbeslut": case "kundbeslut":
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? `${h.kommentar}` : ""}`; return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? `${h.kommentar}` : ""}`;
case "atgard_utford": case "atgard_utford":
return h.utford return h.utford
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}` ? `Action performed: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
: `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`; : `No action performed — ${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":
return `Export skapad: ${h.format}, version ${h.version}`; return `Export skapad: ${h.format}, version ${h.version}`;
case "ai_svar": { case "ai_svar": {
const forsta = h.rader[0]; const forsta = h.rader[0];
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`; return `AI: ${forsta ? `${forsta.text} ` : ""}— Next step: ${h.nastaSteg}`;
} }
case "arende_avslutat": case "arende_avslutat":
return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Diagnosis closed"; return h.signatur ? `Diagnosis closed — signed by ${h.signatur}` : "Diagnosis closed";
} }
} }
+2 -2
View File
@@ -751,7 +751,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
rubrik: "Photographs present for checks that require them", rubrik: "Photographs present for checks that require them",
ok: fotoKravUtanFoto === 0, ok: fotoKravUtanFoto === 0,
kravs: true, kravs: true,
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined, detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} check(s) requiring a photograph have no image in the log.` : undefined,
}); });
// Compliance Engine: ärendetypens regler ur det aktiva regelpaketet // Compliance Engine: ärendetypens regler ur det aktiva regelpaketet
@@ -794,7 +794,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
rader.push({ rader.push({
id: "evidensniva", id: "evidensniva",
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`, rubrik: `Evidence level: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
ok: evidensNiva(arende) !== "E0", ok: evidensNiva(arende) !== "E0",
kravs: true, kravs: true,
detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined, detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined,
@@ -252,7 +252,7 @@ export function formateraTid(ms: number): string {
const h = Math.floor(minuter / 60); const h = Math.floor(minuter / 60);
const m = minuter % 60; const m = minuter % 60;
if (h === 0) return `${m} min`; if (h === 0) return `${m} min`;
return `${h} tim ${m} min`; return `${h} h ${m} min`;
} }
export interface TillforlitlighetsRad { export interface TillforlitlighetsRad {
@@ -328,9 +328,9 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief {
export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string): string { export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string): string {
const b = brief(arende, metodik, nu); const b = brief(arende, metodik, nu);
const rader: string[] = []; const rader: string[] = [];
rader.push(`ÖVERLÄMNING Ärende #${arende.nummer}`); rader.push(`HANDOVER Case #${arende.nummer}`);
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` Kund: ${b.objekt.kund}` : ""}`); if (b.objekt) rader.push(`Object: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` Customer: ${b.objekt.kund}` : ""}`);
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`); if (b.felbeskrivning) rader.push(`Fault description: ${b.felbeskrivning}`);
rader.push(""); rader.push("");
rader.push("Checks performed:"); rader.push("Checks performed:");
if (b.utfordaKontroller.length === 0) rader.push(" (none yet)"); if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
+119 -42
View File
@@ -1,9 +1,32 @@
// Delade UI-komponenter för Guidad Felsökning. // Delade UI-komponenter för Guidad Felsökning.
// Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition — //
// plana ytor, skarpa kanter, tät informationslayout, dämpad palett // ---- Ett system, inte två -----------------------------------------------
// (ljusgrå ytor, djup marinblå som primärfärg), inga dekorationer. //
// Verktygsrad ~44 px, rektangulära knappar (max 4 px radie), kompakta // Det här filhuvudet beskrev tidigare en egen designfilosofi: "industriellt
// formulär med vänsterställda etiketter. // verkstadsverktyg i ETKA-tradition" med egen palett, eget snitt och egen
// rytm. Den beskrivningen var inte fel i sak — men den var en ANDRA
// beskrivning, vid sidan av ALVA-SPEC-001, och två designsystem i samma
// produkt blir aldrig lika. De blev det inte heller: portalen höll åtta
// färger, arbetsvyn fyrtiosju.
//
// Skillnaden syntes värst i typsnittet. Arbetsvyn låg utanför `.alva-yta`
// och ärvde därför värdapplikationens antikva — samma fynd som en gång
// gjordes om portalen, fast här hade det stått kvar. En tekniker som
// klickar från portalen in i ett ärende bytte alltså både färg och
// bokstavsform, och läste det som två produkter.
//
// Komponenterna nedan bär nu ALVA:s språk: FARG-paletten, 8 px-rutnätet,
// versala sektionsetiketter, inga skuggor, ingen rörelse.
//
// ---- Det som ändå skiljer, och varför ------------------------------------
//
// STORLEK. Knappar och träffytor är större här än i portalen. Det är inte
// en annan estetik utan ett annat bruk: portalen läses vid ett skrivbord,
// arbetsvyn används med handskar bredvid ett fordon. Måtten följer samma
// rutnät, de är bara fler steg på det.
//
// TÄTHET. Arbetsvyn visar mer per skärm. Samma skäl: teknikern ska se
// ärendets tillstånd utan att bläddra.
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useRef, type ReactNode } from "react"; import { useRef, type ReactNode } from "react";
@@ -11,13 +34,8 @@ import type { Tillforlitlighet } from "./domain";
import { TILLFORLITLIGHET_LABEL } from "./domain"; import { TILLFORLITLIGHET_LABEL } from "./domain";
import { MikrofonKnapp } from "./Mikrofon"; import { MikrofonKnapp } from "./Mikrofon";
import { IkonPunkt } from "./ikoner"; import { IkonPunkt } from "./ikoner";
import { Etikett, FARG } from "@/alva/komponenter";
// Industriell palett (inspirerad av tyska verkstadssystem): import { ALVA } from "@/alva/system";
// bakgrund #ECECEC · paneler #F7F7F7 · kanter #C6C6C6
// primär (djup blå) #00437A · markerad rad #D6E4F2
// varning #9A6700 · fel #8B1A1A · ok #1E6B34
const TYPSNITT = "'Inter', 'IBM Plex Sans', 'Segoe UI', Roboto, system-ui, sans-serif";
export function FelsokningSkal({ export function FelsokningSkal({
rubrik, rubrik,
@@ -34,32 +52,62 @@ export function FelsokningSkal({
}) { }) {
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl"; const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
return ( return (
// `alva-yta` bär typografin ur ALVA-SPEC-001 §6 och håller den skild
// från värdapplikationens antikva. Utan den klassen ärver arbetsvyn
// butikens snitt, vilket är exakt vad som hände innan.
<div <div
className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]" className="alva-yta felsokning-print min-h-screen text-[13px]"
style={{ fontFamily: TYPSNITT }} style={{ background: FARG.background, color: FARG.graphite }}
> >
{/* Verktygsrad: låg, mörk marinblå — breadcrumb till vänster, {/* Verktygsraden är vit med en enda linje under, precis som
status till höger. Inget stort sidhuvud. */} portalens sidhuvud. Den var tidigare mörkt marinblå — ett eget
<header className="sticky top-0 z-20 border-b border-[#0A2A4A] bg-[#123A63] px-3 text-white print:static"> varumärke inuti varumärket. */}
<div className={`mx-auto flex min-h-11 ${maxBredd} items-center justify-between gap-3 py-1`}> <header
className="sticky top-0 z-20 border-b px-4 print:static"
style={{ borderColor: FARG.lightSteel, background: FARG.white }}
>
<div className={`mx-auto flex min-h-12 ${maxBredd} items-center justify-between gap-4 py-2`}>
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
{/* Ordmärket står först, precis som i portalens sidhuvud.
Utan det bytte teknikern huvud när den klickade sig från
portalen in i ett ärende, och två sidhuvuden i rad läses
som två system. Länken går till översikten och inte till
portalen: arbetsvyn fungerar i lokalt läge utan session,
och en länk som studsar till inloggningen vore sämre än
ingen länk. */}
<Link
to="/alva"
className="hidden text-[14px] font-semibold tracking-[0.02em] sm:inline"
style={{ color: FARG.graphite }}
>
{ALVA.namn}
</Link>
<span className="hidden sm:inline" style={{ color: FARG.lightSteel }}>
/
</span>
{tillbaka && ( {tillbaka && (
<> <>
<Link <Link
to={tillbaka.till} to={tillbaka.till}
className="whitespace-nowrap text-[12px] font-medium text-[#A9C3DE] hover:text-white hover:underline" className="whitespace-nowrap text-[12px] font-semibold uppercase tracking-[0.08em] hover:underline"
style={{ color: FARG.steel }}
> >
{tillbaka.text} {tillbaka.text}
</Link> </Link>
<span className="text-[#5B7CA0]">/</span> <span style={{ color: FARG.lightSteel }}>/</span>
</> </>
)} )}
<h1 className="truncate text-[14px] font-semibold tracking-tight">{rubrik}</h1> <h1
className="truncate text-[14px] font-semibold uppercase tracking-[0.02em]"
style={{ color: FARG.graphite }}
>
{rubrik}
</h1>
</div> </div>
{hoger} {hoger}
</div> </div>
</header> </header>
<main className={`mx-auto ${maxBredd} px-3 py-4 pb-20`}>{children}</main> <main className={`mx-auto ${maxBredd} px-4 py-4 pb-20`}>{children}</main>
</div> </div>
); );
} }
@@ -79,18 +127,22 @@ export function StorKnapp({
type?: "button" | "submit"; type?: "button" | "submit";
className?: string; className?: string;
}) { }) {
const farg = // Samma tre vikter som portalens Knapp: fylld, kontur, och den spärrade
// varianten i oxidrött. Ingen hovring som byter färg — en yta som
// ändrar sig när muspekaren råkar passera är rörelse utan innehåll.
const stil =
variant === "primar" variant === "primar"
? "border border-[#003561] bg-[#00437A] text-white hover:bg-[#005A9E] active:bg-[#003561]" ? { background: FARG.graphite, color: FARG.white, borderColor: FARG.graphite }
: variant === "fara" : variant === "fara"
? "border border-[#6E1414] bg-[#8B1A1A] text-white hover:bg-[#A32020] active:bg-[#6E1414]" ? { background: FARG.stoppat, color: FARG.white, borderColor: FARG.stoppat }
: "border border-[#ADADAD] bg-white text-[#1A1A1A] hover:bg-[#EDF2F7] hover:border-[#00437A] active:bg-[#D6E4F2]"; : { background: FARG.white, color: FARG.graphite, borderColor: FARG.steel };
return ( return (
<button <button
type={type} type={type}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={`min-h-10 w-full px-3 py-1.5 text-[13px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`} className={`min-h-12 w-full border px-4 py-2 text-[13px] font-semibold tracking-[0.02em] disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${className}`}
style={stil}
> >
{children} {children}
</button> </button>
@@ -98,27 +150,44 @@ export function StorKnapp({
} }
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) { export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
// Samma form som portalens Block: vit ruta, en linje runt, och en
// rubrikrad i bakgrundstonen. Ingen skugga — ALVA:s ytor ligger i
// samma plan.
return ( return (
<section className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-3 shadow-[0_1px_2px_rgba(0,0,0,0.06)]"> <section className="mb-4 border" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
{rubrik && ( {rubrik && (
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]"> <header
{rubrik} className="border-b px-4 py-2"
</h2> style={{ borderColor: FARG.lightSteel, background: FARG.background }}
>
<h2
className="text-[11px] font-semibold uppercase tracking-[0.08em]"
style={{ color: FARG.steel }}
>
{rubrik}
</h2>
</header>
)} )}
{children} <div className="px-4 py-2">{children}</div>
</section> </section>
); );
} }
// Tillförlitlighet anges med ord OCH punkt, aldrig med enbart färg.
// Grönt finns inte i ALVA:s palett: blått bär "verifierat" (se
// STATUSFARG i komponenter.tsx), oxidgult "osäkert", oxidrött "svagt".
const NIVA_FARG: Record<Tillforlitlighet, string> = { const NIVA_FARG: Record<Tillforlitlighet, string> = {
hog: "#1E6B34", hog: FARG.blue,
medel: "#C08A00", medel: FARG.varning,
lag: "#8B1A1A", lag: FARG.stoppat,
}; };
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) { export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
return ( return (
<span className="inline-flex items-center gap-1 whitespace-nowrap text-[12px] font-semibold"> <span
className="inline-flex items-center gap-2 whitespace-nowrap text-[12px] font-semibold uppercase tracking-[0.08em]"
style={{ color: NIVA_FARG[niva] }}
>
<IkonPunkt farg={NIVA_FARG[niva]} /> <IkonPunkt farg={NIVA_FARG[niva]} />
{TILLFORLITLIGHET_LABEL[niva]} {TILLFORLITLIGHET_LABEL[niva]}
</span> </span>
@@ -146,12 +215,12 @@ export function TextFalt({
// granskar och bekräftar alltid innan något sparas i loggen. // granskar och bekräftar alltid innan något sparas i loggen.
const vardeRef = useRef(varde); const vardeRef = useRef(varde);
vardeRef.current = varde; vardeRef.current = varde;
const klass = const klass = "w-full border px-2 py-2 text-[13px] focus:outline-none";
"w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] text-[#1A1A1A] placeholder-[#9AA0A6] focus:border-[#00437A] focus:outline-none focus:ring-1 focus:ring-[#00437A]"; const stil = { borderColor: FARG.steel, background: FARG.white, color: FARG.graphite };
return ( return (
<label className="mb-2.5 block"> <label className="mb-4 block">
<span className="mb-1 flex min-h-6 items-center justify-between gap-2"> <span className="mb-2 flex min-h-6 items-center justify-between gap-2">
<span className="text-left text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">{label}</span> <Etikett>{label}</Etikett>
{rost && ( {rost && (
<MikrofonKnapp <MikrofonKnapp
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)} paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
@@ -159,7 +228,14 @@ export function TextFalt({
)} )}
</span> </span>
{flerRad ? ( {flerRad ? (
<textarea value={varde} onChange={(e) => satt(e.target.value)} placeholder={platshallare} rows={3} className={klass} /> <textarea
value={varde}
onChange={(e) => satt(e.target.value)}
placeholder={platshallare}
rows={3}
className={klass}
style={stil}
/>
) : ( ) : (
<input <input
type={losenord ? "password" : "text"} type={losenord ? "password" : "text"}
@@ -167,6 +243,7 @@ export function TextFalt({
onChange={(e) => satt(e.target.value)} onChange={(e) => satt(e.target.value)}
placeholder={platshallare} placeholder={platshallare}
className={klass} className={klass}
style={stil}
/> />
)} )}
</label> </label>
+224 -209
View File
@@ -5,7 +5,7 @@ import { KUNDBESLUT_LABEL, KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLI
import type { Metodik, NastaSteg } from "@/felsokning/metodik"; import type { Metodik, NastaSteg } from "@/felsokning/metodik";
import { nastaSteg } from "@/felsokning/metodik"; import { nastaSteg } from "@/felsokning/metodik";
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs"; import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
import { Fasrad } from "@/alva/komponenter"; import { FARG, Fasrad } from "@/alva/komponenter";
import { Slutsatspanel } from "@/felsokning/Slutsats"; import { Slutsatspanel } from "@/felsokning/Slutsats";
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs"; import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { grinda } from "../../../../services/gemensam/grind.mjs"; import { grinda } from "../../../../services/gemensam/grind.mjs";
@@ -145,7 +145,7 @@ export default function ArendeSida() {
if (!arende || !id) { if (!arende || !id) {
return ( return (
<FelsokningSkal rubrik="Case not found" tillbaka={{ till: "/felsokning", text: "Cases" }}> <FelsokningSkal rubrik="Case not found" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<p className="text-[14px] text-[#333333]">The case does not exist on this device.</p> <p className="text-[14px] text-[#1B1E22]">The case does not exist on this device.</p>
</FelsokningSkal> </FelsokningSkal>
); );
} }
@@ -175,9 +175,24 @@ export default function ArendeSida() {
tillbaka={{ till: "/felsokning", text: "Cases" }} 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> {/* Vit text var rätt när verktygsraden var mörkt marinblå. När
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]"> raden blev vit försvann tiden helt och statusraden blev en
{avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]} grå skugga — det syntes först i en skärmdump från en
telefon, vilket är den enda kontroll som ser vad
teknikern ser. */}
<p className="font-mono text-[13px] font-semibold" style={{ color: FARG.graphite }}>
{total}
</p>
{/* Synkläget bryts bort på telefon i stället för att bryta
raden. Ärendets tillstånd är det som måste synas; var
loggen ligger är en driftdetalj och står kvar under
Technical information. */}
<p
className="whitespace-nowrap text-[10px] font-semibold uppercase tracking-[0.08em]"
style={{ color: FARG.steel }}
>
{avslutat ? "Closed" : "In progress"}
<span className="hidden sm:inline"> · {SYNKSTATUS_LABEL[synkStatus]}</span>
</p> </p>
</div> </div>
} }
@@ -189,13 +204,13 @@ export default function ArendeSida() {
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till {/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
vänster, arbetsyta i mitten, kontextpanel till höger. På smala vänster, arbetsyta i mitten, kontextpanel till höger. På smala
skärmar: flikrad + en kolumn. */} skärmar: flikrad + en kolumn. */}
<nav className="mb-3 grid grid-cols-4 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden"> <nav className="mb-4 grid grid-cols-4 gap-2 border border-[#D7DCE2] bg-[#F6F7F8] p-2 lg:hidden">
{FLIKAR.map((f) => ( {FLIKAR.map((f) => (
<button <button
key={f.id} key={f.id}
onClick={() => setFlik(f.id)} onClick={() => setFlik(f.id)}
className={`min-h-9 text-[13px] font-semibold transition-colors ${ className={`min-h-9 text-[13px] font-semibold ${
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]" flik === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
}`} }`}
> >
{f.label} {f.label}
@@ -203,7 +218,7 @@ export default function ArendeSida() {
))} ))}
</nav> </nav>
<div className="lg:grid lg:grid-cols-[210px_minmax(0,1fr)] lg:items-start lg:gap-3 xl:grid-cols-[210px_minmax(0,1fr)_300px]"> <div className="lg:grid lg:grid-cols-[210px_minmax(0,1fr)] lg:items-start lg:gap-4 xl:grid-cols-[210px_minmax(0,1fr)_300px]">
<aside className="sticky top-14 hidden lg:block print:hidden"> <aside className="sticky top-14 hidden lg:block print:hidden">
<VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} /> <VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} />
</aside> </aside>
@@ -232,8 +247,8 @@ function identitetsRader(rader: [string, string | undefined][]) {
return rader return rader
.filter(([, varde]) => varde) .filter(([, varde]) => varde)
.map(([etikett, varde]) => ( .map(([etikett, varde]) => (
<div key={etikett} className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0"> <div key={etikett} className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
<span className="text-[#4A5560]">{etikett}</span> <span className="text-[#4D5662]">{etikett}</span>
<span className="text-right font-medium">{varde}</span> <span className="text-right font-medium">{varde}</span>
</div> </div>
)); ));
@@ -248,12 +263,12 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
const del = (etikett: string, varde?: string) => const del = (etikett: string, varde?: string) =>
varde ? ( varde ? (
<span className="whitespace-nowrap"> <span className="whitespace-nowrap">
{etikett && <span className="text-[#707070]">{etikett} </span>} {etikett && <span className="text-[#4D5662]">{etikett} </span>}
<span className="font-semibold text-[#1A1A1A]">{varde}</span> <span className="font-semibold text-[#1B1E22]">{varde}</span>
</span> </span>
) : 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-4 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#D7DCE2] bg-[#F6F7F8] px-4 py-2 text-[12px] print:hidden">
{del("WO", idn.arbetsorder)} {del("WO", idn.arbetsorder)}
{del("Claim", idn.claim)} {del("Claim", idn.claim)}
{del("Claim no.", idn.skadenummer)} {del("Claim no.", idn.skadenummer)}
@@ -262,13 +277,13 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
{del("VIN", idn.vin)} {del("VIN", idn.vin)}
{del("Mileage", idn.miltal)} {del("Mileage", idn.miltal)}
{del("Responsible", idn.ansvarig)} {del("Responsible", idn.ansvarig)}
<label className="ml-auto flex items-center gap-1"> <label className="ml-auto flex items-center gap-2">
<span className="text-[#707070]">Case type</span> <span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">Case type</span>
<select <select
value={typ} value={typ}
disabled={idn.avslutat} disabled={idn.avslutat}
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })} onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
className="border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none" className="border border-[#4D5662] bg-white px-2 py-2 text-[12px] focus:outline-none"
> >
{ARENDETYPER.map((t) => ( {ARENDETYPER.map((t) => (
<option key={t}>{t}</option> <option key={t}>{t}</option>
@@ -326,15 +341,15 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
<IkonKamera /> Photograph the instrument panel <IkonKamera /> Photograph the instrument panel
</StorKnapp> </StorKnapp>
)} )}
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer </p>} {laser && <p className="py-2 text-center text-[12px] font-semibold text-[#005CA9]">Reading the odometer </p>}
{foto && !laser && ( {foto && !laser && (
<div className="mt-2"> <div className="mt-2">
{demo && ( {demo && (
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]"> <p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[11px] font-semibold text-[#8A5A00]">
Demo reading image interpretation requires sign-in. Check the value. Demo reading image interpretation requires sign-in. Check the value.
</p> </p>
)} )}
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#C6C6C6]" /> <img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#D7DCE2]" />
<TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" /> <TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
<StorKnapp <StorKnapp
disabled={!varde.trim()} disabled={!varde.trim()}
@@ -391,24 +406,24 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
if (rader.length === 0) return null; if (rader.length === 0) return null;
return ( return (
<div className="mb-2 border border-[#C6C6C6] bg-white p-2"> <div className="mb-2 border border-[#D7DCE2] bg-white p-2">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
Previous cases for {identifierare} review before signing off the history Previous cases for {identifierare} review before signing off the history
</p> </p>
{rader.map((rad) => ( {rader.map((rad) => (
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0"> <div key={rad.id} className="border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
<p> <p>
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "} <span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
{rad.avslutat ? "closed" : "in progress"} {rad.avslutat ? "closed" : "in progress"}
{rad.felbeskrivning && <span className="text-[#4A5560]"> {rad.felbeskrivning}</span>} {rad.felbeskrivning && <span className="text-[#4D5662]"> {rad.felbeskrivning}</span>}
</p> </p>
{rad.felorsaker.map((f, i) => ( {rad.felorsaker.map((f, i) => (
<p key={i} className="pl-3 text-[#4A5560]"> <p key={i} className="pl-4 text-[#4D5662]">
Felorsak: {f.avvikelse} ({f.orsaker.join(", ")}) Felorsak: {f.avvikelse} ({f.orsaker.join(", ")})
</p> </p>
))} ))}
<button <button
className="mt-1 text-[11px] font-semibold text-[#00437A] underline-offset-2 hover:underline" className="mt-2 text-[11px] font-semibold text-[#005CA9] underline-offset-2 hover:underline"
onClick={() => onClick={() =>
skicka({ skicka({
typ: "kommentar", typ: "kommentar",
@@ -439,15 +454,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
return ( return (
<Panel rubrik="Pre-diagnostics — before the work begins"> <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 text-[13px]">
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik} <span className={r.klar ? "text-[#005CA9]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
{r.varning && <span className="ml-1 text-[11px] font-semibold text-[#9A6700]"><IkonVarning /> {r.varning}</span>} {r.varning && <span className="ml-2 text-[11px] font-semibold text-[#8A5A00]"><IkonVarning /> {r.varning}</span>}
</p> </p>
))} ))}
{!rad("historik").klar && ( {!rad("historik").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2"> <div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns) Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
</p> </p>
<TidigareArenden arende={arende} skicka={skicka} /> <TidigareArenden arende={arende} skicka={skicka} />
@@ -484,8 +499,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)} )}
{!rad("matarstallning_in").klar && ( {!rad("matarstallning_in").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2"> <div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Incoming odometer reading — photograph the instrument panel Incoming odometer reading — photograph the instrument panel
</p> </p>
<MatarstallningSteg lage="ingaende" skicka={skicka} /> <MatarstallningSteg lage="ingaende" skicka={skicka} />
@@ -493,15 +508,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)} )}
{!rad("felbeskrivning").klar && ( {!rad("felbeskrivning").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2"> <div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Is the customer's fault description correctly recorded? Is the customer's fault description correctly recorded?
</p> </p>
{fb && <p className="mb-2 text-[13px]">{fb}</p>} {fb && <p className="mb-2 text-[13px]">{fb}</p>}
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}> <StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
Correct verified Correct verified
</StorKnapp> </StorKnapp>
<p className="mt-1 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
Additional symptoms are documented as separate observations do not mix them Additional symptoms are documented as separate observations do not mix them
with the customer's description. with the customer's description.
</p> </p>
@@ -509,8 +524,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)} )}
{!rad("tidiga_observationer").klar && ( {!rad("tidiga_observationer").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2"> <div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …) Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -525,7 +540,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
The observations are documented The observations are documented
</StorKnapp> </StorKnapp>
</div> </div>
<p className="mt-1 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
Document with a photograph or an observation in the panel below — the button unlocks once something is logged. Document with a photograph or an observation in the panel below — the button unlocks once something is logged.
</p> </p>
</div> </div>
@@ -548,7 +563,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
: "Rationale (required) — why could the fault not be reproduced?"; : "Rationale (required) — why could the fault not be reproduced?";
return ( return (
<Panel rubrik="Symptom verification — reproduction"> <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-[#4D5662]">
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">
@@ -603,7 +618,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
if (orsaker.length === 0) return setFel("Select at least one cause category."); if (orsaker.length === 0) return setFel("Select at least one cause category.");
if (underlag.length === 0) return setFel("Link at least one evidence source to the assessment."); 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(`The evidence source “${saknat}” is not in the case log — document it first, or choose another source.`);
if (orsaker.includes("Unknown cause") && !motivering.trim()) if (orsaker.includes("Unknown cause") && !motivering.trim())
return setFel("An unknown cause requires a reason why the cause could not be established."); 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())
@@ -636,9 +651,9 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
const h = p.handelse; const h = p.handelse;
if (h.typ !== "felorsak") return null; if (h.typ !== "felorsak") return null;
return ( return (
<div key={p.id} className="mb-2 border-b border-[#EBEBEB] pb-2 text-[13px] last:border-0"> <div key={p.id} className="mb-2 border-b border-[#D7DCE2] pb-2 text-[13px] last:border-0">
<p className="font-semibold">{h.avvikelse}</p> <p className="font-semibold">{h.avvikelse}</p>
<p className="text-[#4A5560]"> <p className="text-[#4D5662]">
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]} Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
</p> </p>
<p>Action: {h.atgard}</p> <p>Action: {h.atgard}</p>
@@ -659,42 +674,42 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
rost rost
platshallare="e.g. The starter motor does not engage despite correct supply voltage and a good earth connection." 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. Most probable cause (one or more)</p> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">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-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
{aktivtRegelpaket().orsakskategorier.map((o) => ( {aktivtRegelpaket().orsakskategorier.map((o) => (
<button <button
key={o} key={o}
onClick={() => vaxla(orsaker, setOrsaker, o)} onClick={() => vaxla(orsaker, setOrsaker, o)}
className={`min-h-8 border px-1.5 text-[12px] font-medium ${ className={`min-h-8 border px-2 text-[12px] font-medium ${
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]" orsaker.includes(o) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{o} {o}
</button> </button>
))} ))}
</div> </div>
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Evidence supporting the assessment</p> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">3. Evidence supporting the assessment</p>
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4"> <div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
{aktivtRegelpaket().underlagskallor.map((k) => ( {aktivtRegelpaket().underlagskallor.map((k) => (
<button <button
key={k} key={k}
onClick={() => vaxla(underlag, setUnderlag, k)} onClick={() => vaxla(underlag, setUnderlag, k)}
className={`min-h-8 border px-1.5 text-[12px] font-medium ${ className={`min-h-8 border px-2 text-[12px] font-medium ${
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]" underlag.includes(k) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{k} {k}
</button> </button>
))} ))}
</div> </div>
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Confidence in the assessment</p> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">4. Confidence in the assessment</p>
<div className="mb-3 grid grid-cols-3 gap-2"> <div className="mb-4 grid grid-cols-3 gap-2">
{(["hog", "medel", "lag"] as const).map((n) => ( {(["hog", "medel", "lag"] as const).map((n) => (
<button <button
key={n} key={n}
onClick={() => setSakerhet(n)} onClick={() => setSakerhet(n)}
className={`min-h-9 border text-[12px] font-semibold ${ className={`min-h-9 border text-[12px] font-semibold ${
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]" sakerhet === n ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{TILLFORLITLIGHET_LABEL[n]} {TILLFORLITLIGHET_LABEL[n]}
@@ -768,20 +783,20 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
const h = p.handelse; const h = p.handelse;
if (h.typ !== "atgard_utford") return null; if (h.typ !== "atgard_utford") 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-[#D7DCE2] py-2 text-[13px] last:border-0">
{h.utford ? ( {h.utford ? (
<> <>
<span className="font-semibold">Action performed:</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-[#4D5662]"> · Delar: {h.delar}</span>}
</> </>
) : ( ) : (
<span className="text-[#9A6700]">No action performed — {h.motivering}</span> <span className="text-[#8A5A00]">No action performed — {h.motivering}</span>
)} )}
</p> </p>
); );
})} })}
{kk && ( {kk && (
<p className={`py-1 text-[13px] font-semibold ${kk.resultat === "symptomet_borta" ? "text-[#1E6B34]" : "text-[#9A6700]"}`}> <p className={`py-2 text-[13px] font-semibold ${kk.resultat === "symptomet_borta" ? "text-[#005CA9]" : "text-[#8A5A00]"}`}>
Kvalitetskontroll: {KVALITETSKONTROLL_LABEL[kk.resultat]} — <span className="font-normal">{kk.beskrivning}</span> Kvalitetskontroll: {KVALITETSKONTROLL_LABEL[kk.resultat]} — <span className="font-normal">{kk.beskrivning}</span>
</p> </p>
)} )}
@@ -792,14 +807,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
const h = p.handelse; const h = p.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-[#D7DCE2] py-2 text-[13px] last:border-0">
<span className="font-semibold">Proposed action for the customer:</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-[#4D5662]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
</p> </p>
); );
})} })}
{beslut && ( {beslut && (
<p className={`py-1 text-[13px] font-semibold ${beslut.beslut === "godkant" ? "text-[#1E6B34]" : beslut.beslut === "avbojt" ? "text-[#8B1A1A]" : "text-[#9A6700]"}`}> <p className={`py-2 text-[13px] font-semibold ${beslut.beslut === "godkant" ? "text-[#005CA9]" : beslut.beslut === "avbojt" ? "text-[#8B1A1A]" : "text-[#8A5A00]"}`}>
Kundens besked ({beslut.kanal}): {KUNDBESLUT_LABEL[beslut.beslut]} Kundens besked ({beslut.kanal}): {KUNDBESLUT_LABEL[beslut.beslut]}
{beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>} {beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>}
</p> </p>
@@ -826,7 +841,7 @@ 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-4 border border-[#D7DCE2] bg-white p-2">
<TextFalt <TextFalt
label="Proposed action (shown to the customer in the shared view)" label="Proposed action (shown to the customer in the shared view)"
varde={forslagText} varde={forslagText}
@@ -860,8 +875,8 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)} )}
{forslag.length > 0 && !beslut && ( {forslag.length > 0 && !beslut && (
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2"> <div className="mb-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#8A5A00]">
Record the customer's decision before the work begins Record the customer's decision before the work begins
</p> </p>
<div className="mb-2 grid grid-cols-3 gap-2"> <div className="mb-2 grid grid-cols-3 gap-2">
@@ -870,7 +885,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
key={val} key={val}
onClick={() => setBeslutsVal(val)} onClick={() => setBeslutsVal(val)}
className={`min-h-9 border text-[12px] font-semibold ${ className={`min-h-9 border text-[12px] font-semibold ${
beslutsVal === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]" beslutsVal === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{KUNDBESLUT_LABEL[val]} {KUNDBESLUT_LABEL[val]}
@@ -879,14 +894,14 @@ 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]">How was the decision given?</p> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">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-2">
{KUNDKANALER.map((k) => ( {KUNDKANALER.map((k) => (
<button <button
key={k} key={k}
onClick={() => setKanal(k)} onClick={() => setKanal(k)}
className={`min-h-8 border px-1 text-[12px] font-medium ${ className={`min-h-8 border px-2 text-[12px] font-medium ${
kanal === k ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]" kanal === k ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{k} {k}
@@ -967,16 +982,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{lage === "ingen" && ( {lage === "ingen" && (
<div className="mt-2"> <div className="mt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Reason no action was performed (required) Reason no action was performed (required)
</p> </p>
<div className="mb-2 grid gap-1"> <div className="mb-2 grid gap-2">
{INGEN_ATGARD_ORSAKER.map((val) => ( {INGEN_ATGARD_ORSAKER.map((val) => (
<button <button
key={val} key={val}
onClick={() => setOrsak(val)} onClick={() => setOrsak(val)}
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${ className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white" orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
}`} }`}
> >
{val} {val}
@@ -985,7 +1000,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
</div> </div>
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost /> <TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
{beslut?.beslut === "avbojt" && ( {beslut?.beslut === "avbojt" && (
<p className="mb-2 text-[11px] text-[#707070]"> <p className="mb-2 text-[11px] text-[#4D5662]">
The customer has declined the action via {beslut.kanal} the decision is recorded in the log. The customer has declined the action via {beslut.kanal} the decision is recorded in the log.
</p> </p>
)} )}
@@ -1008,12 +1023,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)} )}
{utford && !kk && ( {utford && !kk && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2"> <div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Quality check is the symptom gone after the action? Quality check is the symptom gone after the action?
</p> </p>
{repro?.status === "ja" && ( {repro?.status === "ja" && (
<p className="mb-2 text-[12px] text-[#707070]"> <p className="mb-2 text-[12px] text-[#4D5662]">
The symptom was reproduced during the examination recreate the same conditions to verify. The symptom was reproduced during the examination recreate the same conditions to verify.
</p> </p>
)} )}
@@ -1023,7 +1038,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
key={val} key={val}
onClick={() => setKkResultat(val)} onClick={() => setKkResultat(val)}
className={`min-h-9 border px-2 text-[12px] font-semibold ${ className={`min-h-9 border px-2 text-[12px] font-semibold ${
kkResultat === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]" kkResultat === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`} }`}
> >
{KVALITETSKONTROLL_LABEL[val]} {KVALITETSKONTROLL_LABEL[val]}
@@ -1080,24 +1095,24 @@ function VyTrad({
const steg = nastaSteg(arende, metodik); const steg = nastaSteg(arende, metodik);
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id); const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
return ( return (
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]"> <nav className="overflow-hidden border border-[#D7DCE2] bg-[#F6F7F8]">
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]"> <p className="border-b border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
Case views Case views
</p> </p>
{FLIKAR.map((f) => ( {FLIKAR.map((f) => (
<button <button
key={f.id} key={f.id}
onClick={() => sattFlik(f.id)} onClick={() => sattFlik(f.id)}
className={`block w-full border-l-2 px-3 py-1.5 text-left text-[13px] transition-colors ${ className={`block w-full border-l-2 px-4 py-2 text-left text-[13px] ${
flik === f.id flik === f.id
? "border-[#00437A] bg-[#D6E4F2] font-semibold text-[#00437A]" ? "border-[#005CA9] bg-[#FFFFFF] font-semibold text-[#005CA9]"
: "border-transparent text-[#333333] hover:bg-[#E4E9EE]" : "border-transparent text-[#1B1E22] hover:bg-[#D7DCE2]"
}`} }`}
> >
{f.label} {f.label}
</button> </button>
))} ))}
<p className="border-y border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]"> <p className="border-y border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
{metodik.namn} {metodik.namn}
</p> </p>
{metodik.steg.map((s, i) => { {metodik.steg.map((s, i) => {
@@ -1106,8 +1121,8 @@ function VyTrad({
return ( return (
<p <p
key={s.id} key={s.id}
className={`px-3 py-1 text-[12px] ${ className={`px-4 py-2 text-[12px] ${
aktuell ? "bg-[#FFF8E1] font-semibold text-[#9A6700]" : klar ? "text-[#1E6B34]" : "text-[#707070]" aktuell ? "bg-[#FFFFFF] font-semibold text-[#8A5A00]" : klar ? "text-[#005CA9]" : "text-[#4D5662]"
}`} }`}
> >
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik} {klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
@@ -1135,9 +1150,9 @@ function KontextPanel({
const senasteAi = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar"); const senasteAi = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar");
const rad = (etikett: string, varde: string | undefined) => const rad = (etikett: string, varde: string | undefined) =>
varde ? ( varde ? (
<div className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[12px] last:border-0"> <div className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
<span className="shrink-0 text-[#4A5560]">{etikett}</span> <span className="shrink-0 text-[#4D5662]">{etikett}</span>
<span className="text-right font-medium text-[#1A1A1A]">{varde}</span> <span className="text-right font-medium text-[#1B1E22]">{varde}</span>
</div> </div>
) : null; ) : null;
return ( return (
@@ -1153,7 +1168,7 @@ function KontextPanel({
</Panel> </Panel>
<Panel rubrik="Reliability"> <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 text-[12px]">
<NivaBadge niva={r.niva} /> {r.text} <NivaBadge niva={r.niva} /> {r.text}
</p> </p>
))} ))}
@@ -1161,7 +1176,7 @@ function KontextPanel({
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && ( {!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
<Panel rubrik="Recommended next step"> <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 text-[12px] text-[#1B1E22]">
{i + 1}. {s} {i + 1}. {s}
</p> </p>
))} ))}
@@ -1170,14 +1185,14 @@ function KontextPanel({
{senasteAi && senasteAi.handelse.typ === "ai_svar" && ( {senasteAi && senasteAi.handelse.typ === "ai_svar" && (
<Panel rubrik="Technical recommendation"> <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 text-[12px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text} <span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
</p> </p>
))} ))}
<p className="mt-1 text-[12px]"> <p className="mt-2 text-[12px]">
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg} <span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
</p> </p>
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]"> <p className="mt-2 text-[10px] uppercase tracking-wide text-[#4D5662]">
Decision support · never presented as an established fault Decision support · never presented as an established fault
</p> </p>
</Panel> </Panel>
@@ -1202,11 +1217,11 @@ function InaktivitetsBanner({
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000); const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
if (minuter < 20) return null; if (minuter < 20) return null;
return ( return (
<div className="mb-4 border border-[#00437A] bg-[#F7F7F7] p-4"> <div className="mb-4 border border-[#005CA9] bg-[#F6F7F8] p-4">
<p className="mb-2 text-[14px] font-semibold text-[#00437A]"> <p className="mb-2 text-[14px] font-semibold text-[#005CA9]">
Ingen aktivitet har registrerats de senaste {minuter} minuterna. Ingen aktivitet har registrerats de senaste {minuter} minuterna.
</p> </p>
<p className="mb-3 text-[#333333]">Briefly describe what was done during this period.</p> <p className="mb-4 text-[#1B1E22]">Briefly describe what was done during this period.</p>
<TextFalt label="What has been done?" varde={text} satt={setText} platshallare="e.g. Removed the instrument panel to reach the wiring harness." 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()}
@@ -1286,7 +1301,7 @@ function GuideFlik({
if (avslutat) { if (avslutat) {
return ( return (
<Panel rubrik="The diagnosis is closed"> <Panel rubrik="The diagnosis is closed">
<p className="text-[14px] text-[#333333]"> <p className="text-[14px] text-[#1B1E22]">
The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export. The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export.
</p> </p>
</Panel> </Panel>
@@ -1339,8 +1354,8 @@ function GuideFlik({
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}> <Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
{steg.klart ? ( {steg.klart ? (
<> <>
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p> <p className="mb-4 text-[15px] font-semibold">Every step in the methodology is documented.</p>
<p className="mb-4 text-[#333333]"> <p className="mb-4 text-[#1B1E22]">
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
with recommended next steps. with recommended next steps.
</p> </p>
@@ -1353,11 +1368,11 @@ function GuideFlik({
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett // Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
// 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-[#8B1A1A] bg-[#FFFFFF] p-4">
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Work must not continue</p> <p className="mb-2 text-[15px] font-semibold text-[#8B1A1A]">Work must not continue</p>
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p> <p className="mb-2 text-[#1B1E22]">{steg.sparr.orsak}</p>
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p> <p className="mb-4 font-semibold text-[#1B1E22]">{steg.sparr.atgard}</p>
<p className="text-[12px] text-[#666666]"> <p className="text-[12px] text-[#4D5662]">
The answer is documented in the log. The methodology opens when the precondition is met and the question is answered The answer is documented in the log. The methodology opens when the precondition is met and the question is answered
jakande. jakande.
</p> </p>
@@ -1377,19 +1392,19 @@ function GuideFlik({
: "Guidance" : "Guidance"
} }
> >
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Analyzing </p>} {aiStatus === "arbetar" && <p className="mb-2 font-semibold text-[#005CA9]">Analyzing </p>}
{aiStatus === "fel" && ( {aiStatus === "fel" && (
<p className="mb-2 font-semibold text-[#8B1A1A]">The analysis could not be retrieved try again. The methodology continues unaffected.</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" && (
<> <>
{senasteAiSvar.handelse.rader.map((rad, i) => ( {senasteAiSvar.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-[14px]"> <p key={i} className="py-0 text-[14px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
<p className="mt-2 text-[14px]"> <p className="mt-2 text-[14px]">
<span className="font-semibold text-[#00437A]">Next step:</span>{" "} <span className="font-semibold text-[#005CA9]">Next step:</span>{" "}
{senasteAiSvar.handelse.nastaSteg} {senasteAiSvar.handelse.nastaSteg}
</p> </p>
</> </>
@@ -1447,15 +1462,15 @@ function GuideFlik({
*/ */
function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) { function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) {
return ( return (
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4"> <div className="mb-2 border-l-2 border-[#8A5A00] bg-[#FFFFFF] px-4 py-4">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]"> <p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#8A5A00]">
The case cannot be closed yet The case cannot be closed yet
</p> </p>
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]"> <ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#1B1E22]">
{hinder.map((h) => ( {hinder.map((h) => (
<li key={h.id}> <li key={h.id}>
{h.rubrik} {h.rubrik}
{h.detalj && <span className="text-[#4A5560]"> {h.detalj}</span>} {h.detalj && <span className="text-[#4D5662]"> {h.detalj}</span>}
</li> </li>
))} ))}
</ul> </ul>
@@ -1469,15 +1484,15 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori; if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori;
} }
return ( return (
<div className="mb-4 flex gap-2 overflow-x-auto pb-1"> <div className="mb-4 flex gap-2 overflow-x-auto pb-2">
{(Object.keys(TIDKATEGORI_LABEL) as TidKategori[]).map((k) => ( {(Object.keys(TIDKATEGORI_LABEL) as TidKategori[]).map((k) => (
<button <button
key={k} key={k}
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })} onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold transition-colors ${ className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold ${
k === aktiv k === aktiv
? "border-[#00437A] bg-[#00437A] text-white" ? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]" : "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
}`} }`}
> >
{TIDKATEGORI_LABEL[k]} {TIDKATEGORI_LABEL[k]}
@@ -1559,7 +1574,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
return ( return (
<> <>
{steg.steg.beskrivning && <p className="mb-2 text-[#4A5560]">{steg.steg.beskrivning}</p>} {steg.steg.beskrivning && <p className="mb-2 text-[#4D5662]">{steg.steg.beskrivning}</p>}
<p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p> <p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p>
{krav === "foto" ? ( {krav === "foto" ? (
<> <>
@@ -1579,7 +1594,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
/> />
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost /> <TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
<StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Take a photograph verifies the check</StorKnapp> <StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Take a photograph verifies the check</StorKnapp>
<p className="mt-2 text-[12px] text-[#707070]">This check is verified with a photograph.</p> <p className="mt-2 text-[12px] text-[#4D5662]">This check is verified with a photograph.</p>
</> </>
) : ( ) : (
<> <>
@@ -1597,7 +1612,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
rost rost
/> />
{!kravUppfyllt && ( {!kravUppfyllt && (
<p className="mb-3 text-[12px] font-semibold text-[#00437A]"> <p className="mb-4 text-[12px] font-semibold text-[#005CA9]">
No {krav === "matvarde" ? "measurement" : "observation"} has been recorded add a short{" "} No {krav === "matvarde" ? "measurement" : "observation"} has been recorded add a short{" "}
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified. {krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
</p> </p>
@@ -1633,24 +1648,24 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
return ( return (
<button <button
onClick={() => setOppen(true)} onClick={() => setOppen(true)}
className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden" className="mt-2 w-full py-2 text-[12px] font-medium text-[#4D5662] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
> >
Evidence cannot be produced Evidence cannot be produced
</button> </button>
); );
} }
return ( return (
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2"> <div className="mt-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]"> <p className="mb-2 text-[12px] font-semibold text-[#8A5A00]">
Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport. Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
</p> </p>
<div className="mb-2 grid grid-cols-1 gap-1"> <div className="mb-2 grid grid-cols-1 gap-2">
{aktivtRegelpaket().undantagsorsaker.map((val) => ( {aktivtRegelpaket().undantagsorsaker.map((val) => (
<button <button
key={val} key={val}
onClick={() => setOrsak(val)} onClick={() => setOrsak(val)}
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${ className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white" orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
}`} }`}
> >
{val} {val}
@@ -1794,10 +1809,10 @@ function SnabbDokumentation({
? instRef.current?.click() ? instRef.current?.click()
: setTyp(typ === d.id ? null : d.id) : setTyp(typ === d.id ? null : d.id)
} }
className={`min-h-9 border text-[12px] font-semibold transition-colors ${ className={`min-h-9 border text-[12px] font-semibold ${
typ === d.id typ === d.id
? "border-[#00437A] bg-[#00437A] text-white" ? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]" : "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
}`} }`}
> >
+ {d.label} + {d.label}
@@ -1850,8 +1865,8 @@ function SnabbDokumentation({
/> />
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>} {videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
{video && ( {video && (
<div className="mt-3 border border-[#C6C6C6] bg-white p-2"> <div className="mt-4 border border-[#D7DCE2] 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-[#D7DCE2]" />
<TextFalt <TextFalt
label="What does the video show? (required — what makes the noise or moves)" label="What does the video show? (required — what makes the noise or moves)"
varde={video.beskrivning} varde={video.beskrivning}
@@ -1876,23 +1891,23 @@ function SnabbDokumentation({
</div> </div>
</div> </div>
)} )}
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Reading the instrument </p>} {laserAv && <p className="mt-2 text-center text-[12px] font-semibold text-[#005CA9]">Reading the instrument </p>}
{avlasning && ( {avlasning && (
<div className="mt-3 border border-[#C6C6C6] bg-white p-2"> <div className="mt-4 border border-[#D7DCE2] bg-white p-2">
{avlasning.demo && ( {avlasning.demo && (
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]"> <p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[11px] font-semibold text-[#8A5A00]">
Demo reading image interpretation requires sign-in. The values are sample data. Demo reading image interpretation requires sign-in. The values are sample data.
</p> </p>
)} )}
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]"> <p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
Read: {avlasning.tolkning.instrumenttyp} confirm before anything is logged Read: {avlasning.tolkning.instrumenttyp} confirm before anything is logged
</p> </p>
{avlasning.tolkning.varden.map((v, i) => ( {avlasning.tolkning.varden.map((v, i) => (
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0"> <div key={i} className="flex items-center justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
<span> <span>
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span> {v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span>
</span> </span>
<span className={`text-[11px] font-semibold ${v.konfidens >= 0.95 ? "text-[#1E6B34]" : v.konfidens >= 0.8 ? "text-[#9A6700]" : "text-[#8B1A1A]"}`}> <span className={`text-[11px] font-semibold ${v.konfidens >= 0.95 ? "text-[#005CA9]" : v.konfidens >= 0.8 ? "text-[#8A5A00]" : "text-[#8B1A1A]"}`}>
{v.konfidens >= 0.95 ? "✓" : `${Math.round(v.konfidens * 100)} %`} {v.konfidens >= 0.95 ? "✓" : `${Math.round(v.konfidens * 100)} %`}
</span> </span>
</div> </div>
@@ -1906,7 +1921,7 @@ function SnabbDokumentation({
</div> </div>
)} )}
{typ && typ !== "foto" && typ !== "instrument" && ( {typ && typ !== "foto" && typ !== "instrument" && (
<div className="mt-3"> <div className="mt-4">
{typ === "hypotes" && ( {typ === "hypotes" && (
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]"> <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
<IkonPunkt farg="#8B1A1A" /> A hypothesis is a possible root cause that requires verification it is never logged as an established fault. <IkonPunkt farg="#8B1A1A" /> A hypothesis is a possible root cause that requires verification it is never logged as an established fault.
@@ -1968,25 +1983,25 @@ 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-[#D7DCE2] bg-[#F6F7F8] p-4">
<h2 className="mb-3 text-[15px] font-semibold">Handover report</h2> <h2 className="mb-4 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-4 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#1B1E22]">
{text} {text}
</pre> </pre>
{aiLage === "vilar" && ( {aiLage === "vilar" && (
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}> <StorKnapp variant="sekundar" className="mb-4" onClick={komplettera}>
Extend the analysis: risks &amp; uncertainties Extend the analysis: risks &amp; uncertainties
</StorKnapp> </StorKnapp>
)} )}
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing </p>} {aiLage === "arbetar" && <p className="mb-4 font-semibold text-[#005CA9]">Summarizing </p>}
{aiLage === "fel" && ( {aiLage === "fel" && (
<p className="mb-3 font-semibold text-[#8B1A1A]">Could not be retrieved requires sign-in. The handover works regardless.</p> <p className="mb-4 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-4 bg-white p-4">
{aiRader.map((rad, i) => ( {aiRader.map((rad, i) => (
<p key={i} className="py-0.5 text-[12px] text-[#333333]"> <p key={i} className="py-0 text-[12px] text-[#1B1E22]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
</div> </div>
@@ -2049,10 +2064,10 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
} }
return ( return (
<div className="mt-3 border border-[#C6C6C6] bg-white p-3"> <div className="mt-4 border border-[#D7DCE2] bg-white p-4">
{fel && <p className="mb-2 font-semibold text-[#8B1A1A]">{fel}</p>} {fel && <p className="mb-2 font-semibold text-[#8B1A1A]">{fel}</p>}
{delningar.map((delning) => ( {delningar.map((delning) => (
<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-[#D7DCE2] 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]">(revoked)</span>} {delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(revoked)</span>}
@@ -2060,7 +2075,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{!delning.aterkallad && ( {!delning.aterkallad && (
<span className="flex gap-2"> <span className="flex gap-2">
<button <button
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]" className="border border-[#D7DCE2] px-4 py-2 font-semibold text-[#1B1E22] hover:border-[#005CA9]"
onClick={() => onClick={() =>
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`) navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
} }
@@ -2068,7 +2083,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
Copy Copy
</button> </button>
<button <button
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]" className="border border-[#D7DCE2] px-4 py-2 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
onClick={async () => { onClick={async () => {
await aterkallaDelning(delning.kod); await aterkallaDelning(delning.kod);
skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` }); skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` });
@@ -2085,7 +2100,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => ( {(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => (
<button <button
key={niva} key={niva}
className="min-h-9 border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]" className="min-h-9 border border-[#D7DCE2] text-[12px] font-semibold text-[#1B1E22] hover:border-[#005CA9]"
onClick={async () => { onClick={async () => {
try { try {
await skapaDelning(arende.id, niva); await skapaDelning(arende.id, niva);
@@ -2100,7 +2115,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
</button> </button>
))} ))}
</div> </div>
<p className="mt-2 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) · Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) ·
Internal: full visibility. Filtering happens on the server. Internal: full visibility. Filtering happens on the server.
</p> </p>
@@ -2113,16 +2128,16 @@ function LoggFlik({ arende }: { arende: Arende }) {
<Panel rubrik="Work log — append-only, nothing is overwritten"> <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-4 border-b border-[#D7DCE2] py-2 last:border-0">
<span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span> <span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p> <p className="text-[13px] text-[#1B1E22]">{handelseRubrik(post)}</p>
<p className="text-[11px] text-[#707070]">{post.anvandare}</p> <p className="text-[11px] text-[#4D5662]">{post.anvandare}</p>
{post.handelse.typ === "foto" && ( {post.handelse.typ === "foto" && (
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 border border-[#C6C6C6]" /> <Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-2 max-h-40 border border-[#D7DCE2]" />
)} )}
{post.handelse.typ === "video" && ( {post.handelse.typ === "video" && (
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 border border-[#C6C6C6]" /> <Klipp bilaga={post.handelse} className="mt-2 max-h-40 border border-[#D7DCE2]" />
)} )}
</div> </div>
</li> </li>
@@ -2164,25 +2179,25 @@ function BriefFlik({
<> <>
{!b.avslutat && ( {!b.avslutat && (
<Panel rubrik="Review of the evidence"> <Panel rubrik="Review of the evidence">
<p className="mb-2 text-[#333333]"> <p className="mb-2 text-[#1B1E22]">
Have the system review the whole record before you hand it on: contradictions between Have the system review the whole record before you hand it on: contradictions between
observations, gaps in the documentation, and conclusions that lack support. observations, gaps in the documentation, and conclusions that lack support.
</p> </p>
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence </p>} {granskning === "arbetar" && <p className="mb-2 font-semibold text-[#005CA9]">Reviewing the evidence </p>}
{granskning === "fel" && ( {granskning === "fel" && (
<p className="mb-2 font-semibold text-[#8B1A1A]">The review could not be retrieved requires sign-in. Try again.</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">
{senasteAi.handelse.rader.map((rad, i) => ( {senasteAi.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-[14px]"> <p key={i} className="py-0 text-[14px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
<p className="mt-1 text-[14px]"> <p className="mt-2 text-[14px]">
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg} <span className="font-semibold text-[#005CA9]">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-2 text-[11px] text-[#4D5662]">Senaste analys ({senasteAi.handelse.modell})</p>
</div> </div>
)} )}
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}> <StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
@@ -2194,63 +2209,63 @@ function BriefFlik({
{b.objekt ? ( {b.objekt ? (
<> <>
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p> <p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
<p className="font-semibold text-[#00437A]">{b.objekt.identifierare}</p> <p className="font-semibold text-[#005CA9]">{b.objekt.identifierare}</p>
{b.objekt.kund && <p className="text-[#333333]">Kund: {b.objekt.kund}</p>} {b.objekt.kund && <p className="text-[#1B1E22]">Kund: {b.objekt.kund}</p>}
{b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</p>} {b.ansvarig && <p className="text-[#1B1E22]">Ansvarig tekniker: {b.ansvarig}</p>}
</> </>
) : ( ) : (
<p className="text-[#4A5560]">Not identified</p> <p className="text-[#4D5662]">Not identified</p>
)} )}
</Panel> </Panel>
<Panel rubrik="Customer's description"> <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="Checks performed"> <Panel rubrik="Checks performed">
{b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">None yet.</p>} {b.utfordaKontroller.length === 0 && <p className="text-[#4D5662]">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 text-[14px] text-[#8A5A00]">
<IkonVarning /> {k.text} underlag saknas: {k.undantag} <IkonVarning /> {k.text} underlag saknas: {k.undantag}
</p> </p>
) : ( ) : (
<p key={i} className="py-0.5 text-[14px]"> <p key={i} className="py-0 text-[14px]">
{k.text} {k.text}
{k.resultat && <span className="text-[#4A5560]"> {k.resultat}</span>} {k.resultat && <span className="text-[#4D5662]"> {k.resultat}</span>}
</p> </p>
), ),
)} )}
</Panel> </Panel>
<Panel rubrik="Observations"> <Panel rubrik="Observations">
{b.observationer.length === 0 && <p className="text-[#4A5560]">None yet.</p>} {b.observationer.length === 0 && <p className="text-[#4D5662]">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 text-[14px]"> {o}</p>
))} ))}
</Panel> </Panel>
{b.hypoteser.length > 0 && ( {b.hypoteser.length > 0 && (
<Panel rubrik="Hypotheses — require verification"> <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 text-[14px]">
<NivaBadge niva={h.niva} /> {h.text} <NivaBadge niva={h.niva} /> {h.text}
</p> </p>
))} ))}
</Panel> </Panel>
)} )}
<Panel rubrik="Not checked"> <Panel rubrik="Not checked">
{b.ejKontrollerat.length === 0 && <p className="text-[#4A5560]">Every step in the methodology is complete.</p>} {b.ejKontrollerat.length === 0 && <p className="text-[#4D5662]">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 text-[14px] text-[#1B1E22]"> {e}</p>
))} ))}
</Panel> </Panel>
{!b.avslutat && ( {!b.avslutat && (
<Panel rubrik="Recommended next step"> <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 text-[14px]">{i + 1}. {s}</p>
))} ))}
</Panel> </Panel>
)} )}
<Panel rubrik="Reliability"> <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 text-[14px]">
<NivaBadge niva={r.niva} /> {r.text} <NivaBadge niva={r.niva} /> {r.text}
</p> </p>
))} ))}
@@ -2333,15 +2348,15 @@ function RapportFlik({
undantag i händelseloggen. */} undantag i händelseloggen. */}
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}> <Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
{grind.map((rad) => ( {grind.map((rad) => (
<div key={rad.id} className="border-b border-[#EBEBEB] py-1 last:border-0"> <div key={rad.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
<p className="text-[13px]"> <p className="text-[13px]">
<span className={rad.ok ? "text-[#1E6B34]" : rad.kravs ? "text-[#8B1A1A]" : "text-[#9A6700]"}> <span className={rad.ok ? "text-[#005CA9]" : rad.kravs ? "text-[#8B1A1A]" : "text-[#8A5A00]"}>
{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]"> (recommended)</span>} {!rad.kravs && <span className="text-[11px] text-[#4D5662]"> (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-[#4D5662]">{rad.detalj}</p>}
</div> </div>
))} ))}
{!godkand && ( {!godkand && (
@@ -2352,7 +2367,7 @@ function RapportFlik({
)} )}
</Panel> </Panel>
<Panel rubrik="Customer report"> <Panel rubrik="Customer report">
<p className="mb-2 text-[#333333]"> <p className="mb-2 text-[#1B1E22]">
A shareable summary of the work performed. Review the content before the report is shared images may A shareable summary of the work performed. Review the content before the report is shared images may
contain details about other customers. contain details about other customers.
</p> </p>
@@ -2365,7 +2380,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" /> Open Live Share view</StorKnapp> <StorKnapp variant="sekundar"><IkonPunkt farg="#005CA9" /> Open Live Share view</StorKnapp>
</Link> </Link>
{plattformAktiv() ? ( {plattformAktiv() ? (
<DelningsHanterare arende={arende} skicka={skicka} /> <DelningsHanterare arende={arende} skicka={skicka} />
@@ -2383,7 +2398,7 @@ function RapportFlik({
</StorKnapp> </StorKnapp>
) )
)} )}
<p className="mt-2 text-[12px] text-[#707070]"> <p className="mt-2 text-[12px] text-[#4D5662]">
The share link requires the case to be synchronized (signed-in user). The share link requires the case to be synchronized (signed-in user).
</p> </p>
</Panel> </Panel>
@@ -2445,19 +2460,19 @@ function RapportFlik({
{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-[#4D5662]">{repro.beskrivning}</p>
</> </>
) : ( ) : (
<p className="text-[14px] text-[#4A5560]">Reproduction not documented yet.</p> <p className="text-[14px] text-[#4D5662]">Reproduction not documented yet.</p>
)} )}
</Panel> </Panel>
<Panel rubrik="Root cause analysis"> <Panel rubrik="Root cause analysis">
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4A5560]">Root cause analysis not documented yet.</p>} {orsakerLista.length === 0 && <p className="text-[14px] text-[#4D5662]">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;
return ( return (
<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-4 border-b border-[#D7DCE2] 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([
["Assessed root cause", h.orsaker.join(", ")], ["Assessed root cause", h.orsaker.join(", ")],
@@ -2495,7 +2510,7 @@ function RapportFlik({
</Panel> </Panel>
)} )}
<Panel rubrik="Action performed and verification"> <Panel rubrik="Action performed and verification">
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4A5560]">No action documented yet.</p>} {atgardsRader.length === 0 && <p className="text-[14px] text-[#4D5662]">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;
@@ -2507,7 +2522,7 @@ function RapportFlik({
["Parts", h.delar], ["Parts", h.delar],
]) ])
) : ( ) : (
<p className="text-[14px] text-[#9A6700]">No action performed {h.motivering}</p> <p className="text-[14px] text-[#8A5A00]">No action performed {h.motivering}</p>
)} )}
</div> </div>
); );
@@ -2523,15 +2538,15 @@ function RapportFlik({
{fordelning.length > 0 && ( {fordelning.length > 0 && (
<div className="mt-2"> <div className="mt-2">
{fordelning.map((r) => ( {fordelning.map((r) => (
<p key={r.label} className="text-[#333333]">{r.label}: {r.tid}</p> <p key={r.label} className="text-[#1B1E22]">{r.label}: {r.tid}</p>
))} ))}
</div> </div>
)} )}
</Panel> </Panel>
<Panel rubrik="Timeline"> <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 text-[13px]">
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "} <span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)} {handelseRubrik(post)}
</p> </p>
))} ))}
@@ -2540,8 +2555,8 @@ function RapportFlik({
<Panel rubrik="Documented 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-[#D7DCE2]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption> <figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</Panel> </Panel>
@@ -2551,8 +2566,8 @@ function RapportFlik({
<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}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#C6C6C6]" /> <Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#D7DCE2]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption> <figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</div> </div>
@@ -2561,11 +2576,11 @@ function RapportFlik({
{!b.avslutat || b.ejKontrollerat.length > 0 ? ( {!b.avslutat || b.ejKontrollerat.length > 0 ? (
<Panel rubrik="Recommended next steps"> <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 text-[14px]">{i + 1}. {s}</p>
))} ))}
</Panel> </Panel>
) : null} ) : null}
<p className="text-center text-[11px] text-[#8A8A8A]"> <p className="text-center text-[11px] text-[#4D5662]">
Generated from the case log · Observations and measurements are reported without conclusions that lack support. Generated from the case log · Observations and measurements are reported without conclusions that lack support.
</p> </p>
<Ritningsstampel arende={arende} /> <Ritningsstampel arende={arende} />
@@ -2632,7 +2647,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
<button <button
type="button" type="button"
onClick={() => setOppen(true)} onClick={() => setOppen(true)}
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline" className="text-[12px] uppercase tracking-wide text-[#4D5662] underline"
> >
Report a problem with the system Report a problem with the system
</button> </button>
@@ -2642,7 +2657,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
return ( return (
<Panel rubrik="Report a problem with the system"> <Panel rubrik="Report a problem with the system">
<p className="mb-2 text-[12px] text-[#707070]"> <p className="mb-2 text-[12px] text-[#4D5662]">
Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent. Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.
</p> </p>
<div className="mb-2 grid grid-cols-3 gap-2"> <div className="mb-2 grid grid-cols-3 gap-2">
@@ -2664,7 +2679,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
platshallare="Whoever answers cannot see your screen." platshallare="Whoever answers cannot see your screen."
flerRad flerRad
/> />
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Reported as {utfall.ok}.</p>} {utfall.ok && <p className="mt-2 text-[13px] text-[#005CA9]">Reported as {utfall.ok}.</p>}
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>} {utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
<div className="mt-2 grid grid-cols-2 gap-2"> <div className="mt-2 grid grid-cols-2 gap-2">
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}> <StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
@@ -2708,11 +2723,11 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
]; ];
return ( return (
<div className="alva-yta mt-3 border border-[#C6C6C6] bg-white"> <div className="alva-yta mt-4 border border-[#D7DCE2] bg-white">
<div className="grid grid-cols-2 sm:grid-cols-3"> <div className="grid grid-cols-2 sm:grid-cols-3">
{falt.map(([etikett, varde]) => ( {falt.map(([etikett, varde]) => (
<div key={etikett} className="border-b border-r border-[#DDDDDD] px-2 py-2 last:border-r-0"> <div key={etikett} className="border-b border-r border-[#D7DCE2] px-2 py-2 last:border-r-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4A5560]">{etikett}</div> <div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{etikett}</div>
<div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div> <div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
</div> </div>
))} ))}
@@ -44,13 +44,13 @@ function PlattformInloggning() {
return ( return (
<> <>
<Panel rubrik="Platform account"> <Panel rubrik="Platform account">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-4">
<p className="text-[14px]"> <p className="text-[14px]">
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "} <span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
{konto.organisation} synchronization and decision support active. {konto.organisation} synchronization and decision support active.
</p> </p>
<button <button
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]" className="whitespace-nowrap border border-[#D7DCE2] px-4 py-2 font-semibold text-[#1B1E22] hover:border-[#4D5662]"
onClick={() => { onClick={() => {
loggaUtPlattform(); loggaUtPlattform();
setKonto(null); setKonto(null);
@@ -60,7 +60,7 @@ function PlattformInloggning() {
</button> </button>
</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-4 block">
<StorKnapp variant="sekundar"><IkonDiagram /> Organization overview</StorKnapp> <StorKnapp variant="sekundar"><IkonDiagram /> Organization overview</StorKnapp>
</Link> </Link>
)} )}
@@ -79,7 +79,7 @@ function PlattformInloggning() {
<Panel rubrik="Platform account"> <Panel rubrik="Platform account">
{lage === "stangd" ? ( {lage === "stangd" ? (
<> <>
<p className="mb-2 text-[#333333]"> <p className="mb-2 text-[#1B1E22]">
Sign in for synchronization across devices, collaboration on cases and decision support. Without sign-in the app works in local mode. New organization? Create an account you become the system administrator. Sign in for synchronization across devices, collaboration on cases and decision support. Without sign-in the app works in local mode. New organization? Create an account you become the system administrator.
</p> </p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@@ -113,7 +113,7 @@ function PlattformInloggning() {
)} )}
<TextFalt label="E-mail" varde={epost} satt={setEpost} platshallare="anna@workshop.example" /> <TextFalt label="E-mail" varde={epost} satt={setEpost} platshallare="anna@workshop.example" />
<TextFalt label="Password (at least 8 characters)" 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-4 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")}>
Cancel Cancel
@@ -175,23 +175,23 @@ function AnvandarAdmin() {
{lista.map((anv) => ( {lista.map((anv) => (
<div <div
key={anv.id} key={anv.id}
className="flex items-center justify-between gap-3 border-b border-[#DDDDDD] py-1.5 last:border-0" className="flex items-center justify-between gap-4 border-b border-[#D7DCE2] py-2 last:border-0"
> >
<p className="min-w-0 text-[14px]"> <p className="min-w-0 text-[14px]">
<span className={`font-semibold ${anv.aktiv === false ? "text-[#8A8A8A] line-through" : ""}`}> <span className={`font-semibold ${anv.aktiv === false ? "text-[#4D5662] line-through" : ""}`}>
{anv.namn} {anv.namn}
</span>{" "} </span>{" "}
· {ROLL_LABEL[anv.roll]} <span className="text-[#707070]">{anv.epost}</span> · {ROLL_LABEL[anv.roll]} <span className="text-[#4D5662]">{anv.epost}</span>
{anv.aktiv === false && ( {anv.aktiv === false && (
<span className="ml-1 text-[12px] font-semibold text-[#8B1A1A]">Disabled</span> <span className="ml-2 text-[12px] font-semibold text-[#8B1A1A]">Disabled</span>
)} )}
</p> </p>
<button <button
type="button" type="button"
className={`shrink-0 border px-2 py-1 text-[12px] font-semibold ${ className={`shrink-0 border px-2 py-2 text-[12px] font-semibold ${
anv.aktiv === false anv.aktiv === false
? "border-[#1E6B34] bg-white text-[#1E6B34]" ? "border-[#005CA9] bg-white text-[#005CA9]"
: "border-[#6E1414] bg-white text-[#8B1A1A]" : "border-[#8B1A1A] bg-white text-[#8B1A1A]"
}`} }`}
onClick={async () => { onClick={async () => {
setFel(""); setFel("");
@@ -207,11 +207,11 @@ function AnvandarAdmin() {
</button> </button>
</div> </div>
))} ))}
<p className="mt-1 text-[12px] text-[#707070]"> <p className="mt-2 text-[12px] text-[#4D5662]">
Disabling takes effect immediately active sessions on that person's devices end at once. Disabling takes effect immediately active sessions on that person's devices end at once.
</p> </p>
<form <form
className="mt-3" className="mt-4"
onSubmit={async (e) => { onSubmit={async (e) => {
e.preventDefault(); e.preventDefault();
setFel(""); setFel("");
@@ -229,7 +229,7 @@ function AnvandarAdmin() {
<TextFalt label="Name" varde={namn} satt={setNamn} /> <TextFalt label="Name" varde={namn} satt={setNamn} />
<TextFalt label="E-mail" varde={epost} satt={setEpost} /> <TextFalt label="E-mail" varde={epost} satt={setEpost} />
<TextFalt label="Password (at least 8 characters)" 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-4 grid grid-cols-3 gap-2">
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => ( {(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
<button <button
key={valbar} key={valbar}
@@ -237,15 +237,15 @@ function AnvandarAdmin() {
onClick={() => setRoll(valbar)} onClick={() => setRoll(valbar)}
className={`min-h-9 border text-[12px] font-semibold ${ className={`min-h-9 border text-[12px] font-semibold ${
roll === valbar roll === valbar
? "border-[#00437A] bg-[#00437A] text-white" ? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]" : "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22]"
}`} }`}
> >
{ROLL_LABEL[valbar]} {ROLL_LABEL[valbar]}
</button> </button>
))} ))}
</div> </div>
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>} {fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}> <StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
Create user Create user
</StorKnapp> </StorKnapp>
@@ -271,7 +271,7 @@ export default function Arendelista() {
return ( return (
<FelsokningSkal rubrik="Guided Diagnostics"> <FelsokningSkal rubrik="Guided Diagnostics">
<Panel rubrik="Who is working?"> <Panel rubrik="Who is working?">
<p className="mb-3 text-[14px] text-[#333333]"> <p className="mb-4 text-[14px] text-[#1B1E22]">
All work is logged with user and timestamp. Enter your name to begin. All work is logged with user and timestamp. Enter your name to begin.
</p> </p>
<form <form
@@ -304,20 +304,20 @@ export default function Arendelista() {
return ( return (
<FelsokningSkal <FelsokningSkal
rubrik="Guided Diagnostics" rubrik="Guided Diagnostics"
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{anvandare}</span>} hoger={<span className="text-[12px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{anvandare}</span>}
> >
<Link to="/felsokning/nytt"> <Link to="/felsokning/nytt">
<StorKnapp className="mb-4">+ New case</StorKnapp> <StorKnapp className="mb-4">+ New case</StorKnapp>
</Link> </Link>
{alla.length > 0 && ( {alla.length > 0 && (
<div className="mb-4 grid grid-cols-3 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1"> <div className="mb-4 grid grid-cols-3 gap-2 border border-[#D7DCE2] bg-[#F6F7F8] p-2">
{FILTER.map((f) => ( {FILTER.map((f) => (
<button <button
key={f.id} key={f.id}
onClick={() => setFilter(f.id)} onClick={() => setFilter(f.id)}
className={`min-h-9 text-[13px] font-semibold transition-colors ${ className={`min-h-9 text-[13px] font-semibold ${
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]" filter === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
}`} }`}
> >
{f.label} · {f.antal} {f.label} · {f.antal}
@@ -328,7 +328,7 @@ export default function Arendelista() {
{alla.length === 0 && ( {alla.length === 0 && (
<Panel rubrik="Get started"> <Panel rubrik="Get started">
<p className="mb-3 text-[14px] text-[#333333]"> <p className="mb-4 text-[14px] text-[#1B1E22]">
No cases yet. Start by identifying an object or explore a finished demo case with a complete work log, brief and customer report. No cases yet. Start by identifying an object or explore a finished demo case with a complete work log, brief and customer report.
</p> </p>
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}> <StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
@@ -338,7 +338,7 @@ export default function Arendelista() {
)} )}
{lista.length === 0 && alla.length > 0 && ( {lista.length === 0 && alla.length > 0 && (
<p className="text-center text-[14px] text-[#4A5560]">No cases in this filter.</p> <p className="text-center text-[14px] text-[#4D5662]">No cases in this filter.</p>
)} )}
{plattformAktiv() && <PlattformInloggning />} {plattformAktiv() && <PlattformInloggning />}
@@ -350,7 +350,7 @@ export default function Arendelista() {
)} )}
<Panel rubrik="Decision support"> <Panel rubrik="Decision support">
<p className="text-[#333333]"> <p className="text-[#1B1E22]">
When you are signed in the system helps while you work: it proposes the next step from what you document, reviews the whole case on request and drafts the handover. It always separates verified from hypothesis and never states a root cause that is not confirmed. Nothing to install or configure; it is part of the service. Without sign-in the checklist guides you step by step. When you are signed in the system helps while you work: it proposes the next step from what you document, reviews the whole case on request and drafts the handover. It always separates verified from hypothesis and never states a root cause that is not confirmed. Nothing to install or configure; it is part of the service. Without sign-in the checklist guides you step by step.
</p> </p>
</Panel> </Panel>
@@ -363,24 +363,24 @@ export default function Arendelista() {
const b = brief(arende, metodikForArende(arende), new Date().toISOString()); const b = brief(arende, metodikForArende(arende), new Date().toISOString());
return ( return (
<Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block"> <Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block">
<div className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]"> <div className="mb-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4 hover:border-[#005CA9]">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]"> <span className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">
Case #{arende.nummer} Case #{arende.nummer}
</span> </span>
<span <span
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${ className={`px-2 py-0 text-[11px] font-semibold uppercase ${
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white" avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`} }`}
> >
{avslutat ? "Closed" : "In progress"} {avslutat ? "Closed" : "In progress"}
</span> </span>
</div> </div>
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</p> <p className="mt-2 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-[#4D5662]">{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-[#1B1E22]">{fel}</p>}
<p className="mt-2 text-[12px] text-[#707070]"> <p className="mt-2 text-[12px] text-[#4D5662]">
{sista ? `Senaste aktivitet ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""} {sista ? `Last activity ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
{" · "} {" · "}
{b.totalArbetstid} {b.totalArbetstid}
</p> </p>
@@ -21,7 +21,7 @@ export default function DelatArende() {
if (!arende) { if (!arende) {
return ( return (
<FelsokningSkal rubrik="Shared case"> <FelsokningSkal rubrik="Shared case">
<p className="text-[14px] text-[#333333]">The case is not available.</p> <p className="text-[14px] text-[#1B1E22]">The case is not available.</p>
</FelsokningSkal> </FelsokningSkal>
); );
} }
@@ -41,10 +41,10 @@ function Vallista({
<button <button
key={val} key={val}
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])} onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
className={`min-h-10 border px-2 font-semibold transition-colors ${ className={`min-h-10 border px-2 font-semibold ${
aktiv aktiv
? "border-[#00437A] bg-[#00437A] text-white" ? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]" : "border-[#D7DCE2] bg-[#F6F7F8] text-[#4D5662]"
}`} }`}
> >
{aktiv ? "✓ " : ""} {aktiv ? "✓ " : ""}
@@ -85,11 +85,11 @@ function Integrationer() {
return ( return (
<Panel rubrik="Brand-specific connectors"> <Panel rubrik="Brand-specific connectors">
<p className="mb-2 text-[12px] text-[#707070]"> <p className="mb-2 text-[12px] text-[#4D5662]">
The organization's own agreements with manufacturers and data providers. The details are stored encrypted on the platform and used only by the server — they are never sent to technicians' devices. The organization's own agreements with manufacturers and data providers. The details are stored encrypted on the platform and used only by the server — they are never sent to technicians' devices.
</p> </p>
{!krypteringKlar && ( {!krypteringKlar && (
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]"> <p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[12px] font-semibold text-[#8A5A00]">
Encryption is not configured in this deployment (INTEGRATION_NYCKEL) details cannot be saved until the key exists. Encryption is not configured in this deployment (INTEGRATION_NYCKEL) details cannot be saved until the key exists.
</p> </p>
)} )}
@@ -97,13 +97,13 @@ function Integrationer() {
{leverantorer.map((lev) => { {leverantorer.map((lev) => {
const finns = befintliga.find((i) => i.leverantor === lev.id); const finns = befintliga.find((i) => i.leverantor === lev.id);
return ( return (
<div key={lev.id} className="border-b border-[#EBEBEB] py-2 last:border-0"> <div key={lev.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-4">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[13px] font-semibold">{lev.namn}</p> <p className="text-[13px] font-semibold">{lev.namn}</p>
{lev.beskrivning && <p className="text-[12px] text-[#707070]">{lev.beskrivning}</p>} {lev.beskrivning && <p className="text-[12px] text-[#4D5662]">{lev.beskrivning}</p>}
{finns && ( {finns && (
<p className="mt-1 text-[12px] text-[#4A5560]"> <p className="mt-2 text-[12px] text-[#4D5662]">
{Object.entries(finns.uppgifter) {Object.entries(finns.uppgifter)
.map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`) .map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`)
.join(" · ")} .join(" · ")}
@@ -111,13 +111,13 @@ function Integrationer() {
</p> </p>
)} )}
</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-[#005CA9]" : "text-[#4D5662]"}`}>
{finns ? "Configured" : "Not configured"} {finns ? "Configured" : "Not configured"}
</span> </span>
</div> </div>
{oppen === lev.id ? ( {oppen === lev.id ? (
<div className="mt-2 border border-[#C6C6C6] bg-white p-2"> <div className="mt-2 border border-[#D7DCE2] bg-white p-2">
{lev.falt.map((falt) => ( {lev.falt.map((falt) => (
<TextFalt <TextFalt
key={falt.nyckel} key={falt.nyckel}
@@ -150,7 +150,7 @@ function Integrationer() {
Save details Save details
</StorKnapp> </StorKnapp>
</div> </div>
<p className="mt-1 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
Secret fields are never shown again after saving enter a new value to replace one. Secret fields are never shown again after saving enter a new value to replace one.
</p> </p>
</div> </div>
@@ -188,7 +188,7 @@ function Integrationer() {
</div> </div>
); );
})} })}
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Details saved for {sparat}.</p>} {sparat && <p className="mt-2 text-[12px] font-semibold text-[#005CA9]">Details saved for {sparat}.</p>}
</Panel> </Panel>
); );
} }
@@ -210,7 +210,7 @@ export default function Installningar() {
return ( return (
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}> <FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel> <Panel>
<p className="text-[14px] text-[#333333]"> <p className="text-[14px] text-[#1B1E22]">
Organization settings are managed by your system administrator. Organization settings are managed by your system administrator.
</p> </p>
</Panel> </Panel>
@@ -243,7 +243,7 @@ export default function Installningar() {
return ( return (
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}> <FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel> <Panel>
<p className="text-[#333333]"> <p className="text-[#1B1E22]">
Choose what is shown when a new case is started.{" "} Choose what is shown when a new case is started.{" "}
{inloggad {inloggad
? "The setting applies to everyone in the organization." ? "The setting applies to everyone in the organization."
@@ -269,8 +269,8 @@ 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-4 font-semibold text-[#8B1A1A]">{felText}</p>}
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]"> Saved</p>} {status === "sparat" && <p className="mb-4 font-semibold text-[#005CA9]"> Saved</p>}
<StorKnapp disabled={status === "sparar"} onClick={spara}> <StorKnapp disabled={status === "sparar"} onClick={spara}>
{status === "sparar" ? "Saving …" : "Save settings"} {status === "sparar" ? "Saving …" : "Save settings"}
</StorKnapp> </StorKnapp>
@@ -176,7 +176,7 @@ export default function NyttArende() {
return ( return (
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}> <FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel rubrik="Scan work order"> <Panel rubrik="Scan work order">
<p className="mb-3 text-[#333333]"> <p className="mb-4 text-[#1B1E22]">
Photograph the front of the work order. The system reads customer, vehicle and case details automatically you review only the uncertain fields and press Start diagnosis. Photograph the front of the work order. The system reads customer, vehicle and case details automatically you review only the uncertain fields and press Start diagnosis.
</p> </p>
<input <input
@@ -192,7 +192,7 @@ export default function NyttArende() {
}} }}
/> />
{skannar ? ( {skannar ? (
<p className="animate-pulse py-2 text-center font-semibold text-[#00437A]">Interpreting the document </p> <p className="py-2 text-center font-semibold text-[#005CA9]">Interpreting the document </p>
) : ( ) : (
<StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp> <StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp>
)} )}
@@ -215,7 +215,7 @@ export default function NyttArende() {
tillbaka={{ till: "/felsokning", text: "Cases" }} 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-4 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[12px] font-semibold text-[#8A5A00]">
Demo interpretation the platform's document interpretation requires sign-in. The values below are sample data. Demo interpretation the platform's document interpretation requires sign-in. The values below are sample data.
</p> </p>
)} )}
@@ -253,7 +253,7 @@ export default function NyttArende() {
</StorKnapp> </StorKnapp>
))} ))}
</div> </div>
<p className="mt-3 text-[12px] text-[#707070]"> <p className="mt-4 text-[12px] text-[#4D5662]">
QR code, barcode and type plate can be read in the next step — or enter the identity manually. QR code, barcode and type plate can be read in the next step — or enter the identity manually.
</p> </p>
</Panel> </Panel>
@@ -272,8 +272,8 @@ export default function NyttArende() {
/> />
{skannarKod ? ( {skannarKod ? (
<div> <div>
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" /> <video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#D7DCE2]" />
<p className="mb-2 text-[12px] text-[#707070]"> <p className="mb-2 text-[12px] text-[#4D5662]">
Point the camera at the QR code, barcode or VIN label. Point the camera at the QR code, barcode or VIN label.
</p> </p>
<StorKnapp variant="sekundar" onClick={stangSkanning}> <StorKnapp variant="sekundar" onClick={stangSkanning}>
@@ -293,7 +293,7 @@ export default function NyttArende() {
</div> </div>
)} )}
{avlastKod && ( {avlastKod && (
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]"> <p className="mt-2 text-[12px] font-semibold text-[#005CA9]">
Read {avlastKod.typ}: {avlastKod.varde} Read {avlastKod.typ}: {avlastKod.varde}
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below. {avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
</p> </p>
@@ -316,11 +316,11 @@ export default function NyttArende() {
return ( return (
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}> <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-[#4D5662]">{objektet.typ}</p>
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p> <p className="mt-2 text-[20px] font-semibold">{objektet.beskrivning}</p>
<p className="mt-1 text-[15px] font-semibold text-[#00437A]">{objektet.identifierare}</p> <p className="mt-2 text-[15px] font-semibold text-[#005CA9]">{objektet.identifierare}</p>
<p className="mt-1 text-[12px] text-[#4A5560]">Identifierad via: {objektet.identifieringsmetod}</p> <p className="mt-2 text-[12px] text-[#4D5662]">Identifierad via: {objektet.identifieringsmetod}</p>
{objektet.kund && <p className="mt-1 text-[14px] text-[#333333]">Kund: {objektet.kund}</p>} {objektet.kund && <p className="mt-2 text-[14px] text-[#1B1E22]">Kund: {objektet.kund}</p>}
</Panel> </Panel>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}> <StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
@@ -346,8 +346,8 @@ export default function NyttArende() {
rost rost
/> />
{metodik && ( {metodik && (
<p className="mb-3 text-[12px] font-semibold text-[#4A5560]"> <p className="mb-4 text-[12px] font-semibold text-[#4D5662]">
Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span> Vald metodik: <span className="text-[#005CA9]">{metodik.namn}</span>
</p> </p>
)} )}
<StorKnapp <StorKnapp
@@ -401,13 +401,13 @@ function GranskaTolkning({
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-4">
<Panel rubrik="Scanned work order"> <Panel rubrik="Scanned work order">
<div className="relative"> <div className="relative">
<img src={foto} alt="Scanned work order" className="w-full border border-[#C6C6C6]" /> <img src={foto} alt="Scanned work order" className="w-full border border-[#D7DCE2]" />
{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-[#005CA9] bg-[#005CA9]/15"
style={{ style={{
left: `${vald.omrade.x * 100}%`, left: `${vald.omrade.x * 100}%`,
top: `${vald.omrade.y * 100}%`, top: `${vald.omrade.y * 100}%`,
@@ -417,7 +417,7 @@ function GranskaTolkning({
/> />
)} )}
</div> </div>
<p className="mt-1 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
{vald?.omrade {vald?.omrade
? `Markerat: ${vald.etikett} (approximate position)` ? `Markerat: ${vald.etikett} (approximate position)`
: "Select a field to see where in the document the value was found."} : "Select a field to see where in the document the value was found."}
@@ -436,27 +436,27 @@ function GranskaTolkning({
<div <div
key={rad.id} key={rad.id}
onClick={() => setMarkerad(rad.id)} onClick={() => setMarkerad(rad.id)}
className={`mb-1.5 cursor-pointer border p-2 ${ className={`mb-2 cursor-pointer border p-2 ${
markerad === rad.id ? "border-[#00437A]" : "border-transparent" markerad === rad.id ? "border-[#005CA9]" : "border-transparent"
} ${niva === "gul" ? "bg-[#FFF8E1]" : niva === "rod" ? "bg-[#FBE9E9]" : "bg-white"}`} } ${niva === "gul" ? "bg-[#FFFFFF]" : niva === "rod" ? "bg-[#FFFFFF]" : "bg-white"}`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]"> <span className="text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
{rad.etikett} {rad.etikett}
</span> </span>
<span <span
className={`text-[11px] font-semibold ${ className={`text-[11px] font-semibold ${
niva === "gron" ? "text-[#1E6B34]" : niva === "gul" ? "text-[#9A6700]" : "text-[#8B1A1A]" niva === "gron" ? "text-[#005CA9]" : niva === "gul" ? "text-[#8A5A00]" : "text-[#8B1A1A]"
}`} }`}
> >
{niva === "gron" ? "" : `${Math.round(rad.konfidens * 100)} %`} {niva === "gron" ? "" : `${Math.round(rad.konfidens * 100)} %`}
</span> </span>
</div> </div>
<div className="mt-1 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
<input <input
value={rad.varde} value={rad.varde}
onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })} onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })}
className="w-full border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none" className="w-full border border-[#D7DCE2] bg-white px-2 py-2 text-[13px] focus:border-[#005CA9] focus:outline-none"
/> />
{niva === "rod" && !rad.bekraftad && ( {niva === "rod" && !rad.bekraftad && (
<button <button
@@ -464,7 +464,7 @@ function GranskaTolkning({
e.stopPropagation(); e.stopPropagation();
uppdatera(rad.id, { bekraftad: true }); uppdatera(rad.id, { bekraftad: true });
}} }}
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white" className="whitespace-nowrap border border-[#8B1A1A] bg-[#8B1A1A] px-4 py-2 text-[12px] font-semibold text-white"
> >
Confirm Confirm
</button> </button>
@@ -45,13 +45,13 @@ 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-[#D7DCE2] bg-white p-2">
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">New responsible technician</p> <p className="mb-2 text-[12px] font-semibold uppercase text-[#4D5662]">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
key={person.id} key={person.id}
className="min-h-9 border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]" className="min-h-9 border border-[#D7DCE2] font-semibold text-[#1B1E22] hover:border-[#005CA9]"
onClick={async () => { onClick={async () => {
const post = nyLoggPost(konto?.namn ?? "arbetsledare", { const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
typ: "ansvarig_satt", typ: "ansvarig_satt",
@@ -69,7 +69,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
</button> </button>
))} ))}
<button <button
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]" className="min-h-9 border border-[#D7DCE2] font-semibold text-[#4D5662]"
onClick={() => setOppen(false)} onClick={() => setOppen(false)}
> >
Cancel Cancel
@@ -101,7 +101,7 @@ export default function Oversikt() {
return ( return (
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}> <FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel> <Panel>
<p className="text-[14px] text-[#333333]"> <p className="text-[14px] text-[#1B1E22]">
The overview requires supervisor or administrator rights on the platform. The overview requires supervisor or administrator rights on the platform.
</p> </p>
</Panel> </Panel>
@@ -153,30 +153,30 @@ export default function Oversikt() {
<FelsokningSkal <FelsokningSkal
rubrik="Organization overview" rubrik="Organization overview"
tillbaka={{ till: "/felsokning", text: "Cases" }} tillbaka={{ till: "/felsokning", text: "Cases" }}
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{konto?.organisation}</span>} hoger={<span className="text-[12px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{konto?.organisation}</span>}
> >
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>} {fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Retrieving </p>} {!rader && !fel && <p className="text-[14px] text-[#4D5662]">Retrieving </p>}
{rader && ( {rader && (
<> <>
<Panel rubrik="Statistics"> <Panel rubrik="Statistics">
<div className="grid grid-cols-3 gap-3 text-center"> <div className="grid grid-cols-3 gap-4 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]">Total</p> <p className="text-[12px] font-semibold uppercase text-[#4D5662]">Total</p>
</div> </div>
<div> <div>
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p> <p className="text-[20px] font-semibold text-[#005CA9]">{pagaende.length}</p>
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">In progress</p> <p className="text-[12px] font-semibold uppercase text-[#4D5662]">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]">Closed</p> <p className="text-[12px] font-semibold uppercase text-[#4D5662]">Closed</p>
</div> </div>
</div> </div>
{avslutade.length > 0 && ( {avslutade.length > 0 && (
<p className="mt-3 text-center text-[#333333]"> <p className="mt-4 text-center text-[#1B1E22]">
Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span> Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
</p> </p>
)} )}
@@ -185,41 +185,41 @@ export default function Oversikt() {
{felorsaker.length > 0 && ( {felorsaker.length > 0 && (
<Panel rubrik="Root cause statistics — recurring faults in the organization"> <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 text-[13px]">
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span> <span className="w-52 shrink-0 text-[#1B1E22]">{rad.orsak}</span>
<span <span
className="h-3 bg-[#00437A]" className="h-3 bg-[#005CA9]"
style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }} style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }}
/> />
<span className="font-semibold">{rad.antal}</span> <span className="font-semibold">{rad.antal}</span>
</div> </div>
))} ))}
<p className="mt-2 text-[11px] text-[#707070]"> <p className="mt-2 text-[11px] text-[#4D5662]">
From documented root cause analyses shows patterns such as wear, previous repairs or possible design problems. From documented root cause analyses shows patterns such as wear, previous repairs or possible design problems.
</p> </p>
</Panel> </Panel>
)} )}
{rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">No cases in the organization yet.</p>} {rader.length === 0 && <p className="text-center text-[14px] text-[#4D5662]">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-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]"> <span className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">
Case #{rad.nummer} Case #{rad.nummer}
</span> </span>
<span <span
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${ className={`px-2 py-0 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white" rad.avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`} }`}
> >
{rad.avslutat ? "Closed" : "In progress"} {rad.avslutat ? "Closed" : "In progress"}
</span> </span>
</div> </div>
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p> <p className="mt-2 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-[#1B1E22]">{rad.felbeskrivning}</p>}
<p className="mt-1 text-[12px] text-[#707070]"> <p className="mt-2 text-[12px] text-[#4D5662]">
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span> Ansvarig: <span className="font-semibold text-[#1B1E22]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
{` · ${rad.antal_handelser} events`} {` · ${rad.antal_handelser} events`}
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""} {rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
</p> </p>
@@ -89,7 +89,7 @@ export default function PublikDelning() {
return ( return (
<FelsokningSkal rubrik="Shared case"> <FelsokningSkal rubrik="Shared case">
<Panel> <Panel>
<p className="text-[14px] text-[#333333]"> <p className="text-[14px] text-[#1B1E22]">
{status === "laddar" {status === "laddar"
? "Retrieving the case …" ? "Retrieving the case …"
: "The case is not available. Check the link with the workshop — the share may have been revoked."} : "The case is not available. Check the link with the workshop — the share may have been revoked."}