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
// texten bryts till två rader blir olika HÖGA, vilket är samma
// 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" : ""
}`}
style={{
@@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar
function Platshallare({ text }: { text: string }) {
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}
</p>
);
+29 -29
View File
@@ -28,8 +28,8 @@ function BeslutsKnappar({
const [fel, setFel] = useState("");
return (
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-2 border-t border-[#D7DCE2] pt-2 print:hidden">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Your decision to the workshop
</p>
<div className="mb-2 grid grid-cols-2 gap-2">
@@ -43,14 +43,14 @@ function BeslutsKnappar({
{val && (
<>
<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)"}
</span>
<input
value={kommentar}
maxLength={500}
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>
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
@@ -66,7 +66,7 @@ function BeslutsKnappar({
>
{skickar ? "Sending …" : "Send decision"}
</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.
</p>
</>
@@ -88,14 +88,14 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole
["Status", avslutat ? "Closed" : "Diagnosis in progress"],
];
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>}
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
{falt
.filter(([, varde]) => varde)
.map(([etikett, varde]) => (
<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>
))}
</p>
@@ -145,8 +145,8 @@ export function DelatArendeVy({
rubrik={b.objekt?.beskrivning ?? "Case"}
hoger={
<span
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
className={`px-4 py-2 text-[12px] font-semibold uppercase ${
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`}
>
{avslutat ? "Closed" : "Diagnosis in progress"}
@@ -185,7 +185,7 @@ export function DelatArendeVy({
{rader.map(([etikett, text]) => (
<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>
<dd className="mt-1 text-[#1B1E22]">{text}</dd>
<dd className="mt-2 text-[#1B1E22]">{text}</dd>
</div>
))}
</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}
</p>
@@ -212,20 +212,20 @@ export function DelatArendeVy({
<div key={p.id} className="mb-2 last:mb-0">
<p className="text-[14px]">{h.beskrivning}</p>
{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>
);
})}
{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]}{" "}
<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>
) : 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.
</p>
)}
@@ -234,11 +234,11 @@ export function DelatArendeVy({
<Panel rubrik="Current status">
{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) => (
<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>
@@ -246,8 +246,8 @@ export function DelatArendeVy({
<Panel rubrik="Video">
{klipp.map((v, i) => (
<figure key={i} className="mb-2">
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#D7DCE2]" />
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
</figure>
))}
</Panel>
@@ -257,8 +257,8 @@ export function DelatArendeVy({
<div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => (
<figure key={i}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#D7DCE2]" />
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
</figure>
))}
</div>
@@ -273,9 +273,9 @@ export function DelatArendeVy({
const h = p.handelse;
if (h.typ !== "matvarde") return null;
return (
<tr key={p.id} className="border-b border-[#DDDDDD] last:border-0">
<td className="py-1 pr-3 text-[#333333]">{h.beskrivning}</td>
<td className="py-1 font-semibold">
<tr key={p.id} className="border-b border-[#D7DCE2] last:border-0">
<td className="py-2 pr-4 text-[#1B1E22]">{h.beskrivning}</td>
<td className="py-2 font-semibold">
{h.varde}
{h.enhet ? ` ${h.enhet}` : ""}
</td>
@@ -289,8 +289,8 @@ export function DelatArendeVy({
<Panel rubrik="Timeline">
{kundposter.map((post) => (
<p key={post.id} className="py-0.5 text-[13px]">
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
<p key={post.id} className="py-0 text-[13px]">
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)}
</p>
))}
@@ -305,13 +305,13 @@ export function DelatArendeVy({
<Panel rubrik="Labour time">
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
{tidsfordelningsRader(arende, nu).map((r) => (
<p key={r.label} className="text-[#333333]">
<p key={r.label} className="text-[#1B1E22]">
{r.label}: {r.tid}
</p>
))}
</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
</p>
</FelsokningSkal>
+3 -3
View File
@@ -48,10 +48,10 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
type="button"
onClick={() => (lyssnar ? stoppa() : starta())}
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
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
? "border-[#8B1A1A] bg-[#8B1A1A] text-white"
: "border-[#D7DCE2] bg-white text-[#1B1E22] hover:border-[#005CA9]"
}`}
>
{lyssnar ? "■" : <IkonMik />}
+2 -2
View File
@@ -171,7 +171,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
>
{etikett}
</label>
<p className="mt-1 text-[13px]" style={{ color: FARG.graphite }}>
<p className="mt-2 text-[13px]" style={{ color: FARG.graphite }}>
{f.fraga}
</p>
<textarea
@@ -189,7 +189,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
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}
</p>
</div>
@@ -147,7 +147,7 @@ describe("AI-orkestern", () => {
"Reläet klickar inte.",
);
expect(prompt).toContain("Volvo XC60 D4 2019");
expect(prompt).toContain("Felbeskrivning:");
expect(prompt).toContain("Fault description:");
expect(prompt).toContain("Kontrollera lufttryck");
expect(prompt).toContain("Ej kontrollerat enligt metodiken:");
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", () => {
const arende = byggDemoArende(1);
const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK);
expect(prompt).toContain("Fullständig arbetslogg:");
expect(prompt).toContain("Arbete överlämnat från Anna till Johan");
expect(prompt).toContain("Full work log:");
expect(prompt).toContain("Work handed over from Anna to Johan");
expect(prompt).not.toContain("Teknikerns nya inmatning");
});
@@ -14,7 +14,16 @@ import { readdirSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
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) =>
readdirSync(katalog)
@@ -28,7 +37,17 @@ const utanKommentarer = (kod: string) =>
describe("ALVA-ytan följer designsystemet", () => {
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) => {
@@ -25,7 +25,7 @@ describe("demoärendet", () => {
it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => {
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(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 () => {
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 rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering");
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 {
const rader: string[] = [];
rader.push(`Metodik: ${metodikNamn}`);
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
rader.push(`Methodology: ${metodikNamn}`);
if (brief.objekt) rader.push(`Object: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
if (brief.felbeskrivning) rader.push(`Fault description: ${brief.felbeskrivning}`);
if (brief.utfordaKontroller.length > 0) {
rader.push("Checks performed:");
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
.map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`)
.join("\n");
return `${kontext}\n\nFullständig arbetslogg:\n${logg}`;
return `${kontext}\n\nFull work log:\n${logg}`;
}
export function tolkaAiSvar(data: unknown): AiSvar {
+5 -5
View File
@@ -16,14 +16,14 @@ function demoFoto(text: string): string {
canvas.height = 360;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("canvas saknas");
ctx.fillStyle = "#27272a";
ctx.fillStyle = "#1B1E22";
ctx.fillRect(0, 0, 480, 360);
ctx.strokeStyle = "#52525b";
ctx.strokeStyle = "#4D5662";
ctx.lineWidth = 22;
ctx.beginPath();
ctx.arc(240, 170, 110, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = "#a1a1aa";
ctx.strokeStyle = "#4D5662";
ctx.lineWidth = 4;
ctx.beginPath();
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.stroke();
}
ctx.fillStyle = "#fbbf24";
ctx.fillStyle = "#8A5A00";
ctx.font = "bold 22px sans-serif";
ctx.textAlign = "center";
ctx.fillText(text, 240, 330);
ctx.fillStyle = "#71717a";
ctx.fillStyle = "#4D5662";
ctx.font = "14px sans-serif";
ctx.fillText("Demobild", 240, 26);
return canvas.toDataURL("image/jpeg", 0.7);
+12 -12
View File
@@ -279,18 +279,18 @@ export function handelseRubrik(post: LoggPost): string {
case "objekt_identifierat":
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
case "arbetsorder_skannad":
return `Arbetsorder skannad — ${h.falt.length} fält tolkade`;
return `Work order scanned — ${h.falt.length} fields interpreted`;
case "felbeskrivning":
return `Felbeskrivning registrerad`;
case "fraga_besvarad":
return `${h.fraga}${h.svar}`;
case "kontroll_utford":
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":
return `Observation: ${h.text}`;
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":
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
case "foto":
@@ -304,18 +304,18 @@ export function handelseRubrik(post: LoggPost): string {
case "inaktivitet_forklarad":
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
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":
return `Ansvarig tekniker: ${h.ansvarig}`;
case "arendetyp_satt":
return `Ärendetyp: ${h.arendetyp}`;
return `Case type: ${h.arendetyp}`;
case "historik_kontrollerad":
return h.kontrollerad
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
case "matarstallning":
if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`;
return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`;
if (h.undantag) return `Odometer reading (${h.lage === "ingaende" ? "in" : "out"}) could not be documented: ${h.undantag}`;
return `Odometer ${h.lage === "ingaende" ? "in" : "out"}: ${h.varde}`;
case "reproducering":
return h.status === "ja"
? `Symptomet reproducerat: ${h.beskrivning}`
@@ -325,22 +325,22 @@ export function handelseRubrik(post: LoggPost): string {
case "felorsak":
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse}${h.orsaker.join(", ")}`;
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":
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? `${h.kommentar}` : ""}`;
case "atgard_utford":
return h.utford
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
: `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`;
? `Action performed: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
: `No action performed — ${h.motivering ?? "reason missing"}`;
case "kvalitetskontroll":
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]}${h.beskrivning}`;
case "export_skapad":
return `Export skapad: ${h.format}, version ${h.version}`;
case "ai_svar": {
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":
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",
ok: fotoKravUtanFoto === 0,
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
@@ -794,7 +794,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
rader.push({
id: "evidensniva",
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
rubrik: `Evidence level: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
ok: evidensNiva(arende) !== "E0",
kravs: true,
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 m = minuter % 60;
if (h === 0) return `${m} min`;
return `${h} tim ${m} min`;
return `${h} h ${m} min`;
}
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 {
const b = brief(arende, metodik, nu);
const rader: string[] = [];
rader.push(`ÖVERLÄMNING Ärende #${arende.nummer}`);
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` Kund: ${b.objekt.kund}` : ""}`);
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
rader.push(`HANDOVER Case #${arende.nummer}`);
if (b.objekt) rader.push(`Object: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` Customer: ${b.objekt.kund}` : ""}`);
if (b.felbeskrivning) rader.push(`Fault description: ${b.felbeskrivning}`);
rader.push("");
rader.push("Checks performed:");
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.
// Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition —
// plana ytor, skarpa kanter, tät informationslayout, dämpad palett
// (ljusgrå ytor, djup marinblå som primärfärg), inga dekorationer.
// Verktygsrad ~44 px, rektangulära knappar (max 4 px radie), kompakta
// formulär med vänsterställda etiketter.
//
// ---- Ett system, inte två -----------------------------------------------
//
// Det här filhuvudet beskrev tidigare en egen designfilosofi: "industriellt
// 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 { useRef, type ReactNode } from "react";
@@ -11,13 +34,8 @@ import type { Tillforlitlighet } from "./domain";
import { TILLFORLITLIGHET_LABEL } from "./domain";
import { MikrofonKnapp } from "./Mikrofon";
import { IkonPunkt } from "./ikoner";
// Industriell palett (inspirerad av tyska verkstadssystem):
// 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";
import { Etikett, FARG } from "@/alva/komponenter";
import { ALVA } from "@/alva/system";
export function FelsokningSkal({
rubrik,
@@ -34,32 +52,62 @@ export function FelsokningSkal({
}) {
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
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
className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]"
style={{ fontFamily: TYPSNITT }}
className="alva-yta felsokning-print min-h-screen text-[13px]"
style={{ background: FARG.background, color: FARG.graphite }}
>
{/* Verktygsrad: låg, mörk marinblå — breadcrumb till vänster,
status till höger. Inget stort sidhuvud. */}
<header className="sticky top-0 z-20 border-b border-[#0A2A4A] bg-[#123A63] px-3 text-white print:static">
<div className={`mx-auto flex min-h-11 ${maxBredd} items-center justify-between gap-3 py-1`}>
{/* Verktygsraden är vit med en enda linje under, precis som
portalens sidhuvud. Den var tidigare mörkt marinblå — ett eget
varumärke inuti varumärket. */}
<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">
{/* 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 && (
<>
<Link
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}
</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>
{hoger}
</div>
</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>
);
}
@@ -79,18 +127,22 @@ export function StorKnapp({
type?: "button" | "submit";
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"
? "border border-[#003561] bg-[#00437A] text-white hover:bg-[#005A9E] active:bg-[#003561]"
? { background: FARG.graphite, color: FARG.white, borderColor: FARG.graphite }
: variant === "fara"
? "border border-[#6E1414] bg-[#8B1A1A] text-white hover:bg-[#A32020] active:bg-[#6E1414]"
: "border border-[#ADADAD] bg-white text-[#1A1A1A] hover:bg-[#EDF2F7] hover:border-[#00437A] active:bg-[#D6E4F2]";
? { background: FARG.stoppat, color: FARG.white, borderColor: FARG.stoppat }
: { background: FARG.white, color: FARG.graphite, borderColor: FARG.steel };
return (
<button
type={type}
onClick={onClick}
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}
</button>
@@ -98,27 +150,44 @@ export function StorKnapp({
}
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 (
<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 && (
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
{rubrik}
</h2>
<header
className="border-b px-4 py-2"
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>
);
}
// 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> = {
hog: "#1E6B34",
medel: "#C08A00",
lag: "#8B1A1A",
hog: FARG.blue,
medel: FARG.varning,
lag: FARG.stoppat,
};
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
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]} />
{TILLFORLITLIGHET_LABEL[niva]}
</span>
@@ -146,12 +215,12 @@ export function TextFalt({
// granskar och bekräftar alltid innan något sparas i loggen.
const vardeRef = useRef(varde);
vardeRef.current = varde;
const klass =
"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 klass = "w-full border px-2 py-2 text-[13px] focus:outline-none";
const stil = { borderColor: FARG.steel, background: FARG.white, color: FARG.graphite };
return (
<label className="mb-2.5 block">
<span className="mb-1 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>
<label className="mb-4 block">
<span className="mb-2 flex min-h-6 items-center justify-between gap-2">
<Etikett>{label}</Etikett>
{rost && (
<MikrofonKnapp
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
@@ -159,7 +228,14 @@ export function TextFalt({
)}
</span>
{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
type={losenord ? "password" : "text"}
@@ -167,6 +243,7 @@ export function TextFalt({
onChange={(e) => satt(e.target.value)}
placeholder={platshallare}
className={klass}
style={stil}
/>
)}
</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 { nastaSteg } from "@/felsokning/metodik";
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 { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { grinda } from "../../../../services/gemensam/grind.mjs";
@@ -145,7 +145,7 @@ export default function ArendeSida() {
if (!arende || !id) {
return (
<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>
);
}
@@ -175,9 +175,24 @@ export default function ArendeSida() {
tillbaka={{ till: "/felsokning", text: "Cases" }}
hoger={
<div className="text-right">
<p className="text-[13px] font-semibold text-white">{total}</p>
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
{avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]}
{/* Vit text var rätt när verktygsraden var mörkt marinblå. När
raden blev vit försvann tiden helt och statusraden blev en
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>
</div>
}
@@ -189,13 +204,13 @@ export default function ArendeSida() {
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
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) => (
<button
key={f.id}
onClick={() => setFlik(f.id)}
className={`min-h-9 text-[13px] font-semibold transition-colors ${
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
className={`min-h-9 text-[13px] font-semibold ${
flik === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
}`}
>
{f.label}
@@ -203,7 +218,7 @@ export default function ArendeSida() {
))}
</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">
<VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} />
</aside>
@@ -232,8 +247,8 @@ function identitetsRader(rader: [string, string | undefined][]) {
return rader
.filter(([, varde]) => varde)
.map(([etikett, varde]) => (
<div key={etikett} className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
<span className="text-[#4A5560]">{etikett}</span>
<div key={etikett} className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
<span className="text-[#4D5662]">{etikett}</span>
<span className="text-right font-medium">{varde}</span>
</div>
));
@@ -248,12 +263,12 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
const del = (etikett: string, varde?: string) =>
varde ? (
<span className="whitespace-nowrap">
{etikett && <span className="text-[#707070]">{etikett} </span>}
<span className="font-semibold text-[#1A1A1A]">{varde}</span>
{etikett && <span className="text-[#4D5662]">{etikett} </span>}
<span className="font-semibold text-[#1B1E22]">{varde}</span>
</span>
) : null;
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("Claim", idn.claim)}
{del("Claim no.", idn.skadenummer)}
@@ -262,13 +277,13 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
{del("VIN", idn.vin)}
{del("Mileage", idn.miltal)}
{del("Responsible", idn.ansvarig)}
<label className="ml-auto flex items-center gap-1">
<span className="text-[#707070]">Case type</span>
<label className="ml-auto flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">Case type</span>
<select
value={typ}
disabled={idn.avslutat}
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) => (
<option key={t}>{t}</option>
@@ -326,15 +341,15 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
<IkonKamera /> Photograph the instrument panel
</StorKnapp>
)}
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer </p>}
{laser && <p className="py-2 text-center text-[12px] font-semibold text-[#005CA9]">Reading the odometer </p>}
{foto && !laser && (
<div className="mt-2">
{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.
</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" />
<StorKnapp
disabled={!varde.trim()}
@@ -391,24 +406,24 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
if (rader.length === 0) return null;
return (
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mb-2 border border-[#D7DCE2] bg-white p-2">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
Previous cases for {identifierare} review before signing off the history
</p>
{rader.map((rad) => (
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
<div key={rad.id} className="border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
<p>
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
{rad.avslutat ? "closed" : "in progress"}
{rad.felbeskrivning && <span className="text-[#4A5560]"> {rad.felbeskrivning}</span>}
{rad.felbeskrivning && <span className="text-[#4D5662]"> {rad.felbeskrivning}</span>}
</p>
{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(", ")})
</p>
))}
<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={() =>
skicka({
typ: "kommentar",
@@ -439,15 +454,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
return (
<Panel rubrik="Pre-diagnostics — before the work begins">
{rader.map((r) => (
<p key={r.id} className="py-0.5 text-[13px]">
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
{r.varning && <span className="ml-1 text-[11px] font-semibold text-[#9A6700]"><IkonVarning /> {r.varning}</span>}
<p key={r.id} className="py-0 text-[13px]">
<span className={r.klar ? "text-[#005CA9]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
{r.varning && <span className="ml-2 text-[11px] font-semibold text-[#8A5A00]"><IkonVarning /> {r.varning}</span>}
</p>
))}
{!rad("historik").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
<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)
</p>
<TidigareArenden arende={arende} skicka={skicka} />
@@ -484,8 +499,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)}
{!rad("matarstallning_in").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Incoming odometer reading — photograph the instrument panel
</p>
<MatarstallningSteg lage="ingaende" skicka={skicka} />
@@ -493,15 +508,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)}
{!rad("felbeskrivning").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Is the customer's fault description correctly recorded?
</p>
{fb && <p className="mb-2 text-[13px]">{fb}</p>}
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
Correct verified
</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
with the customer's description.
</p>
@@ -509,8 +524,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
)}
{!rad("tidiga_observationer").klar && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
</p>
<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
</StorKnapp>
</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.
</p>
</div>
@@ -548,7 +563,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
: "Rationale (required) — why could the fault not be reproduced?";
return (
<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?
</p>
<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 (underlag.length === 0) return setFel("Link at least one evidence source to the assessment.");
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())
return setFel("An unknown cause requires a reason why the cause could not be established.");
if (sakerhet !== "hog" && !ytterligare.trim())
@@ -636,9 +651,9 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
const h = p.handelse;
if (h.typ !== "felorsak") return null;
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="text-[#4A5560]">
<p className="text-[#4D5662]">
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
</p>
<p>Action: {h.atgard}</p>
@@ -659,42 +674,42 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
rost
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>
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">2. Most probable cause (one or more)</p>
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
{aktivtRegelpaket().orsakskategorier.map((o) => (
<button
key={o}
onClick={() => vaxla(orsaker, setOrsaker, o)}
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
className={`min-h-8 border px-2 text-[12px] font-medium ${
orsaker.includes(o) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`}
>
{o}
</button>
))}
</div>
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Evidence supporting the assessment</p>
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">3. Evidence supporting the assessment</p>
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
{aktivtRegelpaket().underlagskallor.map((k) => (
<button
key={k}
onClick={() => vaxla(underlag, setUnderlag, k)}
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
className={`min-h-8 border px-2 text-[12px] font-medium ${
underlag.includes(k) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`}
>
{k}
</button>
))}
</div>
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Confidence in the assessment</p>
<div className="mb-3 grid grid-cols-3 gap-2">
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">4. Confidence in the assessment</p>
<div className="mb-4 grid grid-cols-3 gap-2">
{(["hog", "medel", "lag"] as const).map((n) => (
<button
key={n}
onClick={() => setSakerhet(n)}
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]}
@@ -768,20 +783,20 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
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 ? (
<>
<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>
);
})}
{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>
</p>
)}
@@ -792,14 +807,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
const h = p.handelse;
if (h.typ !== "atgardsforslag") return null;
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}
{h.uppskattadKostnad && <span className="text-[#4A5560]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
{h.uppskattadKostnad && <span className="text-[#4D5662]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
</p>
);
})}
{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]}
{beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>}
</p>
@@ -826,7 +841,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslagsLage && (
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
<div className="mb-4 border border-[#D7DCE2] bg-white p-2">
<TextFalt
label="Proposed action (shown to the customer in the shared view)"
varde={forslagText}
@@ -860,8 +875,8 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslag.length > 0 && !beslut && (
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
<div className="mb-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#8A5A00]">
Record the customer's decision before the work begins
</p>
<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}
onClick={() => setBeslutsVal(val)}
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]}
@@ -879,14 +894,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
</div>
{beslutsVal && (
<>
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">How was the decision given?</p>
<div className="mb-2 grid grid-cols-3 gap-1">
<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-2">
{KUNDKANALER.map((k) => (
<button
key={k}
onClick={() => setKanal(k)}
className={`min-h-8 border px-1 text-[12px] font-medium ${
kanal === k ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
className={`min-h-8 border px-2 text-[12px] font-medium ${
kanal === k ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
}`}
>
{k}
@@ -967,16 +982,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
{lage === "ingen" && (
<div className="mt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Reason no action was performed (required)
</p>
<div className="mb-2 grid gap-1">
<div className="mb-2 grid gap-2">
{INGEN_ATGARD_ORSAKER.map((val) => (
<button
key={val}
onClick={() => setOrsak(val)}
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}
@@ -985,7 +1000,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
</div>
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
{beslut?.beslut === "avbojt" && (
<p className="mb-2 text-[11px] text-[#707070]">
<p className="mb-2 text-[11px] text-[#4D5662]">
The customer has declined the action via {beslut.kanal} the decision is recorded in the log.
</p>
)}
@@ -1008,12 +1023,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{utford && !kk && (
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
Quality check is the symptom gone after the action?
</p>
{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.
</p>
)}
@@ -1023,7 +1038,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
key={val}
onClick={() => setKkResultat(val)}
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]}
@@ -1080,24 +1095,24 @@ function VyTrad({
const steg = nastaSteg(arende, metodik);
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
return (
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
<nav className="overflow-hidden border border-[#D7DCE2] bg-[#F6F7F8]">
<p className="border-b border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
Case views
</p>
{FLIKAR.map((f) => (
<button
key={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
? "border-[#00437A] bg-[#D6E4F2] font-semibold text-[#00437A]"
: "border-transparent text-[#333333] hover:bg-[#E4E9EE]"
? "border-[#005CA9] bg-[#FFFFFF] font-semibold text-[#005CA9]"
: "border-transparent text-[#1B1E22] hover:bg-[#D7DCE2]"
}`}
>
{f.label}
</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}
</p>
{metodik.steg.map((s, i) => {
@@ -1106,8 +1121,8 @@ function VyTrad({
return (
<p
key={s.id}
className={`px-3 py-1 text-[12px] ${
aktuell ? "bg-[#FFF8E1] font-semibold text-[#9A6700]" : klar ? "text-[#1E6B34]" : "text-[#707070]"
className={`px-4 py-2 text-[12px] ${
aktuell ? "bg-[#FFFFFF] font-semibold text-[#8A5A00]" : klar ? "text-[#005CA9]" : "text-[#4D5662]"
}`}
>
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
@@ -1135,9 +1150,9 @@ function KontextPanel({
const senasteAi = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar");
const rad = (etikett: string, varde: string | undefined) =>
varde ? (
<div className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[12px] last:border-0">
<span className="shrink-0 text-[#4A5560]">{etikett}</span>
<span className="text-right font-medium text-[#1A1A1A]">{varde}</span>
<div className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
<span className="shrink-0 text-[#4D5662]">{etikett}</span>
<span className="text-right font-medium text-[#1B1E22]">{varde}</span>
</div>
) : null;
return (
@@ -1153,7 +1168,7 @@ function KontextPanel({
</Panel>
<Panel rubrik="Reliability">
{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}
</p>
))}
@@ -1161,7 +1176,7 @@ function KontextPanel({
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
<Panel rubrik="Recommended next step">
{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}
</p>
))}
@@ -1170,14 +1185,14 @@ function KontextPanel({
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
<Panel rubrik="Technical recommendation">
{senasteAi.handelse.rader.map((r, i) => (
<p key={i} className="py-0.5 text-[12px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
<p key={i} className="py-0 text-[12px]">
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
</p>
))}
<p className="mt-1 text-[12px]">
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
<p className="mt-2 text-[12px]">
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
</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
</p>
</Panel>
@@ -1202,11 +1217,11 @@ function InaktivitetsBanner({
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
if (minuter < 20) return null;
return (
<div className="mb-4 border border-[#00437A] bg-[#F7F7F7] p-4">
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
<div className="mb-4 border border-[#005CA9] bg-[#F6F7F8] p-4">
<p className="mb-2 text-[14px] font-semibold text-[#005CA9]">
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
</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 />
<StorKnapp
disabled={!text.trim()}
@@ -1286,7 +1301,7 @@ function GuideFlik({
if (avslutat) {
return (
<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.
</p>
</Panel>
@@ -1339,8 +1354,8 @@ function GuideFlik({
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
{steg.klart ? (
<>
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
<p className="mb-4 text-[#333333]">
<p className="mb-4 text-[15px] font-semibold">Every step in the methodology is documented.</p>
<p className="mb-4 text-[#1B1E22]">
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
with recommended next steps.
</p>
@@ -1353,11 +1368,11 @@ function GuideFlik({
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
// hinder, inte en varning. Enda vägen framåt är att ändra
// förutsättningen, inte att klicka förbi.
<div className="border-2 border-[#B42318] bg-[#FEF3F2] p-4">
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Work must not continue</p>
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
<p className="text-[12px] text-[#666666]">
<div className="border-2 border-[#8B1A1A] bg-[#FFFFFF] p-4">
<p className="mb-2 text-[15px] font-semibold text-[#8B1A1A]">Work must not continue</p>
<p className="mb-2 text-[#1B1E22]">{steg.sparr.orsak}</p>
<p className="mb-4 font-semibold text-[#1B1E22]">{steg.sparr.atgard}</p>
<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
jakande.
</p>
@@ -1377,19 +1392,19 @@ function GuideFlik({
: "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" && (
<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.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-[14px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
<p key={i} className="py-0 text-[14px]">
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p>
))}
<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}
</p>
</>
@@ -1447,15 +1462,15 @@ function GuideFlik({
*/
function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) {
return (
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]">
<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-[#8A5A00]">
The case cannot be closed yet
</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) => (
<li key={h.id}>
{h.rubrik}
{h.detalj && <span className="text-[#4A5560]"> {h.detalj}</span>}
{h.detalj && <span className="text-[#4D5662]"> {h.detalj}</span>}
</li>
))}
</ul>
@@ -1469,15 +1484,15 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori;
}
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) => (
<button
key={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
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
}`}
>
{TIDKATEGORI_LABEL[k]}
@@ -1559,7 +1574,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
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>
{krav === "foto" ? (
<>
@@ -1579,7 +1594,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
/>
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
<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
/>
{!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{" "}
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
</p>
@@ -1633,24 +1648,24 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
return (
<button
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
</button>
);
}
return (
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
<div className="mt-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#8A5A00]">
Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
</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) => (
<button
key={val}
onClick={() => setOrsak(val)}
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}
@@ -1794,10 +1809,10 @@ function SnabbDokumentation({
? instRef.current?.click()
: 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
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
}`}
>
+ {d.label}
@@ -1850,8 +1865,8 @@ function SnabbDokumentation({
/>
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
{video && (
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#C6C6C6]" />
<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-[#D7DCE2]" />
<TextFalt
label="What does the video show? (required — what makes the noise or moves)"
varde={video.beskrivning}
@@ -1876,23 +1891,23 @@ function SnabbDokumentation({
</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 && (
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
<div className="mt-4 border border-[#D7DCE2] bg-white p-2">
{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.
</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
</p>
{avlasning.tolkning.varden.map((v, i) => (
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
<span>
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</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)} %`}
</span>
</div>
@@ -1906,7 +1921,7 @@ function SnabbDokumentation({
</div>
)}
{typ && typ !== "foto" && typ !== "instrument" && (
<div className="mt-3">
<div className="mt-4">
{typ === "hypotes" && (
<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.
@@ -1968,25 +1983,25 @@ function OverlamningDialog({
return (
<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">
<h2 className="mb-3 text-[15px] font-semibold">Handover report</h2>
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#D7DCE2] bg-[#F6F7F8] p-4">
<h2 className="mb-4 text-[15px] font-semibold">Handover report</h2>
<pre className="mb-4 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#1B1E22]">
{text}
</pre>
{aiLage === "vilar" && (
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
<StorKnapp variant="sekundar" className="mb-4" onClick={komplettera}>
Extend the analysis: risks &amp; uncertainties
</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" && (
<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 && (
<div className="mb-3 bg-white p-4">
<div className="mb-4 bg-white p-4">
{aiRader.map((rad, i) => (
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
<p key={i} className="py-0 text-[12px] text-[#1B1E22]">
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p>
))}
</div>
@@ -2049,10 +2064,10 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
}
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>}
{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]">
{NIVA_LABEL[delning.niva]}{" "}
{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 && (
<span className="flex gap-2">
<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={() =>
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
}
@@ -2068,7 +2083,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
Copy
</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 () => {
await aterkallaDelning(delning.kod);
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) => (
<button
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 () => {
try {
await skapaDelning(arende.id, niva);
@@ -2100,7 +2115,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
</button>
))}
</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) ·
Internal: full visibility. Filtering happens on the server.
</p>
@@ -2113,16 +2128,16 @@ function LoggFlik({ arende }: { arende: Arende }) {
<Panel rubrik="Work log — append-only, nothing is overwritten">
<ol>
{arende.handelser.map((post) => (
<li key={post.id} className="flex gap-3 border-b border-[#DDDDDD] py-2 last:border-0">
<span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>
<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-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>
<div className="min-w-0">
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
<p className="text-[13px] text-[#1B1E22]">{handelseRubrik(post)}</p>
<p className="text-[11px] text-[#4D5662]">{post.anvandare}</p>
{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" && (
<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>
</li>
@@ -2164,25 +2179,25 @@ function BriefFlik({
<>
{!b.avslutat && (
<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
observations, gaps in the documentation, and conclusions that lack support.
</p>
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence </p>}
{granskning === "arbetar" && <p className="mb-2 font-semibold text-[#005CA9]">Reviewing the evidence </p>}
{granskning === "fel" && (
<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" && (
<div className="mb-2">
{senasteAi.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-[14px]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
<p key={i} className="py-0 text-[14px]">
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p>
))}
<p className="mt-1 text-[14px]">
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
<p className="mt-2 text-[14px]">
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
</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>
)}
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
@@ -2194,63 +2209,63 @@ function BriefFlik({
{b.objekt ? (
<>
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
<p className="font-semibold text-[#00437A]">{b.objekt.identifierare}</p>
{b.objekt.kund && <p className="text-[#333333]">Kund: {b.objekt.kund}</p>}
{b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</p>}
<p className="font-semibold text-[#005CA9]">{b.objekt.identifierare}</p>
{b.objekt.kund && <p className="text-[#1B1E22]">Kund: {b.objekt.kund}</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 rubrik="Customer's description">
<p className="text-[14px]">{b.felbeskrivning ? `${b.felbeskrivning}` : "—"}</p>
</Panel>
<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) =>
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}
</p>
) : (
<p key={i} className="py-0.5 text-[14px]">
<p key={i} className="py-0 text-[14px]">
{k.text}
{k.resultat && <span className="text-[#4A5560]"> {k.resultat}</span>}
{k.resultat && <span className="text-[#4D5662]"> {k.resultat}</span>}
</p>
),
)}
</Panel>
<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) => (
<p key={i} className="py-0.5 text-[14px]"> {o}</p>
<p key={i} className="py-0 text-[14px]"> {o}</p>
))}
</Panel>
{b.hypoteser.length > 0 && (
<Panel rubrik="Hypotheses — require verification">
{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}
</p>
))}
</Panel>
)}
<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) => (
<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>
{!b.avslutat && (
<Panel rubrik="Recommended next step">
{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 rubrik="Reliability">
{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}
</p>
))}
@@ -2333,15 +2348,15 @@ function RapportFlik({
undantag i händelseloggen. */}
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
{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]">
<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 />}
</span>{" "}
{rad.rubrik}
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (recommended)</span>}
{!rad.kravs && <span className="text-[11px] text-[#4D5662]"> (recommended)</span>}
</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>
))}
{!godkand && (
@@ -2352,7 +2367,7 @@ function RapportFlik({
)}
</Panel>
<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
contain details about other customers.
</p>
@@ -2365,7 +2380,7 @@ function RapportFlik({
</StorKnapp>
</div>
<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>
{plattformAktiv() ? (
<DelningsHanterare arende={arende} skicka={skicka} />
@@ -2383,7 +2398,7 @@ function RapportFlik({
</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).
</p>
</Panel>
@@ -2445,19 +2460,19 @@ function RapportFlik({
{repro ? (
<>
<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 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) => {
const h = p.handelse;
if (h.typ !== "felorsak") return null;
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>
{identitetsRader([
["Assessed root cause", h.orsaker.join(", ")],
@@ -2495,7 +2510,7 @@ function RapportFlik({
</Panel>
)}
<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) => {
const h = p.handelse;
if (h.typ !== "atgard_utford") return null;
@@ -2507,7 +2522,7 @@ function RapportFlik({
["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>
);
@@ -2523,15 +2538,15 @@ function RapportFlik({
{fordelning.length > 0 && (
<div className="mt-2">
{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>
)}
</Panel>
<Panel rubrik="Timeline">
{kundposter.map((post) => (
<p key={post.id} className="py-0.5 text-[13px]">
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
<p key={post.id} className="py-0 text-[13px]">
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)}
</p>
))}
@@ -2540,8 +2555,8 @@ function RapportFlik({
<Panel rubrik="Documented video">
{klipp.map((v, i) => (
<figure key={i} className="mb-2">
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
<Klipp bilaga={v.bilaga} className="w-full border border-[#D7DCE2]" />
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
</figure>
))}
</Panel>
@@ -2551,8 +2566,8 @@ function RapportFlik({
<div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => (
<figure key={i}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#D7DCE2]" />
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
</figure>
))}
</div>
@@ -2561,11 +2576,11 @@ function RapportFlik({
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
<Panel rubrik="Recommended next steps">
{(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>
) : 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.
</p>
<Ritningsstampel arende={arende} />
@@ -2632,7 +2647,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
<button
type="button"
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
</button>
@@ -2642,7 +2657,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
return (
<Panel rubrik="Report a problem with the system">
<p className="mb-2 text-[12px] text-[#707070]">
<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.
</p>
<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."
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>}
<div className="mt-2 grid grid-cols-2 gap-2">
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
@@ -2708,11 +2723,11 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
];
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">
{falt.map(([etikett, varde]) => (
<div key={etikett} className="border-b border-r border-[#DDDDDD] px-2 py-2 last:border-r-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4A5560]">{etikett}</div>
<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-[#4D5662]">{etikett}</div>
<div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
</div>
))}
@@ -44,13 +44,13 @@ function PlattformInloggning() {
return (
<>
<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]">
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
{konto.organisation} synchronization and decision support active.
</p>
<button
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
className="whitespace-nowrap border border-[#D7DCE2] px-4 py-2 font-semibold text-[#1B1E22] hover:border-[#4D5662]"
onClick={() => {
loggaUtPlattform();
setKonto(null);
@@ -60,7 +60,7 @@ function PlattformInloggning() {
</button>
</div>
{(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>
</Link>
)}
@@ -79,7 +79,7 @@ function PlattformInloggning() {
<Panel rubrik="Platform account">
{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.
</p>
<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="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">
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
Cancel
@@ -175,23 +175,23 @@ function AnvandarAdmin() {
{lista.map((anv) => (
<div
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]">
<span className={`font-semibold ${anv.aktiv === false ? "text-[#8A8A8A] line-through" : ""}`}>
<span className={`font-semibold ${anv.aktiv === false ? "text-[#4D5662] line-through" : ""}`}>
{anv.namn}
</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 && (
<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>
<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
? "border-[#1E6B34] bg-white text-[#1E6B34]"
: "border-[#6E1414] bg-white text-[#8B1A1A]"
? "border-[#005CA9] bg-white text-[#005CA9]"
: "border-[#8B1A1A] bg-white text-[#8B1A1A]"
}`}
onClick={async () => {
setFel("");
@@ -207,11 +207,11 @@ function AnvandarAdmin() {
</button>
</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.
</p>
<form
className="mt-3"
className="mt-4"
onSubmit={async (e) => {
e.preventDefault();
setFel("");
@@ -229,7 +229,7 @@ function AnvandarAdmin() {
<TextFalt label="Name" varde={namn} satt={setNamn} />
<TextFalt label="E-mail" varde={epost} satt={setEpost} />
<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) => (
<button
key={valbar}
@@ -237,15 +237,15 @@ function AnvandarAdmin() {
onClick={() => setRoll(valbar)}
className={`min-h-9 border text-[12px] font-semibold ${
roll === valbar
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22]"
}`}
>
{ROLL_LABEL[valbar]}
</button>
))}
</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}>
Create user
</StorKnapp>
@@ -271,7 +271,7 @@ export default function Arendelista() {
return (
<FelsokningSkal rubrik="Guided Diagnostics">
<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.
</p>
<form
@@ -304,20 +304,20 @@ export default function Arendelista() {
return (
<FelsokningSkal
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">
<StorKnapp className="mb-4">+ New case</StorKnapp>
</Link>
{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) => (
<button
key={f.id}
onClick={() => setFilter(f.id)}
className={`min-h-9 text-[13px] font-semibold transition-colors ${
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
className={`min-h-9 text-[13px] font-semibold ${
filter === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
}`}
>
{f.label} · {f.antal}
@@ -328,7 +328,7 @@ export default function Arendelista() {
{alla.length === 0 && (
<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.
</p>
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
@@ -338,7 +338,7 @@ export default function Arendelista() {
)}
{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 />}
@@ -350,7 +350,7 @@ export default function Arendelista() {
)}
<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.
</p>
</Panel>
@@ -363,24 +363,24 @@ export default function Arendelista() {
const b = brief(arende, metodikForArende(arende), new Date().toISOString());
return (
<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">
<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}
</span>
<span
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`}
>
{avslutat ? "Closed" : "In progress"}
</span>
</div>
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</p>
{o && <p className="text-[12px] font-semibold text-[#4A5560]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
{fel && <p className="mt-2 text-[14px] text-[#333333]">{fel}</p>}
<p className="mt-2 text-[12px] text-[#707070]">
{sista ? `Senaste aktivitet ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
<p className="mt-2 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</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-[#1B1E22]">{fel}</p>}
<p className="mt-2 text-[12px] text-[#4D5662]">
{sista ? `Last activity ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
{" · "}
{b.totalArbetstid}
</p>
@@ -21,7 +21,7 @@ export default function DelatArende() {
if (!arende) {
return (
<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>
);
}
@@ -41,10 +41,10 @@ function Vallista({
<button
key={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
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
? "border-[#005CA9] bg-[#005CA9] text-white"
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#4D5662]"
}`}
>
{aktiv ? "✓ " : ""}
@@ -85,11 +85,11 @@ function Integrationer() {
return (
<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.
</p>
{!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.
</p>
)}
@@ -97,13 +97,13 @@ function Integrationer() {
{leverantorer.map((lev) => {
const finns = befintliga.find((i) => i.leverantor === lev.id);
return (
<div key={lev.id} className="border-b border-[#EBEBEB] py-2 last:border-0">
<div className="flex items-start justify-between gap-3">
<div key={lev.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<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 && (
<p className="mt-1 text-[12px] text-[#4A5560]">
<p className="mt-2 text-[12px] text-[#4D5662]">
{Object.entries(finns.uppgifter)
.map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`)
.join(" · ")}
@@ -111,13 +111,13 @@ function Integrationer() {
</p>
)}
</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"}
</span>
</div>
{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) => (
<TextFalt
key={falt.nyckel}
@@ -150,7 +150,7 @@ function Integrationer() {
Save details
</StorKnapp>
</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.
</p>
</div>
@@ -188,7 +188,7 @@ function Integrationer() {
</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>
);
}
@@ -210,7 +210,7 @@ export default function Installningar() {
return (
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel>
<p className="text-[14px] text-[#333333]">
<p className="text-[14px] text-[#1B1E22]">
Organization settings are managed by your system administrator.
</p>
</Panel>
@@ -243,7 +243,7 @@ export default function Installningar() {
return (
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel>
<p className="text-[#333333]">
<p className="text-[#1B1E22]">
Choose what is shown when a new case is started.{" "}
{inloggad
? "The setting applies to everyone in the organization."
@@ -269,8 +269,8 @@ export default function Installningar() {
{inloggad && <Integrationer />}
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]"> Saved</p>}
{status === "fel" && <p className="mb-4 font-semibold text-[#8B1A1A]">{felText}</p>}
{status === "sparat" && <p className="mb-4 font-semibold text-[#005CA9]"> Saved</p>}
<StorKnapp disabled={status === "sparar"} onClick={spara}>
{status === "sparar" ? "Saving …" : "Save settings"}
</StorKnapp>
@@ -176,7 +176,7 @@ export default function NyttArende() {
return (
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<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.
</p>
<input
@@ -192,7 +192,7 @@ export default function NyttArende() {
}}
/>
{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>
)}
@@ -215,7 +215,7 @@ export default function NyttArende() {
tillbaka={{ till: "/felsokning", text: "Cases" }}
>
{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.
</p>
)}
@@ -253,7 +253,7 @@ export default function NyttArende() {
</StorKnapp>
))}
</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.
</p>
</Panel>
@@ -272,8 +272,8 @@ export default function NyttArende() {
/>
{skannarKod ? (
<div>
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
<p className="mb-2 text-[12px] text-[#707070]">
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#D7DCE2]" />
<p className="mb-2 text-[12px] text-[#4D5662]">
Point the camera at the QR code, barcode or VIN label.
</p>
<StorKnapp variant="sekundar" onClick={stangSkanning}>
@@ -293,7 +293,7 @@ export default function NyttArende() {
</div>
)}
{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}
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
</p>
@@ -316,11 +316,11 @@ export default function NyttArende() {
return (
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel>
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
<p className="mt-1 text-[15px] font-semibold text-[#00437A]">{objektet.identifierare}</p>
<p className="mt-1 text-[12px] text-[#4A5560]">Identifierad via: {objektet.identifieringsmetod}</p>
{objektet.kund && <p className="mt-1 text-[14px] text-[#333333]">Kund: {objektet.kund}</p>}
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">{objektet.typ}</p>
<p className="mt-2 text-[20px] font-semibold">{objektet.beskrivning}</p>
<p className="mt-2 text-[15px] font-semibold text-[#005CA9]">{objektet.identifierare}</p>
<p className="mt-2 text-[12px] text-[#4D5662]">Identifierad via: {objektet.identifieringsmetod}</p>
{objektet.kund && <p className="mt-2 text-[14px] text-[#1B1E22]">Kund: {objektet.kund}</p>}
</Panel>
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
@@ -346,8 +346,8 @@ export default function NyttArende() {
rost
/>
{metodik && (
<p className="mb-3 text-[12px] font-semibold text-[#4A5560]">
Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span>
<p className="mb-4 text-[12px] font-semibold text-[#4D5662]">
Vald metodik: <span className="text-[#005CA9]">{metodik.namn}</span>
</p>
)}
<StorKnapp
@@ -401,13 +401,13 @@ function GranskaTolkning({
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
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">
<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 && (
<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={{
left: `${vald.omrade.x * 100}%`,
top: `${vald.omrade.y * 100}%`,
@@ -417,7 +417,7 @@ function GranskaTolkning({
/>
)}
</div>
<p className="mt-1 text-[11px] text-[#707070]">
<p className="mt-2 text-[11px] text-[#4D5662]">
{vald?.omrade
? `Markerat: ${vald.etikett} (approximate position)`
: "Select a field to see where in the document the value was found."}
@@ -436,27 +436,27 @@ function GranskaTolkning({
<div
key={rad.id}
onClick={() => setMarkerad(rad.id)}
className={`mb-1.5 cursor-pointer border p-2 ${
markerad === rad.id ? "border-[#00437A]" : "border-transparent"
} ${niva === "gul" ? "bg-[#FFF8E1]" : niva === "rod" ? "bg-[#FBE9E9]" : "bg-white"}`}
className={`mb-2 cursor-pointer border p-2 ${
markerad === rad.id ? "border-[#005CA9]" : "border-transparent"
} ${niva === "gul" ? "bg-[#FFFFFF]" : niva === "rod" ? "bg-[#FFFFFF]" : "bg-white"}`}
>
<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}
</span>
<span
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)} %`}
</span>
</div>
<div className="mt-1 flex items-center gap-2">
<div className="mt-2 flex items-center gap-2">
<input
value={rad.varde}
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 && (
<button
@@ -464,7 +464,7 @@ function GranskaTolkning({
e.stopPropagation();
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
</button>
@@ -45,13 +45,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
);
}
return (
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">New responsible technician</p>
<div className="col-span-2 border border-[#D7DCE2] bg-white p-2">
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4D5662]">New responsible technician</p>
<div className="grid grid-cols-2 gap-2">
{personer.map((person) => (
<button
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 () => {
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
typ: "ansvarig_satt",
@@ -69,7 +69,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
</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)}
>
Cancel
@@ -101,7 +101,7 @@ export default function Oversikt() {
return (
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
<Panel>
<p className="text-[14px] text-[#333333]">
<p className="text-[14px] text-[#1B1E22]">
The overview requires supervisor or administrator rights on the platform.
</p>
</Panel>
@@ -153,30 +153,30 @@ export default function Oversikt() {
<FelsokningSkal
rubrik="Organization overview"
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>}
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Retrieving </p>}
{fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
{!rader && !fel && <p className="text-[14px] text-[#4D5662]">Retrieving </p>}
{rader && (
<>
<Panel rubrik="Statistics">
<div className="grid grid-cols-3 gap-3 text-center">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<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>
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">In progress</p>
<p className="text-[20px] font-semibold text-[#005CA9]">{pagaende.length}</p>
<p className="text-[12px] font-semibold uppercase text-[#4D5662]">In progress</p>
</div>
<div>
<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>
{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>
</p>
)}
@@ -185,41 +185,41 @@ export default function Oversikt() {
{felorsaker.length > 0 && (
<Panel rubrik="Root cause statistics — recurring faults in the organization">
{felorsaker.map((rad) => (
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
<div key={rad.orsak} className="flex items-center gap-2 py-0 text-[13px]">
<span className="w-52 shrink-0 text-[#1B1E22]">{rad.orsak}</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 }}
/>
<span className="font-semibold">{rad.antal}</span>
</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.
</p>
</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) => (
<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">
<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}
</span>
<span
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
}`}
>
{rad.avslutat ? "Closed" : "In progress"}
</span>
</div>
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
{rad.felbeskrivning && <p className="text-[14px] text-[#333333]">{rad.felbeskrivning}</p>}
<p className="mt-1 text-[12px] text-[#707070]">
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
<p className="mt-2 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
{rad.felbeskrivning && <p className="text-[14px] text-[#1B1E22]">{rad.felbeskrivning}</p>}
<p className="mt-2 text-[12px] text-[#4D5662]">
Ansvarig: <span className="font-semibold text-[#1B1E22]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
{` · ${rad.antal_handelser} events`}
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
</p>
@@ -89,7 +89,7 @@ export default function PublikDelning() {
return (
<FelsokningSkal rubrik="Shared case">
<Panel>
<p className="text-[14px] text-[#333333]">
<p className="text-[14px] text-[#1B1E22]">
{status === "laddar"
? "Retrieving the case …"
: "The case is not available. Check the link with the workshop — the share may have been revoked."}