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.
|
- **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 (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
|
## Medvetna avgränsningar
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)}
|
||||||
|
|||||||
@@ -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 på den här enheten.</p>
|
<p className="text-[14px] text-[#333333]">Ärendet finns inte på 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 & osäkerheter
|
✨ AI-komplettera: risker & 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 på servern.
|
Filtreringen sker på 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 gå igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
|
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.
|
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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 på begäran och skriver utkast till överlämningen. Den
|
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
|
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());
|
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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 på plattformen.
|
Översikten kräver arbetsledar- eller administratörsbehörighet på 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>
|
||||||
|
|||||||
@@ -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."}
|
||||||
|
|||||||
Reference in New Issue
Block a user