Bäddad typografi, tätare spärrning, räta hörn och ritningsstämpel

Fyra saker som alla svarar på samma sak: ALVA såg fortfarande inte ut som
det den påstår sig vara.

Snittet som fil. Fallkedjan säkrade ordningen men inte utfallet — utan
DIN installerad föll ytan ned till vad enheten råkade ha, Helvetica på en
iPhone och Roboto på en Android. Ett system som beskriver sig som en
standard får inte se olika ut beroende på var det öppnas. IBM Plex Sans,
specens tredje tillåtna snitt, bakas därför in: latinsk delmängd, tre
textvikter och två för monospace, drygt 100 kB. Licensen följer med.

Vite bakar in woff2 som data-URI. Det är inte en optimering utan ett
krav: ALVA publiceras bland annat som en självbärande sida bakom en CSP
där varje extern begäran blockeras, och en @font-face mot en assetfil
hade tyst fallit tillbaka till enhetens eget snitt — alltså precis det
inbäddningen ska avskaffa. Bara woff2; värdapplikationens bilder är
megabyte stora och förblir egna filer.

Spärrningen ett steg tätare genomgående. DIN-satta OEM-dokument är tätare
än vad ytan var, och med rätt bokstavsform tål den det.

Räta hörn i felsökningen. 69 förekomster av rundad radie, alla ur
Lovable-arvet. Ett verkstadssystem har räta hörn.

Ritningsstämpel sist i rapporten: beteckning, regelpaket, evidensmodell,
plattformsversion, status och utgåvedatum. En granskare måste kunna se
vilka versioner som gällde när ärendet stängdes, inte vilka som gäller
den dag rapporten läses.

