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:
+1
-1
@@ -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.
|
||||
- **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.
|
||||
- **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 (11–15 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
|
||||
|
||||
|
||||
@@ -40,31 +40,31 @@ export function DelatArendeVy({
|
||||
rubrik={b.objekt?.beskrivning ?? "Ärende"}
|
||||
hoger={
|
||||
<span
|
||||
className={`rounded px-3 py-1 text-sm font-extrabold uppercase ${
|
||||
avslutat ? "bg-zinc-700 text-zinc-200" : "bg-green-500 text-zinc-950"
|
||||
className={`rounded px-3 py-1 text-[12px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
|
||||
}`}
|
||||
>
|
||||
{avslutat ? "Avslutat" : "🟢 Felsökning pågår"}
|
||||
</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}
|
||||
</p>
|
||||
|
||||
{b.felbeskrivning && (
|
||||
<Panel rubrik="Kundens felbeskrivning">
|
||||
<p className="text-lg">”{b.felbeskrivning}”</p>
|
||||
<p className="text-[14px]">”{b.felbeskrivning}”</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<Panel rubrik="Aktuell status">
|
||||
{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) => (
|
||||
<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>
|
||||
|
||||
@@ -73,8 +73,8 @@ export function DelatArendeVy({
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{bilder.map((bild, i) => (
|
||||
<figure key={i}>
|
||||
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-zinc-700" />
|
||||
<figcaption className="mt-1 text-xs text-zinc-400">{bild.beskrivning}</figcaption>
|
||||
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
@@ -83,15 +83,15 @@ export function DelatArendeVy({
|
||||
|
||||
{matvarden.length > 0 && (
|
||||
<Panel rubrik="Mätvärden">
|
||||
<table className="w-full text-lg">
|
||||
<table className="w-full text-[14px]">
|
||||
<tbody>
|
||||
{matvarden.map((p) => {
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "matvarde") return null;
|
||||
return (
|
||||
<tr key={p.id} className="border-b border-zinc-800 last:border-0">
|
||||
<td className="py-1 pr-3 text-zinc-300">{h.beskrivning}</td>
|
||||
<td className="py-1 font-bold">
|
||||
<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">
|
||||
{h.varde}
|
||||
{h.enhet ? ` ${h.enhet}` : ""}
|
||||
</td>
|
||||
@@ -105,8 +105,8 @@ export function DelatArendeVy({
|
||||
|
||||
<Panel rubrik="Tidslinje">
|
||||
{kundposter.map((post) => (
|
||||
<p key={post.id} className="py-0.5 text-base">
|
||||
<span className="font-mono font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
<p key={post.id} className="py-0.5 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
{handelseRubrik(post)}
|
||||
</p>
|
||||
))}
|
||||
@@ -114,20 +114,20 @@ export function DelatArendeVy({
|
||||
|
||||
{!avslutat && pagaende && (
|
||||
<Panel rubrik="Rekommenderat nästa steg">
|
||||
<p className="text-lg">{pagaende}</p>
|
||||
<p className="text-[14px]">{pagaende}</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
<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) => (
|
||||
<p key={r.label} className="text-zinc-300">
|
||||
<p key={r.label} className="text-[#333333]">
|
||||
{r.label}: {r.tid}
|
||||
</p>
|
||||
))}
|
||||
</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
|
||||
</p>
|
||||
</FelsokningSkal>
|
||||
|
||||
@@ -47,16 +47,16 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
||||
type="button"
|
||||
onClick={() => (lyssnar ? stoppa() : starta())}
|
||||
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
|
||||
? "animate-pulse border-red-500 bg-red-600 text-white"
|
||||
: "border-zinc-600 bg-zinc-800 text-zinc-100 hover:border-amber-400"
|
||||
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
|
||||
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
|
||||
}`}
|
||||
>
|
||||
{lyssnar ? "■" : "🎤"}
|
||||
</button>
|
||||
{lyssnar && (
|
||||
<span className="text-sm font-bold text-red-400">
|
||||
<span className="text-[12px] font-semibold text-[#8B1A1A]">
|
||||
● Inspelning pågår{interim ? ` — ”${interim}”` : "…"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -17,10 +17,10 @@ describe("organisationsinställningar", () => {
|
||||
|
||||
it("okända värden filtreras bort och ordningen följer standardlistan", () => {
|
||||
const inst = normalisera({
|
||||
objekttyper: ["Hydraulik", "Påhittad typ", "Fordon"],
|
||||
objekttyper: ["Hydraulik", "Påhittad typ", "Personbil"],
|
||||
identifieringsmetoder: ["VIN"],
|
||||
});
|
||||
expect(inst.objekttyper).toEqual(["Fordon", "Hydraulik"]);
|
||||
expect(inst.objekttyper).toEqual(["Personbil", "Hydraulik"]);
|
||||
expect(inst.identifieringsmetoder).toEqual(["VIN"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ function byggArende(poster: [string, Handelse][]): Arende {
|
||||
const OBJEKT: Handelse = {
|
||||
typ: "objekt_identifierat",
|
||||
objekt: {
|
||||
typ: "Fordon",
|
||||
typ: "Personbil",
|
||||
identifierare: "ABC123",
|
||||
identifieringsmetod: "Registreringsnummer",
|
||||
beskrivning: "Volvo XC60 D4 2019",
|
||||
|
||||
@@ -57,9 +57,9 @@ export function byggDemoArende(nummer: number, anvandare = "Anna"): Arende {
|
||||
post(0, {
|
||||
typ: "objekt_identifierat",
|
||||
objekt: {
|
||||
typ: "Fordon",
|
||||
typ: "Personbil",
|
||||
identifierare: "ABC123",
|
||||
identifieringsmetod: "Registreringsnummer",
|
||||
identifieringsmetod: "Regnr",
|
||||
beskrivning: "Volvo XC60 D4 2019",
|
||||
kund: "Anders Svensson",
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@ export const TIDKATEGORI_LABEL: Record<TidKategori, string> = {
|
||||
export interface Objekt {
|
||||
typ: string; // t.ex. "Fordon", "Industrimaskin"
|
||||
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"
|
||||
kund?: string;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
|
||||
|
||||
export const ALLA_OBJEKTTYPER = [
|
||||
"Fordon",
|
||||
"Personbil",
|
||||
"Lastbil/Buss",
|
||||
"Entreprenadmaskin",
|
||||
"Industrimaskin",
|
||||
@@ -17,7 +17,7 @@ export const ALLA_OBJEKTTYPER = [
|
||||
];
|
||||
|
||||
export const ALLA_IDENTIFIERINGSMETODER = [
|
||||
"Registreringsnummer",
|
||||
"Regnr",
|
||||
"VIN",
|
||||
"Serienummer",
|
||||
"Maskinnummer",
|
||||
|
||||
+50
-22
@@ -1,6 +1,9 @@
|
||||
// Delade UI-komponenter för Guidad Felsökning.
|
||||
// Designfilosofi: professionellt fabriksverktyg — hög kontrast, stora
|
||||
// träffytor (handskvänligt), få val per skärm, inga distraktioner.
|
||||
// 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.
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { useRef, type ReactNode } from "react";
|
||||
@@ -8,33 +11,54 @@ import type { Tillforlitlighet } from "./domain";
|
||||
import { TILLFORLITLIGHET_LABEL } from "./domain";
|
||||
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({
|
||||
rubrik,
|
||||
tillbaka,
|
||||
hoger,
|
||||
bred,
|
||||
children,
|
||||
}: {
|
||||
rubrik: string;
|
||||
tillbaka?: { till: string; text: string };
|
||||
hoger?: ReactNode;
|
||||
bred?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
|
||||
return (
|
||||
<div className="felsokning-print min-h-screen bg-zinc-950 text-zinc-50">
|
||||
<header className="sticky top-0 z-20 border-b-2 border-zinc-700 bg-zinc-900 px-4 py-3 print:static">
|
||||
<div className="mx-auto flex max-w-3xl items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]"
|
||||
style={{ fontFamily: TYPSNITT }}
|
||||
>
|
||||
{/* 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 && (
|
||||
<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>
|
||||
{hoger}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -56,16 +80,16 @@ export function StorKnapp({
|
||||
}) {
|
||||
const farg =
|
||||
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"
|
||||
? "bg-red-600 text-white hover:bg-red-500 active:bg-red-700"
|
||||
: "bg-zinc-800 text-zinc-50 border-2 border-zinc-600 hover:bg-zinc-700 active:bg-zinc-600";
|
||||
? "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]";
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
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}
|
||||
</button>
|
||||
@@ -74,15 +98,19 @@ export function StorKnapp({
|
||||
|
||||
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
|
||||
return (
|
||||
<section className="mb-4 rounded-lg border-2 border-zinc-700 bg-zinc-900 p-4">
|
||||
{rubrik && <h2 className="mb-3 text-sm font-extrabold uppercase tracking-widest text-zinc-400">{rubrik}</h2>}
|
||||
<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-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
||||
{rubrik}
|
||||
</h2>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -107,11 +135,11 @@ export function TextFalt({
|
||||
const vardeRef = useRef(varde);
|
||||
vardeRef.current = varde;
|
||||
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 (
|
||||
<label className="mb-3 block">
|
||||
<span className="mb-1 flex min-h-11 items-center justify-between gap-2">
|
||||
<span className="text-sm font-bold uppercase tracking-wide text-zinc-400">{label}</span>
|
||||
<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>
|
||||
{rost && (
|
||||
<MikrofonKnapp
|
||||
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function ArendeSida() {
|
||||
if (!arende || !id) {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Ärendet hittades inte" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<p className="text-lg text-zinc-300">Ärendet finns inte på den här enheten.</p>
|
||||
<p className="text-[14px] text-[#333333]">Ärendet finns inte på den här enheten.</p>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
}
|
||||
@@ -106,24 +106,28 @@ export default function ArendeSida() {
|
||||
|
||||
return (
|
||||
<FelsokningSkal
|
||||
bred
|
||||
rubrik={`#${arende.nummer} ${o?.beskrivning ?? ""}`}
|
||||
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
|
||||
hoger={
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-extrabold text-amber-400">{total}</p>
|
||||
<p className="text-xs font-bold uppercase text-zinc-500">
|
||||
<p className="text-[13px] font-semibold text-white">{total}</p>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
|
||||
{avslutat ? "Avslutat" : "Pågår"} · {SYNKSTATUS_LABEL[synkStatus]}
|
||||
</p>
|
||||
</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) => (
|
||||
<button
|
||||
key={f.id}
|
||||
onClick={() => setFlik(f.id)}
|
||||
className={`min-h-12 rounded-md text-base font-extrabold transition-colors ${
|
||||
flik === f.id ? "bg-amber-400 text-zinc-950" : "text-zinc-300 hover:bg-zinc-800"
|
||||
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
|
||||
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -131,18 +135,152 @@ export default function ArendeSida() {
|
||||
))}
|
||||
</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" && (
|
||||
<GuideFlik arende={arende} metodik={metodik} avslutat={avslutat} skicka={skicka} nu={nu} />
|
||||
)}
|
||||
{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 className="min-w-0">
|
||||
{!avslutat && <InaktivitetsBanner arende={arende} nu={nu} skicka={skicka} />}
|
||||
|
||||
{flik === "guide" && (
|
||||
<GuideFlik arende={arende} metodik={metodik} avslutat={avslutat} skicka={skicka} nu={nu} />
|
||||
)}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
function InaktivitetsBanner({
|
||||
arende,
|
||||
@@ -159,11 +297,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 rounded-lg border-2 border-amber-400 bg-zinc-900 p-4">
|
||||
<p className="mb-2 text-lg font-bold text-amber-400">
|
||||
<div className="mb-4 rounded border border-[#00437A] bg-[#F7F7F7] p-4">
|
||||
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
|
||||
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
||||
</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 />
|
||||
<StorKnapp
|
||||
disabled={!text.trim()}
|
||||
@@ -216,7 +354,7 @@ function GuideFlik({
|
||||
if (avslutat) {
|
||||
return (
|
||||
<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.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -229,8 +367,8 @@ function GuideFlik({
|
||||
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
|
||||
{steg.klart ? (
|
||||
<>
|
||||
<p className="mb-3 text-xl font-bold">Samtliga steg i metodiken är dokumenterade.</p>
|
||||
<p className="mb-4 text-zinc-300">
|
||||
<p className="mb-3 text-[15px] font-semibold">Samtliga steg i metodiken är dokumenterade.</p>
|
||||
<p className="mb-4 text-[#333333]">
|
||||
Om felorsaken inte är verifierad: dokumentera en hypotes och utöka felsökningen, eller avsluta ärendet
|
||||
med rekommenderade nästa steg.
|
||||
</p>
|
||||
@@ -253,19 +391,19 @@ function GuideFlik({
|
||||
: "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" && (
|
||||
<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.handelse.rader.map((rad, i) => (
|
||||
<p key={i} className="py-0.5 text-lg">
|
||||
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-2 text-lg">
|
||||
<span className="font-extrabold text-amber-400">Nästa steg:</span>{" "}
|
||||
<p className="mt-2 text-[14px]">
|
||||
<span className="font-semibold text-[#00437A]">Nästa steg:</span>{" "}
|
||||
{senasteAiSvar.handelse.nastaSteg}
|
||||
</p>
|
||||
</>
|
||||
@@ -302,10 +440,10 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
|
||||
<button
|
||||
key={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
|
||||
? "border-amber-400 bg-amber-400 text-zinc-950"
|
||||
: "border-zinc-600 bg-zinc-900 text-zinc-300 hover:border-zinc-400"
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
||||
}`}
|
||||
>
|
||||
{TIDKATEGORI_LABEL[k]}
|
||||
@@ -324,7 +462,7 @@ function FrageKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse) =>
|
||||
|
||||
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" && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp onClick={() => svara("Ja")}>Ja</StorKnapp>
|
||||
@@ -374,8 +512,8 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
|
||||
return (
|
||||
<>
|
||||
{steg.steg.beskrivning && <p className="mb-2 text-zinc-400">{steg.steg.beskrivning}</p>}
|
||||
<p className="mb-4 text-2xl font-extrabold leading-snug">{kontroll.text}</p>
|
||||
{steg.steg.beskrivning && <p className="mb-2 text-[#4A5560]">{steg.steg.beskrivning}</p>}
|
||||
<p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p>
|
||||
{krav === "foto" ? (
|
||||
<>
|
||||
<input
|
||||
@@ -394,7 +532,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
/>
|
||||
<TextFalt label="Observation (valfritt)" varde={resultat} satt={setResultat} rost />
|
||||
<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
|
||||
/>
|
||||
{!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{" "}
|
||||
{krav === "matvarde" ? "mätuppgift" : "kommentar"} innan kontrollen kan verifieras.
|
||||
</p>
|
||||
@@ -484,10 +622,10 @@ function SnabbDokumentation({
|
||||
<button
|
||||
key={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
|
||||
? "border-amber-400 bg-amber-400 text-zinc-950"
|
||||
: "border-zinc-600 bg-zinc-900 text-zinc-200 hover:border-zinc-400"
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
||||
}`}
|
||||
>
|
||||
+ {d.label}
|
||||
@@ -511,7 +649,7 @@ function SnabbDokumentation({
|
||||
{typ && typ !== "foto" && (
|
||||
<div className="mt-3">
|
||||
{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.
|
||||
</p>
|
||||
)}
|
||||
@@ -571,9 +709,9 @@ 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 rounded-lg border-2 border-zinc-600 bg-zinc-900 p-4">
|
||||
<h2 className="mb-3 text-xl font-extrabold">Ö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">
|
||||
<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-[15px] font-semibold">Överlämningsrapport</h2>
|
||||
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded bg-white p-4 text-[12px] text-[#333333]">
|
||||
{text}
|
||||
</pre>
|
||||
{aiLage === "vilar" && (
|
||||
@@ -581,15 +719,15 @@ function OverlamningDialog({
|
||||
✨ AI-komplettera: risker & osäkerheter
|
||||
</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" && (
|
||||
<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 && (
|
||||
<div className="mb-3 rounded-lg bg-zinc-950 p-4">
|
||||
<div className="mb-3 rounded bg-white p-4">
|
||||
{aiRader.map((rad, i) => (
|
||||
<p key={i} className="py-0.5 text-sm text-zinc-200">
|
||||
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
@@ -652,18 +790,18 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-lg border-2 border-zinc-700 bg-zinc-950 p-3">
|
||||
{fel && <p className="mb-2 font-bold text-red-400">{fel}</p>}
|
||||
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-3">
|
||||
{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-zinc-800 py-2 last:border-0">
|
||||
<span className="text-lg">
|
||||
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#DDDDDD] py-2 last:border-0">
|
||||
<span className="text-[14px]">
|
||||
{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>
|
||||
{!delning.aterkallad && (
|
||||
<span className="flex gap-2">
|
||||
<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={() =>
|
||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
||||
}
|
||||
@@ -671,7 +809,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
Kopiera
|
||||
</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 () => {
|
||||
await aterkallaDelning(delning.kod);
|
||||
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) => (
|
||||
<button
|
||||
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 () => {
|
||||
try {
|
||||
await skapaDelning(arende.id, niva);
|
||||
@@ -703,7 +841,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
</button>
|
||||
))}
|
||||
</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.
|
||||
Filtreringen sker på servern.
|
||||
</p>
|
||||
@@ -716,13 +854,13 @@ function LoggFlik({ arende }: { arende: Arende }) {
|
||||
<Panel rubrik="Arbetslogg — append-only, ingenting skrivs över">
|
||||
<ol>
|
||||
{arende.handelser.map((post) => (
|
||||
<li key={post.id} className="flex gap-3 border-b border-zinc-800 py-2 last:border-0">
|
||||
<span className="w-14 shrink-0 font-mono text-sm font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span>
|
||||
<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>
|
||||
<div className="min-w-0">
|
||||
<p className="text-base text-zinc-100">{handelseRubrik(post)}</p>
|
||||
<p className="text-xs text-zinc-500">{post.anvandare}</p>
|
||||
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
|
||||
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
|
||||
{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>
|
||||
</li>
|
||||
@@ -764,25 +902,25 @@ function BriefFlik({
|
||||
<>
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="AI-granskning">
|
||||
<p className="mb-2 text-zinc-300">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
Låt AI:n gå igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
|
||||
observationer, luckor i dokumentationen och slutsatser som saknar stöd.
|
||||
</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" && (
|
||||
<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" && (
|
||||
<div className="mb-2">
|
||||
{senasteAi.handelse.rader.map((rad, i) => (
|
||||
<p key={i} className="py-0.5 text-lg">
|
||||
<span className="font-extrabold text-zinc-400">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-1 text-lg">
|
||||
<span className="font-extrabold text-amber-400">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
|
||||
<p className="mt-1 text-[14px]">
|
||||
<span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
|
||||
</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>
|
||||
)}
|
||||
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
|
||||
@@ -793,64 +931,64 @@ function BriefFlik({
|
||||
<Panel rubrik="Objekt">
|
||||
{b.objekt ? (
|
||||
<>
|
||||
<p className="text-xl font-extrabold">{b.objekt.beskrivning}</p>
|
||||
<p className="font-bold text-amber-400">{b.objekt.identifierare}</p>
|
||||
{b.objekt.kund && <p className="text-zinc-300">Kund: {b.objekt.kund}</p>}
|
||||
{b.ansvarig && <p className="text-zinc-300">Ansvarig tekniker: {b.ansvarig}</p>}
|
||||
<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="text-zinc-400">Ej identifierat</p>
|
||||
<p className="text-[#4A5560]">Ej identifierat</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Kundens beskrivning">
|
||||
<p className="text-lg">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||
</Panel>
|
||||
<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) => (
|
||||
<p key={i} className="py-0.5 text-lg">
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
✓ {k.text}
|
||||
{k.resultat && <span className="text-zinc-400"> — {k.resultat}</span>}
|
||||
{k.resultat && <span className="text-[#4A5560]"> — {k.resultat}</span>}
|
||||
</p>
|
||||
))}
|
||||
</Panel>
|
||||
<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) => (
|
||||
<p key={i} className="py-0.5 text-lg">• {o}</p>
|
||||
<p key={i} className="py-0.5 text-[14px]">• {o}</p>
|
||||
))}
|
||||
</Panel>
|
||||
{b.hypoteser.length > 0 && (
|
||||
<Panel rubrik="Hypoteser — kräver verifiering">
|
||||
{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}
|
||||
</p>
|
||||
))}
|
||||
</Panel>
|
||||
)}
|
||||
<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) => (
|
||||
<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>
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="Rekommenderat nästa steg">
|
||||
{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 rubrik="Tillförlitlighet">
|
||||
{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}
|
||||
</p>
|
||||
))}
|
||||
</Panel>
|
||||
<Panel rubrik="Total arbetstid">
|
||||
<p className="text-2xl font-extrabold">{b.totalArbetstid}</p>
|
||||
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
@@ -900,7 +1038,7 @@ function RapportFlik({
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
innehålla uppgifter om andra kunder.
|
||||
</p>
|
||||
@@ -931,30 +1069,30 @@ function RapportFlik({
|
||||
</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).
|
||||
</p>
|
||||
</Panel>
|
||||
<Panel rubrik={`Ärende #${arende.nummer}`}>
|
||||
{b.objekt && (
|
||||
<p className="text-lg font-bold">
|
||||
<p className="text-[14px] font-semibold">
|
||||
{b.objekt.beskrivning} · {b.objekt.identifierare}
|
||||
</p>
|
||||
)}
|
||||
{b.felbeskrivning && <p className="mt-1 text-lg">Felbeskrivning: ”{b.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-lg">Total arbetstid: <span className="font-extrabold">{b.totalArbetstid}</span></p>
|
||||
{b.felbeskrivning && <p className="mt-1 text-[14px]">Felbeskrivning: ”{b.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-[14px]">Total arbetstid: <span className="font-semibold">{b.totalArbetstid}</span></p>
|
||||
{fordelning.length > 0 && (
|
||||
<div className="mt-2">
|
||||
{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>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Tidslinje">
|
||||
{kundposter.map((post) => (
|
||||
<p key={post.id} className="py-0.5 text-base">
|
||||
<span className="font-mono font-bold text-amber-400">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
<p key={post.id} className="py-0.5 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
{handelseRubrik(post)}
|
||||
</p>
|
||||
))}
|
||||
@@ -964,8 +1102,8 @@ function RapportFlik({
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{bilder.map((bild, i) => (
|
||||
<figure key={i}>
|
||||
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-zinc-700" />
|
||||
<figcaption className="mt-1 text-xs text-zinc-400">{bild.beskrivning}</figcaption>
|
||||
<img src={bild.dataUrl} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
@@ -974,11 +1112,11 @@ function RapportFlik({
|
||||
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
||||
<Panel rubrik="Rekommenderade nästa steg">
|
||||
{(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>
|
||||
) : 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.
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -43,12 +43,12 @@ function PlattformInloggning() {
|
||||
<>
|
||||
<Panel rubrik="Plattformskonto">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-lg">
|
||||
<span className="font-extrabold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||
<p className="text-[14px]">
|
||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||
{konto.organisation} — synk och AI aktiva.
|
||||
</p>
|
||||
<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={() => {
|
||||
loggaUtPlattform();
|
||||
setKonto(null);
|
||||
@@ -77,7 +77,7 @@ function PlattformInloggning() {
|
||||
<Panel rubrik="Plattformskonto">
|
||||
{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
|
||||
arbetar appen i lokalt läge. Ny organisation? Skapa konto — du blir systemadministratör.
|
||||
</p>
|
||||
@@ -112,7 +112,7 @@ function PlattformInloggning() {
|
||||
)}
|
||||
<TextFalt label="E-post" varde={epost} satt={setEpost} platshallare="anna@verkstaden.se" />
|
||||
<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">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||
Avbryt
|
||||
@@ -172,9 +172,9 @@ function AnvandarAdmin() {
|
||||
return (
|
||||
<Panel rubrik="Användare">
|
||||
{lista.map((anv) => (
|
||||
<p key={anv.id} className="border-b border-zinc-800 py-1 text-lg last:border-0">
|
||||
<span className="font-extrabold">{anv.namn}</span> · {ROLL_LABEL[anv.roll]}{" "}
|
||||
<span className="text-zinc-500">{anv.epost}</span>
|
||||
<p key={anv.id} className="border-b border-[#DDDDDD] py-1 text-[14px] last:border-0">
|
||||
<span className="font-semibold">{anv.namn}</span> · {ROLL_LABEL[anv.roll]}{" "}
|
||||
<span className="text-[#707070]">{anv.epost}</span>
|
||||
</p>
|
||||
))}
|
||||
<form
|
||||
@@ -202,17 +202,17 @@ function AnvandarAdmin() {
|
||||
key={valbar}
|
||||
type="button"
|
||||
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
|
||||
? "border-amber-400 bg-amber-400 text-zinc-950"
|
||||
: "border-zinc-600 bg-zinc-900 text-zinc-200"
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
|
||||
}`}
|
||||
>
|
||||
{ROLL_LABEL[valbar]}
|
||||
</button>
|
||||
))}
|
||||
</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}>
|
||||
Skapa användare
|
||||
</StorKnapp>
|
||||
@@ -232,7 +232,7 @@ export default function Arendelista() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Guidad Felsökning">
|
||||
<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.
|
||||
</p>
|
||||
<form
|
||||
@@ -265,20 +265,20 @@ export default function Arendelista() {
|
||||
return (
|
||||
<FelsokningSkal
|
||||
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">
|
||||
<StorKnapp className="mb-4">+ Nytt ärende</StorKnapp>
|
||||
</Link>
|
||||
|
||||
{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) => (
|
||||
<button
|
||||
key={f.id}
|
||||
onClick={() => setFilter(f.id)}
|
||||
className={`min-h-12 rounded-md text-base font-extrabold transition-colors ${
|
||||
filter === f.id ? "bg-amber-400 text-zinc-950" : "text-zinc-300 hover:bg-zinc-800"
|
||||
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
|
||||
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
||||
}`}
|
||||
>
|
||||
{f.label} · {f.antal}
|
||||
@@ -289,7 +289,7 @@ export default function Arendelista() {
|
||||
|
||||
{alla.length === 0 && (
|
||||
<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
|
||||
med komplett arbetslogg, brief och kundrapport.
|
||||
</p>
|
||||
@@ -300,7 +300,7 @@ export default function Arendelista() {
|
||||
)}
|
||||
|
||||
{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 />}
|
||||
@@ -312,7 +312,7 @@ export default function Arendelista() {
|
||||
)}
|
||||
|
||||
<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
|
||||
dokumenterar, granskar hela ärendet på begäran och skriver utkast till överlämningen. Den
|
||||
skiljer alltid på verifierat och hypotes — och slår aldrig fast en felorsak som inte är
|
||||
@@ -329,23 +329,23 @@ 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 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">
|
||||
<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}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-extrabold uppercase ${
|
||||
avslutat ? "bg-zinc-700 text-zinc-300" : "bg-amber-400 text-zinc-950"
|
||||
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||
}`}
|
||||
>
|
||||
{avslutat ? "Avslutat" : "Pågående"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-extrabold">{o ? o.beskrivning : "Okänt objekt"}</p>
|
||||
{o && <p className="text-sm font-bold text-zinc-400">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
|
||||
{fel && <p className="mt-2 text-lg text-zinc-200">”{fel}”</p>}
|
||||
<p className="mt-2 text-sm text-zinc-500">
|
||||
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Okänt objekt"}</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)}` : ""}
|
||||
{" · "}
|
||||
{b.totalArbetstid}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function DelatArende() {
|
||||
if (!arende) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ function Vallista({
|
||||
<button
|
||||
key={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
|
||||
? "border-amber-400 bg-amber-400 text-zinc-950"
|
||||
: "border-zinc-600 bg-zinc-900 text-zinc-400"
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
|
||||
}`}
|
||||
>
|
||||
{aktiv ? "✓ " : ""}
|
||||
@@ -64,7 +64,7 @@ export default function Installningar() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-lg text-zinc-300">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Organisationens inställningar hanteras av er systemadministratör.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -97,7 +97,7 @@ export default function Installningar() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-zinc-300">
|
||||
<p className="text-[#333333]">
|
||||
Välj vad som visas när ett nytt ärende startas.{" "}
|
||||
{inloggad
|
||||
? "Valet gäller alla i organisationen."
|
||||
@@ -121,8 +121,8 @@ export default function Installningar() {
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
{status === "fel" && <p className="mb-3 font-bold text-red-400">{felText}</p>}
|
||||
{status === "sparat" && <p className="mb-3 font-bold text-green-400">✓ Sparat</p>}
|
||||
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
|
||||
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]">✓ Sparat</p>}
|
||||
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
||||
{status === "sparar" ? "Sparar …" : "Spara inställningar"}
|
||||
</StorKnapp>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function NyttArende() {
|
||||
</StorKnapp>
|
||||
))}
|
||||
</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.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -80,11 +80,11 @@ export default function NyttArende() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Bekräfta objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-sm font-extrabold uppercase tracking-widest text-zinc-400">{objektet.typ}</p>
|
||||
<p className="mt-1 text-3xl font-extrabold">{objektet.beskrivning}</p>
|
||||
<p className="mt-1 text-xl font-bold text-amber-400">{objektet.identifierare}</p>
|
||||
<p className="mt-1 text-sm text-zinc-400">Identifierad via: {objektet.identifieringsmetod}</p>
|
||||
{objektet.kund && <p className="mt-1 text-lg text-zinc-200">Kund: {objektet.kund}</p>}
|
||||
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
|
||||
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
||||
<p className="mt-1 text-[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>}
|
||||
</Panel>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
@@ -110,8 +110,8 @@ export default function NyttArende() {
|
||||
rost
|
||||
/>
|
||||
{metodik && (
|
||||
<p className="mb-3 text-sm font-bold text-zinc-400">
|
||||
Vald metodik: <span className="text-amber-400">{metodik.namn}</span>
|
||||
<p className="mb-3 text-[12px] font-semibold text-[#4A5560]">
|
||||
Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span>
|
||||
</p>
|
||||
)}
|
||||
<StorKnapp
|
||||
|
||||
@@ -44,13 +44,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="col-span-2 rounded-lg border-2 border-zinc-700 bg-zinc-950 p-2">
|
||||
<p className="mb-2 text-sm font-bold uppercase text-zinc-400">Ny ansvarig tekniker</p>
|
||||
<div className="col-span-2 rounded border border-[#C6C6C6] bg-white p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">Ny ansvarig tekniker</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{personer.map((person) => (
|
||||
<button
|
||||
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 () => {
|
||||
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
|
||||
typ: "ansvarig_satt",
|
||||
@@ -68,7 +68,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
</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)}
|
||||
>
|
||||
Avbryt
|
||||
@@ -97,7 +97,7 @@ export default function Oversikt() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Organisationsöversikt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-lg text-zinc-300">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
Översikten kräver arbetsledar- eller administratörsbehörighet på plattformen.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -149,55 +149,55 @@ export default function Oversikt() {
|
||||
<FelsokningSkal
|
||||
rubrik="Organisationsöversikt"
|
||||
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>}
|
||||
{!rader && !fel && <p className="animate-pulse text-lg text-zinc-400">Hämtar …</p>}
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Hämtar …</p>}
|
||||
|
||||
{rader && (
|
||||
<>
|
||||
<Panel rubrik="Statistik">
|
||||
<div className="grid grid-cols-3 gap-3 text-center">
|
||||
<div>
|
||||
<p className="text-3xl font-extrabold">{rader.length}</p>
|
||||
<p className="text-sm font-bold uppercase text-zinc-400">Totalt</p>
|
||||
<p className="text-[20px] font-semibold">{rader.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Totalt</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-extrabold text-amber-400">{pagaende.length}</p>
|
||||
<p className="text-sm font-bold uppercase text-zinc-400">Pågående</p>
|
||||
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Pågående</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-extrabold">{avslutade.length}</p>
|
||||
<p className="text-sm font-bold uppercase text-zinc-400">Avslutade</p>
|
||||
<p className="text-[20px] font-semibold">{avslutade.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Avslutade</p>
|
||||
</div>
|
||||
</div>
|
||||
{avslutade.length > 0 && (
|
||||
<p className="mt-3 text-center text-zinc-300">
|
||||
Genomsnittlig ledtid för avslutade: <span className="font-extrabold">{formateraTid(medelLedtidMs)}</span>
|
||||
<p className="mt-3 text-center text-[#333333]">
|
||||
Genomsnittlig ledtid för avslutade: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
</p>
|
||||
)}
|
||||
</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) => (
|
||||
<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">
|
||||
<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}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-extrabold uppercase ${
|
||||
rad.avslutat ? "bg-zinc-700 text-zinc-300" : "bg-amber-400 text-zinc-950"
|
||||
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||
}`}
|
||||
>
|
||||
{rad.avslutat ? "Avslutat" : "Pågående"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xl font-extrabold">{rad.objekt ?? "Okänt objekt"}</p>
|
||||
{rad.felbeskrivning && <p className="text-lg text-zinc-200">”{rad.felbeskrivning}”</p>}
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
Ansvarig: <span className="font-bold text-zinc-300">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Okänt objekt"}</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>
|
||||
{` · ${rad.antal_handelser} händelser`}
|
||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||
</p>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PublikDelning() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Delat ärende">
|
||||
<Panel>
|
||||
<p className="text-lg text-zinc-300">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
{status === "laddar"
|
||||
? "Hämtar ärendet …"
|
||||
: "Ärendet är inte tillgängligt. Kontrollera länken med verkstaden — delningen kan ha stängts av."}
|
||||
|
||||
Reference in New Issue
Block a user