Industriell ETKA-inspirerad design + Personbil/Regnr

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-03 08:43:57 +00:00
parent 63b7720159
commit c3e831b478
16 changed files with 386 additions and 220 deletions
+234 -96
View File
@@ -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 den här enheten.</p>
<p className="text-[14px] text-[#333333]">Ärendet finns inte 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 &amp; 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 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 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>
</>
+28 -28
View File
@@ -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 begäran och skriver utkast till överlämningen. Den
skiljer alltid verifierat och hypotes och slår aldrig fast en felorsak som inte är
@@ -329,23 +329,23 @@ export default function Arendelista() {
const b = brief(arende, metodikForArende(arende), new Date().toISOString());
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}
+1 -1
View File
@@ -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>
);
}
+7 -7
View File
@@ -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>
+8 -8
View File
@@ -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
+25 -25
View File
@@ -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 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>
+1 -1
View File
@@ -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."}