Fyra tester låser inbäddningen, inklusive att ingen @font-face pekar mot
en extern värd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-06 03:08:30 +00:00
parent 9668cc256b
commit 66762629df
27 changed files with 367 additions and 90 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ function Tal({ tal }: { tal: Nyckeltal }) {
</>
)}
</div>
<div className="mt-2 text-[11px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
<div className="mt-2 text-[11px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{tal.riktning === "hogre" ? "↑ bättre" : "↓ bättre"} · {tal.underlag}
</div>
<p className="mt-4 border-t pt-4 text-[12px] leading-[18px]" style={{ borderColor: FARG.lightSteel, color: FARG.steel }}>
+2 -2
View File
@@ -89,7 +89,7 @@ export default function Ansokan() {
<Block>
{FALT.map((f) => (
<div key={f.namn} className="border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
<label htmlFor={f.namn} className="block text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: FARG.steel }}>
<label htmlFor={f.namn} className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{f.etikett}
{f.kravs && <span aria-hidden="true"> ·</span>}
{f.kravs && <span className="sr-only"> (required)</span>}
@@ -105,7 +105,7 @@ export default function Ansokan() {
</div>
))}
<div className="border-t py-2" style={{ borderColor: FARG.lightSteel }}>
<label htmlFor="noteringar" className="block text-[11px] font-semibold uppercase tracking-[0.12em]" style={{ color: FARG.steel }}>
<label htmlFor="noteringar" className="block text-[11px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
Notes
</label>
<textarea
+2 -2
View File
@@ -44,7 +44,7 @@ export default function LoggaIn() {
<div key={f.namn} className="border-t py-2 first:border-t-0" style={{ borderColor: FARG.lightSteel }}>
<label
htmlFor={f.namn}
className="block text-[11px] font-semibold uppercase tracking-[0.12em]"
className="block text-[11px] font-semibold uppercase tracking-[0.08em]"
style={{ color: FARG.steel }}
>
{f.etikett}
@@ -69,7 +69,7 @@ export default function LoggaIn() {
<div className="flex items-center justify-between">
<Knapp type="submit">Sign in</Knapp>
<a href="/alva/ansokan" className="text-[12px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
<a href="/alva/ansokan" className="text-[12px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
Forgot password
</a>
</div>
+3 -3
View File
@@ -34,10 +34,10 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
<header className="border-b" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
<div className="mx-auto flex max-w-[1040px] flex-wrap items-center justify-between gap-4 px-6 py-4">
<Link to={portal ? "/alva/portal" : "/alva"} className="flex items-baseline gap-4">
<span className="text-[20px] font-semibold tracking-[0.04em]" style={{ color: FARG.graphite }}>
<span className="text-[20px] font-semibold tracking-[0.02em]" style={{ color: FARG.graphite }}>
{ALVA.namn}
</span>
<span className="hidden text-[10px] uppercase tracking-[0.16em] sm:inline" style={{ color: FARG.steel }}>
<span className="hidden text-[10px] uppercase tracking-[0.08em] sm:inline" style={{ color: FARG.steel }}>
{ALVA.utlast}
</span>
</Link>
@@ -54,7 +54,7 @@ export function Ram({ children, portal = false }: { children: ReactNode; portal?
<Link
to={l.till}
aria-current={aktiv ? "page" : undefined}
className="text-[12px] font-semibold uppercase tracking-[0.1em]"
className="text-[12px] font-semibold uppercase tracking-[0.08em]"
style={{ color: aktiv ? FARG.blue : FARG.steel }}
>
{l.text}
+4 -4
View File
@@ -40,10 +40,10 @@ export default function Start() {
<h1 className="text-[64px] font-semibold leading-none tracking-[0.02em]" style={{ color: FARG.graphite }}>
{ALVA.namn}
</h1>
<p className="mt-4 text-[15px] uppercase tracking-[0.18em]" style={{ color: FARG.steel }}>
<p className="mt-4 text-[15px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{ALVA.utlast}
</p>
<p className="mt-2 text-[13px] uppercase tracking-[0.14em]" style={{ color: FARG.steel }}>
<p className="mt-2 text-[13px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{ALVA.position}
</p>
@@ -80,7 +80,7 @@ export default function Start() {
{String(i + 1).padStart(2, "0")}
</span>
</div>
<div className="mt-4 text-[13px] font-semibold uppercase tracking-[0.1em]" style={{ color: FARG.graphite }}>
<div className="mt-4 text-[13px] font-semibold uppercase tracking-[0.08em]" style={{ color: FARG.graphite }}>
{f.namn}
</div>
<p className="mt-2 text-[14px] leading-[20px]" style={{ color: FARG.graphite }}>
@@ -191,7 +191,7 @@ export default function Start() {
<div className="mt-4 text-[15px] font-semibold" style={{ color: FARG.graphite }}>
{namn}
</div>
<div className="mt-2 text-[12px] uppercase tracking-[0.1em]" style={{ color: FARG.steel }}>
<div className="mt-2 text-[12px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
{status}
</div>
</div>
@@ -9,7 +9,7 @@ import { Fasrad } from "@/alva/komponenter";
import { Slutsatspanel } from "@/felsokning/Slutsats";
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { grinda } from "../../../../services/gemensam/grind.mjs";
import { arendebeteckning, fasDefinition } from "@/alva/system";
import { PLATTFORMSVERSION, arendebeteckning, fasDefinition } from "@/alva/system";
import {
arAvslutat,
arendeidentitet,
@@ -188,12 +188,12 @@ export default function ArendeSida() {
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
skärmar: flikrad + en kolumn. */}
<nav className="mb-3 grid grid-cols-4 gap-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
<nav className="mb-3 grid grid-cols-4 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
{FLIKAR.map((f) => (
<button
key={f.id}
onClick={() => setFlik(f.id)}
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
className={`min-h-9 text-[13px] font-semibold transition-colors ${
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`}
>
@@ -252,7 +252,7 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
</span>
) : null;
return (
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
<div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 text-[12px] print:hidden">
{del("AO", idn.arbetsorder)}
{del("Claim", idn.claim)}
{del("Skadenr", idn.skadenummer)}
@@ -267,7 +267,7 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
value={typ}
disabled={idn.avslutat}
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
className="rounded border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
className="border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
>
{ARENDETYPER.map((t) => (
<option key={t}>{t}</option>
@@ -329,11 +329,11 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
{foto && !laser && (
<div className="mt-2">
{demo && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
Demo-avläsning bildtolkningen kräver inloggning. Kontrollera värdet.
</p>
)}
<img src={foto} alt="Instrumentpanel" className="mb-2 max-h-40 rounded border border-[#C6C6C6]" />
<img src={foto} alt="Instrumentpanel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
<TextFalt label={`Mätarställning (${lage === "ingaende" ? "ingående" : "utgående"})`} varde={varde} satt={setVarde} platshallare="T.ex. 84 320 km" />
<StorKnapp
disabled={!varde.trim()}
@@ -390,7 +390,7 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
if (rader.length === 0) return null;
return (
<div className="mb-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
Tidigare ärenden {identifierare} granska innan historiken kvitteras
</p>
@@ -663,7 +663,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={o}
onClick={() => vaxla(orsaker, setOrsaker, o)}
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -677,7 +677,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={k}
onClick={() => vaxla(underlag, setUnderlag, k)}
className={`min-h-8 rounded border px-1.5 text-[12px] font-medium ${
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -691,7 +691,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
<button
key={n}
onClick={() => setSakerhet(n)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -824,7 +824,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslagsLage && (
<div className="mb-3 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
<TextFalt
label="Föreslagen åtgärd (visas för kunden i delningsvyn)"
varde={forslagText}
@@ -858,7 +858,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
)}
{forslag.length > 0 && !beslut && (
<div className="mb-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2">
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
Registrera kundens besked innan arbetet påbörjas
</p>
@@ -867,7 +867,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setBeslutsVal(val)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
beslutsVal === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -883,7 +883,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={k}
onClick={() => setKanal(k)}
className={`min-h-8 rounded border px-1 text-[12px] font-medium ${
className={`min-h-8 border px-1 text-[12px] font-medium ${
kanal === k ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
}`}
>
@@ -973,7 +973,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
@@ -1020,7 +1020,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
<button
key={val}
onClick={() => setKkResultat(val)}
className={`min-h-9 rounded border px-2 text-[12px] font-semibold ${
className={`min-h-9 border px-2 text-[12px] font-semibold ${
kkResultat === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
}`}
>
@@ -1078,7 +1078,7 @@ function VyTrad({
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)]">
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
Ärendevyer
</p>
@@ -1200,7 +1200,7 @@ 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 border border-[#00437A] bg-[#F7F7F7] p-4">
<div className="mb-4 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>
@@ -1472,7 +1472,7 @@ 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 border px-4 py-2 text-[12px] font-semibold transition-colors ${
className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold transition-colors ${
k === aktiv
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
@@ -1638,7 +1638,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
);
}
return (
<div className="mt-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2">
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
</p>
@@ -1647,7 +1647,7 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
@@ -1780,7 +1780,7 @@ function SnabbDokumentation({
? instRef.current?.click()
: setTyp(typ === d.id ? null : d.id)
}
className={`min-h-9 rounded border text-[12px] font-semibold transition-colors ${
className={`min-h-9 border text-[12px] font-semibold transition-colors ${
typ === d.id
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
@@ -1836,8 +1836,8 @@ function SnabbDokumentation({
/>
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
{video && (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-2">
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full rounded border border-[#C6C6C6]" />
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#C6C6C6]" />
<TextFalt
label="Vad visar videon? (obligatoriskt — det som låter eller rör sig)"
varde={video.beskrivning}
@@ -1864,9 +1864,9 @@ function SnabbDokumentation({
)}
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av instrumentet </p>}
{avlasning && (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
{avlasning.demo && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
Demo-avläsning bildtolkningen kräver inloggning. Värdena är exempeldata.
</p>
)}
@@ -1954,9 +1954,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 border border-[#ADADAD] bg-[#F7F7F7] p-4">
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#ADADAD] bg-[#F7F7F7] p-4">
<h2 className="mb-3 text-[15px] font-semibold">Överlämningsrapport</h2>
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded bg-white p-4 text-[12px] text-[#333333]">
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
{text}
</pre>
{aiLage === "vilar" && (
@@ -1969,7 +1969,7 @@ function OverlamningDialog({
<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 bg-white p-4">
<div className="mb-3 bg-white p-4">
{aiRader.map((rad, i) => (
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
@@ -2035,7 +2035,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
}
return (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-3">
<div className="mt-3 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-[#DDDDDD] py-2 last:border-0">
@@ -2046,7 +2046,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{!delning.aterkallad && (
<span className="flex gap-2">
<button
className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]"
className="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}`)
}
@@ -2054,7 +2054,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
Kopiera
</button>
<button
className="rounded border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
className="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.` });
@@ -2071,7 +2071,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
{(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => (
<button
key={niva}
className="min-h-9 rounded border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
className="min-h-9 border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => {
try {
await skapaDelning(arende.id, niva);
@@ -2105,10 +2105,10 @@ function LoggFlik({ arende }: { arende: Arende }) {
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
{post.handelse.typ === "foto" && (
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 rounded border border-[#C6C6C6]" />
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 border border-[#C6C6C6]" />
)}
{post.handelse.typ === "video" && (
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 rounded border border-[#C6C6C6]" />
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 border border-[#C6C6C6]" />
)}
</div>
</li>
@@ -2526,7 +2526,7 @@ function RapportFlik({
<Panel rubrik="Dokumenterad video">
{klipp.map((v, i) => (
<figure key={i} className="mb-2">
<Klipp bilaga={v.bilaga} className="w-full rounded border border-[#C6C6C6]" />
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
</figure>
))}
@@ -2537,7 +2537,7 @@ function RapportFlik({
<div className="grid grid-cols-2 gap-2">
{bilder.map((bild, i) => (
<figure key={i}>
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="rounded border border-[#C6C6C6]" />
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#C6C6C6]" />
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
</figure>
))}
@@ -2554,6 +2554,46 @@ function RapportFlik({
<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>
<Ritningsstampel arende={arende} />
</>
);
}
/**
* Ritningsstämpel.
*
* En teknisk ritning bär alltid ett hörn med beteckning, revision och
* status, så att ett papper på en verkstadsbänk går att härleda till
* exakt vilken utgåva det kom ur. En rapport som ska hålla i en tvist
* behöver samma sak: läsaren måste kunna se vilket regelpaket och vilken
* evidensmodell som gällde när ärendet stängdes — inte vilka som gäller
* den dag rapporten läses.
*
* Varje fält är något en granskare efterfrågar innan den litar på resten
* av dokumentet. Inget av dem är dekoration.
*/
function Ritningsstampel({ arende }: { arende: Arende }) {
const paket = aktivtRegelpaket();
const avslutad = [...arende.handelser].reverse().find((p) => p.handelse.typ === "arende_avslutat");
const falt: [string, string][] = [
["Beteckning", arendebeteckning(arende.nummer)],
["Regelpaket", paket.version],
["Evidensmodell", `ECM ${ECM_VERSION}`],
["Plattform", PLATTFORMSVERSION],
["Status", avslutad ? "Closed" : "In progress"],
["Utgåva", avslutad ? new Date(avslutad.tidpunkt).toISOString().slice(0, 10) : "—"],
];
return (
<div className="alva-yta mt-3 border border-[#C6C6C6] bg-white">
<div className="grid grid-cols-2 sm:grid-cols-3">
{falt.map(([etikett, varde]) => (
<div key={etikett} className="border-b border-r border-[#DDDDDD] px-2 py-2 last:border-r-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4A5560]">{etikett}</div>
<div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
</div>
))}
</div>
</div>
);
}
@@ -50,7 +50,7 @@ function PlattformInloggning() {
{konto.organisation} synk och beslutsstöd aktiva.
</p>
<button
className="whitespace-nowrap rounded border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
onClick={() => {
loggaUtPlattform();
setKonto(null);
@@ -189,7 +189,7 @@ function AnvandarAdmin() {
</p>
<button
type="button"
className={`shrink-0 rounded border px-2 py-1 text-[12px] font-semibold ${
className={`shrink-0 border px-2 py-1 text-[12px] font-semibold ${
anv.aktiv === false
? "border-[#1E6B34] bg-white text-[#1E6B34]"
: "border-[#6E1414] bg-white text-[#8B1A1A]"
@@ -236,7 +236,7 @@ function AnvandarAdmin() {
key={valbar}
type="button"
onClick={() => setRoll(valbar)}
className={`min-h-9 rounded border text-[12px] font-semibold ${
className={`min-h-9 border text-[12px] font-semibold ${
roll === valbar
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
@@ -312,12 +312,12 @@ export default function Arendelista() {
</Link>
{alla.length > 0 && (
<div className="mb-4 grid grid-cols-3 gap-1 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-1">
<div className="mb-4 grid grid-cols-3 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1">
{FILTER.map((f) => (
<button
key={f.id}
onClick={() => setFilter(f.id)}
className={`min-h-9 rounded text-[13px] font-semibold transition-colors ${
className={`min-h-9 text-[13px] font-semibold transition-colors ${
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
}`}
>
@@ -369,13 +369,13 @@ 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 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
<div className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
<div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{arende.nummer}
</span>
<span
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>
@@ -41,7 +41,7 @@ function Vallista({
<button
key={val}
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
className={`min-h-10 rounded border px-2 font-semibold transition-colors ${
className={`min-h-10 border px-2 font-semibold transition-colors ${
aktiv
? "border-[#00437A] bg-[#00437A] text-white"
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
@@ -90,7 +90,7 @@ function Integrationer() {
plattformen och används bara av servern de skickas aldrig till teknikernas enheter.
</p>
{!krypteringKlar && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
Kryptering är inte konfigurerad i driften (INTEGRATION_NYCKEL) uppgifter kan inte sparas förrän
nyckeln finns.
</p>
@@ -119,7 +119,7 @@ function Integrationer() {
</div>
{oppen === lev.id ? (
<div className="mt-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="mt-2 border border-[#C6C6C6] bg-white p-2">
{lev.falt.map((falt) => (
<TextFalt
key={falt.nyckel}
@@ -216,7 +216,7 @@ export default function NyttArende() {
tillbaka={{ till: "/felsokning", text: "Ärenden" }}
>
{tolkning.demo && (
<p className="mb-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
Demo-tolkning plattformens dokumenttolkning kräver inloggning. Värdena nedan är exempeldata.
</p>
)}
@@ -273,7 +273,7 @@ export default function NyttArende() {
/>
{skannarKod ? (
<div>
<video ref={videoRef} playsInline muted className="mb-2 w-full rounded border border-[#C6C6C6]" />
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
<p className="mb-2 text-[12px] text-[#707070]">
Rikta kameran mot QR-koden, streckkoden eller VIN-etiketten.
</p>
@@ -405,10 +405,10 @@ function GranskaTolkning({
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-3">
<Panel rubrik="Skannad arbetsorder">
<div className="relative">
<img src={foto} alt="Skannad arbetsorder" className="w-full rounded border border-[#C6C6C6]" />
<img src={foto} alt="Skannad arbetsorder" className="w-full border border-[#C6C6C6]" />
{vald?.omrade && (
<div
className="pointer-events-none absolute rounded-sm border-2 border-[#00437A] bg-[#00437A]/15"
className="pointer-events-none absolute border-2 border-[#00437A] bg-[#00437A]/15"
style={{
left: `${vald.omrade.x * 100}%`,
top: `${vald.omrade.y * 100}%`,
@@ -437,7 +437,7 @@ function GranskaTolkning({
<div
key={rad.id}
onClick={() => setMarkerad(rad.id)}
className={`mb-1.5 cursor-pointer rounded border p-2 ${
className={`mb-1.5 cursor-pointer border p-2 ${
markerad === rad.id ? "border-[#00437A]" : "border-transparent"
} ${niva === "gul" ? "bg-[#FFF8E1]" : niva === "rod" ? "bg-[#FBE9E9]" : "bg-white"}`}
>
@@ -457,7 +457,7 @@ function GranskaTolkning({
<input
value={rad.varde}
onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })}
className="w-full rounded border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none"
className="w-full border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none"
/>
{niva === "rod" && !rad.bekraftad && (
<button
@@ -465,7 +465,7 @@ function GranskaTolkning({
e.stopPropagation();
uppdatera(rad.id, { bekraftad: true });
}}
className="whitespace-nowrap rounded border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
>
Bekräfta
</button>
@@ -45,13 +45,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
);
}
return (
<div className="col-span-2 rounded border border-[#C6C6C6] bg-white p-2">
<div className="col-span-2 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-9 rounded border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
className="min-h-9 border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
onClick={async () => {
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
typ: "ansvarig_satt",
@@ -69,7 +69,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
</button>
))}
<button
className="min-h-9 rounded border border-[#ADADAD] font-semibold text-[#4A5560]"
className="min-h-9 border border-[#ADADAD] font-semibold text-[#4A5560]"
onClick={() => setOppen(false)}
>
Avbryt
@@ -188,7 +188,7 @@ export default function Oversikt() {
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
<span
className="h-3 rounded-sm bg-[#00437A]"
className="h-3 bg-[#00437A]"
style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }}
/>
<span className="font-semibold">{rad.antal}</span>
@@ -203,13 +203,13 @@ export default function Oversikt() {
{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 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
<div className="flex items-center justify-between gap-2">
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
Ärende #{rad.nummer}
</span>
<span
className={`rounded px-2 py-0.5 text-[11px] font-semibold uppercase ${
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
}`}
>