Industriell ETKA-inspirerad design + Personbil/Regnr

Hela felsökningsmodulen omdesignad enligt tysk verkstadstradition
(ETKA/Bosch/Siemens-känsla): plana ljusgrå ytor, skarpa kanter, djup
marinblå primärfärg i stället för amber, tät typografi 11–15 px,
rektangulära knappar (max 4 px radie) och låg verktygsrad (~44 px)
med breadcrumb.

Ärendesidan får klassisk trekolumnslayout på skrivbord:
- Vänster: navigationsträd med ärendevyer + metodikstegens status
- Mitten: arbetsytan (guiden, en fråga i taget)
- Höger: kontextpanel med teknisk information, tillförlitlighet,
  rekommenderat nästa steg och senaste tekniska rekommendation (AI)
Smala skärmar behåller flikrad och en kolumn.

Dessutom: objekttypen "Fordon" heter nu "Personbil" och
"Registreringsnummer" har kortats till "Regnr".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-03 08:43:57 +00:00
parent 63b7720159
commit c3e831b478
16 changed files with 386 additions and 220 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
- **Händelseloggen är enda sanningskällan.** `src/felsokning/domain.ts` definierar händelsetyperna; poster läggs endast till. - **Händelseloggen är enda sanningskällan.** `src/felsokning/domain.ts` definierar händelsetyperna; poster läggs endast till.
- **Alla vyer är projektioner.** `src/felsokning/projektioner.ts` — brief, tidsfördelning, överlämningstext och kundrapport är rena funktioner av loggen och kan alltid regenereras. Testerna i `src/felsokning/__tests__/` låser detta. - **Alla vyer är projektioner.** `src/felsokning/projektioner.ts` — brief, tidsfördelning, överlämningstext och kundrapport är rena funktioner av loggen och kan alltid regenereras. Testerna i `src/felsokning/__tests__/` låser detta.
- **Metodikmotorn är deterministisk.** `src/felsokning/metodik.ts` — nästa steg härleds ur vad som redan dokumenterats. Det är här den framtida AI:n ansluter, utan att logg eller projektioner ändras. - **Metodikmotorn är deterministisk.** `src/felsokning/metodik.ts` — nästa steg härleds ur vad som redan dokumenterats. Det är här den framtida AI:n ansluter, utan att logg eller projektioner ändras.
- **Fabriksverktygs-UI.** `src/felsokning/ui.tsx` — hög kontrast, minst 56 px höga knappar, få val per skärm. - **Industriellt verkstads-UI (ETKA-inspirerat).** `src/felsokning/ui.tsx` — plana ljusgrå ytor (#ECECEC/#F7F7F7), skarpa kanter, djup marinblå som primärfärg, tät typografi (1115 px), rektangulära knappar (max 4 px radie), verktygsrad ~44 px. Ärendesidan har klassisk trekolumnslayout på skrivbord: navigationsträd (vyer + metodikstegens status) till vänster, arbetsyta i mitten, kontextpanel (teknisk information, tillförlitlighet, teknisk rekommendation) till höger; en kolumn med flikrad på smala skärmar.
## Medvetna avgränsningar ## Medvetna avgränsningar
+19 -19
View File
@@ -40,31 +40,31 @@ export function DelatArendeVy({
rubrik={b.objekt?.beskrivning ?? "Ärende"} rubrik={b.objekt?.beskrivning ?? "Ärende"}
hoger={ hoger={
<span <span
className={`rounded px-3 py-1 text-sm font-extrabold uppercase ${ className={`rounded px-3 py-1 text-[12px] font-semibold uppercase ${
avslutat ? "bg-zinc-700 text-zinc-200" : "bg-green-500 text-zinc-950" avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
}`} }`}
> >
{avslutat ? "Avslutat" : "🟢 Felsökning pågår"} {avslutat ? "Avslutat" : "🟢 Felsökning pågår"}
</span> </span>
} }
> >
<p className="mb-4 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-3 text-sm text-zinc-400 print:hidden"> <p className="mb-4 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
{notis} {notis}
</p> </p>
{b.felbeskrivning && ( {b.felbeskrivning && (
<Panel rubrik="Kundens felbeskrivning"> <Panel rubrik="Kundens felbeskrivning">
<p className="text-lg">{b.felbeskrivning}</p> <p className="text-[14px]">{b.felbeskrivning}</p>
</Panel> </Panel>
)} )}
<Panel rubrik="Aktuell status"> <Panel rubrik="Aktuell status">
{b.utfordaKontroller.map((k, i) => ( {b.utfordaKontroller.map((k, i) => (
<p key={`u${i}`} className="py-0.5 text-lg"> {k.text}</p> <p key={`u${i}`} className="py-0.5 text-[14px]"> {k.text}</p>
))} ))}
{!avslutat && pagaende && <p className="py-0.5 text-lg text-amber-400">🔄 {pagaende}</p>} {!avslutat && pagaende && <p className="py-0.5 text-[14px] text-[#00437A]">🔄 {pagaende}</p>}
{b.ejKontrollerat.slice(avslutat ? 0 : 1).map((e, i) => ( {b.ejKontrollerat.slice(avslutat ? 0 : 1).map((e, i) => (
<p key={`e${i}`} className="py-0.5 text-lg text-zinc-400"> {e}</p> <p key={`e${i}`} className="py-0.5 text-[14px] text-[#4A5560]"> {e}</p>
))} ))}
</Panel> </Panel>
@@ -73,8 +73,8 @@ export function DelatArendeVy({
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => ( {bilder.map((bild, i) => (
<figure key={i}> <figure key={i}>
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-zinc-700" /> <img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
<figcaption className="mt-1 text-xs text-zinc-400">{bild.beskrivning}</figcaption> <figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</div> </div>
@@ -83,15 +83,15 @@ export function DelatArendeVy({
{matvarden.length > 0 && ( {matvarden.length > 0 && (
<Panel rubrik="Mätvärden"> <Panel rubrik="Mätvärden">
<table className="w-full text-lg"> <table className="w-full text-[14px]">
<tbody> <tbody>
{matvarden.map((p) => { {matvarden.map((p) => {
const h = p.handelse; const h = p.handelse;
if (h.typ !== "matvarde") return null; if (h.typ !== "matvarde") return null;
return ( return (
<tr key={p.id} className="border-b border-zinc-800 last:border-0"> <tr key={p.id} className="border-b border-[#DDDDDD] last:border-0">
<td className="py-1 pr-3 text-zinc-300">{h.beskrivning}</td> <td className="py-1 pr-3 text-[#333333]">{h.beskrivning}</td>
<td className="py-1 font-bold"> <td className="py-1 font-semibold">
{h.varde} {h.varde}
{h.enhet ? ` ${h.enhet}` : ""} {h.enhet ? ` ${h.enhet}` : ""}
</td> </td>
@@ -105,8 +105,8 @@ export function DelatArendeVy({
<Panel rubrik="Tidslinje"> <Panel rubrik="Tidslinje">
{kundposter.map((post) => ( {kundposter.map((post) => (
<p key={post.id} className="py-0.5 text-base"> <p key={post.id} className="py-0.5 text-[13px]">
<span className="font-mono font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span>{" "} <span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)} {handelseRubrik(post)}
</p> </p>
))} ))}
@@ -114,20 +114,20 @@ export function DelatArendeVy({
{!avslutat && pagaende && ( {!avslutat && pagaende && (
<Panel rubrik="Rekommenderat nästa steg"> <Panel rubrik="Rekommenderat nästa steg">
<p className="text-lg">{pagaende}</p> <p className="text-[14px]">{pagaende}</p>
</Panel> </Panel>
)} )}
<Panel rubrik="Arbetstid"> <Panel rubrik="Arbetstid">
<p className="text-2xl font-extrabold">{b.totalArbetstid}</p> <p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
{tidsfordelningsRader(arende, nu).map((r) => ( {tidsfordelningsRader(arende, nu).map((r) => (
<p key={r.label} className="text-zinc-300"> <p key={r.label} className="text-[#333333]">
{r.label}: {r.tid} {r.label}: {r.tid}
</p> </p>
))} ))}
</Panel> </Panel>
<p className="text-center text-xs text-zinc-600"> <p className="text-center text-[11px] text-[#8A8A8A]">
Ärende #{arende.nummer} · startat {tidDatum(arende.skapad)} · genererad ur ärendets händelselogg Ärende #{arende.nummer} · startat {tidDatum(arende.skapad)} · genererad ur ärendets händelselogg
</p> </p>
</FelsokningSkal> </FelsokningSkal>
+4 -4
View File
@@ -47,16 +47,16 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
type="button" type="button"
onClick={() => (lyssnar ? stoppa() : starta())} onClick={() => (lyssnar ? stoppa() : starta())}
aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"} aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
className={`min-h-11 min-w-11 rounded-full border-2 px-3 text-lg font-bold transition-colors ${ className={`min-h-8 min-w-11 rounded border px-3 text-[14px] font-semibold transition-colors ${
lyssnar lyssnar
? "animate-pulse border-red-500 bg-red-600 text-white" ? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
: "border-zinc-600 bg-zinc-800 text-zinc-100 hover:border-amber-400" : "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
}`} }`}
> >
{lyssnar ? "■" : "🎤"} {lyssnar ? "■" : "🎤"}
</button> </button>
{lyssnar && ( {lyssnar && (
<span className="text-sm font-bold text-red-400"> <span className="text-[12px] font-semibold text-[#8B1A1A]">
Inspelning pågår{interim ? ` — ”${interim}` : "…"} Inspelning pågår{interim ? ` — ”${interim}` : "…"}
</span> </span>
)} )}
@@ -17,10 +17,10 @@ describe("organisationsinställningar", () => {
it("okända värden filtreras bort och ordningen följer standardlistan", () => { it("okända värden filtreras bort och ordningen följer standardlistan", () => {
const inst = normalisera({ const inst = normalisera({
objekttyper: ["Hydraulik", "Påhittad typ", "Fordon"], objekttyper: ["Hydraulik", "Påhittad typ", "Personbil"],
identifieringsmetoder: ["VIN"], identifieringsmetoder: ["VIN"],
}); });
expect(inst.objekttyper).toEqual(["Fordon", "Hydraulik"]); expect(inst.objekttyper).toEqual(["Personbil", "Hydraulik"]);
expect(inst.identifieringsmetoder).toEqual(["VIN"]); expect(inst.identifieringsmetoder).toEqual(["VIN"]);
}); });
@@ -18,7 +18,7 @@ function byggArende(poster: [string, Handelse][]): Arende {
const OBJEKT: Handelse = { const OBJEKT: Handelse = {
typ: "objekt_identifierat", typ: "objekt_identifierat",
objekt: { objekt: {
typ: "Fordon", typ: "Personbil",
identifierare: "ABC123", identifierare: "ABC123",
identifieringsmetod: "Registreringsnummer", identifieringsmetod: "Registreringsnummer",
beskrivning: "Volvo XC60 D4 2019", beskrivning: "Volvo XC60 D4 2019",
+2 -2
View File
@@ -57,9 +57,9 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
post(0, { post(0, {
typ: "objekt_identifierat", typ: "objekt_identifierat",
objekt: { objekt: {
typ: "Fordon", typ: "Personbil",
identifierare: "ABC123", identifierare: "ABC123",
identifieringsmetod: "Registreringsnummer", identifieringsmetod: "Regnr",
beskrivning: "Volvo XC60 D4 2019", beskrivning: "Volvo XC60 D4 2019",
kund: "Anders Svensson", kund: "Anders Svensson",
}, },
+1 -1
View File
@@ -32,7 +32,7 @@ export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
export interface Objekt { export interface Objekt {
typ: string; // t.ex. "Fordon", "Industrimaskin" typ: string; // t.ex. "Fordon", "Industrimaskin"
identifierare: string; // reg.nr, VIN, serienummer … identifierare: string; // reg.nr, VIN, serienummer …
identifieringsmetod: string; // "Registreringsnummer", "VIN", "Serienummer", "Manuell inmatning" identifieringsmetod: string; // "Regnr", "VIN", "Serienummer", "Manuell inmatning"
beskrivning: string; // t.ex. "Volvo XC60 D4 2019" beskrivning: string; // t.ex. "Volvo XC60 D4 2019"
kund?: string; kund?: string;
} }
+2 -2
View File
@@ -7,7 +7,7 @@
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform"; import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
export const ALLA_OBJEKTTYPER = [ export const ALLA_OBJEKTTYPER = [
"Fordon", "Personbil",
"Lastbil/Buss", "Lastbil/Buss",
"Entreprenadmaskin", "Entreprenadmaskin",
"Industrimaskin", "Industrimaskin",
@@ -17,7 +17,7 @@ export const ALLA_OBJEKTTYPER = [
]; ];
export const ALLA_IDENTIFIERINGSMETODER = [ export const ALLA_IDENTIFIERINGSMETODER = [
"Registreringsnummer", "Regnr",
"VIN", "VIN",
"Serienummer", "Serienummer",
"Maskinnummer", "Maskinnummer",
+50 -22
View File
@@ -1,6 +1,9 @@
// Delade UI-komponenter för Guidad Felsökning. // Delade UI-komponenter för Guidad Felsökning.
// Designfilosofi: professionellt fabriksverktyg — hög kontrast, stora // Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition —
// träffytor (handskvänligt), få val per skärm, inga distraktioner. // 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.
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useRef, type ReactNode } from "react"; import { useRef, type ReactNode } from "react";
@@ -8,33 +11,54 @@ import type { Tillforlitlighet } from "./domain";
import { TILLFORLITLIGHET_LABEL } from "./domain"; import { TILLFORLITLIGHET_LABEL } from "./domain";
import { MikrofonKnapp } from "./Mikrofon"; import { MikrofonKnapp } from "./Mikrofon";
// 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";
export function FelsokningSkal({ export function FelsokningSkal({
rubrik, rubrik,
tillbaka, tillbaka,
hoger, hoger,
bred,
children, children,
}: { }: {
rubrik: string; rubrik: string;
tillbaka?: { till: string; text: string }; tillbaka?: { till: string; text: string };
hoger?: ReactNode; hoger?: ReactNode;
bred?: boolean;
children: ReactNode; children: ReactNode;
}) { }) {
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
return ( return (
<div className="felsokning-print min-h-screen bg-zinc-950 text-zinc-50"> <div
<header className="sticky top-0 z-20 border-b-2 border-zinc-700 bg-zinc-900 px-4 py-3 print:static"> className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]"
<div className="mx-auto flex max-w-3xl items-center justify-between gap-3"> style={{ fontFamily: TYPSNITT }}
<div className="min-w-0"> >
{/* 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`}>
<div className="flex min-w-0 items-center gap-2">
{tillbaka && ( {tillbaka && (
<Link to={tillbaka.till} className="text-sm font-semibold text-amber-400 hover:underline"> <>
{tillbaka.text} <Link
</Link> to={tillbaka.till}
className="whitespace-nowrap text-[12px] font-medium text-[#A9C3DE] hover:text-white hover:underline"
>
{tillbaka.text}
</Link>
<span className="text-[#5B7CA0]">/</span>
</>
)} )}
<h1 className="truncate text-xl font-extrabold tracking-tight">{rubrik}</h1> <h1 className="truncate text-[14px] font-semibold tracking-tight">{rubrik}</h1>
</div> </div>
{hoger} {hoger}
</div> </div>
</header> </header>
<main className="mx-auto max-w-3xl px-4 py-5 pb-24">{children}</main> <main className={`mx-auto ${maxBredd} px-3 py-4 pb-20`}>{children}</main>
</div> </div>
); );
} }
@@ -56,16 +80,16 @@ export function StorKnapp({
}) { }) {
const farg = const farg =
variant === "primar" variant === "primar"
? "bg-amber-400 text-zinc-950 hover:bg-amber-300 active:bg-amber-500" ? "border border-[#003561] bg-[#00437A] text-white hover:bg-[#005A9E] active:bg-[#003561]"
: variant === "fara" : variant === "fara"
? "bg-red-600 text-white hover:bg-red-500 active:bg-red-700" ? "border border-[#6E1414] bg-[#8B1A1A] text-white hover:bg-[#A32020] active:bg-[#6E1414]"
: "bg-zinc-800 text-zinc-50 border-2 border-zinc-600 hover:bg-zinc-700 active:bg-zinc-600"; : "border border-[#ADADAD] bg-white text-[#1A1A1A] hover:bg-[#EDF2F7] hover:border-[#00437A] active:bg-[#D6E4F2]";
return ( return (
<button <button
type={type} type={type}
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={`min-h-14 w-full rounded-lg px-5 py-3 text-lg font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`} className={`min-h-10 w-full rounded px-3 py-1.5 text-[13px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`}
> >
{children} {children}
</button> </button>
@@ -74,15 +98,19 @@ export function StorKnapp({
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) { export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
return ( return (
<section className="mb-4 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-4"> <section className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
{rubrik && <h2 className="mb-3 text-sm font-extrabold uppercase tracking-widest text-zinc-400">{rubrik}</h2>} {rubrik && (
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
{rubrik}
</h2>
)}
{children} {children}
</section> </section>
); );
} }
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) { export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
return <span className="whitespace-nowrap text-sm font-bold">{TILLFORLITLIGHET_LABEL[niva]}</span>; return <span className="whitespace-nowrap text-[12px] font-semibold">{TILLFORLITLIGHET_LABEL[niva]}</span>;
} }
export function TextFalt({ export function TextFalt({
@@ -107,11 +135,11 @@ export function TextFalt({
const vardeRef = useRef(varde); const vardeRef = useRef(varde);
vardeRef.current = varde; vardeRef.current = varde;
const klass = const klass =
"w-full rounded-lg border-2 border-zinc-600 bg-zinc-950 px-4 py-3 text-lg text-zinc-50 placeholder-zinc-500 focus:border-amber-400 focus:outline-none"; "w-full rounded 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]";
return ( return (
<label className="mb-3 block"> <label className="mb-2.5 block">
<span className="mb-1 flex min-h-11 items-center justify-between gap-2"> <span className="mb-1 flex min-h-6 items-center justify-between gap-2">
<span className="text-sm font-bold uppercase tracking-wide text-zinc-400">{label}</span> <span className="text-left text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">{label}</span>
{rost && ( {rost && (
<MikrofonKnapp <MikrofonKnapp
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)} paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
+234 -96
View File
@@ -94,7 +94,7 @@ export default function ArendeSida() {
if (!arende || !id) { if (!arende || !id) {
return ( return (
<FelsokningSkal rubrik="Ärendet hittades inte" tillbaka={{ till: "/felsokning", text: "Ärenden" }}> <FelsokningSkal rubrik="Ärendet hittades inte" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
<p className="text-lg text-zinc-300">Ärendet finns inte den här enheten.</p> <p className="text-[14px] text-[#333333]">Ärendet finns inte den här enheten.</p>
</FelsokningSkal> </FelsokningSkal>
); );
} }
@@ -106,24 +106,28 @@ export default function ArendeSida() {
return ( return (
<FelsokningSkal <FelsokningSkal
bred
rubrik={`#${arende.nummer} ${o?.beskrivning ?? ""}`} rubrik={`#${arende.nummer} ${o?.beskrivning ?? ""}`}
tillbaka={{ till: "/felsokning", text: "Ärenden" }} tillbaka={{ till: "/felsokning", text: "Ärenden" }}
hoger={ hoger={
<div className="text-right"> <div className="text-right">
<p className="text-lg font-extrabold text-amber-400">{total}</p> <p className="text-[13px] font-semibold text-white">{total}</p>
<p className="text-xs font-bold uppercase text-zinc-500"> <p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
{avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]} {avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]}
</p> </p>
</div> </div>
} }
> >
<nav className="mb-4 grid grid-cols-4 gap-1 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-1"> {/* 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 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
{FLIKAR.map((f) => ( {FLIKAR.map((f) => (
<button <button
key={f.id} key={f.id}
onClick={() => setFlik(f.id)} onClick={() => setFlik(f.id)}
className={`min-h-12 rounded-md text-base font-extrabold transition-colors ${ className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
flik === f.id ? "bg-amber-400 text-zinc-950" : "text-zinc-300 hover:bg-zinc-800" flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`} }`}
> >
{f.label} {f.label}
@@ -131,18 +135,152 @@ export default function ArendeSida() {
))} ))}
</nav> </nav>
{!avslutat && <InaktivitetsBanner arende={arende} nu={nu} skicka={skicka} />} <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]">
<aside className="sticky top-14 hidden lg:block print:hidden">
<VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} />
</aside>
{flik === "guide" && ( <div className="min-w-0">
<GuideFlik arende={arende} metodik={metodik} avslutat={avslutat} skicka={skicka} nu={nu} /> {!avslutat && <InaktivitetsBanner arende={arende} nu={nu} skicka={skicka} />}
)}
{flik === "logg" && <LoggFlik arende={arende} />} {flik === "guide" && (
{flik === "brief" && <BriefFlik arende={arende} metodik={metodik} nu={nu} skicka={skicka} />} <GuideFlik arende={arende} metodik={metodik} avslutat={avslutat} skicka={skicka} nu={nu} />
{flik === "rapport" && <RapportFlik arende={arende} metodik={metodik} nu={nu} skicka={skicka} />} )}
{flik === "logg" && <LoggFlik arende={arende} />}
{flik === "brief" && <BriefFlik arende={arende} metodik={metodik} nu={nu} skicka={skicka} />}
{flik === "rapport" && <RapportFlik arende={arende} metodik={metodik} nu={nu} skicka={skicka} />}
</div>
<aside className="sticky top-14 hidden xl:block print:hidden">
<KontextPanel arende={arende} metodik={metodik} nu={nu} synkStatus={synkStatus} />
</aside>
</div>
</FelsokningSkal> </FelsokningSkal>
); );
} }
// Navigationsträd (vänsterkolumnen): ärendets vyer plus metodikens steg
// med status — som en mappstruktur i ett klassiskt verkstadssystem.
function VyTrad({
flik,
sattFlik,
arende,
metodik,
}: {
flik: Flik;
sattFlik: (f: Flik) => void;
arende: Arende;
metodik: Metodik;
}) {
const steg = nastaSteg(arende, metodik);
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
return (
<nav className="overflow-hidden rounded border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
Ärendevyer
</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 ${
flik === f.id
? "border-[#00437A] bg-[#D6E4F2] font-semibold text-[#00437A]"
: "border-transparent text-[#333333] hover:bg-[#E4E9EE]"
}`}
>
{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]">
{metodik.namn}
</p>
{metodik.steg.map((s, i) => {
const klar = steg.klart || i < aktuellIndex;
const aktuell = !steg.klart && i === aktuellIndex;
return (
<p
key={s.id}
className={`px-3 py-1 text-[12px] ${
aktuell ? "bg-[#FFF8E1] font-semibold text-[#9A6700]" : klar ? "text-[#1E6B34]" : "text-[#707070]"
}`}
>
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
</p>
);
})}
</nav>
);
}
// Kontextpanel (högerkolumnen): teknisk information, tillförlitlighet och
// senaste tekniska rekommendation — alltid synlig på breda skärmar.
function KontextPanel({
arende,
metodik,
nu,
synkStatus,
}: {
arende: Arende;
metodik: Metodik;
nu: string;
synkStatus: SynkStatus;
}) {
const b = brief(arende, metodik, nu);
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>
) : null;
return (
<div>
<Panel rubrik="Teknisk information">
{rad("Objekt", b.objekt?.beskrivning)}
{rad("Ident", b.objekt?.identifierare)}
{rad("Kund", b.objekt?.kund)}
{rad("Ansvarig", b.ansvarig)}
{rad("Status", b.avslutat ? "Avslutat" : "Pågår")}
{rad("Synk", SYNKSTATUS_LABEL[synkStatus])}
{rad("Arbetstid", b.totalArbetstid)}
</Panel>
<Panel rubrik="Tillförlitlighet">
{b.tillforlitlighet.map((r, i) => (
<p key={i} className="py-0.5 text-[12px]">
<NivaBadge niva={r.niva} /> {r.text}
</p>
))}
</Panel>
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
<Panel rubrik="Rekommenderat nästa steg">
{b.rekommenderatNastaSteg.map((s, i) => (
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
{i + 1}. {s}
</p>
))}
</Panel>
)}
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
<Panel rubrik="Teknisk rekommendation">
{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>
))}
<p className="mt-1 text-[12px]">
<span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
</p>
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
AI-underlag · presenteras aldrig som konstaterat fel
</p>
</Panel>
)}
</div>
);
}
// Vid längre inaktivitet ber systemet om sammanhang — svaret blir en loggpost. // Vid längre inaktivitet ber systemet om sammanhang — svaret blir en loggpost.
function InaktivitetsBanner({ function InaktivitetsBanner({
arende, arende,
@@ -159,11 +297,11 @@ function InaktivitetsBanner({
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000); const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
if (minuter < 20) return null; if (minuter < 20) return null;
return ( return (
<div className="mb-4 rounded-lg border-2 border-amber-400 bg-zinc-900 p-4"> <div className="mb-4 rounded border border-[#00437A] bg-[#F7F7F7] p-4">
<p className="mb-2 text-lg font-bold text-amber-400"> <p className="mb-2 text-[14px] font-semibold text-[#00437A]">
Ingen aktivitet har registrerats de senaste {minuter} minuterna. Ingen aktivitet har registrerats de senaste {minuter} minuterna.
</p> </p>
<p className="mb-3 text-zinc-300">Beskriv kort vad som gjorts under denna period.</p> <p className="mb-3 text-[#333333]">Beskriv kort vad som gjorts under denna period.</p>
<TextFalt label="Vad har gjorts?" varde={text} satt={setText} platshallare="T.ex. Demonterade instrumentpanelen för att komma åt kabelstammen." rost /> <TextFalt label="Vad har gjorts?" varde={text} satt={setText} platshallare="T.ex. Demonterade instrumentpanelen för att komma åt kabelstammen." rost />
<StorKnapp <StorKnapp
disabled={!text.trim()} disabled={!text.trim()}
@@ -216,7 +354,7 @@ function GuideFlik({
if (avslutat) { if (avslutat) {
return ( return (
<Panel rubrik="Felsökningen är avslutad"> <Panel rubrik="Felsökningen är avslutad">
<p className="text-lg text-zinc-300"> <p className="text-[14px] text-[#333333]">
Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export. Ärendet är låst för nya guidesteg. Loggen, briefen och rapporten finns kvar för spårbarhet och export.
</p> </p>
</Panel> </Panel>
@@ -229,8 +367,8 @@ function GuideFlik({
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}> <Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
{steg.klart ? ( {steg.klart ? (
<> <>
<p className="mb-3 text-xl font-bold">Samtliga steg i metodiken är dokumenterade.</p> <p className="mb-3 text-[15px] font-semibold">Samtliga steg i metodiken är dokumenterade.</p>
<p className="mb-4 text-zinc-300"> <p className="mb-4 text-[#333333]">
Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet
med rekommenderade nästa steg. med rekommenderade nästa steg.
</p> </p>
@@ -253,19 +391,19 @@ function GuideFlik({
: "AI-handledning" : "AI-handledning"
} }
> >
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-bold text-amber-400">AI analyserar </p>} {aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">AI analyserar </p>}
{aiStatus === "fel" && ( {aiStatus === "fel" && (
<p className="mb-2 font-bold text-red-400">AI-svaret kunde inte hämtas försök igen. Metodiken fortsätter som vanligt.</p> <p className="mb-2 font-semibold text-[#8B1A1A]">AI-svaret kunde inte hämtas försök igen. Metodiken fortsätter som vanligt.</p>
)} )}
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && ( {senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
<> <>
{senasteAiSvar.handelse.rader.map((rad, i) => ( {senasteAiSvar.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-lg"> <p key={i} className="py-0.5 text-[14px]">
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
<p className="mt-2 text-lg"> <p className="mt-2 text-[14px]">
<span className="font-extrabold text-amber-400">Nästa steg:</span>{" "} <span className="font-semibold text-[#00437A]">Nästa steg:</span>{" "}
{senasteAiSvar.handelse.nastaSteg} {senasteAiSvar.handelse.nastaSteg}
</p> </p>
</> </>
@@ -302,10 +440,10 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
<button <button
key={k} key={k}
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })} onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
className={`whitespace-nowrap rounded-full border-2 px-4 py-2 text-sm font-extrabold transition-colors ${ className={`whitespace-nowrap rounded border px-4 py-2 text-[12px] font-semibold transition-colors ${
k === aktiv k === aktiv
? "border-amber-400 bg-amber-400 text-zinc-950" ? "border-[#00437A] bg-[#00437A] text-white"
: "border-zinc-600 bg-zinc-900 text-zinc-300 hover:border-zinc-400" : "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
}`} }`}
> >
{TIDKATEGORI_LABEL[k]} {TIDKATEGORI_LABEL[k]}
@@ -324,7 +462,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
return ( return (
<> <>
<p className="mb-4 text-2xl font-extrabold leading-snug">{fraga.text}</p> <p className="mb-4 text-[17px] font-semibold leading-snug">{fraga.text}</p>
{fraga.svarstyp === "janej" && ( {fraga.svarstyp === "janej" && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<StorKnapp onClick={() => svara("Ja")}>Ja</StorKnapp> <StorKnapp onClick={() => svara("Ja")}>Ja</StorKnapp>
@@ -374,8 +512,8 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
return ( return (
<> <>
{steg.steg.beskrivning && <p className="mb-2 text-zinc-400">{steg.steg.beskrivning}</p>} {steg.steg.beskrivning && <p className="mb-2 text-[#4A5560]">{steg.steg.beskrivning}</p>}
<p className="mb-4 text-2xl font-extrabold leading-snug">{kontroll.text}</p> <p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p>
{krav === "foto" ? ( {krav === "foto" ? (
<> <>
<input <input
@@ -394,7 +532,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
/> />
<TextFalt label="Observation (valfritt)" varde={resultat} satt={setResultat} rost /> <TextFalt label="Observation (valfritt)" varde={resultat} satt={setResultat} rost />
<StorKnapp onClick={() => filRef.current?.click()}>📷 Ta foto verifierar kontrollen</StorKnapp> <StorKnapp onClick={() => filRef.current?.click()}>📷 Ta foto verifierar kontrollen</StorKnapp>
<p className="mt-2 text-sm text-zinc-500">Denna kontroll verifieras med foto.</p> <p className="mt-2 text-[12px] text-[#707070]">Denna kontroll verifieras med foto.</p>
</> </>
) : ( ) : (
<> <>
@@ -412,7 +550,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
rost rost
/> />
{!kravUppfyllt && ( {!kravUppfyllt && (
<p className="mb-3 text-sm font-bold text-amber-400"> <p className="mb-3 text-[12px] font-semibold text-[#00437A]">
Ingen {krav === "matvarde" ? "mätning" : "observation"} har registrerats lägg till en kort{" "} Ingen {krav === "matvarde" ? "mätning" : "observation"} har registrerats lägg till en kort{" "}
{krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras. {krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras.
</p> </p>
@@ -484,10 +622,10 @@ function SnabbDokumentation({
<button <button
key={d.id} key={d.id}
onClick={() => (d.id === "foto" ? filRef.current?.click() : setTyp(typ === d.id ? null : d.id))} onClick={() => (d.id === "foto" ? filRef.current?.click() : setTyp(typ === d.id ? null : d.id))}
className={`min-h-12 rounded-lg border-2 text-sm font-extrabold transition-colors ${ className={`min-h-9 rounded border text-[12px] font-semibold transition-colors ${
typ === d.id typ === d.id
? "border-amber-400 bg-amber-400 text-zinc-950" ? "border-[#00437A] bg-[#00437A] text-white"
: "border-zinc-600 bg-zinc-900 text-zinc-200 hover:border-zinc-400" : "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
}`} }`}
> >
+ {d.label} + {d.label}
@@ -511,7 +649,7 @@ function SnabbDokumentation({
{typ && typ !== "foto" && ( {typ && typ !== "foto" && (
<div className="mt-3"> <div className="mt-3">
{typ === "hypotes" && ( {typ === "hypotes" && (
<p className="mb-2 text-sm font-bold text-red-400"> <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
🔴 En hypotes är en möjlig felorsak som kräver verifiering den loggas aldrig som ett konstaterat fel. 🔴 En hypotes är en möjlig felorsak som kräver verifiering den loggas aldrig som ett konstaterat fel.
</p> </p>
)} )}
@@ -571,9 +709,9 @@ function OverlamningDialog({
return ( return (
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center"> <div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
<div className="max-h-full w-full max-w-2xl overflow-y-auto rounded-lg border-2 border-zinc-600 bg-zinc-900 p-4"> <div className="max-h-full w-full max-w-2xl overflow-y-auto rounded border border-[#ADADAD] bg-[#F7F7F7] p-4">
<h2 className="mb-3 text-xl font-extrabold">Överlämningsrapport</h2> <h2 className="mb-3 text-[15px] font-semibold">Överlämningsrapport</h2>
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-lg bg-zinc-950 p-4 text-sm text-zinc-200"> <pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded bg-white p-4 text-[12px] text-[#333333]">
{text} {text}
</pre> </pre>
{aiLage === "vilar" && ( {aiLage === "vilar" && (
@@ -581,15 +719,15 @@ function OverlamningDialog({
AI-komplettera: risker &amp; osäkerheter AI-komplettera: risker &amp; osäkerheter
</StorKnapp> </StorKnapp>
)} )}
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-bold text-amber-400">AI sammanfattar </p>} {aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">AI sammanfattar </p>}
{aiLage === "fel" && ( {aiLage === "fel" && (
<p className="mb-3 font-bold text-red-400">Kunde inte hämtas kräver inloggning. Överlämningen fungerar ändå.</p> <p className="mb-3 font-semibold text-[#8B1A1A]">Kunde inte hämtas kräver inloggning. Överlämningen fungerar ändå.</p>
)} )}
{aiLage === "klar" && aiRader.length > 0 && ( {aiLage === "klar" && aiRader.length > 0 && (
<div className="mb-3 rounded-lg bg-zinc-950 p-4"> <div className="mb-3 rounded bg-white p-4">
{aiRader.map((rad, i) => ( {aiRader.map((rad, i) => (
<p key={i} className="py-0.5 text-sm text-zinc-200"> <p key={i} className="py-0.5 text-[12px] text-[#333333]">
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
</div> </div>
@@ -652,18 +790,18 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
} }
return ( return (
<div className="mt-3 rounded-lg border-2 border-zinc-700 bg-zinc-950 p-3"> <div className="mt-3 rounded border border-[#C6C6C6] bg-white p-3">
{fel && <p className="mb-2 font-bold text-red-400">{fel}</p>} {fel && <p className="mb-2 font-semibold text-[#8B1A1A]">{fel}</p>}
{delningar.map((delning) => ( {delningar.map((delning) => (
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-zinc-800 py-2 last:border-0"> <div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#DDDDDD] py-2 last:border-0">
<span className="text-lg"> <span className="text-[14px]">
{NIVA_LABEL[delning.niva]}{" "} {NIVA_LABEL[delning.niva]}{" "}
{delning.aterkallad && <span className="text-sm font-bold text-red-400">(återkallad)</span>} {delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(återkallad)</span>}
</span> </span>
{!delning.aterkallad && ( {!delning.aterkallad && (
<span className="flex gap-2"> <span className="flex gap-2">
<button <button
className="rounded border-2 border-zinc-600 px-3 py-1 font-bold text-zinc-200 hover:border-amber-400" className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]"
onClick={() => onClick={() =>
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`) navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
} }
@@ -671,7 +809,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
Kopiera Kopiera
</button> </button>
<button <button
className="rounded border-2 border-zinc-600 px-3 py-1 font-bold text-red-400 hover:border-red-400" className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
onClick={async () => { onClick={async () => {
await aterkallaDelning(delning.kod); await aterkallaDelning(delning.kod);
skicka({ typ: "kommentar", text: `Delningslänk (${NIVA_LABEL[delning.niva]}) återkallad.` }); skicka({ typ: "kommentar", text: `Delningslänk (${NIVA_LABEL[delning.niva]}) återkallad.` });
@@ -688,7 +826,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => ( {(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => (
<button <button
key={niva} key={niva}
className="min-h-12 rounded-lg border-2 border-zinc-600 text-sm font-extrabold text-zinc-200 hover:border-amber-400" className="min-h-9 rounded border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => { onClick={async () => {
try { try {
await skapaDelning(arende.id, niva); await skapaDelning(arende.id, niva);
@@ -703,7 +841,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
</button> </button>
))} ))}
</div> </div>
<p className="mt-2 text-xs text-zinc-500"> <p className="mt-2 text-[11px] text-[#707070]">
Kund: det kunddelbara · Extern partner: även hypoteser (märkta ej verifierade) · Intern: full insyn. Kund: det kunddelbara · Extern partner: även hypoteser (märkta ej verifierade) · Intern: full insyn.
Filtreringen sker servern. Filtreringen sker servern.
</p> </p>
@@ -716,13 +854,13 @@ function LoggFlik({ arende }: { arende: Arende }) {
<Panel rubrik="Arbetslogg — append-only, ingenting skrivs över"> <Panel rubrik="Arbetslogg — append-only, ingenting skrivs över">
<ol> <ol>
{arende.handelser.map((post) => ( {arende.handelser.map((post) => (
<li key={post.id} className="flex gap-3 border-b border-zinc-800 py-2 last:border-0"> <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-sm font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span> <span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-base text-zinc-100">{handelseRubrik(post)}</p> <p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
<p className="text-xs text-zinc-500">{post.anvandare}</p> <p className="text-[11px] text-[#707070]">{post.anvandare}</p>
{post.handelse.typ === "foto" && ( {post.handelse.typ === "foto" && (
<img src={post.handelse.dataUrl} alt={post.handelse.beskrivning} className="mt-1 max-h-40 rounded border border-zinc-700" /> <img src={post.handelse.dataUrl} alt={post.handelse.beskrivning} className="mt-1 max-h-40 rounded border border-[#C6C6C6]" />
)} )}
</div> </div>
</li> </li>
@@ -764,25 +902,25 @@ function BriefFlik({
<> <>
{!b.avslutat && ( {!b.avslutat && (
<Panel rubrik="AI-granskning"> <Panel rubrik="AI-granskning">
<p className="mb-2 text-zinc-300"> <p className="mb-2 text-[#333333]">
Låt AI:n igenom hela underlaget innan du lämnar det vidare: motsägelser mellan Låt AI:n igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
observationer, luckor i dokumentationen och slutsatser som saknar stöd. observationer, luckor i dokumentationen och slutsatser som saknar stöd.
</p> </p>
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-bold text-amber-400">AI granskar underlaget </p>} {granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">AI granskar underlaget </p>}
{granskning === "fel" && ( {granskning === "fel" && (
<p className="mb-2 font-bold text-red-400">Granskningen kunde inte hämtas kräver inloggning. Försök igen.</p> <p className="mb-2 font-semibold text-[#8B1A1A]">Granskningen kunde inte hämtas kräver inloggning. Försök igen.</p>
)} )}
{senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && ( {senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && (
<div className="mb-2"> <div className="mb-2">
{senasteAi.handelse.rader.map((rad, i) => ( {senasteAi.handelse.rader.map((rad, i) => (
<p key={i} className="py-0.5 text-lg"> <p key={i} className="py-0.5 text-[14px]">
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text} <span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
</p> </p>
))} ))}
<p className="mt-1 text-lg"> <p className="mt-1 text-[14px]">
<span className="font-extrabold text-amber-400">Nästa steg:</span> {senasteAi.handelse.nastaSteg} <span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
</p> </p>
<p className="mt-1 text-xs text-zinc-500">Senaste AI-svar ({senasteAi.handelse.modell})</p> <p className="mt-1 text-[11px] text-[#707070]">Senaste AI-svar ({senasteAi.handelse.modell})</p>
</div> </div>
)} )}
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}> <StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
@@ -793,64 +931,64 @@ function BriefFlik({
<Panel rubrik="Objekt"> <Panel rubrik="Objekt">
{b.objekt ? ( {b.objekt ? (
<> <>
<p className="text-xl font-extrabold">{b.objekt.beskrivning}</p> <p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
<p className="font-bold text-amber-400">{b.objekt.identifierare}</p> <p className="font-semibold text-[#00437A]">{b.objekt.identifierare}</p>
{b.objekt.kund && <p className="text-zinc-300">Kund: {b.objekt.kund}</p>} {b.objekt.kund && <p className="text-[#333333]">Kund: {b.objekt.kund}</p>}
{b.ansvarig && <p className="text-zinc-300">Ansvarig tekniker: {b.ansvarig}</p>} {b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</p>}
</> </>
) : ( ) : (
<p className="text-zinc-400">Ej identifierat</p> <p className="text-[#4A5560]">Ej identifierat</p>
)} )}
</Panel> </Panel>
<Panel rubrik="Kundens beskrivning"> <Panel rubrik="Kundens beskrivning">
<p className="text-lg">{b.felbeskrivning ? `${b.felbeskrivning}` : "—"}</p> <p className="text-[14px]">{b.felbeskrivning ? `${b.felbeskrivning}` : "—"}</p>
</Panel> </Panel>
<Panel rubrik="Utförda kontroller"> <Panel rubrik="Utförda kontroller">
{b.utfordaKontroller.length === 0 && <p className="text-zinc-400">Inga ännu.</p>} {b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
{b.utfordaKontroller.map((k, i) => ( {b.utfordaKontroller.map((k, i) => (
<p key={i} className="py-0.5 text-lg"> <p key={i} className="py-0.5 text-[14px]">
{k.text} {k.text}
{k.resultat && <span className="text-zinc-400"> {k.resultat}</span>} {k.resultat && <span className="text-[#4A5560]"> {k.resultat}</span>}
</p> </p>
))} ))}
</Panel> </Panel>
<Panel rubrik="Observationer"> <Panel rubrik="Observationer">
{b.observationer.length === 0 && <p className="text-zinc-400">Inga ännu.</p>} {b.observationer.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
{b.observationer.map((o, i) => ( {b.observationer.map((o, i) => (
<p key={i} className="py-0.5 text-lg"> {o}</p> <p key={i} className="py-0.5 text-[14px]"> {o}</p>
))} ))}
</Panel> </Panel>
{b.hypoteser.length > 0 && ( {b.hypoteser.length > 0 && (
<Panel rubrik="Hypoteser — kräver verifiering"> <Panel rubrik="Hypoteser — kräver verifiering">
{b.hypoteser.map((h, i) => ( {b.hypoteser.map((h, i) => (
<p key={i} className="py-0.5 text-lg"> <p key={i} className="py-0.5 text-[14px]">
<NivaBadge niva={h.niva} /> {h.text} <NivaBadge niva={h.niva} /> {h.text}
</p> </p>
))} ))}
</Panel> </Panel>
)} )}
<Panel rubrik="Ej kontrollerat"> <Panel rubrik="Ej kontrollerat">
{b.ejKontrollerat.length === 0 && <p className="text-zinc-400">Allt i metodiken är utfört.</p>} {b.ejKontrollerat.length === 0 && <p className="text-[#4A5560]">Allt i metodiken är utfört.</p>}
{b.ejKontrollerat.map((e, i) => ( {b.ejKontrollerat.map((e, i) => (
<p key={i} className="py-0.5 text-lg text-zinc-300"> {e}</p> <p key={i} className="py-0.5 text-[14px] text-[#333333]"> {e}</p>
))} ))}
</Panel> </Panel>
{!b.avslutat && ( {!b.avslutat && (
<Panel rubrik="Rekommenderat nästa steg"> <Panel rubrik="Rekommenderat nästa steg">
{b.rekommenderatNastaSteg.map((s, i) => ( {b.rekommenderatNastaSteg.map((s, i) => (
<p key={i} className="py-0.5 text-lg">{i + 1}. {s}</p> <p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
))} ))}
</Panel> </Panel>
)} )}
<Panel rubrik="Tillförlitlighet"> <Panel rubrik="Tillförlitlighet">
{b.tillforlitlighet.map((r, i) => ( {b.tillforlitlighet.map((r, i) => (
<p key={i} className="py-0.5 text-lg"> <p key={i} className="py-0.5 text-[14px]">
<NivaBadge niva={r.niva} /> {r.text} <NivaBadge niva={r.niva} /> {r.text}
</p> </p>
))} ))}
</Panel> </Panel>
<Panel rubrik="Total arbetstid"> <Panel rubrik="Total arbetstid">
<p className="text-2xl font-extrabold">{b.totalArbetstid}</p> <p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
</Panel> </Panel>
</> </>
); );
@@ -900,7 +1038,7 @@ function RapportFlik({
return ( return (
<> <>
<Panel rubrik="Kundrapport"> <Panel rubrik="Kundrapport">
<p className="mb-2 text-zinc-300"> <p className="mb-2 text-[#333333]">
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas bilder kan Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas bilder kan
innehålla uppgifter om andra kunder. innehålla uppgifter om andra kunder.
</p> </p>
@@ -931,30 +1069,30 @@ function RapportFlik({
</StorKnapp> </StorKnapp>
) )
)} )}
<p className="mt-2 text-sm text-zinc-500"> <p className="mt-2 text-[12px] text-[#707070]">
Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare). Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare).
</p> </p>
</Panel> </Panel>
<Panel rubrik={`Ärende #${arende.nummer}`}> <Panel rubrik={`Ärende #${arende.nummer}`}>
{b.objekt && ( {b.objekt && (
<p className="text-lg font-bold"> <p className="text-[14px] font-semibold">
{b.objekt.beskrivning} · {b.objekt.identifierare} {b.objekt.beskrivning} · {b.objekt.identifierare}
</p> </p>
)} )}
{b.felbeskrivning && <p className="mt-1 text-lg">Felbeskrivning: {b.felbeskrivning}</p>} {b.felbeskrivning && <p className="mt-1 text-[14px]">Felbeskrivning: {b.felbeskrivning}</p>}
<p className="mt-1 text-lg">Total arbetstid: <span className="font-extrabold">{b.totalArbetstid}</span></p> <p className="mt-1 text-[14px]">Total arbetstid: <span className="font-semibold">{b.totalArbetstid}</span></p>
{fordelning.length > 0 && ( {fordelning.length > 0 && (
<div className="mt-2"> <div className="mt-2">
{fordelning.map((r) => ( {fordelning.map((r) => (
<p key={r.label} className="text-zinc-300">{r.label}: {r.tid}</p> <p key={r.label} className="text-[#333333]">{r.label}: {r.tid}</p>
))} ))}
</div> </div>
)} )}
</Panel> </Panel>
<Panel rubrik="Tidslinje"> <Panel rubrik="Tidslinje">
{kundposter.map((post) => ( {kundposter.map((post) => (
<p key={post.id} className="py-0.5 text-base"> <p key={post.id} className="py-0.5 text-[13px]">
<span className="font-mono font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span>{" "} <span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
{handelseRubrik(post)} {handelseRubrik(post)}
</p> </p>
))} ))}
@@ -964,8 +1102,8 @@ function RapportFlik({
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => ( {bilder.map((bild, i) => (
<figure key={i}> <figure key={i}>
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-zinc-700" /> <img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
<figcaption className="mt-1 text-xs text-zinc-400">{bild.beskrivning}</figcaption> <figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
</figure> </figure>
))} ))}
</div> </div>
@@ -974,11 +1112,11 @@ function RapportFlik({
{!b.avslutat || b.ejKontrollerat.length > 0 ? ( {!b.avslutat || b.ejKontrollerat.length > 0 ? (
<Panel rubrik="Rekommenderade nästa steg"> <Panel rubrik="Rekommenderade nästa steg">
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => ( {(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
<p key={i} className="py-0.5 text-lg">{i + 1}. {s}</p> <p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
))} ))}
</Panel> </Panel>
) : null} ) : null}
<p className="text-center text-xs text-zinc-600"> <p className="text-center text-[11px] text-[#8A8A8A]">
Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd. Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd.
</p> </p>
</> </>
+28 -28
View File
@@ -43,12 +43,12 @@ function PlattformInloggning() {
<> <>
<Panel rubrik="Plattformskonto"> <Panel rubrik="Plattformskonto">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<p className="text-lg"> <p className="text-[14px]">
<span className="font-extrabold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "} <span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
{konto.organisation} synk och AI aktiva. {konto.organisation} synk och AI aktiva.
</p> </p>
<button <button
className="whitespace-nowrap rounded-lg border-2 border-zinc-600 px-4 py-2 font-bold text-zinc-300 hover:border-zinc-400" className="whitespace-nowrap rounded border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
onClick={() => { onClick={() => {
loggaUtPlattform(); loggaUtPlattform();
setKonto(null); setKonto(null);
@@ -77,7 +77,7 @@ function PlattformInloggning() {
<Panel rubrik="Plattformskonto"> <Panel rubrik="Plattformskonto">
{lage === "stangd" ? ( {lage === "stangd" ? (
<> <>
<p className="mb-2 text-zinc-300"> <p className="mb-2 text-[#333333]">
Logga in för synk mellan enheter, samarbete i ärenden och AI-stöd. Utan inloggning Logga in för synk mellan enheter, samarbete i ärenden och AI-stöd. Utan inloggning
arbetar appen i lokalt läge. Ny organisation? Skapa konto du blir systemadministratör. arbetar appen i lokalt läge. Ny organisation? Skapa konto du blir systemadministratör.
</p> </p>
@@ -112,7 +112,7 @@ function PlattformInloggning() {
)} )}
<TextFalt label="E-post" varde={epost} satt={setEpost} platshallare="anna@verkstaden.se" /> <TextFalt label="E-post" varde={epost} satt={setEpost} platshallare="anna@verkstaden.se" />
<TextFalt label="Lösenord (minst 8 tecken)" varde={losenord} satt={setLosenord} losenord /> <TextFalt label="Lösenord (minst 8 tecken)" varde={losenord} satt={setLosenord} losenord />
{fel && <p className="mb-3 font-bold text-red-400">{fel}</p>} {fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}> <StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
Avbryt Avbryt
@@ -172,9 +172,9 @@ function AnvandarAdmin() {
return ( return (
<Panel rubrik="Användare"> <Panel rubrik="Användare">
{lista.map((anv) => ( {lista.map((anv) => (
<p key={anv.id} className="border-b border-zinc-800 py-1 text-lg last:border-0"> <p key={anv.id} className="border-b border-[#DDDDDD] py-1 text-[14px] last:border-0">
<span className="font-extrabold">{anv.namn}</span> · {ROLL_LABEL[anv.roll]}{" "} <span className="font-semibold">{anv.namn}</span> · {ROLL_LABEL[anv.roll]}{" "}
<span className="text-zinc-500">{anv.epost}</span> <span className="text-[#707070]">{anv.epost}</span>
</p> </p>
))} ))}
<form <form
@@ -202,17 +202,17 @@ function AnvandarAdmin() {
key={valbar} key={valbar}
type="button" type="button"
onClick={() => setRoll(valbar)} onClick={() => setRoll(valbar)}
className={`min-h-12 rounded-lg border-2 text-sm font-extrabold ${ className={`min-h-9 rounded border text-[12px] font-semibold ${
roll === valbar roll === valbar
? "border-amber-400 bg-amber-400 text-zinc-950" ? "border-[#00437A] bg-[#00437A] text-white"
: "border-zinc-600 bg-zinc-900 text-zinc-200" : "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
}`} }`}
> >
{ROLL_LABEL[valbar]} {ROLL_LABEL[valbar]}
</button> </button>
))} ))}
</div> </div>
{fel && <p className="mb-3 font-bold text-red-400">{fel}</p>} {fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}> <StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
Skapa användare Skapa användare
</StorKnapp> </StorKnapp>
@@ -232,7 +232,7 @@ export default function Arendelista() {
return ( return (
<FelsokningSkal rubrik="Guidad Felsökning"> <FelsokningSkal rubrik="Guidad Felsökning">
<Panel rubrik="Vem arbetar?"> <Panel rubrik="Vem arbetar?">
<p className="mb-3 text-lg text-zinc-300"> <p className="mb-3 text-[14px] text-[#333333]">
Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja. Allt arbete loggas med användare och tidpunkt. Ange ditt namn för att börja.
</p> </p>
<form <form
@@ -265,20 +265,20 @@ export default function Arendelista() {
return ( return (
<FelsokningSkal <FelsokningSkal
rubrik="Guidad Felsökning" rubrik="Guidad Felsökning"
hoger={<span className="text-sm font-bold text-zinc-400">{anvandare}</span>} hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{anvandare}</span>}
> >
<Link to="/felsokning/nytt"> <Link to="/felsokning/nytt">
<StorKnapp className="mb-4">+ Nytt ärende</StorKnapp> <StorKnapp className="mb-4">+ Nytt ärende</StorKnapp>
</Link> </Link>
{alla.length > 0 && ( {alla.length > 0 && (
<div className="mb-4 grid grid-cols-3 gap-1 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-1"> <div className="mb-4 grid grid-cols-3 gap-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1">
{FILTER.map((f) => ( {FILTER.map((f) => (
<button <button
key={f.id} key={f.id}
onClick={() => setFilter(f.id)} onClick={() => setFilter(f.id)}
className={`min-h-12 rounded-md text-base font-extrabold transition-colors ${ className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
filter === f.id ? "bg-amber-400 text-zinc-950" : "text-zinc-300 hover:bg-zinc-800" filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`} }`}
> >
{f.label} · {f.antal} {f.label} · {f.antal}
@@ -289,7 +289,7 @@ export default function Arendelista() {
{alla.length === 0 && ( {alla.length === 0 && (
<Panel rubrik="Kom igång"> <Panel rubrik="Kom igång">
<p className="mb-3 text-lg text-zinc-300"> <p className="mb-3 text-[14px] text-[#333333]">
Inga ärenden ännu. Starta med att identifiera ett objekt eller utforska ett färdigt demoärende Inga ärenden ännu. Starta med att identifiera ett objekt eller utforska ett färdigt demoärende
med komplett arbetslogg, brief och kundrapport. med komplett arbetslogg, brief och kundrapport.
</p> </p>
@@ -300,7 +300,7 @@ export default function Arendelista() {
)} )}
{lista.length === 0 && alla.length > 0 && ( {lista.length === 0 && alla.length > 0 && (
<p className="text-center text-lg text-zinc-400">Inga ärenden i det här filtret.</p> <p className="text-center text-[14px] text-[#4A5560]">Inga ärenden i det här filtret.</p>
)} )}
{plattformAktiv() && <PlattformInloggning />} {plattformAktiv() && <PlattformInloggning />}
@@ -312,7 +312,7 @@ export default function Arendelista() {
)} )}
<Panel rubrik="AI-stöd"> <Panel rubrik="AI-stöd">
<p className="text-zinc-300"> <p className="text-[#333333]">
När du är inloggad hjälper AI:n dig medan du arbetar: föreslår nästa steg utifrån det du När du är inloggad hjälper AI:n dig medan du arbetar: föreslår nästa steg utifrån det du
dokumenterar, granskar hela ärendet begäran och skriver utkast till överlämningen. Den dokumenterar, granskar hela ärendet begäran och skriver utkast till överlämningen. Den
skiljer alltid verifierat och hypotes och slår aldrig fast en felorsak som inte är skiljer alltid verifierat och hypotes och slår aldrig fast en felorsak som inte är
@@ -329,23 +329,23 @@ export default function Arendelista() {
const b = brief(arende, metodikForArende(arende), new Date().toISOString()); const b = brief(arende, metodikForArende(arende), new Date().toISOString());
return ( return (
<Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block"> <Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block">
<div className="mb-3 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-4 transition-colors hover:border-amber-400"> <div className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-sm font-extrabold uppercase tracking-widest text-zinc-400"> <span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{arende.nummer} Ärende #{arende.nummer}
</span> </span>
<span <span
className={`rounded px-2 py-0.5 text-xs font-extrabold uppercase ${ className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
avslutat ? "bg-zinc-700 text-zinc-300" : "bg-amber-400 text-zinc-950" avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`} }`}
> >
{avslutat ? "Avslutat" : "Pågående"} {avslutat ? "Avslutat" : "Pågående"}
</span> </span>
</div> </div>
<p className="mt-1 text-xl font-extrabold">{o ? o.beskrivning : "Okänt objekt"}</p> <p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Okänt objekt"}</p>
{o && <p className="text-sm font-bold text-zinc-400">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>} {o && <p className="text-[12px] font-semibold text-[#4A5560]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
{fel && <p className="mt-2 text-lg text-zinc-200">{fel}</p>} {fel && <p className="mt-2 text-[14px] text-[#333333]">{fel}</p>}
<p className="mt-2 text-sm text-zinc-500"> <p className="mt-2 text-[12px] text-[#707070]">
{sista ? `Senaste aktivitet ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""} {sista ? `Senaste aktivitet ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
{" · "} {" · "}
{b.totalArbetstid} {b.totalArbetstid}
+1 -1
View File
@@ -21,7 +21,7 @@ export default function DelatArende() {
if (!arende) { if (!arende) {
return ( return (
<FelsokningSkal rubrik="Delat ärende"> <FelsokningSkal rubrik="Delat ärende">
<p className="text-lg text-zinc-300">Ärendet är inte tillgängligt.</p> <p className="text-[14px] text-[#333333]">Ärendet är inte tillgängligt.</p>
</FelsokningSkal> </FelsokningSkal>
); );
} }
+7 -7
View File
@@ -32,10 +32,10 @@ function Vallista({
<button <button
key={val} key={val}
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])} onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
className={`min-h-14 rounded-lg border-2 px-2 font-extrabold transition-colors ${ className={`min-h-10 rounded border px-2 font-semibold transition-colors ${
aktiv aktiv
? "border-amber-400 bg-amber-400 text-zinc-950" ? "border-[#00437A] bg-[#00437A] text-white"
: "border-zinc-600 bg-zinc-900 text-zinc-400" : "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
}`} }`}
> >
{aktiv ? "✓ " : ""} {aktiv ? "✓ " : ""}
@@ -64,7 +64,7 @@ export default function Installningar() {
return ( return (
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}> <FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
<Panel> <Panel>
<p className="text-lg text-zinc-300"> <p className="text-[14px] text-[#333333]">
Organisationens inställningar hanteras av er systemadministratör. Organisationens inställningar hanteras av er systemadministratör.
</p> </p>
</Panel> </Panel>
@@ -97,7 +97,7 @@ export default function Installningar() {
return ( return (
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}> <FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
<Panel> <Panel>
<p className="text-zinc-300"> <p className="text-[#333333]">
Välj vad som visas när ett nytt ärende startas.{" "} Välj vad som visas när ett nytt ärende startas.{" "}
{inloggad {inloggad
? "Valet gäller alla i organisationen." ? "Valet gäller alla i organisationen."
@@ -121,8 +121,8 @@ export default function Installningar() {
/> />
</Panel> </Panel>
{status === "fel" && <p className="mb-3 font-bold text-red-400">{felText}</p>} {status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
{status === "sparat" && <p className="mb-3 font-bold text-green-400"> Sparat</p>} {status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]"> Sparat</p>}
<StorKnapp disabled={status === "sparar"} onClick={spara}> <StorKnapp disabled={status === "sparar"} onClick={spara}>
{status === "sparar" ? "Sparar …" : "Spara inställningar"} {status === "sparar" ? "Sparar …" : "Spara inställningar"}
</StorKnapp> </StorKnapp>
+8 -8
View File
@@ -60,7 +60,7 @@ export default function NyttArende() {
</StorKnapp> </StorKnapp>
))} ))}
</div> </div>
<p className="mt-3 text-sm text-zinc-500"> <p className="mt-3 text-[12px] text-[#707070]">
QR-kod, streckkod och OCR från typskylt tillkommer i senare version ange identiteten manuellt. QR-kod, streckkod och OCR från typskylt tillkommer i senare version ange identiteten manuellt.
</p> </p>
</Panel> </Panel>
@@ -80,11 +80,11 @@ export default function NyttArende() {
return ( return (
<FelsokningSkal rubrik="Bekräfta objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}> <FelsokningSkal rubrik="Bekräfta objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
<Panel> <Panel>
<p className="text-sm font-extrabold uppercase tracking-widest text-zinc-400">{objektet.typ}</p> <p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
<p className="mt-1 text-3xl font-extrabold">{objektet.beskrivning}</p> <p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
<p className="mt-1 text-xl font-bold text-amber-400">{objektet.identifierare}</p> <p className="mt-1 text-[15px] font-semibold text-[#00437A]">{objektet.identifierare}</p>
<p className="mt-1 text-sm text-zinc-400">Identifierad via: {objektet.identifieringsmetod}</p> <p className="mt-1 text-[12px] text-[#4A5560]">Identifierad via: {objektet.identifieringsmetod}</p>
{objektet.kund && <p className="mt-1 text-lg text-zinc-200">Kund: {objektet.kund}</p>} {objektet.kund && <p className="mt-1 text-[14px] text-[#333333]">Kund: {objektet.kund}</p>}
</Panel> </Panel>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}> <StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
@@ -110,8 +110,8 @@ export default function NyttArende() {
rost rost
/> />
{metodik && ( {metodik && (
<p className="mb-3 text-sm font-bold text-zinc-400"> <p className="mb-3 text-[12px] font-semibold text-[#4A5560]">
Vald metodik: <span className="text-amber-400">{metodik.namn}</span> Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span>
</p> </p>
)} )}
<StorKnapp <StorKnapp
+25 -25
View File
@@ -44,13 +44,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
); );
} }
return ( return (
<div className="col-span-2 rounded-lg border-2 border-zinc-700 bg-zinc-950 p-2"> <div className="col-span-2 rounded border border-[#C6C6C6] bg-white p-2">
<p className="mb-2 text-sm font-bold uppercase text-zinc-400">Ny ansvarig tekniker</p> <p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">Ny ansvarig tekniker</p>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{personer.map((person) => ( {personer.map((person) => (
<button <button
key={person.id} key={person.id}
className="min-h-12 rounded-lg border-2 border-zinc-600 font-extrabold text-zinc-200 hover:border-amber-400" className="min-h-9 rounded border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => { onClick={async () => {
const post = nyLoggPost(konto?.namn ?? "arbetsledare", { const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
typ: "ansvarig_satt", typ: "ansvarig_satt",
@@ -68,7 +68,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
</button> </button>
))} ))}
<button <button
className="min-h-12 rounded-lg border-2 border-zinc-600 font-bold text-zinc-400" className="min-h-9 rounded border border-[#ADADAD] font-semibold text-[#4A5560]"
onClick={() => setOppen(false)} onClick={() => setOppen(false)}
> >
Avbryt Avbryt
@@ -97,7 +97,7 @@ export default function Oversikt() {
return ( return (
<FelsokningSkal rubrik="Organisationsöversikt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}> <FelsokningSkal rubrik="Organisationsöversikt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
<Panel> <Panel>
<p className="text-lg text-zinc-300"> <p className="text-[14px] text-[#333333]">
Översikten kräver arbetsledar- eller administratörsbehörighet plattformen. Översikten kräver arbetsledar- eller administratörsbehörighet plattformen.
</p> </p>
</Panel> </Panel>
@@ -149,55 +149,55 @@ export default function Oversikt() {
<FelsokningSkal <FelsokningSkal
rubrik="Organisationsöversikt" rubrik="Organisationsöversikt"
tillbaka={{ till: "/felsokning", text: "Ärenden" }} tillbaka={{ till: "/felsokning", text: "Ärenden" }}
hoger={<span className="text-sm font-bold text-zinc-400">{konto?.organisation}</span>} hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{konto?.organisation}</span>}
> >
{fel && <p className="mb-3 font-bold text-red-400">{fel}</p>} {fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
{!rader && !fel && <p className="animate-pulse text-lg text-zinc-400">Hämtar </p>} {!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Hämtar </p>}
{rader && ( {rader && (
<> <>
<Panel rubrik="Statistik"> <Panel rubrik="Statistik">
<div className="grid grid-cols-3 gap-3 text-center"> <div className="grid grid-cols-3 gap-3 text-center">
<div> <div>
<p className="text-3xl font-extrabold">{rader.length}</p> <p className="text-[20px] font-semibold">{rader.length}</p>
<p className="text-sm font-bold uppercase text-zinc-400">Totalt</p> <p className="text-[12px] font-semibold uppercase text-[#4A5560]">Totalt</p>
</div> </div>
<div> <div>
<p className="text-3xl font-extrabold text-amber-400">{pagaende.length}</p> <p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
<p className="text-sm font-bold uppercase text-zinc-400">Pågående</p> <p className="text-[12px] font-semibold uppercase text-[#4A5560]">Pågående</p>
</div> </div>
<div> <div>
<p className="text-3xl font-extrabold">{avslutade.length}</p> <p className="text-[20px] font-semibold">{avslutade.length}</p>
<p className="text-sm font-bold uppercase text-zinc-400">Avslutade</p> <p className="text-[12px] font-semibold uppercase text-[#4A5560]">Avslutade</p>
</div> </div>
</div> </div>
{avslutade.length > 0 && ( {avslutade.length > 0 && (
<p className="mt-3 text-center text-zinc-300"> <p className="mt-3 text-center text-[#333333]">
Genomsnittlig ledtid för avslutade: <span className="font-extrabold">{formateraTid(medelLedtidMs)}</span> Genomsnittlig ledtid för avslutade: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
</p> </p>
)} )}
</Panel> </Panel>
{rader.length === 0 && <p className="text-center text-lg text-zinc-400">Inga ärenden i organisationen ännu.</p>} {rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">Inga ärenden i organisationen ännu.</p>}
{rader.map((rad) => ( {rader.map((rad) => (
<div key={rad.id} className="mb-3 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-4"> <div key={rad.id} className="mb-3 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-sm font-extrabold uppercase tracking-widest text-zinc-400"> <span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{rad.nummer} Ärende #{rad.nummer}
</span> </span>
<span <span
className={`rounded px-2 py-0.5 text-xs font-extrabold uppercase ${ className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-zinc-700 text-zinc-300" : "bg-amber-400 text-zinc-950" rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`} }`}
> >
{rad.avslutat ? "Avslutat" : "Pågående"} {rad.avslutat ? "Avslutat" : "Pågående"}
</span> </span>
</div> </div>
<p className="mt-1 text-xl font-extrabold">{rad.objekt ?? "Okänt objekt"}</p> <p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Okänt objekt"}</p>
{rad.felbeskrivning && <p className="text-lg text-zinc-200">{rad.felbeskrivning}</p>} {rad.felbeskrivning && <p className="text-[14px] text-[#333333]">{rad.felbeskrivning}</p>}
<p className="mt-1 text-sm text-zinc-500"> <p className="mt-1 text-[12px] text-[#707070]">
Ansvarig: <span className="font-bold text-zinc-300">{rad.ansvarig ?? rad.skapare ?? "—"}</span> Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
{` · ${rad.antal_handelser} händelser`} {` · ${rad.antal_handelser} händelser`}
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""} {rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
</p> </p>
+1 -1
View File
@@ -89,7 +89,7 @@ export default function PublikDelning() {
return ( return (
<FelsokningSkal rubrik="Delat ärende"> <FelsokningSkal rubrik="Delat ärende">
<Panel> <Panel>
<p className="text-lg text-zinc-300"> <p className="text-[14px] text-[#333333]">
{status === "laddar" {status === "laddar"
? "Hämtar ärendet …" ? "Hämtar ärendet …"
: "Ärendet är inte tillgängligt. Kontrollera länken med verkstaden — delningen kan ha stängts av."} : "Ärendet är inte tillgängligt. Kontrollera länken med verkstaden — delningen kan ha stängts av."}