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:
@@ -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 }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 på {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"
|
||||
}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user