Ett designsystem, inte två
Portalen höll åtta färger. Arbetsvyn höll fyrtiosju. Det var inte ett slarv utan två system: ui.tsx bar en egen designfilosofi i sitt filhuvud — "industriellt verkstadsverktyg i ETKA-tradition", egen palett, eget snitt, egen rytm — vid sidan av ALVA-SPEC-001. Två beskrivningar av hur en produkt ska se ut blir aldrig lika. Värst syntes det i typsnittet. Arbetsvyn låg utanför `.alva-yta` och ärvde därför värdapplikationens antikva — exakt samma fynd som en gång gjordes om portalen, fast här hade det stått kvar. En tekniker som klickade från portalen in i ett ärende bytte både färg och bokstavsform. De fyrtiosju färgerna avbildas nu på ALVA:s åtta. Tonade fyllningar avskaffas helt: allvarlighet bärs i linje och text, aldrig i en fylld yta. En gul varningsruta drar mer uppmärksamhet än sitt innehåll, och när tre ligger på samma skärm bär ingen av dem något. Grönt finns inte i paletten — blått bär "verifierat", vilket STATUSFARG redan sa. Skuggor och övergångar bort. Panelen är samma form som portalens Block, verktygsraden samma vita rad med en linje under, och ordmärket står först i den precis som i portalens sidhuvud. 176 avståndsklasser snäpptes till 8 px-rutnätet. Knapparna är gemener nu, i BÅDA ytorna. Etiketter och rubriker är versala i ALVA — de är uppmärkning. En knapp är en uppmaning, och i arbetsvyn är uppmaningen ofta en hel mening: "Photograph the instrument panel". Versal sats läses långsammare och bryts sämre, och just den knappen trycks med handskar bredvid ett fordon. Att låta portalen behålla versaler hade gett två knappspråk i samma produkt, vilket var det som skulle bort. Designtestet omfattar nu hela ytan — marknadswebb, portal OCH arbetsvy. Att det bara gällde halva produkten var själva orsaken till att de kunde glida isär: ett designsystem som gäller halva ytan är en stilguide. Mutationsprövat: en enda återinförd blå utanför paletten fäller det. Två fel som bara en skärmdump från en telefon kunde hitta, båda samma sak — text som var vit eller ljusgrå mot den gamla mörkblå raden och blev osynlig mot den vita: tiden i ärendehuvudet och användarnamnet i listan. Tjugo svenska strängar till, alla i mall-literaler och därför osynliga för de tidigare sveparna: loggens radtexter, överlämningsrapporten, AI-promptens fältnamn och "1 tim 37 min". 750 tester, 186 integrationskontroller, genomgång, portalspärr, typkontroll och lint gröna. Artefakten mätt på 390 och 1280 px över femton vyer: inget spill, inga externa begäranden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -341,7 +341,19 @@ export function Knapp({
|
||||
// `h-full` och inte bara `w-full`: lika breda knappar där den ena
|
||||
// texten bryts till två rader blir olika HÖGA, vilket är samma
|
||||
// obalans en rad ned. Bägge fyller sin cell.
|
||||
className={`border px-4 py-2 text-[13px] font-semibold uppercase tracking-[0.08em] disabled:opacity-40${
|
||||
// Gemener, inte versaler.
|
||||
//
|
||||
// Etiketter och rubriker är versala i ALVA — de är uppmärkning, och
|
||||
// ett tekniskt dokument märker upp sina fält så. En knapp är inte
|
||||
// uppmärkning utan en uppmaning, och i arbetsvyn är uppmaningen ofta
|
||||
// en hel mening: "Photograph the instrument panel". Versal sats är
|
||||
// långsammare att läsa och bryts sämre, och just den knappen trycks
|
||||
// med handskar bredvid ett fordon.
|
||||
//
|
||||
// Regeln gäller båda ytorna. Att låta portalen behålla versaler hade
|
||||
// gett två knappspråk i samma produkt, vilket är precis det som
|
||||
// skulle bort.
|
||||
className={`border px-4 py-2 text-[13px] font-semibold tracking-[0.02em] disabled:opacity-40${
|
||||
bred ? " h-full w-full" : ""
|
||||
}`}
|
||||
style={{
|
||||
|
||||
@@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar
|
||||
|
||||
function Platshallare({ text }: { text: string }) {
|
||||
return (
|
||||
<p className="border border-dashed border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#707070]">
|
||||
<p className="border border-dashed border-[#D7DCE2] bg-[#F6F7F8] p-4 text-[12px] text-[#4D5662]">
|
||||
{text}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -28,8 +28,8 @@ function BeslutsKnappar({
|
||||
const [fel, setFel] = useState("");
|
||||
|
||||
return (
|
||||
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-2 border-t border-[#D7DCE2] pt-2 print:hidden">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Your decision to the workshop
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||
@@ -43,14 +43,14 @@ function BeslutsKnappar({
|
||||
{val && (
|
||||
<>
|
||||
<label className="mb-2 block">
|
||||
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
{val === "avbojt" ? "Short rationale (optional)" : "Comment (optional)"}
|
||||
</span>
|
||||
<input
|
||||
value={kommentar}
|
||||
maxLength={500}
|
||||
onChange={(e) => setKommentar(e.target.value)}
|
||||
className="w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] focus:border-[#00437A] focus:outline-none"
|
||||
className="w-full border border-[#D7DCE2] bg-white px-2 py-2 text-[13px] focus:border-[#005CA9] focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
@@ -66,7 +66,7 @@ function BeslutsKnappar({
|
||||
>
|
||||
{skickar ? "Sending …" : "Send decision"}
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
The decision is recorded in the case and cannot be changed here — contact the workshop if you change your mind.
|
||||
</p>
|
||||
</>
|
||||
@@ -88,14 +88,14 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole
|
||||
["Status", avslutat ? "Closed" : "Diagnosis in progress"],
|
||||
];
|
||||
return (
|
||||
<div className="sticky top-11 z-10 mb-3 border border-[#C6C6C6] bg-[#F7F7F7] px-3 py-1.5 shadow-[0_1px_3px_rgba(0,0,0,0.1)] print:static">
|
||||
<div className="sticky top-12 z-10 mb-4 border border-[#D7DCE2] bg-[#F6F7F8] px-4 py-2 print:static">
|
||||
{idn.beskrivning && <p className="text-[13px] font-semibold">{idn.beskrivning}</p>}
|
||||
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
|
||||
{falt
|
||||
.filter(([, varde]) => varde)
|
||||
.map(([etikett, varde]) => (
|
||||
<span key={etikett} className="whitespace-nowrap">
|
||||
<span className="text-[#707070]">{etikett}:</span> <span className="font-medium">{varde}</span>
|
||||
<span className="text-[#4D5662]">{etikett}:</span> <span className="font-medium">{varde}</span>
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
@@ -145,8 +145,8 @@ export function DelatArendeVy({
|
||||
rubrik={b.objekt?.beskrivning ?? "Case"}
|
||||
hoger={
|
||||
<span
|
||||
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
|
||||
className={`px-4 py-2 text-[12px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||
}`}
|
||||
>
|
||||
{avslutat ? "Closed" : "Diagnosis in progress"}
|
||||
@@ -185,7 +185,7 @@ export function DelatArendeVy({
|
||||
{rader.map(([etikett, text]) => (
|
||||
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
|
||||
<dt className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#4D5662]">{etikett}</dt>
|
||||
<dd className="mt-1 text-[#1B1E22]">{text}</dd>
|
||||
<dd className="mt-2 text-[#1B1E22]">{text}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@@ -193,7 +193,7 @@ export function DelatArendeVy({
|
||||
);
|
||||
})()}
|
||||
|
||||
<p className="mb-4 border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
|
||||
<p className="mb-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4 text-[12px] text-[#4D5662] print:hidden">
|
||||
{notis}
|
||||
</p>
|
||||
|
||||
@@ -212,20 +212,20 @@ export function DelatArendeVy({
|
||||
<div key={p.id} className="mb-2 last:mb-0">
|
||||
<p className="text-[14px]">{h.beskrivning}</p>
|
||||
{h.uppskattadKostnad && (
|
||||
<p className="text-[13px] text-[#4A5560]">Uppskattad kostnad: {h.uppskattadKostnad}</p>
|
||||
<p className="text-[13px] text-[#4D5662]">Uppskattad kostnad: {h.uppskattadKostnad}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{beslut?.handelse.typ === "kundbeslut" ? (
|
||||
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[13px] font-semibold">
|
||||
<p className="mt-2 border-t border-[#D7DCE2] pt-2 text-[13px] font-semibold">
|
||||
Ditt besked: {KUNDBESLUT_LABEL[beslut.handelse.beslut]}{" "}
|
||||
<span className="font-normal text-[#4A5560]">(registrerat via {beslut.handelse.kanal})</span>
|
||||
<span className="font-normal text-[#4D5662]">(registrerat via {beslut.handelse.kanal})</span>
|
||||
</p>
|
||||
) : vidBeslut ? (
|
||||
<BeslutsKnappar vidBeslut={vidBeslut} />
|
||||
) : (
|
||||
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[12px] text-[#707070]">
|
||||
<p className="mt-2 border-t border-[#D7DCE2] pt-2 text-[12px] text-[#4D5662]">
|
||||
Awaiting your decision — contact the workshop to approve or decline.
|
||||
</p>
|
||||
)}
|
||||
@@ -234,11 +234,11 @@ export function DelatArendeVy({
|
||||
|
||||
<Panel rubrik="Current status">
|
||||
{b.utfordaKontroller.map((k, i) => (
|
||||
<p key={`u${i}`} className="py-0.5 text-[14px]"><span className="text-[#1E6B34]"><IkonCheck /></span> {k.text}</p>
|
||||
<p key={`u${i}`} className="py-0 text-[14px]"><span className="text-[#005CA9]"><IkonCheck /></span> {k.text}</p>
|
||||
))}
|
||||
{!avslutat && pagaende && <p className="py-0.5 text-[14px] text-[#00437A]"><IkonUppdatera /> {pagaende}</p>}
|
||||
{!avslutat && pagaende && <p className="py-0 text-[14px] text-[#005CA9]"><IkonUppdatera /> {pagaende}</p>}
|
||||
{b.ejKontrollerat.slice(avslutat ? 0 : 1).map((e, i) => (
|
||||
<p key={`e${i}`} className="py-0.5 text-[14px] text-[#4A5560]"><IkonKlocka /> {e}</p>
|
||||
<p key={`e${i}`} className="py-0 text-[14px] text-[#4D5662]"><IkonKlocka /> {e}</p>
|
||||
))}
|
||||
</Panel>
|
||||
|
||||
@@ -246,8 +246,8 @@ export function DelatArendeVy({
|
||||
<Panel rubrik="Video">
|
||||
{klipp.map((v, i) => (
|
||||
<figure key={i} className="mb-2">
|
||||
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
|
||||
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#D7DCE2]" />
|
||||
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</Panel>
|
||||
@@ -257,8 +257,8 @@ export function DelatArendeVy({
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{bilder.map((bild, i) => (
|
||||
<figure key={i}>
|
||||
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
||||
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#D7DCE2]" />
|
||||
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
@@ -273,9 +273,9 @@ export function DelatArendeVy({
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "matvarde") return null;
|
||||
return (
|
||||
<tr key={p.id} className="border-b border-[#DDDDDD] last:border-0">
|
||||
<td className="py-1 pr-3 text-[#333333]">{h.beskrivning}</td>
|
||||
<td className="py-1 font-semibold">
|
||||
<tr key={p.id} className="border-b border-[#D7DCE2] last:border-0">
|
||||
<td className="py-2 pr-4 text-[#1B1E22]">{h.beskrivning}</td>
|
||||
<td className="py-2 font-semibold">
|
||||
{h.varde}
|
||||
{h.enhet ? ` ${h.enhet}` : ""}
|
||||
</td>
|
||||
@@ -289,8 +289,8 @@ export function DelatArendeVy({
|
||||
|
||||
<Panel rubrik="Timeline">
|
||||
{kundposter.map((post) => (
|
||||
<p key={post.id} className="py-0.5 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
<p key={post.id} className="py-0 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
{handelseRubrik(post)}
|
||||
</p>
|
||||
))}
|
||||
@@ -305,13 +305,13 @@ export function DelatArendeVy({
|
||||
<Panel rubrik="Labour time">
|
||||
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
||||
{tidsfordelningsRader(arende, nu).map((r) => (
|
||||
<p key={r.label} className="text-[#333333]">
|
||||
<p key={r.label} className="text-[#1B1E22]">
|
||||
{r.label}: {r.tid}
|
||||
</p>
|
||||
))}
|
||||
</Panel>
|
||||
|
||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
||||
<p className="text-center text-[11px] text-[#4D5662]">
|
||||
Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log
|
||||
</p>
|
||||
</FelsokningSkal>
|
||||
|
||||
@@ -48,10 +48,10 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
||||
type="button"
|
||||
onClick={() => (lyssnar ? stoppa() : starta())}
|
||||
aria-label={lyssnar ? "Stop recording" : "Start voice input"}
|
||||
className={`min-h-8 min-w-11 border px-3 text-[14px] font-semibold transition-colors ${
|
||||
className={`min-h-8 min-w-11 border px-4 text-[14px] font-semibold ${
|
||||
lyssnar
|
||||
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
|
||||
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
|
||||
? "border-[#8B1A1A] bg-[#8B1A1A] text-white"
|
||||
: "border-[#D7DCE2] bg-white text-[#1B1E22] hover:border-[#005CA9]"
|
||||
}`}
|
||||
>
|
||||
{lyssnar ? "■" : <IkonMik />}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
||||
>
|
||||
{etikett}
|
||||
</label>
|
||||
<p className="mt-1 text-[13px]" style={{ color: FARG.graphite }}>
|
||||
<p className="mt-2 text-[13px]" style={{ color: FARG.graphite }}>
|
||||
{f.fraga}
|
||||
</p>
|
||||
<textarea
|
||||
@@ -189,7 +189,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
||||
color: FARG.graphite,
|
||||
}}
|
||||
/>
|
||||
<p id={`slutsats-${f.id}-hjalp`} className="mt-1 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
||||
<p id={`slutsats-${f.id}-hjalp`} className="mt-2 text-[12px] leading-[16px]" style={{ color: FARG.steel }}>
|
||||
{fel ? fel.text : f.hjalp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@ describe("AI-orkestern", () => {
|
||||
"Reläet klickar inte.",
|
||||
);
|
||||
expect(prompt).toContain("Volvo XC60 D4 2019");
|
||||
expect(prompt).toContain("Felbeskrivning:");
|
||||
expect(prompt).toContain("Fault description:");
|
||||
expect(prompt).toContain("Kontrollera lufttryck");
|
||||
expect(prompt).toContain("Ej kontrollerat enligt metodiken:");
|
||||
expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte.");
|
||||
@@ -156,8 +156,8 @@ describe("AI-orkestern", () => {
|
||||
it("granskningsprompten innehåller hela arbetsloggen men ingen ny inmatning", () => {
|
||||
const arende = byggDemoArende(1);
|
||||
const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK);
|
||||
expect(prompt).toContain("Fullständig arbetslogg:");
|
||||
expect(prompt).toContain("Arbete överlämnat från Anna till Johan");
|
||||
expect(prompt).toContain("Full work log:");
|
||||
expect(prompt).toContain("Work handed over from Anna to Johan");
|
||||
expect(prompt).not.toContain("Teknikerns nya inmatning");
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,16 @@ import { readdirSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FORBJUDNA_ORD } from "@/alva/sprak";
|
||||
|
||||
const KATALOGER = ["src/alva", "src/pages/alva"];
|
||||
// ---- Vad testet omfattar ------------------------------------------------
|
||||
//
|
||||
// Arbetsvyn låg länge utanför. Följden var förutsägbar och blev verklig:
|
||||
// portalen höll åtta färger, arbetsvyn fyrtiosju, och de två delarna av
|
||||
// samma produkt såg ut som två produkter. Ett designsystem som bara gäller
|
||||
// halva ytan är en stilguide, inte ett system.
|
||||
//
|
||||
// Katalogerna nedan är därför hela ALVA-ytan — marknadswebb, portal OCH
|
||||
// det gränssnitt teknikern faktiskt arbetar i.
|
||||
const KATALOGER = ["src/alva", "src/pages/alva", "src/felsokning", "src/pages/felsokning"];
|
||||
|
||||
const filer = KATALOGER.flatMap((katalog) =>
|
||||
readdirSync(katalog)
|
||||
@@ -28,7 +37,17 @@ const utanKommentarer = (kod: string) =>
|
||||
|
||||
describe("ALVA-ytan följer designsystemet", () => {
|
||||
it("hittar filerna att granska", () => {
|
||||
expect(filer.length).toBeGreaterThanOrEqual(7);
|
||||
// Höjt från 7 när arbetsvyn kom med. Talet är motvikten mot en
|
||||
// katalog som råkar tömmas: ett test som granskar noll filer är
|
||||
// grönt av fel skäl.
|
||||
expect(filer.length).toBeGreaterThanOrEqual(25);
|
||||
});
|
||||
|
||||
it("granskar både portalen och arbetsvyn", () => {
|
||||
// Det som gick fel var att halva produkten stod utanför regeln.
|
||||
expect(filer.some(([n]) => n.startsWith("src/pages/alva/"))).toBe(true);
|
||||
expect(filer.some(([n]) => n.startsWith("src/pages/felsokning/"))).toBe(true);
|
||||
expect(filer.some(([n]) => n === "src/felsokning/ui.tsx")).toBe(true);
|
||||
});
|
||||
|
||||
it.each(filer)("%s använder ingen gradient, glaseffekt eller skugga", (_namn, kod) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("demoärendet", () => {
|
||||
|
||||
it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => {
|
||||
const b = brief(arende, VIBRATION_METODIK);
|
||||
expect(b.totalArbetstid).toBe("1 tim 35 min");
|
||||
expect(b.totalArbetstid).toBe("1 h 35 min");
|
||||
expect(arende.handelser.some((p) => p.handelse.typ === "overlamning")).toBe(true);
|
||||
expect(new Set(arende.handelser.map((p) => p.anvandare)).size).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
@@ -380,7 +380,7 @@ describe("Videoevidens (E3) och signerat avslut", () => {
|
||||
|
||||
it("avslutet signeras och grinden redovisar signaturen", async () => {
|
||||
const { handelseRubrik, nyLoggPost: ny } = await import("../domain");
|
||||
expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signerad av Erik");
|
||||
expect(handelseRubrik(ny("Erik", { typ: "arende_avslutat", signatur: "Erik" }))).toContain("signed by Erik");
|
||||
const stangd = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat", signatur: "Erik" }]);
|
||||
const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering");
|
||||
expect(rad?.ok).toBe(true);
|
||||
|
||||
@@ -54,9 +54,9 @@ export interface AiSvarMedModell extends AiSvar {
|
||||
|
||||
export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string {
|
||||
const rader: string[] = [];
|
||||
rader.push(`Metodik: ${metodikNamn}`);
|
||||
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
||||
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
|
||||
rader.push(`Methodology: ${metodikNamn}`);
|
||||
if (brief.objekt) rader.push(`Object: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
||||
if (brief.felbeskrivning) rader.push(`Fault description: ${brief.felbeskrivning}`);
|
||||
if (brief.utfordaKontroller.length > 0) {
|
||||
rader.push("Checks performed:");
|
||||
for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`);
|
||||
@@ -81,7 +81,7 @@ export function byggGranskningsPrompt(arende: Arende, metodik: Metodik): string
|
||||
const logg = arende.handelser
|
||||
.map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`)
|
||||
.join("\n");
|
||||
return `${kontext}\n\nFullständig arbetslogg:\n${logg}`;
|
||||
return `${kontext}\n\nFull work log:\n${logg}`;
|
||||
}
|
||||
|
||||
export function tolkaAiSvar(data: unknown): AiSvar {
|
||||
|
||||
@@ -16,14 +16,14 @@ function demoFoto(text: string): string {
|
||||
canvas.height = 360;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("canvas saknas");
|
||||
ctx.fillStyle = "#27272a";
|
||||
ctx.fillStyle = "#1B1E22";
|
||||
ctx.fillRect(0, 0, 480, 360);
|
||||
ctx.strokeStyle = "#52525b";
|
||||
ctx.strokeStyle = "#4D5662";
|
||||
ctx.lineWidth = 22;
|
||||
ctx.beginPath();
|
||||
ctx.arc(240, 170, 110, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = "#a1a1aa";
|
||||
ctx.strokeStyle = "#4D5662";
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(240, 170, 58, 0, Math.PI * 2);
|
||||
@@ -35,11 +35,11 @@ function demoFoto(text: string): string {
|
||||
ctx.lineTo(240 + Math.cos(v) * 58, 170 + Math.sin(v) * 58);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = "#fbbf24";
|
||||
ctx.fillStyle = "#8A5A00";
|
||||
ctx.font = "bold 22px sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText(text, 240, 330);
|
||||
ctx.fillStyle = "#71717a";
|
||||
ctx.fillStyle = "#4D5662";
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.fillText("Demobild", 240, 26);
|
||||
return canvas.toDataURL("image/jpeg", 0.7);
|
||||
|
||||
@@ -279,18 +279,18 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
case "objekt_identifierat":
|
||||
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
|
||||
case "arbetsorder_skannad":
|
||||
return `Arbetsorder skannad — ${h.falt.length} fält tolkade`;
|
||||
return `Work order scanned — ${h.falt.length} fields interpreted`;
|
||||
case "felbeskrivning":
|
||||
return `Felbeskrivning registrerad`;
|
||||
case "fraga_besvarad":
|
||||
return `${h.fraga} — ${h.svar}`;
|
||||
case "kontroll_utford":
|
||||
if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`;
|
||||
return h.resultat ? `${h.text} — ${h.resultat}` : `${h.text} — utförd`;
|
||||
return h.resultat ? `${h.text} — ${h.resultat}` : `${h.text} — performed`;
|
||||
case "observation":
|
||||
return `Observation: ${h.text}`;
|
||||
case "matvarde":
|
||||
return `Mätvärde: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`;
|
||||
return `Measurement: ${h.beskrivning} = ${h.varde}${h.enhet ? ` ${h.enhet}` : ""}`;
|
||||
case "hypotes":
|
||||
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
|
||||
case "foto":
|
||||
@@ -304,18 +304,18 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
case "inaktivitet_forklarad":
|
||||
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
|
||||
case "overlamning":
|
||||
return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`;
|
||||
return h.till ? `Work handed over from ${h.fran} to ${h.till}` : `Work handed over by ${h.fran}`;
|
||||
case "ansvarig_satt":
|
||||
return `Ansvarig tekniker: ${h.ansvarig}`;
|
||||
case "arendetyp_satt":
|
||||
return `Ärendetyp: ${h.arendetyp}`;
|
||||
return `Case type: ${h.arendetyp}`;
|
||||
case "historik_kontrollerad":
|
||||
return h.kontrollerad
|
||||
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
|
||||
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
|
||||
case "matarstallning":
|
||||
if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`;
|
||||
return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`;
|
||||
if (h.undantag) return `Odometer reading (${h.lage === "ingaende" ? "in" : "out"}) could not be documented: ${h.undantag}`;
|
||||
return `Odometer ${h.lage === "ingaende" ? "in" : "out"}: ${h.varde}`;
|
||||
case "reproducering":
|
||||
return h.status === "ja"
|
||||
? `Symptomet reproducerat: ${h.beskrivning}`
|
||||
@@ -325,22 +325,22 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
case "felorsak":
|
||||
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse} — ${h.orsaker.join(", ")}`;
|
||||
case "atgardsforslag":
|
||||
return `Åtgärdsförslag till kund: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`;
|
||||
return `Proposed action for the customer: ${h.beskrivning}${h.uppskattadKostnad ? ` — uppskattad kostnad ${h.uppskattadKostnad}` : ""}`;
|
||||
case "kundbeslut":
|
||||
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? ` — ${h.kommentar}` : ""}`;
|
||||
case "atgard_utford":
|
||||
return h.utford
|
||||
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
||||
: `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`;
|
||||
? `Action performed: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
||||
: `No action performed — ${h.motivering ?? "reason missing"}`;
|
||||
case "kvalitetskontroll":
|
||||
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
|
||||
case "export_skapad":
|
||||
return `Export skapad: ${h.format}, version ${h.version}`;
|
||||
case "ai_svar": {
|
||||
const forsta = h.rader[0];
|
||||
return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`;
|
||||
return `AI: ${forsta ? `${forsta.text} ` : ""}— Next step: ${h.nastaSteg}`;
|
||||
}
|
||||
case "arende_avslutat":
|
||||
return h.signatur ? `Felsökning avslutad — signerad av ${h.signatur}` : "Diagnosis closed";
|
||||
return h.signatur ? `Diagnosis closed — signed by ${h.signatur}` : "Diagnosis closed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,7 +751,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
rubrik: "Photographs present for checks that require them",
|
||||
ok: fotoKravUtanFoto === 0,
|
||||
kravs: true,
|
||||
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined,
|
||||
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} check(s) requiring a photograph have no image in the log.` : undefined,
|
||||
});
|
||||
|
||||
// Compliance Engine: ärendetypens regler ur det aktiva regelpaketet
|
||||
@@ -794,7 +794,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
||||
|
||||
rader.push({
|
||||
id: "evidensniva",
|
||||
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
|
||||
rubrik: `Evidence level: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
|
||||
ok: evidensNiva(arende) !== "E0",
|
||||
kravs: true,
|
||||
detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined,
|
||||
|
||||
@@ -252,7 +252,7 @@ export function formateraTid(ms: number): string {
|
||||
const h = Math.floor(minuter / 60);
|
||||
const m = minuter % 60;
|
||||
if (h === 0) return `${m} min`;
|
||||
return `${h} tim ${m} min`;
|
||||
return `${h} h ${m} min`;
|
||||
}
|
||||
|
||||
export interface TillforlitlighetsRad {
|
||||
@@ -328,9 +328,9 @@ export function brief(arende: Arende, metodik: Metodik, nu?: string): Brief {
|
||||
export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string): string {
|
||||
const b = brief(arende, metodik, nu);
|
||||
const rader: string[] = [];
|
||||
rader.push(`ÖVERLÄMNING – Ärende #${arende.nummer}`);
|
||||
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`);
|
||||
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
|
||||
rader.push(`HANDOVER – Case #${arende.nummer}`);
|
||||
if (b.objekt) rader.push(`Object: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Customer: ${b.objekt.kund}` : ""}`);
|
||||
if (b.felbeskrivning) rader.push(`Fault description: ${b.felbeskrivning}`);
|
||||
rader.push("");
|
||||
rader.push("Checks performed:");
|
||||
if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
// Delade UI-komponenter för Guidad Felsökning.
|
||||
// Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition —
|
||||
// plana ytor, skarpa kanter, tät informationslayout, dämpad palett
|
||||
// (ljusgrå ytor, djup marinblå som primärfärg), inga dekorationer.
|
||||
// Verktygsrad ~44 px, rektangulära knappar (max 4 px radie), kompakta
|
||||
// formulär med vänsterställda etiketter.
|
||||
//
|
||||
// ---- Ett system, inte två -----------------------------------------------
|
||||
//
|
||||
// Det här filhuvudet beskrev tidigare en egen designfilosofi: "industriellt
|
||||
// verkstadsverktyg i ETKA-tradition" med egen palett, eget snitt och egen
|
||||
// rytm. Den beskrivningen var inte fel i sak — men den var en ANDRA
|
||||
// beskrivning, vid sidan av ALVA-SPEC-001, och två designsystem i samma
|
||||
// produkt blir aldrig lika. De blev det inte heller: portalen höll åtta
|
||||
// färger, arbetsvyn fyrtiosju.
|
||||
//
|
||||
// Skillnaden syntes värst i typsnittet. Arbetsvyn låg utanför `.alva-yta`
|
||||
// och ärvde därför värdapplikationens antikva — samma fynd som en gång
|
||||
// gjordes om portalen, fast här hade det stått kvar. En tekniker som
|
||||
// klickar från portalen in i ett ärende bytte alltså både färg och
|
||||
// bokstavsform, och läste det som två produkter.
|
||||
//
|
||||
// Komponenterna nedan bär nu ALVA:s språk: FARG-paletten, 8 px-rutnätet,
|
||||
// versala sektionsetiketter, inga skuggor, ingen rörelse.
|
||||
//
|
||||
// ---- Det som ändå skiljer, och varför ------------------------------------
|
||||
//
|
||||
// STORLEK. Knappar och träffytor är större här än i portalen. Det är inte
|
||||
// en annan estetik utan ett annat bruk: portalen läses vid ett skrivbord,
|
||||
// arbetsvyn används med handskar bredvid ett fordon. Måtten följer samma
|
||||
// rutnät, de är bara fler steg på det.
|
||||
//
|
||||
// TÄTHET. Arbetsvyn visar mer per skärm. Samma skäl: teknikern ska se
|
||||
// ärendets tillstånd utan att bläddra.
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { useRef, type ReactNode } from "react";
|
||||
@@ -11,13 +34,8 @@ import type { Tillforlitlighet } from "./domain";
|
||||
import { TILLFORLITLIGHET_LABEL } from "./domain";
|
||||
import { MikrofonKnapp } from "./Mikrofon";
|
||||
import { IkonPunkt } from "./ikoner";
|
||||
|
||||
// Industriell palett (inspirerad av tyska verkstadssystem):
|
||||
// bakgrund #ECECEC · paneler #F7F7F7 · kanter #C6C6C6
|
||||
// primär (djup blå) #00437A · markerad rad #D6E4F2
|
||||
// varning #9A6700 · fel #8B1A1A · ok #1E6B34
|
||||
|
||||
const TYPSNITT = "'Inter', 'IBM Plex Sans', 'Segoe UI', Roboto, system-ui, sans-serif";
|
||||
import { Etikett, FARG } from "@/alva/komponenter";
|
||||
import { ALVA } from "@/alva/system";
|
||||
|
||||
export function FelsokningSkal({
|
||||
rubrik,
|
||||
@@ -34,32 +52,62 @@ export function FelsokningSkal({
|
||||
}) {
|
||||
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
|
||||
return (
|
||||
// `alva-yta` bär typografin ur ALVA-SPEC-001 §6 och håller den skild
|
||||
// från värdapplikationens antikva. Utan den klassen ärver arbetsvyn
|
||||
// butikens snitt, vilket är exakt vad som hände innan.
|
||||
<div
|
||||
className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]"
|
||||
style={{ fontFamily: TYPSNITT }}
|
||||
className="alva-yta felsokning-print min-h-screen text-[13px]"
|
||||
style={{ background: FARG.background, color: FARG.graphite }}
|
||||
>
|
||||
{/* Verktygsrad: låg, mörk marinblå — breadcrumb till vänster,
|
||||
status till höger. Inget stort sidhuvud. */}
|
||||
<header className="sticky top-0 z-20 border-b border-[#0A2A4A] bg-[#123A63] px-3 text-white print:static">
|
||||
<div className={`mx-auto flex min-h-11 ${maxBredd} items-center justify-between gap-3 py-1`}>
|
||||
{/* Verktygsraden är vit med en enda linje under, precis som
|
||||
portalens sidhuvud. Den var tidigare mörkt marinblå — ett eget
|
||||
varumärke inuti varumärket. */}
|
||||
<header
|
||||
className="sticky top-0 z-20 border-b px-4 print:static"
|
||||
style={{ borderColor: FARG.lightSteel, background: FARG.white }}
|
||||
>
|
||||
<div className={`mx-auto flex min-h-12 ${maxBredd} items-center justify-between gap-4 py-2`}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{/* Ordmärket står först, precis som i portalens sidhuvud.
|
||||
Utan det bytte teknikern huvud när den klickade sig från
|
||||
portalen in i ett ärende, och två sidhuvuden i rad läses
|
||||
som två system. Länken går till översikten och inte till
|
||||
portalen: arbetsvyn fungerar i lokalt läge utan session,
|
||||
och en länk som studsar till inloggningen vore sämre än
|
||||
ingen länk. */}
|
||||
<Link
|
||||
to="/alva"
|
||||
className="hidden text-[14px] font-semibold tracking-[0.02em] sm:inline"
|
||||
style={{ color: FARG.graphite }}
|
||||
>
|
||||
{ALVA.namn}
|
||||
</Link>
|
||||
<span className="hidden sm:inline" style={{ color: FARG.lightSteel }}>
|
||||
/
|
||||
</span>
|
||||
{tillbaka && (
|
||||
<>
|
||||
<Link
|
||||
to={tillbaka.till}
|
||||
className="whitespace-nowrap text-[12px] font-medium text-[#A9C3DE] hover:text-white hover:underline"
|
||||
className="whitespace-nowrap text-[12px] font-semibold uppercase tracking-[0.08em] hover:underline"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
← {tillbaka.text}
|
||||
</Link>
|
||||
<span className="text-[#5B7CA0]">/</span>
|
||||
<span style={{ color: FARG.lightSteel }}>/</span>
|
||||
</>
|
||||
)}
|
||||
<h1 className="truncate text-[14px] font-semibold tracking-tight">{rubrik}</h1>
|
||||
<h1
|
||||
className="truncate text-[14px] font-semibold uppercase tracking-[0.02em]"
|
||||
style={{ color: FARG.graphite }}
|
||||
>
|
||||
{rubrik}
|
||||
</h1>
|
||||
</div>
|
||||
{hoger}
|
||||
</div>
|
||||
</header>
|
||||
<main className={`mx-auto ${maxBredd} px-3 py-4 pb-20`}>{children}</main>
|
||||
<main className={`mx-auto ${maxBredd} px-4 py-4 pb-20`}>{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -79,18 +127,22 @@ export function StorKnapp({
|
||||
type?: "button" | "submit";
|
||||
className?: string;
|
||||
}) {
|
||||
const farg =
|
||||
// Samma tre vikter som portalens Knapp: fylld, kontur, och den spärrade
|
||||
// varianten i oxidrött. Ingen hovring som byter färg — en yta som
|
||||
// ändrar sig när muspekaren råkar passera är rörelse utan innehåll.
|
||||
const stil =
|
||||
variant === "primar"
|
||||
? "border border-[#003561] bg-[#00437A] text-white hover:bg-[#005A9E] active:bg-[#003561]"
|
||||
? { background: FARG.graphite, color: FARG.white, borderColor: FARG.graphite }
|
||||
: variant === "fara"
|
||||
? "border border-[#6E1414] bg-[#8B1A1A] text-white hover:bg-[#A32020] active:bg-[#6E1414]"
|
||||
: "border border-[#ADADAD] bg-white text-[#1A1A1A] hover:bg-[#EDF2F7] hover:border-[#00437A] active:bg-[#D6E4F2]";
|
||||
? { background: FARG.stoppat, color: FARG.white, borderColor: FARG.stoppat }
|
||||
: { background: FARG.white, color: FARG.graphite, borderColor: FARG.steel };
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={`min-h-10 w-full px-3 py-1.5 text-[13px] font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${farg} ${className}`}
|
||||
className={`min-h-12 w-full border px-4 py-2 text-[13px] font-semibold tracking-[0.02em] disabled:cursor-not-allowed disabled:opacity-40 print:hidden ${className}`}
|
||||
style={stil}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
@@ -98,27 +150,44 @@ export function StorKnapp({
|
||||
}
|
||||
|
||||
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
|
||||
// Samma form som portalens Block: vit ruta, en linje runt, och en
|
||||
// rubrikrad i bakgrundstonen. Ingen skugga — ALVA:s ytor ligger i
|
||||
// samma plan.
|
||||
return (
|
||||
<section className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-3 shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
|
||||
<section className="mb-4 border" style={{ borderColor: FARG.lightSteel, background: FARG.white }}>
|
||||
{rubrik && (
|
||||
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
||||
{rubrik}
|
||||
</h2>
|
||||
<header
|
||||
className="border-b px-4 py-2"
|
||||
style={{ borderColor: FARG.lightSteel, background: FARG.background }}
|
||||
>
|
||||
<h2
|
||||
className="text-[11px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
{rubrik}
|
||||
</h2>
|
||||
</header>
|
||||
)}
|
||||
{children}
|
||||
<div className="px-4 py-2">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Tillförlitlighet anges med ord OCH punkt, aldrig med enbart färg.
|
||||
// Grönt finns inte i ALVA:s palett: blått bär "verifierat" (se
|
||||
// STATUSFARG i komponenter.tsx), oxidgult "osäkert", oxidrött "svagt".
|
||||
const NIVA_FARG: Record<Tillforlitlighet, string> = {
|
||||
hog: "#1E6B34",
|
||||
medel: "#C08A00",
|
||||
lag: "#8B1A1A",
|
||||
hog: FARG.blue,
|
||||
medel: FARG.varning,
|
||||
lag: FARG.stoppat,
|
||||
};
|
||||
|
||||
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap text-[12px] font-semibold">
|
||||
<span
|
||||
className="inline-flex items-center gap-2 whitespace-nowrap text-[12px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: NIVA_FARG[niva] }}
|
||||
>
|
||||
<IkonPunkt farg={NIVA_FARG[niva]} />
|
||||
{TILLFORLITLIGHET_LABEL[niva]}
|
||||
</span>
|
||||
@@ -146,12 +215,12 @@ export function TextFalt({
|
||||
// granskar och bekräftar alltid innan något sparas i loggen.
|
||||
const vardeRef = useRef(varde);
|
||||
vardeRef.current = varde;
|
||||
const klass =
|
||||
"w-full border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] text-[#1A1A1A] placeholder-[#9AA0A6] focus:border-[#00437A] focus:outline-none focus:ring-1 focus:ring-[#00437A]";
|
||||
const klass = "w-full border px-2 py-2 text-[13px] focus:outline-none";
|
||||
const stil = { borderColor: FARG.steel, background: FARG.white, color: FARG.graphite };
|
||||
return (
|
||||
<label className="mb-2.5 block">
|
||||
<span className="mb-1 flex min-h-6 items-center justify-between gap-2">
|
||||
<span className="text-left text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">{label}</span>
|
||||
<label className="mb-4 block">
|
||||
<span className="mb-2 flex min-h-6 items-center justify-between gap-2">
|
||||
<Etikett>{label}</Etikett>
|
||||
{rost && (
|
||||
<MikrofonKnapp
|
||||
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
|
||||
@@ -159,7 +228,14 @@ export function TextFalt({
|
||||
)}
|
||||
</span>
|
||||
{flerRad ? (
|
||||
<textarea value={varde} onChange={(e) => satt(e.target.value)} placeholder={platshallare} rows={3} className={klass} />
|
||||
<textarea
|
||||
value={varde}
|
||||
onChange={(e) => satt(e.target.value)}
|
||||
placeholder={platshallare}
|
||||
rows={3}
|
||||
className={klass}
|
||||
style={stil}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type={losenord ? "password" : "text"}
|
||||
@@ -167,6 +243,7 @@ export function TextFalt({
|
||||
onChange={(e) => satt(e.target.value)}
|
||||
placeholder={platshallare}
|
||||
className={klass}
|
||||
style={stil}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { KUNDBESLUT_LABEL, KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLI
|
||||
import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
||||
import { nastaSteg } from "@/felsokning/metodik";
|
||||
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
|
||||
import { Fasrad } from "@/alva/komponenter";
|
||||
import { FARG, Fasrad } from "@/alva/komponenter";
|
||||
import { Slutsatspanel } from "@/felsokning/Slutsats";
|
||||
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
|
||||
import { grinda } from "../../../../services/gemensam/grind.mjs";
|
||||
@@ -145,7 +145,7 @@ export default function ArendeSida() {
|
||||
if (!arende || !id) {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Case not found" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<p className="text-[14px] text-[#333333]">The case does not exist on this device.</p>
|
||||
<p className="text-[14px] text-[#1B1E22]">The case does not exist on this device.</p>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
}
|
||||
@@ -175,9 +175,24 @@ export default function ArendeSida() {
|
||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||
hoger={
|
||||
<div className="text-right">
|
||||
<p className="text-[13px] font-semibold text-white">{total}</p>
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
|
||||
{avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]}
|
||||
{/* Vit text var rätt när verktygsraden var mörkt marinblå. När
|
||||
raden blev vit försvann tiden helt och statusraden blev en
|
||||
grå skugga — det syntes först i en skärmdump från en
|
||||
telefon, vilket är den enda kontroll som ser vad
|
||||
teknikern ser. */}
|
||||
<p className="font-mono text-[13px] font-semibold" style={{ color: FARG.graphite }}>
|
||||
{total}
|
||||
</p>
|
||||
{/* Synkläget bryts bort på telefon i stället för att bryta
|
||||
raden. Ärendets tillstånd är det som måste synas; var
|
||||
loggen ligger är en driftdetalj och står kvar under
|
||||
Technical information. */}
|
||||
<p
|
||||
className="whitespace-nowrap text-[10px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{ color: FARG.steel }}
|
||||
>
|
||||
{avslutat ? "Closed" : "In progress"}
|
||||
<span className="hidden sm:inline"> · {SYNKSTATUS_LABEL[synkStatus]}</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
@@ -189,13 +204,13 @@ 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 border border-[#C6C6C6] bg-[#F7F7F7] p-1 lg:hidden">
|
||||
<nav className="mb-4 grid grid-cols-4 gap-2 border border-[#D7DCE2] bg-[#F6F7F8] p-2 lg:hidden">
|
||||
{FLIKAR.map((f) => (
|
||||
<button
|
||||
key={f.id}
|
||||
onClick={() => setFlik(f.id)}
|
||||
className={`min-h-9 text-[13px] font-semibold transition-colors ${
|
||||
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
||||
className={`min-h-9 text-[13px] font-semibold ${
|
||||
flik === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -203,7 +218,7 @@ export default function ArendeSida() {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<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]">
|
||||
<div className="lg:grid lg:grid-cols-[210px_minmax(0,1fr)] lg:items-start lg:gap-4 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>
|
||||
@@ -232,8 +247,8 @@ function identitetsRader(rader: [string, string | undefined][]) {
|
||||
return rader
|
||||
.filter(([, varde]) => varde)
|
||||
.map(([etikett, varde]) => (
|
||||
<div key={etikett} className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
<span className="text-[#4A5560]">{etikett}</span>
|
||||
<div key={etikett} className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
|
||||
<span className="text-[#4D5662]">{etikett}</span>
|
||||
<span className="text-right font-medium">{varde}</span>
|
||||
</div>
|
||||
));
|
||||
@@ -248,12 +263,12 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
|
||||
const del = (etikett: string, varde?: string) =>
|
||||
varde ? (
|
||||
<span className="whitespace-nowrap">
|
||||
{etikett && <span className="text-[#707070]">{etikett} </span>}
|
||||
<span className="font-semibold text-[#1A1A1A]">{varde}</span>
|
||||
{etikett && <span className="text-[#4D5662]">{etikett} </span>}
|
||||
<span className="font-semibold text-[#1B1E22]">{varde}</span>
|
||||
</span>
|
||||
) : null;
|
||||
return (
|
||||
<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">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-1 border border-[#D7DCE2] bg-[#F6F7F8] px-4 py-2 text-[12px] print:hidden">
|
||||
{del("WO", idn.arbetsorder)}
|
||||
{del("Claim", idn.claim)}
|
||||
{del("Claim no.", idn.skadenummer)}
|
||||
@@ -262,13 +277,13 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
|
||||
{del("VIN", idn.vin)}
|
||||
{del("Mileage", idn.miltal)}
|
||||
{del("Responsible", idn.ansvarig)}
|
||||
<label className="ml-auto flex items-center gap-1">
|
||||
<span className="text-[#707070]">Case type</span>
|
||||
<label className="ml-auto flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">Case type</span>
|
||||
<select
|
||||
value={typ}
|
||||
disabled={idn.avslutat}
|
||||
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
|
||||
className="border border-[#ADADAD] bg-white px-1.5 py-0.5 text-[12px] focus:border-[#00437A] focus:outline-none"
|
||||
className="border border-[#4D5662] bg-white px-2 py-2 text-[12px] focus:outline-none"
|
||||
>
|
||||
{ARENDETYPER.map((t) => (
|
||||
<option key={t}>{t}</option>
|
||||
@@ -326,15 +341,15 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
||||
<IkonKamera /> Photograph the instrument panel
|
||||
</StorKnapp>
|
||||
)}
|
||||
{laser && <p className="animate-pulse py-1 text-center text-[12px] font-semibold text-[#00437A]">Reading the odometer …</p>}
|
||||
{laser && <p className="py-2 text-center text-[12px] font-semibold text-[#005CA9]">Reading the odometer …</p>}
|
||||
{foto && !laser && (
|
||||
<div className="mt-2">
|
||||
{demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
<p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[11px] font-semibold text-[#8A5A00]">
|
||||
Demo reading — image interpretation requires sign-in. Check the value.
|
||||
</p>
|
||||
)}
|
||||
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#C6C6C6]" />
|
||||
<img src={foto} alt="Instrument panel" className="mb-2 max-h-40 border border-[#D7DCE2]" />
|
||||
<TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||
<StorKnapp
|
||||
disabled={!varde.trim()}
|
||||
@@ -391,24 +406,24 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
||||
|
||||
if (rader.length === 0) return null;
|
||||
return (
|
||||
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mb-2 border border-[#D7DCE2] bg-white p-2">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Previous cases for {identifierare} — review before signing off the history
|
||||
</p>
|
||||
{rader.map((rad) => (
|
||||
<div key={rad.id} className="border-b border-[#EBEBEB] py-1.5 text-[12px] last:border-0">
|
||||
<div key={rad.id} className="border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
|
||||
<p>
|
||||
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||
{rad.avslutat ? "closed" : "in progress"}
|
||||
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
||||
{rad.felbeskrivning && <span className="text-[#4D5662]"> — ”{rad.felbeskrivning}”</span>}
|
||||
</p>
|
||||
{rad.felorsaker.map((f, i) => (
|
||||
<p key={i} className="pl-3 text-[#4A5560]">
|
||||
<p key={i} className="pl-4 text-[#4D5662]">
|
||||
Felorsak: {f.avvikelse} ({f.orsaker.join(", ")})
|
||||
</p>
|
||||
))}
|
||||
<button
|
||||
className="mt-1 text-[11px] font-semibold text-[#00437A] underline-offset-2 hover:underline"
|
||||
className="mt-2 text-[11px] font-semibold text-[#005CA9] underline-offset-2 hover:underline"
|
||||
onClick={() =>
|
||||
skicka({
|
||||
typ: "kommentar",
|
||||
@@ -439,15 +454,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
return (
|
||||
<Panel rubrik="Pre-diagnostics — before the work begins">
|
||||
{rader.map((r) => (
|
||||
<p key={r.id} className="py-0.5 text-[13px]">
|
||||
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
||||
{r.varning && <span className="ml-1 text-[11px] font-semibold text-[#9A6700]"><IkonVarning /> {r.varning}</span>}
|
||||
<p key={r.id} className="py-0 text-[13px]">
|
||||
<span className={r.klar ? "text-[#005CA9]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
||||
{r.varning && <span className="ml-2 text-[11px] font-semibold text-[#8A5A00]"><IkonVarning /> {r.varning}</span>}
|
||||
</p>
|
||||
))}
|
||||
|
||||
{!rad("historik").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
|
||||
</p>
|
||||
<TidigareArenden arende={arende} skicka={skicka} />
|
||||
@@ -484,8 +499,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
)}
|
||||
|
||||
{!rad("matarstallning_in").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Incoming odometer reading — photograph the instrument panel
|
||||
</p>
|
||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||
@@ -493,15 +508,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
)}
|
||||
|
||||
{!rad("felbeskrivning").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Is the customer's fault description correctly recorded?
|
||||
</p>
|
||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||
Correct — verified
|
||||
</StorKnapp>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
Additional symptoms are documented as separate observations — do not mix them
|
||||
with the customer's description.
|
||||
</p>
|
||||
@@ -509,8 +524,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
)}
|
||||
|
||||
{!rad("tidiga_observationer").klar && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -525,7 +540,7 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
||||
The observations are documented
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
Document with a photograph or an observation in the panel below — the button unlocks once something is logged.
|
||||
</p>
|
||||
</div>
|
||||
@@ -548,7 +563,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
||||
: "Rationale (required) — why could the fault not be reproduced?";
|
||||
return (
|
||||
<Panel rubrik="Symptom verification — reproduction">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Har kundens fel kunnat reproduceras?
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
@@ -603,7 +618,7 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
if (orsaker.length === 0) return setFel("Select at least one cause category.");
|
||||
if (underlag.length === 0) return setFel("Link at least one evidence source to the assessment.");
|
||||
const saknat = underlag.find((k) => !underlagFinns(arende, k));
|
||||
if (saknat) return setFel(`Underlaget ”${saknat}” finns inte i ärendets logg — dokumentera det först eller välj en annan källa.`);
|
||||
if (saknat) return setFel(`The evidence source “${saknat}” is not in the case log — document it first, or choose another source.`);
|
||||
if (orsaker.includes("Unknown cause") && !motivering.trim())
|
||||
return setFel("An unknown cause requires a reason why the cause could not be established.");
|
||||
if (sakerhet !== "hog" && !ytterligare.trim())
|
||||
@@ -636,9 +651,9 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "felorsak") return null;
|
||||
return (
|
||||
<div key={p.id} className="mb-2 border-b border-[#EBEBEB] pb-2 text-[13px] last:border-0">
|
||||
<div key={p.id} className="mb-2 border-b border-[#D7DCE2] pb-2 text-[13px] last:border-0">
|
||||
<p className="font-semibold">{h.avvikelse}</p>
|
||||
<p className="text-[#4A5560]">
|
||||
<p className="text-[#4D5662]">
|
||||
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||
</p>
|
||||
<p>Action: {h.atgard}</p>
|
||||
@@ -659,42 +674,42 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
||||
rost
|
||||
platshallare="e.g. The starter motor does not engage despite correct supply voltage and a good earth connection."
|
||||
/>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">2. Most probable cause (one or more)</p>
|
||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">2. Most probable cause (one or more)</p>
|
||||
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
{aktivtRegelpaket().orsakskategorier.map((o) => (
|
||||
<button
|
||||
key={o}
|
||||
onClick={() => vaxla(orsaker, setOrsaker, o)}
|
||||
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]"
|
||||
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||
orsaker.includes(o) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{o}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Evidence supporting the assessment</p>
|
||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">3. Evidence supporting the assessment</p>
|
||||
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{aktivtRegelpaket().underlagskallor.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => vaxla(underlag, setUnderlag, k)}
|
||||
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]"
|
||||
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||
underlag.includes(k) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{k}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Confidence in the assessment</p>
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">4. Confidence in the assessment</p>
|
||||
<div className="mb-4 grid grid-cols-3 gap-2">
|
||||
{(["hog", "medel", "lag"] as const).map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setSakerhet(n)}
|
||||
className={`min-h-9 border text-[12px] font-semibold ${
|
||||
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
||||
sakerhet === n ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{TILLFORLITLIGHET_LABEL[n]}
|
||||
@@ -768,20 +783,20 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "atgard_utford") return null;
|
||||
return (
|
||||
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
<p key={p.id} className="border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
|
||||
{h.utford ? (
|
||||
<>
|
||||
<span className="font-semibold">Action performed:</span> {h.beskrivning}
|
||||
{h.delar && <span className="text-[#4A5560]"> · Delar: {h.delar}</span>}
|
||||
{h.delar && <span className="text-[#4D5662]"> · Delar: {h.delar}</span>}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-[#9A6700]">No action performed — {h.motivering}</span>
|
||||
<span className="text-[#8A5A00]">No action performed — {h.motivering}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{kk && (
|
||||
<p className={`py-1 text-[13px] font-semibold ${kk.resultat === "symptomet_borta" ? "text-[#1E6B34]" : "text-[#9A6700]"}`}>
|
||||
<p className={`py-2 text-[13px] font-semibold ${kk.resultat === "symptomet_borta" ? "text-[#005CA9]" : "text-[#8A5A00]"}`}>
|
||||
Kvalitetskontroll: {KVALITETSKONTROLL_LABEL[kk.resultat]} — <span className="font-normal">{kk.beskrivning}</span>
|
||||
</p>
|
||||
)}
|
||||
@@ -792,14 +807,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "atgardsforslag") return null;
|
||||
return (
|
||||
<p key={p.id} className="border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
<p key={p.id} className="border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
|
||||
<span className="font-semibold">Proposed action for the customer:</span> {h.beskrivning}
|
||||
{h.uppskattadKostnad && <span className="text-[#4A5560]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
|
||||
{h.uppskattadKostnad && <span className="text-[#4D5662]"> · Uppskattad kostnad: {h.uppskattadKostnad}</span>}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{beslut && (
|
||||
<p className={`py-1 text-[13px] font-semibold ${beslut.beslut === "godkant" ? "text-[#1E6B34]" : beslut.beslut === "avbojt" ? "text-[#8B1A1A]" : "text-[#9A6700]"}`}>
|
||||
<p className={`py-2 text-[13px] font-semibold ${beslut.beslut === "godkant" ? "text-[#005CA9]" : beslut.beslut === "avbojt" ? "text-[#8B1A1A]" : "text-[#8A5A00]"}`}>
|
||||
Kundens besked ({beslut.kanal}): {KUNDBESLUT_LABEL[beslut.beslut]}
|
||||
{beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>}
|
||||
</p>
|
||||
@@ -826,7 +841,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
)}
|
||||
|
||||
{forslagsLage && (
|
||||
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
|
||||
<div className="mb-4 border border-[#D7DCE2] bg-white p-2">
|
||||
<TextFalt
|
||||
label="Proposed action (shown to the customer in the shared view)"
|
||||
varde={forslagText}
|
||||
@@ -860,8 +875,8 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
)}
|
||||
|
||||
{forslag.length > 0 && !beslut && (
|
||||
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
|
||||
<div className="mb-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#8A5A00]">
|
||||
Record the customer's decision before the work begins
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
@@ -870,7 +885,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
key={val}
|
||||
onClick={() => setBeslutsVal(val)}
|
||||
className={`min-h-9 border text-[12px] font-semibold ${
|
||||
beslutsVal === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
||||
beslutsVal === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{KUNDBESLUT_LABEL[val]}
|
||||
@@ -879,14 +894,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
</div>
|
||||
{beslutsVal && (
|
||||
<>
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">How was the decision given?</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-1">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">How was the decision given?</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{KUNDKANALER.map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => setKanal(k)}
|
||||
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]"
|
||||
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||
kanal === k ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{k}
|
||||
@@ -967,16 +982,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
|
||||
{lage === "ingen" && (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Reason no action was performed (required)
|
||||
</p>
|
||||
<div className="mb-2 grid gap-1">
|
||||
<div className="mb-2 grid gap-2">
|
||||
{INGEN_ATGARD_ORSAKER.map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
onClick={() => setOrsak(val)}
|
||||
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
||||
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
|
||||
orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
|
||||
}`}
|
||||
>
|
||||
{val}
|
||||
@@ -985,7 +1000,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
</div>
|
||||
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||
{beslut?.beslut === "avbojt" && (
|
||||
<p className="mb-2 text-[11px] text-[#707070]">
|
||||
<p className="mb-2 text-[11px] text-[#4D5662]">
|
||||
The customer has declined the action via {beslut.kanal} — the decision is recorded in the log.
|
||||
</p>
|
||||
)}
|
||||
@@ -1008,12 +1023,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
)}
|
||||
|
||||
{utford && !kk && (
|
||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<div className="mt-4 border-t border-[#D7DCE2] pt-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Quality check — is the symptom gone after the action?
|
||||
</p>
|
||||
{repro?.status === "ja" && (
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
<p className="mb-2 text-[12px] text-[#4D5662]">
|
||||
The symptom was reproduced during the examination — recreate the same conditions to verify.
|
||||
</p>
|
||||
)}
|
||||
@@ -1023,7 +1038,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
||||
key={val}
|
||||
onClick={() => setKkResultat(val)}
|
||||
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]"
|
||||
kkResultat === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{KVALITETSKONTROLL_LABEL[val]}
|
||||
@@ -1080,24 +1095,24 @@ function VyTrad({
|
||||
const steg = nastaSteg(arende, metodik);
|
||||
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
|
||||
return (
|
||||
<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]">
|
||||
<nav className="overflow-hidden border border-[#D7DCE2] bg-[#F6F7F8]">
|
||||
<p className="border-b border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
|
||||
Case views
|
||||
</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 ${
|
||||
className={`block w-full border-l-2 px-4 py-2 text-left text-[13px] ${
|
||||
flik === f.id
|
||||
? "border-[#00437A] bg-[#D6E4F2] font-semibold text-[#00437A]"
|
||||
: "border-transparent text-[#333333] hover:bg-[#E4E9EE]"
|
||||
? "border-[#005CA9] bg-[#FFFFFF] font-semibold text-[#005CA9]"
|
||||
: "border-transparent text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||
}`}
|
||||
>
|
||||
{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]">
|
||||
<p className="border-y border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
|
||||
{metodik.namn}
|
||||
</p>
|
||||
{metodik.steg.map((s, i) => {
|
||||
@@ -1106,8 +1121,8 @@ function VyTrad({
|
||||
return (
|
||||
<p
|
||||
key={s.id}
|
||||
className={`px-3 py-1 text-[12px] ${
|
||||
aktuell ? "bg-[#FFF8E1] font-semibold text-[#9A6700]" : klar ? "text-[#1E6B34]" : "text-[#707070]"
|
||||
className={`px-4 py-2 text-[12px] ${
|
||||
aktuell ? "bg-[#FFFFFF] font-semibold text-[#8A5A00]" : klar ? "text-[#005CA9]" : "text-[#4D5662]"
|
||||
}`}
|
||||
>
|
||||
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
|
||||
@@ -1135,9 +1150,9 @@ function KontextPanel({
|
||||
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 className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
|
||||
<span className="shrink-0 text-[#4D5662]">{etikett}</span>
|
||||
<span className="text-right font-medium text-[#1B1E22]">{varde}</span>
|
||||
</div>
|
||||
) : null;
|
||||
return (
|
||||
@@ -1153,7 +1168,7 @@ function KontextPanel({
|
||||
</Panel>
|
||||
<Panel rubrik="Reliability">
|
||||
{b.tillforlitlighet.map((r, i) => (
|
||||
<p key={i} className="py-0.5 text-[12px]">
|
||||
<p key={i} className="py-0 text-[12px]">
|
||||
<NivaBadge niva={r.niva} /> {r.text}
|
||||
</p>
|
||||
))}
|
||||
@@ -1161,7 +1176,7 @@ function KontextPanel({
|
||||
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
|
||||
<Panel rubrik="Recommended next step">
|
||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
||||
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
|
||||
<p key={i} className="py-0 text-[12px] text-[#1B1E22]">
|
||||
{i + 1}. {s}
|
||||
</p>
|
||||
))}
|
||||
@@ -1170,14 +1185,14 @@ function KontextPanel({
|
||||
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
|
||||
<Panel rubrik="Technical recommendation">
|
||||
{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 key={i} className="py-0 text-[12px]">
|
||||
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-1 text-[12px]">
|
||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
<p className="mt-2 text-[12px]">
|
||||
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
|
||||
<p className="mt-2 text-[10px] uppercase tracking-wide text-[#4D5662]">
|
||||
Decision support · never presented as an established fault
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -1202,11 +1217,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 border border-[#00437A] bg-[#F7F7F7] p-4">
|
||||
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
|
||||
<div className="mb-4 border border-[#005CA9] bg-[#F6F7F8] p-4">
|
||||
<p className="mb-2 text-[14px] font-semibold text-[#005CA9]">
|
||||
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
||||
</p>
|
||||
<p className="mb-3 text-[#333333]">Briefly describe what was done during this period.</p>
|
||||
<p className="mb-4 text-[#1B1E22]">Briefly describe what was done during this period.</p>
|
||||
<TextFalt label="What has been done?" varde={text} satt={setText} platshallare="e.g. Removed the instrument panel to reach the wiring harness." rost />
|
||||
<StorKnapp
|
||||
disabled={!text.trim()}
|
||||
@@ -1286,7 +1301,7 @@ function GuideFlik({
|
||||
if (avslutat) {
|
||||
return (
|
||||
<Panel rubrik="The diagnosis is closed">
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
<p className="text-[14px] text-[#1B1E22]">
|
||||
The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -1339,8 +1354,8 @@ function GuideFlik({
|
||||
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
|
||||
{steg.klart ? (
|
||||
<>
|
||||
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||
<p className="mb-4 text-[#333333]">
|
||||
<p className="mb-4 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||
<p className="mb-4 text-[#1B1E22]">
|
||||
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
|
||||
with recommended next steps.
|
||||
</p>
|
||||
@@ -1353,11 +1368,11 @@ function GuideFlik({
|
||||
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
|
||||
// hinder, inte en varning. Enda vägen framåt är att ändra
|
||||
// förutsättningen, inte att klicka förbi.
|
||||
<div className="border-2 border-[#B42318] bg-[#FEF3F2] p-4">
|
||||
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Work must not continue</p>
|
||||
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
||||
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
||||
<p className="text-[12px] text-[#666666]">
|
||||
<div className="border-2 border-[#8B1A1A] bg-[#FFFFFF] p-4">
|
||||
<p className="mb-2 text-[15px] font-semibold text-[#8B1A1A]">Work must not continue</p>
|
||||
<p className="mb-2 text-[#1B1E22]">{steg.sparr.orsak}</p>
|
||||
<p className="mb-4 font-semibold text-[#1B1E22]">{steg.sparr.atgard}</p>
|
||||
<p className="text-[12px] text-[#4D5662]">
|
||||
The answer is documented in the log. The methodology opens when the precondition is met and the question is answered
|
||||
jakande.
|
||||
</p>
|
||||
@@ -1377,19 +1392,19 @@ function GuideFlik({
|
||||
: "Guidance"
|
||||
}
|
||||
>
|
||||
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Analyzing …</p>}
|
||||
{aiStatus === "arbetar" && <p className="mb-2 font-semibold text-[#005CA9]">Analyzing …</p>}
|
||||
{aiStatus === "fel" && (
|
||||
<p className="mb-2 font-semibold text-[#8B1A1A]">The analysis could not be retrieved — try again. The methodology continues unaffected.</p>
|
||||
)}
|
||||
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
|
||||
<>
|
||||
{senasteAiSvar.handelse.rader.map((rad, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
<p key={i} className="py-0 text-[14px]">
|
||||
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-2 text-[14px]">
|
||||
<span className="font-semibold text-[#00437A]">Next step:</span>{" "}
|
||||
<span className="font-semibold text-[#005CA9]">Next step:</span>{" "}
|
||||
{senasteAiSvar.handelse.nastaSteg}
|
||||
</p>
|
||||
</>
|
||||
@@ -1447,15 +1462,15 @@ function GuideFlik({
|
||||
*/
|
||||
function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) {
|
||||
return (
|
||||
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]">
|
||||
<div className="mb-2 border-l-2 border-[#8A5A00] bg-[#FFFFFF] px-4 py-4">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#8A5A00]">
|
||||
The case cannot be closed yet
|
||||
</p>
|
||||
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
|
||||
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#1B1E22]">
|
||||
{hinder.map((h) => (
|
||||
<li key={h.id}>
|
||||
{h.rubrik}
|
||||
{h.detalj && <span className="text-[#4A5560]"> — {h.detalj}</span>}
|
||||
{h.detalj && <span className="text-[#4D5662]"> — {h.detalj}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -1469,15 +1484,15 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
|
||||
if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori;
|
||||
}
|
||||
return (
|
||||
<div className="mb-4 flex gap-2 overflow-x-auto pb-1">
|
||||
<div className="mb-4 flex gap-2 overflow-x-auto pb-2">
|
||||
{(Object.keys(TIDKATEGORI_LABEL) as TidKategori[]).map((k) => (
|
||||
<button
|
||||
key={k}
|
||||
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
|
||||
className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold transition-colors ${
|
||||
className={`whitespace-nowrap border px-4 py-2 text-[12px] font-semibold ${
|
||||
k === aktiv
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
||||
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
|
||||
}`}
|
||||
>
|
||||
{TIDKATEGORI_LABEL[k]}
|
||||
@@ -1559,7 +1574,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
|
||||
return (
|
||||
<>
|
||||
{steg.steg.beskrivning && <p className="mb-2 text-[#4A5560]">{steg.steg.beskrivning}</p>}
|
||||
{steg.steg.beskrivning && <p className="mb-2 text-[#4D5662]">{steg.steg.beskrivning}</p>}
|
||||
<p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p>
|
||||
{krav === "foto" ? (
|
||||
<>
|
||||
@@ -1579,7 +1594,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
/>
|
||||
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
|
||||
<StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Take a photograph — verifies the check</StorKnapp>
|
||||
<p className="mt-2 text-[12px] text-[#707070]">This check is verified with a photograph.</p>
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">This check is verified with a photograph.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -1597,7 +1612,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
||||
rost
|
||||
/>
|
||||
{!kravUppfyllt && (
|
||||
<p className="mb-3 text-[12px] font-semibold text-[#00437A]">
|
||||
<p className="mb-4 text-[12px] font-semibold text-[#005CA9]">
|
||||
No {krav === "matvarde" ? "measurement" : "observation"} has been recorded — add a short{" "}
|
||||
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
|
||||
</p>
|
||||
@@ -1633,24 +1648,24 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setOppen(true)}
|
||||
className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
|
||||
className="mt-2 w-full py-2 text-[12px] font-medium text-[#4D5662] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
|
||||
>
|
||||
Evidence cannot be produced …
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
<div className="mt-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8A5A00]">
|
||||
Kontrollen dokumenteras utan underlag — ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-1 gap-1">
|
||||
<div className="mb-2 grid grid-cols-1 gap-2">
|
||||
{aktivtRegelpaket().undantagsorsaker.map((val) => (
|
||||
<button
|
||||
key={val}
|
||||
onClick={() => setOrsak(val)}
|
||||
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
||||
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
|
||||
orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
|
||||
}`}
|
||||
>
|
||||
{val}
|
||||
@@ -1794,10 +1809,10 @@ function SnabbDokumentation({
|
||||
? instRef.current?.click()
|
||||
: setTyp(typ === d.id ? null : d.id)
|
||||
}
|
||||
className={`min-h-9 border text-[12px] font-semibold transition-colors ${
|
||||
className={`min-h-9 border text-[12px] font-semibold ${
|
||||
typ === d.id
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
||||
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
|
||||
}`}
|
||||
>
|
||||
+ {d.label}
|
||||
@@ -1850,8 +1865,8 @@ function SnabbDokumentation({
|
||||
/>
|
||||
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
|
||||
{video && (
|
||||
<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]" />
|
||||
<div className="mt-4 border border-[#D7DCE2] bg-white p-2">
|
||||
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#D7DCE2]" />
|
||||
<TextFalt
|
||||
label="What does the video show? (required — what makes the noise or moves)"
|
||||
varde={video.beskrivning}
|
||||
@@ -1876,23 +1891,23 @@ function SnabbDokumentation({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Reading the instrument …</p>}
|
||||
{laserAv && <p className="mt-2 text-center text-[12px] font-semibold text-[#005CA9]">Reading the instrument …</p>}
|
||||
{avlasning && (
|
||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
||||
<div className="mt-4 border border-[#D7DCE2] bg-white p-2">
|
||||
{avlasning.demo && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
|
||||
<p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[11px] font-semibold text-[#8A5A00]">
|
||||
Demo reading — image interpretation requires sign-in. The values are sample data.
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
Read: {avlasning.tolkning.instrumenttyp} — confirm before anything is logged
|
||||
</p>
|
||||
{avlasning.tolkning.varden.map((v, i) => (
|
||||
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
||||
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
|
||||
<span>
|
||||
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span>
|
||||
</span>
|
||||
<span className={`text-[11px] font-semibold ${v.konfidens >= 0.95 ? "text-[#1E6B34]" : v.konfidens >= 0.8 ? "text-[#9A6700]" : "text-[#8B1A1A]"}`}>
|
||||
<span className={`text-[11px] font-semibold ${v.konfidens >= 0.95 ? "text-[#005CA9]" : v.konfidens >= 0.8 ? "text-[#8A5A00]" : "text-[#8B1A1A]"}`}>
|
||||
{v.konfidens >= 0.95 ? "✓" : `${Math.round(v.konfidens * 100)} %`}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1906,7 +1921,7 @@ function SnabbDokumentation({
|
||||
</div>
|
||||
)}
|
||||
{typ && typ !== "foto" && typ !== "instrument" && (
|
||||
<div className="mt-3">
|
||||
<div className="mt-4">
|
||||
{typ === "hypotes" && (
|
||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
||||
<IkonPunkt farg="#8B1A1A" /> A hypothesis is a possible root cause that requires verification — it is never logged as an established fault.
|
||||
@@ -1968,25 +1983,25 @@ 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 border border-[#ADADAD] bg-[#F7F7F7] p-4">
|
||||
<h2 className="mb-3 text-[15px] font-semibold">Handover report</h2>
|
||||
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#333333]">
|
||||
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#D7DCE2] bg-[#F6F7F8] p-4">
|
||||
<h2 className="mb-4 text-[15px] font-semibold">Handover report</h2>
|
||||
<pre className="mb-4 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#1B1E22]">
|
||||
{text}
|
||||
</pre>
|
||||
{aiLage === "vilar" && (
|
||||
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
||||
<StorKnapp variant="sekundar" className="mb-4" onClick={komplettera}>
|
||||
Extend the analysis: risks & uncertainties
|
||||
</StorKnapp>
|
||||
)}
|
||||
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Summarizing …</p>}
|
||||
{aiLage === "arbetar" && <p className="mb-4 font-semibold text-[#005CA9]">Summarizing …</p>}
|
||||
{aiLage === "fel" && (
|
||||
<p className="mb-3 font-semibold text-[#8B1A1A]">Could not be retrieved — requires sign-in. The handover works regardless.</p>
|
||||
<p className="mb-4 font-semibold text-[#8B1A1A]">Could not be retrieved — requires sign-in. The handover works regardless.</p>
|
||||
)}
|
||||
{aiLage === "klar" && aiRader.length > 0 && (
|
||||
<div className="mb-3 bg-white p-4">
|
||||
<div className="mb-4 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}
|
||||
<p key={i} className="py-0 text-[12px] text-[#1B1E22]">
|
||||
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -2049,10 +2064,10 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-3">
|
||||
<div className="mt-4 border border-[#D7DCE2] bg-white p-4">
|
||||
{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">
|
||||
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#D7DCE2] py-2 last:border-0">
|
||||
<span className="text-[14px]">
|
||||
{NIVA_LABEL[delning.niva]}{" "}
|
||||
{delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(revoked)</span>}
|
||||
@@ -2060,7 +2075,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
{!delning.aterkallad && (
|
||||
<span className="flex gap-2">
|
||||
<button
|
||||
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#333333] hover:border-[#00437A]"
|
||||
className="border border-[#D7DCE2] px-4 py-2 font-semibold text-[#1B1E22] hover:border-[#005CA9]"
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
||||
}
|
||||
@@ -2068,7 +2083,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
className="border border-[#ADADAD] px-3 py-1 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
||||
className="border border-[#D7DCE2] px-4 py-2 font-semibold text-[#8B1A1A] hover:border-[#8B1A1A]"
|
||||
onClick={async () => {
|
||||
await aterkallaDelning(delning.kod);
|
||||
skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` });
|
||||
@@ -2085,7 +2100,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 border border-[#ADADAD] text-[12px] font-semibold text-[#333333] hover:border-[#00437A]"
|
||||
className="min-h-9 border border-[#D7DCE2] text-[12px] font-semibold text-[#1B1E22] hover:border-[#005CA9]"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await skapaDelning(arende.id, niva);
|
||||
@@ -2100,7 +2115,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) ·
|
||||
Internal: full visibility. Filtering happens on the server.
|
||||
</p>
|
||||
@@ -2113,16 +2128,16 @@ function LoggFlik({ arende }: { arende: Arende }) {
|
||||
<Panel rubrik="Work log — append-only, nothing is overwritten">
|
||||
<ol>
|
||||
{arende.handelser.map((post) => (
|
||||
<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>
|
||||
<li key={post.id} className="flex gap-4 border-b border-[#D7DCE2] py-2 last:border-0">
|
||||
<span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
|
||||
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
|
||||
<p className="text-[13px] text-[#1B1E22]">{handelseRubrik(post)}</p>
|
||||
<p className="text-[11px] text-[#4D5662]">{post.anvandare}</p>
|
||||
{post.handelse.typ === "foto" && (
|
||||
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-1 max-h-40 border border-[#C6C6C6]" />
|
||||
<Bild bilaga={post.handelse} alt={post.handelse.beskrivning} className="mt-2 max-h-40 border border-[#D7DCE2]" />
|
||||
)}
|
||||
{post.handelse.typ === "video" && (
|
||||
<Klipp bilaga={post.handelse} className="mt-1 max-h-40 border border-[#C6C6C6]" />
|
||||
<Klipp bilaga={post.handelse} className="mt-2 max-h-40 border border-[#D7DCE2]" />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
@@ -2164,25 +2179,25 @@ function BriefFlik({
|
||||
<>
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="Review of the evidence">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
<p className="mb-2 text-[#1B1E22]">
|
||||
Have the system review the whole record before you hand it on: contradictions between
|
||||
observations, gaps in the documentation, and conclusions that lack support.
|
||||
</p>
|
||||
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Reviewing the evidence …</p>}
|
||||
{granskning === "arbetar" && <p className="mb-2 font-semibold text-[#005CA9]">Reviewing the evidence …</p>}
|
||||
{granskning === "fel" && (
|
||||
<p className="mb-2 font-semibold text-[#8B1A1A]">The review could not be retrieved — requires sign-in. Try again.</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-[14px]">
|
||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
<p key={i} className="py-0 text-[14px]">
|
||||
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||
</p>
|
||||
))}
|
||||
<p className="mt-1 text-[14px]">
|
||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
<p className="mt-2 text-[14px]">
|
||||
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">Senaste analys ({senasteAi.handelse.modell})</p>
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">Senaste analys ({senasteAi.handelse.modell})</p>
|
||||
</div>
|
||||
)}
|
||||
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
|
||||
@@ -2194,63 +2209,63 @@ function BriefFlik({
|
||||
{b.objekt ? (
|
||||
<>
|
||||
<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="font-semibold text-[#005CA9]">{b.objekt.identifierare}</p>
|
||||
{b.objekt.kund && <p className="text-[#1B1E22]">Kund: {b.objekt.kund}</p>}
|
||||
{b.ansvarig && <p className="text-[#1B1E22]">Ansvarig tekniker: {b.ansvarig}</p>}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[#4A5560]">Not identified</p>
|
||||
<p className="text-[#4D5662]">Not identified</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Customer's description">
|
||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||
</Panel>
|
||||
<Panel rubrik="Checks performed">
|
||||
{b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">None yet.</p>}
|
||||
{b.utfordaKontroller.length === 0 && <p className="text-[#4D5662]">None yet.</p>}
|
||||
{b.utfordaKontroller.map((k, i) =>
|
||||
k.undantag ? (
|
||||
<p key={i} className="py-0.5 text-[14px] text-[#9A6700]">
|
||||
<p key={i} className="py-0 text-[14px] text-[#8A5A00]">
|
||||
<IkonVarning /> {k.text} — underlag saknas: {k.undantag}
|
||||
</p>
|
||||
) : (
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<p key={i} className="py-0 text-[14px]">
|
||||
✓ {k.text}
|
||||
{k.resultat && <span className="text-[#4A5560]"> — {k.resultat}</span>}
|
||||
{k.resultat && <span className="text-[#4D5662]"> — {k.resultat}</span>}
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Observations">
|
||||
{b.observationer.length === 0 && <p className="text-[#4A5560]">None yet.</p>}
|
||||
{b.observationer.length === 0 && <p className="text-[#4D5662]">None yet.</p>}
|
||||
{b.observationer.map((o, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">• {o}</p>
|
||||
<p key={i} className="py-0 text-[14px]">• {o}</p>
|
||||
))}
|
||||
</Panel>
|
||||
{b.hypoteser.length > 0 && (
|
||||
<Panel rubrik="Hypotheses — require verification">
|
||||
{b.hypoteser.map((h, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<p key={i} className="py-0 text-[14px]">
|
||||
<NivaBadge niva={h.niva} /> {h.text}
|
||||
</p>
|
||||
))}
|
||||
</Panel>
|
||||
)}
|
||||
<Panel rubrik="Not checked">
|
||||
{b.ejKontrollerat.length === 0 && <p className="text-[#4A5560]">Every step in the methodology is complete.</p>}
|
||||
{b.ejKontrollerat.length === 0 && <p className="text-[#4D5662]">Every step in the methodology is complete.</p>}
|
||||
{b.ejKontrollerat.map((e, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px] text-[#333333]">– {e}</p>
|
||||
<p key={i} className="py-0 text-[14px] text-[#1B1E22]">– {e}</p>
|
||||
))}
|
||||
</Panel>
|
||||
{!b.avslutat && (
|
||||
<Panel rubrik="Recommended next step">
|
||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
||||
<p key={i} className="py-0 text-[14px]">{i + 1}. {s}</p>
|
||||
))}
|
||||
</Panel>
|
||||
)}
|
||||
<Panel rubrik="Reliability">
|
||||
{b.tillforlitlighet.map((r, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">
|
||||
<p key={i} className="py-0 text-[14px]">
|
||||
<NivaBadge niva={r.niva} /> {r.text}
|
||||
</p>
|
||||
))}
|
||||
@@ -2333,15 +2348,15 @@ function RapportFlik({
|
||||
undantag i händelseloggen. */}
|
||||
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
|
||||
{grind.map((rad) => (
|
||||
<div key={rad.id} className="border-b border-[#EBEBEB] py-1 last:border-0">
|
||||
<div key={rad.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
|
||||
<p className="text-[13px]">
|
||||
<span className={rad.ok ? "text-[#1E6B34]" : rad.kravs ? "text-[#8B1A1A]" : "text-[#9A6700]"}>
|
||||
<span className={rad.ok ? "text-[#005CA9]" : rad.kravs ? "text-[#8B1A1A]" : "text-[#8A5A00]"}>
|
||||
{rad.ok ? <IkonCheck /> : rad.kravs ? <IkonKryss /> : <IkonVarning />}
|
||||
</span>{" "}
|
||||
{rad.rubrik}
|
||||
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (recommended)</span>}
|
||||
{!rad.kravs && <span className="text-[11px] text-[#4D5662]"> (recommended)</span>}
|
||||
</p>
|
||||
{rad.detalj && <p className="pl-6 text-[12px] text-[#707070]">{rad.detalj}</p>}
|
||||
{rad.detalj && <p className="pl-6 text-[12px] text-[#4D5662]">{rad.detalj}</p>}
|
||||
</div>
|
||||
))}
|
||||
{!godkand && (
|
||||
@@ -2352,7 +2367,7 @@ function RapportFlik({
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Customer report">
|
||||
<p className="mb-2 text-[#333333]">
|
||||
<p className="mb-2 text-[#1B1E22]">
|
||||
A shareable summary of the work performed. Review the content before the report is shared — images may
|
||||
contain details about other customers.
|
||||
</p>
|
||||
@@ -2365,7 +2380,7 @@ function RapportFlik({
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
||||
<StorKnapp variant="sekundar"><IkonPunkt farg="#1E6B34" /> Open Live Share view</StorKnapp>
|
||||
<StorKnapp variant="sekundar"><IkonPunkt farg="#005CA9" /> Open Live Share view</StorKnapp>
|
||||
</Link>
|
||||
{plattformAktiv() ? (
|
||||
<DelningsHanterare arende={arende} skicka={skicka} />
|
||||
@@ -2383,7 +2398,7 @@ function RapportFlik({
|
||||
</StorKnapp>
|
||||
)
|
||||
)}
|
||||
<p className="mt-2 text-[12px] text-[#707070]">
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||
The share link requires the case to be synchronized (signed-in user).
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -2445,19 +2460,19 @@ function RapportFlik({
|
||||
{repro ? (
|
||||
<>
|
||||
<p className="text-[14px] font-semibold">{reproduceringsText(repro.status)}</p>
|
||||
<p className="text-[13px] text-[#4A5560]">{repro.beskrivning}</p>
|
||||
<p className="text-[13px] text-[#4D5662]">{repro.beskrivning}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-[14px] text-[#4A5560]">Reproduction not documented yet.</p>
|
||||
<p className="text-[14px] text-[#4D5662]">Reproduction not documented yet.</p>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Root cause analysis">
|
||||
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4A5560]">Root cause analysis not documented yet.</p>}
|
||||
{orsakerLista.length === 0 && <p className="text-[14px] text-[#4D5662]">Root cause analysis not documented yet.</p>}
|
||||
{orsakerLista.map((p) => {
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "felorsak") return null;
|
||||
return (
|
||||
<div key={p.id} className="mb-3 border-b border-[#EBEBEB] pb-2 last:mb-0 last:border-0">
|
||||
<div key={p.id} className="mb-4 border-b border-[#D7DCE2] pb-2 last:mb-0 last:border-0">
|
||||
<p className="text-[14px] font-semibold">{h.avvikelse}</p>
|
||||
{identitetsRader([
|
||||
["Assessed root cause", h.orsaker.join(", ")],
|
||||
@@ -2495,7 +2510,7 @@ function RapportFlik({
|
||||
</Panel>
|
||||
)}
|
||||
<Panel rubrik="Action performed and verification">
|
||||
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4A5560]">No action documented yet.</p>}
|
||||
{atgardsRader.length === 0 && <p className="text-[14px] text-[#4D5662]">No action documented yet.</p>}
|
||||
{atgardsRader.map((p) => {
|
||||
const h = p.handelse;
|
||||
if (h.typ !== "atgard_utford") return null;
|
||||
@@ -2507,7 +2522,7 @@ function RapportFlik({
|
||||
["Parts", h.delar],
|
||||
])
|
||||
) : (
|
||||
<p className="text-[14px] text-[#9A6700]">No action performed — {h.motivering}</p>
|
||||
<p className="text-[14px] text-[#8A5A00]">No action performed — {h.motivering}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2523,15 +2538,15 @@ function RapportFlik({
|
||||
{fordelning.length > 0 && (
|
||||
<div className="mt-2">
|
||||
{fordelning.map((r) => (
|
||||
<p key={r.label} className="text-[#333333]">{r.label}: {r.tid}</p>
|
||||
<p key={r.label} className="text-[#1B1E22]">{r.label}: {r.tid}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel rubrik="Timeline">
|
||||
{kundposter.map((post) => (
|
||||
<p key={post.id} className="py-0.5 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
<p key={post.id} className="py-0 text-[13px]">
|
||||
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||
{handelseRubrik(post)}
|
||||
</p>
|
||||
))}
|
||||
@@ -2540,8 +2555,8 @@ function RapportFlik({
|
||||
<Panel rubrik="Documented video">
|
||||
{klipp.map((v, i) => (
|
||||
<figure key={i} className="mb-2">
|
||||
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
|
||||
<Klipp bilaga={v.bilaga} className="w-full border border-[#D7DCE2]" />
|
||||
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</Panel>
|
||||
@@ -2551,8 +2566,8 @@ 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="border border-[#C6C6C6]" />
|
||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
||||
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#D7DCE2]" />
|
||||
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
@@ -2561,11 +2576,11 @@ function RapportFlik({
|
||||
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
||||
<Panel rubrik="Recommended next steps">
|
||||
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
|
||||
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
||||
<p key={i} className="py-0 text-[14px]">{i + 1}. {s}</p>
|
||||
))}
|
||||
</Panel>
|
||||
) : null}
|
||||
<p className="text-center text-[11px] text-[#8A8A8A]">
|
||||
<p className="text-center text-[11px] text-[#4D5662]">
|
||||
Generated from the case log · Observations and measurements are reported without conclusions that lack support.
|
||||
</p>
|
||||
<Ritningsstampel arende={arende} />
|
||||
@@ -2632,7 +2647,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOppen(true)}
|
||||
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline"
|
||||
className="text-[12px] uppercase tracking-wide text-[#4D5662] underline"
|
||||
>
|
||||
Report a problem with the system
|
||||
</button>
|
||||
@@ -2642,7 +2657,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
|
||||
return (
|
||||
<Panel rubrik="Report a problem with the system">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
<p className="mb-2 text-[12px] text-[#4D5662]">
|
||||
Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
@@ -2664,7 +2679,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
||||
platshallare="Whoever answers cannot see your screen."
|
||||
flerRad
|
||||
/>
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Reported as {utfall.ok}.</p>}
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#005CA9]">Reported as {utfall.ok}.</p>}
|
||||
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||
@@ -2708,11 +2723,11 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="alva-yta mt-3 border border-[#C6C6C6] bg-white">
|
||||
<div className="alva-yta mt-4 border border-[#D7DCE2] 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 key={etikett} className="border-b border-r border-[#D7DCE2] px-2 py-2 last:border-r-0">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{etikett}</div>
|
||||
<div className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -44,13 +44,13 @@ function PlattformInloggning() {
|
||||
return (
|
||||
<>
|
||||
<Panel rubrik="Platform account">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<p className="text-[14px]">
|
||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||
{konto.organisation} — synchronization and decision support active.
|
||||
</p>
|
||||
<button
|
||||
className="whitespace-nowrap border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
|
||||
className="whitespace-nowrap border border-[#D7DCE2] px-4 py-2 font-semibold text-[#1B1E22] hover:border-[#4D5662]"
|
||||
onClick={() => {
|
||||
loggaUtPlattform();
|
||||
setKonto(null);
|
||||
@@ -60,7 +60,7 @@ function PlattformInloggning() {
|
||||
</button>
|
||||
</div>
|
||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
||||
<Link to="/felsokning/oversikt" className="mt-3 block">
|
||||
<Link to="/felsokning/oversikt" className="mt-4 block">
|
||||
<StorKnapp variant="sekundar"><IkonDiagram /> Organization overview</StorKnapp>
|
||||
</Link>
|
||||
)}
|
||||
@@ -79,7 +79,7 @@ function PlattformInloggning() {
|
||||
<Panel rubrik="Platform account">
|
||||
{lage === "stangd" ? (
|
||||
<>
|
||||
<p className="mb-2 text-[#333333]">
|
||||
<p className="mb-2 text-[#1B1E22]">
|
||||
Sign in for synchronization across devices, collaboration on cases and decision support. Without sign-in the app works in local mode. New organization? Create an account — you become the system administrator.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -113,7 +113,7 @@ function PlattformInloggning() {
|
||||
)}
|
||||
<TextFalt label="E-mail" varde={epost} satt={setEpost} platshallare="anna@workshop.example" />
|
||||
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
{fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||
Cancel
|
||||
@@ -175,23 +175,23 @@ function AnvandarAdmin() {
|
||||
{lista.map((anv) => (
|
||||
<div
|
||||
key={anv.id}
|
||||
className="flex items-center justify-between gap-3 border-b border-[#DDDDDD] py-1.5 last:border-0"
|
||||
className="flex items-center justify-between gap-4 border-b border-[#D7DCE2] py-2 last:border-0"
|
||||
>
|
||||
<p className="min-w-0 text-[14px]">
|
||||
<span className={`font-semibold ${anv.aktiv === false ? "text-[#8A8A8A] line-through" : ""}`}>
|
||||
<span className={`font-semibold ${anv.aktiv === false ? "text-[#4D5662] line-through" : ""}`}>
|
||||
{anv.namn}
|
||||
</span>{" "}
|
||||
· {ROLL_LABEL[anv.roll]} <span className="text-[#707070]">{anv.epost}</span>
|
||||
· {ROLL_LABEL[anv.roll]} <span className="text-[#4D5662]">{anv.epost}</span>
|
||||
{anv.aktiv === false && (
|
||||
<span className="ml-1 text-[12px] font-semibold text-[#8B1A1A]">Disabled</span>
|
||||
<span className="ml-2 text-[12px] font-semibold text-[#8B1A1A]">Disabled</span>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className={`shrink-0 border px-2 py-1 text-[12px] font-semibold ${
|
||||
className={`shrink-0 border px-2 py-2 text-[12px] font-semibold ${
|
||||
anv.aktiv === false
|
||||
? "border-[#1E6B34] bg-white text-[#1E6B34]"
|
||||
: "border-[#6E1414] bg-white text-[#8B1A1A]"
|
||||
? "border-[#005CA9] bg-white text-[#005CA9]"
|
||||
: "border-[#8B1A1A] bg-white text-[#8B1A1A]"
|
||||
}`}
|
||||
onClick={async () => {
|
||||
setFel("");
|
||||
@@ -207,11 +207,11 @@ function AnvandarAdmin() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-1 text-[12px] text-[#707070]">
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||
Disabling takes effect immediately — active sessions on that person's devices end at once.
|
||||
</p>
|
||||
<form
|
||||
className="mt-3"
|
||||
className="mt-4"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setFel("");
|
||||
@@ -229,7 +229,7 @@ function AnvandarAdmin() {
|
||||
<TextFalt label="Name" varde={namn} satt={setNamn} />
|
||||
<TextFalt label="E-mail" varde={epost} satt={setEpost} />
|
||||
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||
<div className="mb-4 grid grid-cols-3 gap-2">
|
||||
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
|
||||
<button
|
||||
key={valbar}
|
||||
@@ -237,15 +237,15 @@ function AnvandarAdmin() {
|
||||
onClick={() => setRoll(valbar)}
|
||||
className={`min-h-9 border text-[12px] font-semibold ${
|
||||
roll === valbar
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
|
||||
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22]"
|
||||
}`}
|
||||
>
|
||||
{ROLL_LABEL[valbar]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
{fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
||||
Create user
|
||||
</StorKnapp>
|
||||
@@ -271,7 +271,7 @@ export default function Arendelista() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||
<Panel rubrik="Who is working?">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
<p className="mb-4 text-[14px] text-[#1B1E22]">
|
||||
All work is logged with user and timestamp. Enter your name to begin.
|
||||
</p>
|
||||
<form
|
||||
@@ -304,20 +304,20 @@ export default function Arendelista() {
|
||||
return (
|
||||
<FelsokningSkal
|
||||
rubrik="Guided Diagnostics"
|
||||
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{anvandare}</span>}
|
||||
hoger={<span className="text-[12px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{anvandare}</span>}
|
||||
>
|
||||
<Link to="/felsokning/nytt">
|
||||
<StorKnapp className="mb-4">+ New case</StorKnapp>
|
||||
</Link>
|
||||
|
||||
{alla.length > 0 && (
|
||||
<div className="mb-4 grid grid-cols-3 gap-1 border border-[#C6C6C6] bg-[#F7F7F7] p-1">
|
||||
<div className="mb-4 grid grid-cols-3 gap-2 border border-[#D7DCE2] bg-[#F6F7F8] p-2">
|
||||
{FILTER.map((f) => (
|
||||
<button
|
||||
key={f.id}
|
||||
onClick={() => setFilter(f.id)}
|
||||
className={`min-h-9 text-[13px] font-semibold transition-colors ${
|
||||
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
||||
className={`min-h-9 text-[13px] font-semibold ${
|
||||
filter === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||
}`}
|
||||
>
|
||||
{f.label} · {f.antal}
|
||||
@@ -328,7 +328,7 @@ export default function Arendelista() {
|
||||
|
||||
{alla.length === 0 && (
|
||||
<Panel rubrik="Get started">
|
||||
<p className="mb-3 text-[14px] text-[#333333]">
|
||||
<p className="mb-4 text-[14px] text-[#1B1E22]">
|
||||
No cases yet. Start by identifying an object — or explore a finished demo case with a complete work log, brief and customer report.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
||||
@@ -338,7 +338,7 @@ export default function Arendelista() {
|
||||
)}
|
||||
|
||||
{lista.length === 0 && alla.length > 0 && (
|
||||
<p className="text-center text-[14px] text-[#4A5560]">No cases in this filter.</p>
|
||||
<p className="text-center text-[14px] text-[#4D5662]">No cases in this filter.</p>
|
||||
)}
|
||||
|
||||
{plattformAktiv() && <PlattformInloggning />}
|
||||
@@ -350,7 +350,7 @@ export default function Arendelista() {
|
||||
)}
|
||||
|
||||
<Panel rubrik="Decision support">
|
||||
<p className="text-[#333333]">
|
||||
<p className="text-[#1B1E22]">
|
||||
When you are signed in the system helps while you work: it proposes the next step from what you document, reviews the whole case on request and drafts the handover. It always separates verified from hypothesis — and never states a root cause that is not confirmed. Nothing to install or configure; it is part of the service. Without sign-in the checklist guides you step by step.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -363,24 +363,24 @@ 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 border border-[#C6C6C6] bg-[#F7F7F7] p-4 transition-colors hover:border-[#00437A]">
|
||||
<div className="mb-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4 hover:border-[#005CA9]">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">
|
||||
Case #{arende.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
|
||||
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||
}`}
|
||||
>
|
||||
{avslutat ? "Closed" : "In progress"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</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)}` : ""}
|
||||
<p className="mt-2 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</p>
|
||||
{o && <p className="text-[12px] font-semibold text-[#4D5662]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
|
||||
{fel && <p className="mt-2 text-[14px] text-[#1B1E22]">”{fel}”</p>}
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||
{sista ? `Last activity ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
|
||||
{" · "}
|
||||
{b.totalArbetstid}
|
||||
</p>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function DelatArende() {
|
||||
if (!arende) {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Shared case">
|
||||
<p className="text-[14px] text-[#333333]">The case is not available.</p>
|
||||
<p className="text-[14px] text-[#1B1E22]">The case is not available.</p>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ function Vallista({
|
||||
<button
|
||||
key={val}
|
||||
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
|
||||
className={`min-h-10 border px-2 font-semibold transition-colors ${
|
||||
className={`min-h-10 border px-2 font-semibold ${
|
||||
aktiv
|
||||
? "border-[#00437A] bg-[#00437A] text-white"
|
||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
|
||||
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#4D5662]"
|
||||
}`}
|
||||
>
|
||||
{aktiv ? "✓ " : ""}
|
||||
@@ -85,11 +85,11 @@ function Integrationer() {
|
||||
|
||||
return (
|
||||
<Panel rubrik="Brand-specific connectors">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
<p className="mb-2 text-[12px] text-[#4D5662]">
|
||||
The organization's own agreements with manufacturers and data providers. The details are stored encrypted on the platform and used only by the server — they are never sent to technicians' devices.
|
||||
</p>
|
||||
{!krypteringKlar && (
|
||||
<p className="mb-2 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
<p className="mb-2 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[12px] font-semibold text-[#8A5A00]">
|
||||
Encryption is not configured in this deployment (INTEGRATION_NYCKEL) — details cannot be saved until the key exists.
|
||||
</p>
|
||||
)}
|
||||
@@ -97,13 +97,13 @@ function Integrationer() {
|
||||
{leverantorer.map((lev) => {
|
||||
const finns = befintliga.find((i) => i.leverantor === lev.id);
|
||||
return (
|
||||
<div key={lev.id} className="border-b border-[#EBEBEB] py-2 last:border-0">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div key={lev.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[13px] font-semibold">{lev.namn}</p>
|
||||
{lev.beskrivning && <p className="text-[12px] text-[#707070]">{lev.beskrivning}</p>}
|
||||
{lev.beskrivning && <p className="text-[12px] text-[#4D5662]">{lev.beskrivning}</p>}
|
||||
{finns && (
|
||||
<p className="mt-1 text-[12px] text-[#4A5560]">
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||
{Object.entries(finns.uppgifter)
|
||||
.map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`)
|
||||
.join(" · ")}
|
||||
@@ -111,13 +111,13 @@ function Integrationer() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`shrink-0 text-[11px] font-semibold ${finns ? "text-[#1E6B34]" : "text-[#707070]"}`}>
|
||||
<span className={`shrink-0 text-[11px] font-semibold ${finns ? "text-[#005CA9]" : "text-[#4D5662]"}`}>
|
||||
{finns ? "Configured" : "Not configured"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{oppen === lev.id ? (
|
||||
<div className="mt-2 border border-[#C6C6C6] bg-white p-2">
|
||||
<div className="mt-2 border border-[#D7DCE2] bg-white p-2">
|
||||
{lev.falt.map((falt) => (
|
||||
<TextFalt
|
||||
key={falt.nyckel}
|
||||
@@ -150,7 +150,7 @@ function Integrationer() {
|
||||
Save details
|
||||
</StorKnapp>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
Secret fields are never shown again after saving — enter a new value to replace one.
|
||||
</p>
|
||||
</div>
|
||||
@@ -188,7 +188,7 @@ function Integrationer() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">Details saved for {sparat}.</p>}
|
||||
{sparat && <p className="mt-2 text-[12px] font-semibold text-[#005CA9]">Details saved for {sparat}.</p>}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -210,7 +210,7 @@ export default function Installningar() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
<p className="text-[14px] text-[#1B1E22]">
|
||||
Organization settings are managed by your system administrator.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -243,7 +243,7 @@ export default function Installningar() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[#333333]">
|
||||
<p className="text-[#1B1E22]">
|
||||
Choose what is shown when a new case is started.{" "}
|
||||
{inloggad
|
||||
? "The setting applies to everyone in the organization."
|
||||
@@ -269,8 +269,8 @@ export default function Installningar() {
|
||||
|
||||
{inloggad && <Integrationer />}
|
||||
|
||||
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
|
||||
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]">✓ Saved</p>}
|
||||
{status === "fel" && <p className="mb-4 font-semibold text-[#8B1A1A]">{felText}</p>}
|
||||
{status === "sparat" && <p className="mb-4 font-semibold text-[#005CA9]">✓ Saved</p>}
|
||||
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
||||
{status === "sparar" ? "Saving …" : "Save settings"}
|
||||
</StorKnapp>
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function NyttArende() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel rubrik="Scan work order">
|
||||
<p className="mb-3 text-[#333333]">
|
||||
<p className="mb-4 text-[#1B1E22]">
|
||||
Photograph the front of the work order. The system reads customer, vehicle and case details automatically — you review only the uncertain fields and press Start diagnosis.
|
||||
</p>
|
||||
<input
|
||||
@@ -192,7 +192,7 @@ export default function NyttArende() {
|
||||
}}
|
||||
/>
|
||||
{skannar ? (
|
||||
<p className="animate-pulse py-2 text-center font-semibold text-[#00437A]">Interpreting the document …</p>
|
||||
<p className="py-2 text-center font-semibold text-[#005CA9]">Interpreting the document …</p>
|
||||
) : (
|
||||
<StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp>
|
||||
)}
|
||||
@@ -215,7 +215,7 @@ export default function NyttArende() {
|
||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||
>
|
||||
{tolkning.demo && (
|
||||
<p className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2 text-[12px] font-semibold text-[#9A6700]">
|
||||
<p className="mb-4 border border-[#8A5A00] bg-[#FFFFFF] p-2 text-[12px] font-semibold text-[#8A5A00]">
|
||||
Demo interpretation — the platform's document interpretation requires sign-in. The values below are sample data.
|
||||
</p>
|
||||
)}
|
||||
@@ -253,7 +253,7 @@ export default function NyttArende() {
|
||||
</StorKnapp>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-[12px] text-[#707070]">
|
||||
<p className="mt-4 text-[12px] text-[#4D5662]">
|
||||
QR code, barcode and type plate can be read in the next step — or enter the identity manually.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -272,8 +272,8 @@ export default function NyttArende() {
|
||||
/>
|
||||
{skannarKod ? (
|
||||
<div>
|
||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#D7DCE2]" />
|
||||
<p className="mb-2 text-[12px] text-[#4D5662]">
|
||||
Point the camera at the QR code, barcode or VIN label.
|
||||
</p>
|
||||
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
||||
@@ -293,7 +293,7 @@ export default function NyttArende() {
|
||||
</div>
|
||||
)}
|
||||
{avlastKod && (
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#1E6B34]">
|
||||
<p className="mt-2 text-[12px] font-semibold text-[#005CA9]">
|
||||
Read {avlastKod.typ}: {avlastKod.varde}
|
||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
||||
</p>
|
||||
@@ -316,11 +316,11 @@ export default function NyttArende() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<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>}
|
||||
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">{objektet.typ}</p>
|
||||
<p className="mt-2 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
||||
<p className="mt-2 text-[15px] font-semibold text-[#005CA9]">{objektet.identifierare}</p>
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">Identifierad via: {objektet.identifieringsmetod}</p>
|
||||
{objektet.kund && <p className="mt-2 text-[14px] text-[#1B1E22]">Kund: {objektet.kund}</p>}
|
||||
</Panel>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||
@@ -346,8 +346,8 @@ export default function NyttArende() {
|
||||
rost
|
||||
/>
|
||||
{metodik && (
|
||||
<p className="mb-3 text-[12px] font-semibold text-[#4A5560]">
|
||||
Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span>
|
||||
<p className="mb-4 text-[12px] font-semibold text-[#4D5662]">
|
||||
Vald metodik: <span className="text-[#005CA9]">{metodik.namn}</span>
|
||||
</p>
|
||||
)}
|
||||
<StorKnapp
|
||||
@@ -401,13 +401,13 @@ function GranskaTolkning({
|
||||
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
|
||||
|
||||
return (
|
||||
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-3">
|
||||
<div className="lg:grid lg:grid-cols-2 lg:items-start lg:gap-4">
|
||||
<Panel rubrik="Scanned work order">
|
||||
<div className="relative">
|
||||
<img src={foto} alt="Scanned work order" className="w-full border border-[#C6C6C6]" />
|
||||
<img src={foto} alt="Scanned work order" className="w-full border border-[#D7DCE2]" />
|
||||
{vald?.omrade && (
|
||||
<div
|
||||
className="pointer-events-none absolute border-2 border-[#00437A] bg-[#00437A]/15"
|
||||
className="pointer-events-none absolute border-2 border-[#005CA9] bg-[#005CA9]/15"
|
||||
style={{
|
||||
left: `${vald.omrade.x * 100}%`,
|
||||
top: `${vald.omrade.y * 100}%`,
|
||||
@@ -417,7 +417,7 @@ function GranskaTolkning({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
{vald?.omrade
|
||||
? `Markerat: ${vald.etikett} (approximate position)`
|
||||
: "Select a field to see where in the document the value was found."}
|
||||
@@ -436,27 +436,27 @@ function GranskaTolkning({
|
||||
<div
|
||||
key={rad.id}
|
||||
onClick={() => setMarkerad(rad.id)}
|
||||
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"}`}
|
||||
className={`mb-2 cursor-pointer border p-2 ${
|
||||
markerad === rad.id ? "border-[#005CA9]" : "border-transparent"
|
||||
} ${niva === "gul" ? "bg-[#FFFFFF]" : niva === "rod" ? "bg-[#FFFFFF]" : "bg-white"}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">
|
||||
{rad.etikett}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[11px] font-semibold ${
|
||||
niva === "gron" ? "text-[#1E6B34]" : niva === "gul" ? "text-[#9A6700]" : "text-[#8B1A1A]"
|
||||
niva === "gron" ? "text-[#005CA9]" : niva === "gul" ? "text-[#8A5A00]" : "text-[#8B1A1A]"
|
||||
}`}
|
||||
>
|
||||
{niva === "gron" ? "✓" : `${Math.round(rad.konfidens * 100)} %`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
value={rad.varde}
|
||||
onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })}
|
||||
className="w-full border border-[#ADADAD] bg-white px-2 py-1 text-[13px] focus:border-[#00437A] focus:outline-none"
|
||||
className="w-full border border-[#D7DCE2] bg-white px-2 py-2 text-[13px] focus:border-[#005CA9] focus:outline-none"
|
||||
/>
|
||||
{niva === "rod" && !rad.bekraftad && (
|
||||
<button
|
||||
@@ -464,7 +464,7 @@ function GranskaTolkning({
|
||||
e.stopPropagation();
|
||||
uppdatera(rad.id, { bekraftad: true });
|
||||
}}
|
||||
className="whitespace-nowrap border border-[#6E1414] bg-[#8B1A1A] px-3 py-1 text-[12px] font-semibold text-white"
|
||||
className="whitespace-nowrap border border-[#8B1A1A] bg-[#8B1A1A] px-4 py-2 text-[12px] font-semibold text-white"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
|
||||
@@ -45,13 +45,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">New responsible technician</p>
|
||||
<div className="col-span-2 border border-[#D7DCE2] bg-white p-2">
|
||||
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4D5662]">New responsible technician</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{personer.map((person) => (
|
||||
<button
|
||||
key={person.id}
|
||||
className="min-h-9 border border-[#ADADAD] font-semibold text-[#333333] hover:border-[#00437A]"
|
||||
className="min-h-9 border border-[#D7DCE2] font-semibold text-[#1B1E22] hover:border-[#005CA9]"
|
||||
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 border border-[#ADADAD] font-semibold text-[#4A5560]"
|
||||
className="min-h-9 border border-[#D7DCE2] font-semibold text-[#4D5662]"
|
||||
onClick={() => setOppen(false)}
|
||||
>
|
||||
Cancel
|
||||
@@ -101,7 +101,7 @@ export default function Oversikt() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
<p className="text-[14px] text-[#1B1E22]">
|
||||
The overview requires supervisor or administrator rights on the platform.
|
||||
</p>
|
||||
</Panel>
|
||||
@@ -153,30 +153,30 @@ export default function Oversikt() {
|
||||
<FelsokningSkal
|
||||
rubrik="Organization overview"
|
||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||
hoger={<span className="text-[12px] font-semibold text-[#A9C3DE]">{konto?.organisation}</span>}
|
||||
hoger={<span className="text-[12px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">{konto?.organisation}</span>}
|
||||
>
|
||||
{fel && <p className="mb-3 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Retrieving …</p>}
|
||||
{fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||
{!rader && !fel && <p className="text-[14px] text-[#4D5662]">Retrieving …</p>}
|
||||
|
||||
{rader && (
|
||||
<>
|
||||
<Panel rubrik="Statistics">
|
||||
<div className="grid grid-cols-3 gap-3 text-center">
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<p className="text-[20px] font-semibold">{rader.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Total</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4D5662]">Total</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">In progress</p>
|
||||
<p className="text-[20px] font-semibold text-[#005CA9]">{pagaende.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4D5662]">In progress</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[20px] font-semibold">{avslutade.length}</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">Closed</p>
|
||||
<p className="text-[12px] font-semibold uppercase text-[#4D5662]">Closed</p>
|
||||
</div>
|
||||
</div>
|
||||
{avslutade.length > 0 && (
|
||||
<p className="mt-3 text-center text-[#333333]">
|
||||
<p className="mt-4 text-center text-[#1B1E22]">
|
||||
Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||
</p>
|
||||
)}
|
||||
@@ -185,41 +185,41 @@ export default function Oversikt() {
|
||||
{felorsaker.length > 0 && (
|
||||
<Panel rubrik="Root cause statistics — recurring faults in the organization">
|
||||
{felorsaker.map((rad) => (
|
||||
<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>
|
||||
<div key={rad.orsak} className="flex items-center gap-2 py-0 text-[13px]">
|
||||
<span className="w-52 shrink-0 text-[#1B1E22]">{rad.orsak}</span>
|
||||
<span
|
||||
className="h-3 bg-[#00437A]"
|
||||
className="h-3 bg-[#005CA9]"
|
||||
style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }}
|
||||
/>
|
||||
<span className="font-semibold">{rad.antal}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="mt-2 text-[11px] text-[#707070]">
|
||||
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||
From documented root cause analyses — shows patterns such as wear, previous repairs or possible design problems.
|
||||
</p>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{rader.length === 0 && <p className="text-center text-[14px] text-[#4A5560]">No cases in the organization yet.</p>}
|
||||
{rader.length === 0 && <p className="text-center text-[14px] text-[#4D5662]">No cases in the organization yet.</p>}
|
||||
|
||||
{rader.map((rad) => (
|
||||
<div key={rad.id} className="mb-3 border border-[#C6C6C6] bg-[#F7F7F7] p-4">
|
||||
<div key={rad.id} className="mb-4 border border-[#D7DCE2] bg-[#F6F7F8] p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">
|
||||
<span className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">
|
||||
Case #{rad.nummer}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
||||
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
||||
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
|
||||
rad.avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||
}`}
|
||||
>
|
||||
{rad.avslutat ? "Closed" : "In progress"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</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>
|
||||
<p className="mt-2 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
|
||||
{rad.felbeskrivning && <p className="text-[14px] text-[#1B1E22]">”{rad.felbeskrivning}”</p>}
|
||||
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||
Ansvarig: <span className="font-semibold text-[#1B1E22]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||
{` · ${rad.antal_handelser} events`}
|
||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||
</p>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PublikDelning() {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Shared case">
|
||||
<Panel>
|
||||
<p className="text-[14px] text-[#333333]">
|
||||
<p className="text-[14px] text-[#1B1E22]">
|
||||
{status === "laddar"
|
||||
? "Retrieving the case …"
|
||||
: "The case is not available. Check the link with the workshop — the share may have been revoked."}
|
||||
|
||||
Reference in New Issue
Block a user