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
|
// `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
|
// texten bryts till två rader blir olika HÖGA, vilket är samma
|
||||||
// obalans en rad ned. Bägge fyller sin cell.
|
// 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" : ""
|
bred ? " h-full w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function useKalla(bilaga: Bilaga, delningskod?: string): string | null | "laddar
|
|||||||
|
|
||||||
function Platshallare({ text }: { text: string }) {
|
function Platshallare({ text }: { text: string }) {
|
||||||
return (
|
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}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ function BeslutsKnappar({
|
|||||||
const [fel, setFel] = useState("");
|
const [fel, setFel] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
|
<div className="mt-2 border-t border-[#D7DCE2] pt-2 print:hidden">
|
||||||
<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]">
|
||||||
Your decision to the workshop
|
Your decision to the workshop
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-2 gap-2">
|
<div className="mb-2 grid grid-cols-2 gap-2">
|
||||||
@@ -43,14 +43,14 @@ function BeslutsKnappar({
|
|||||||
{val && (
|
{val && (
|
||||||
<>
|
<>
|
||||||
<label className="mb-2 block">
|
<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)"}
|
{val === "avbojt" ? "Short rationale (optional)" : "Comment (optional)"}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
value={kommentar}
|
value={kommentar}
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
onChange={(e) => setKommentar(e.target.value)}
|
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>
|
</label>
|
||||||
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
@@ -66,7 +66,7 @@ function BeslutsKnappar({
|
|||||||
>
|
>
|
||||||
{skickar ? "Sending …" : "Send decision"}
|
{skickar ? "Sending …" : "Send decision"}
|
||||||
</StorKnapp>
|
</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.
|
The decision is recorded in the case and cannot be changed here — contact the workshop if you change your mind.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -88,14 +88,14 @@ function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boole
|
|||||||
["Status", avslutat ? "Closed" : "Diagnosis in progress"],
|
["Status", avslutat ? "Closed" : "Diagnosis in progress"],
|
||||||
];
|
];
|
||||||
return (
|
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>}
|
{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]">
|
<p className="flex flex-wrap gap-x-4 gap-y-0.5 text-[12px]">
|
||||||
{falt
|
{falt
|
||||||
.filter(([, varde]) => varde)
|
.filter(([, varde]) => varde)
|
||||||
.map(([etikett, varde]) => (
|
.map(([etikett, varde]) => (
|
||||||
<span key={etikett} className="whitespace-nowrap">
|
<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>
|
</span>
|
||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
@@ -145,8 +145,8 @@ export function DelatArendeVy({
|
|||||||
rubrik={b.objekt?.beskrivning ?? "Case"}
|
rubrik={b.objekt?.beskrivning ?? "Case"}
|
||||||
hoger={
|
hoger={
|
||||||
<span
|
<span
|
||||||
className={`px-3 py-1 text-[12px] font-semibold uppercase ${
|
className={`px-4 py-2 text-[12px] font-semibold uppercase ${
|
||||||
avslutat ? "bg-[#3E5A78] text-white" : "bg-[#1E6B34] text-white"
|
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{avslutat ? "Closed" : "Diagnosis in progress"}
|
{avslutat ? "Closed" : "Diagnosis in progress"}
|
||||||
@@ -185,7 +185,7 @@ export function DelatArendeVy({
|
|||||||
{rader.map(([etikett, text]) => (
|
{rader.map(([etikett, text]) => (
|
||||||
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
|
<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>
|
<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>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</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}
|
{notis}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -212,20 +212,20 @@ export function DelatArendeVy({
|
|||||||
<div key={p.id} className="mb-2 last:mb-0">
|
<div key={p.id} className="mb-2 last:mb-0">
|
||||||
<p className="text-[14px]">{h.beskrivning}</p>
|
<p className="text-[14px]">{h.beskrivning}</p>
|
||||||
{h.uppskattadKostnad && (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{beslut?.handelse.typ === "kundbeslut" ? (
|
{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]}{" "}
|
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>
|
</p>
|
||||||
) : vidBeslut ? (
|
) : vidBeslut ? (
|
||||||
<BeslutsKnappar vidBeslut={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.
|
Awaiting your decision — contact the workshop to approve or decline.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -234,11 +234,11 @@ export function DelatArendeVy({
|
|||||||
|
|
||||||
<Panel rubrik="Current status">
|
<Panel rubrik="Current status">
|
||||||
{b.utfordaKontroller.map((k, i) => (
|
{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) => (
|
{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>
|
</Panel>
|
||||||
|
|
||||||
@@ -246,8 +246,8 @@ export function DelatArendeVy({
|
|||||||
<Panel rubrik="Video">
|
<Panel rubrik="Video">
|
||||||
{klipp.map((v, i) => (
|
{klipp.map((v, i) => (
|
||||||
<figure key={i} className="mb-2">
|
<figure key={i} className="mb-2">
|
||||||
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#C6C6C6]" />
|
<Klipp bilaga={v.bilaga} delningskod={delningskod} className="w-full border border-[#D7DCE2]" />
|
||||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
|
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -257,8 +257,8 @@ export function DelatArendeVy({
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{bilder.map((bild, i) => (
|
{bilder.map((bild, i) => (
|
||||||
<figure key={i}>
|
<figure key={i}>
|
||||||
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#C6C6C6]" />
|
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} delningskod={delningskod} className="border border-[#D7DCE2]" />
|
||||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -273,9 +273,9 @@ export function DelatArendeVy({
|
|||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "matvarde") return null;
|
if (h.typ !== "matvarde") return null;
|
||||||
return (
|
return (
|
||||||
<tr key={p.id} className="border-b border-[#DDDDDD] last:border-0">
|
<tr key={p.id} className="border-b border-[#D7DCE2] last:border-0">
|
||||||
<td className="py-1 pr-3 text-[#333333]">{h.beskrivning}</td>
|
<td className="py-2 pr-4 text-[#1B1E22]">{h.beskrivning}</td>
|
||||||
<td className="py-1 font-semibold">
|
<td className="py-2 font-semibold">
|
||||||
{h.varde}
|
{h.varde}
|
||||||
{h.enhet ? ` ${h.enhet}` : ""}
|
{h.enhet ? ` ${h.enhet}` : ""}
|
||||||
</td>
|
</td>
|
||||||
@@ -289,8 +289,8 @@ export function DelatArendeVy({
|
|||||||
|
|
||||||
<Panel rubrik="Timeline">
|
<Panel rubrik="Timeline">
|
||||||
{kundposter.map((post) => (
|
{kundposter.map((post) => (
|
||||||
<p key={post.id} className="py-0.5 text-[13px]">
|
<p key={post.id} className="py-0 text-[13px]">
|
||||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||||
{handelseRubrik(post)}
|
{handelseRubrik(post)}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -305,13 +305,13 @@ export function DelatArendeVy({
|
|||||||
<Panel rubrik="Labour time">
|
<Panel rubrik="Labour time">
|
||||||
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
<p className="text-[17px] font-semibold">{b.totalArbetstid}</p>
|
||||||
{tidsfordelningsRader(arende, nu).map((r) => (
|
{tidsfordelningsRader(arende, nu).map((r) => (
|
||||||
<p key={r.label} className="text-[#333333]">
|
<p key={r.label} className="text-[#1B1E22]">
|
||||||
{r.label}: {r.tid}
|
{r.label}: {r.tid}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</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
|
Case #{arende.nummer} · started {tidDatum(arende.skapad)} · generated from the case log
|
||||||
</p>
|
</p>
|
||||||
</FelsokningSkal>
|
</FelsokningSkal>
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => (lyssnar ? stoppa() : starta())}
|
onClick={() => (lyssnar ? stoppa() : starta())}
|
||||||
aria-label={lyssnar ? "Stop recording" : "Start voice input"}
|
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
|
lyssnar
|
||||||
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
|
? "border-[#8B1A1A] bg-[#8B1A1A] text-white"
|
||||||
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
|
: "border-[#D7DCE2] bg-white text-[#1B1E22] hover:border-[#005CA9]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{lyssnar ? "■" : <IkonMik />}
|
{lyssnar ? "■" : <IkonMik />}
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
>
|
>
|
||||||
{etikett}
|
{etikett}
|
||||||
</label>
|
</label>
|
||||||
<p className="mt-1 text-[13px]" style={{ color: FARG.graphite }}>
|
<p className="mt-2 text-[13px]" style={{ color: FARG.graphite }}>
|
||||||
{f.fraga}
|
{f.fraga}
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -189,7 +189,7 @@ export function Slutsatspanel({ arende, skicka }: { arende: Arende; skicka: (h:
|
|||||||
color: FARG.graphite,
|
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}
|
{fel ? fel.text : f.hjalp}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ describe("AI-orkestern", () => {
|
|||||||
"Reläet klickar inte.",
|
"Reläet klickar inte.",
|
||||||
);
|
);
|
||||||
expect(prompt).toContain("Volvo XC60 D4 2019");
|
expect(prompt).toContain("Volvo XC60 D4 2019");
|
||||||
expect(prompt).toContain("Felbeskrivning:");
|
expect(prompt).toContain("Fault description:");
|
||||||
expect(prompt).toContain("Kontrollera lufttryck");
|
expect(prompt).toContain("Kontrollera lufttryck");
|
||||||
expect(prompt).toContain("Ej kontrollerat enligt metodiken:");
|
expect(prompt).toContain("Ej kontrollerat enligt metodiken:");
|
||||||
expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte.");
|
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", () => {
|
it("granskningsprompten innehåller hela arbetsloggen men ingen ny inmatning", () => {
|
||||||
const arende = byggDemoArende(1);
|
const arende = byggDemoArende(1);
|
||||||
const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK);
|
const prompt = byggGranskningsPrompt(arende, VIBRATION_METODIK);
|
||||||
expect(prompt).toContain("Fullständig arbetslogg:");
|
expect(prompt).toContain("Full work log:");
|
||||||
expect(prompt).toContain("Arbete överlämnat från Anna till Johan");
|
expect(prompt).toContain("Work handed over from Anna to Johan");
|
||||||
expect(prompt).not.toContain("Teknikerns nya inmatning");
|
expect(prompt).not.toContain("Teknikerns nya inmatning");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ import { readdirSync, readFileSync } from "node:fs";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { FORBJUDNA_ORD } from "@/alva/sprak";
|
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) =>
|
const filer = KATALOGER.flatMap((katalog) =>
|
||||||
readdirSync(katalog)
|
readdirSync(katalog)
|
||||||
@@ -28,7 +37,17 @@ const utanKommentarer = (kod: string) =>
|
|||||||
|
|
||||||
describe("ALVA-ytan följer designsystemet", () => {
|
describe("ALVA-ytan följer designsystemet", () => {
|
||||||
it("hittar filerna att granska", () => {
|
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) => {
|
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", () => {
|
it("har cirka 1,5 timmars loggad arbetstid och en överlämning", () => {
|
||||||
const b = brief(arende, VIBRATION_METODIK);
|
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(arende.handelser.some((p) => p.handelse.typ === "overlamning")).toBe(true);
|
||||||
expect(new Set(arende.handelser.map((p) => p.anvandare)).size).toBeGreaterThanOrEqual(2);
|
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 () => {
|
it("avslutet signeras och grinden redovisar signaturen", async () => {
|
||||||
const { handelseRubrik, nyLoggPost: ny } = await import("../domain");
|
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 stangd = byggArende([OBJEKT, ...PREDIAG, { typ: "arende_avslutat", signatur: "Erik" }]);
|
||||||
const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering");
|
const rad = kvalitetsgrind(stangd, VIBRATION_METODIK).find((r) => r.id === "signering");
|
||||||
expect(rad?.ok).toBe(true);
|
expect(rad?.ok).toBe(true);
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ export interface AiSvarMedModell extends AiSvar {
|
|||||||
|
|
||||||
export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string {
|
export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning?: string): string {
|
||||||
const rader: string[] = [];
|
const rader: string[] = [];
|
||||||
rader.push(`Metodik: ${metodikNamn}`);
|
rader.push(`Methodology: ${metodikNamn}`);
|
||||||
if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
if (brief.objekt) rader.push(`Object: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`);
|
||||||
if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`);
|
if (brief.felbeskrivning) rader.push(`Fault description: ${brief.felbeskrivning}`);
|
||||||
if (brief.utfordaKontroller.length > 0) {
|
if (brief.utfordaKontroller.length > 0) {
|
||||||
rader.push("Checks performed:");
|
rader.push("Checks performed:");
|
||||||
for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`);
|
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
|
const logg = arende.handelser
|
||||||
.map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`)
|
.map((p) => `${p.tidpunkt.slice(11, 16)} ${p.anvandare}: ${handelseRubrik(p)}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
return `${kontext}\n\nFullständig arbetslogg:\n${logg}`;
|
return `${kontext}\n\nFull work log:\n${logg}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tolkaAiSvar(data: unknown): AiSvar {
|
export function tolkaAiSvar(data: unknown): AiSvar {
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ function demoFoto(text: string): string {
|
|||||||
canvas.height = 360;
|
canvas.height = 360;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) throw new Error("canvas saknas");
|
if (!ctx) throw new Error("canvas saknas");
|
||||||
ctx.fillStyle = "#27272a";
|
ctx.fillStyle = "#1B1E22";
|
||||||
ctx.fillRect(0, 0, 480, 360);
|
ctx.fillRect(0, 0, 480, 360);
|
||||||
ctx.strokeStyle = "#52525b";
|
ctx.strokeStyle = "#4D5662";
|
||||||
ctx.lineWidth = 22;
|
ctx.lineWidth = 22;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(240, 170, 110, 0, Math.PI * 2);
|
ctx.arc(240, 170, 110, 0, Math.PI * 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.strokeStyle = "#a1a1aa";
|
ctx.strokeStyle = "#4D5662";
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 4;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(240, 170, 58, 0, Math.PI * 2);
|
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.lineTo(240 + Math.cos(v) * 58, 170 + Math.sin(v) * 58);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
ctx.fillStyle = "#fbbf24";
|
ctx.fillStyle = "#8A5A00";
|
||||||
ctx.font = "bold 22px sans-serif";
|
ctx.font = "bold 22px sans-serif";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.fillText(text, 240, 330);
|
ctx.fillText(text, 240, 330);
|
||||||
ctx.fillStyle = "#71717a";
|
ctx.fillStyle = "#4D5662";
|
||||||
ctx.font = "14px sans-serif";
|
ctx.font = "14px sans-serif";
|
||||||
ctx.fillText("Demobild", 240, 26);
|
ctx.fillText("Demobild", 240, 26);
|
||||||
return canvas.toDataURL("image/jpeg", 0.7);
|
return canvas.toDataURL("image/jpeg", 0.7);
|
||||||
|
|||||||
@@ -279,18 +279,18 @@ export function handelseRubrik(post: LoggPost): string {
|
|||||||
case "objekt_identifierat":
|
case "objekt_identifierat":
|
||||||
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
|
return `Objekt identifierat: ${h.objekt.beskrivning} (${h.objekt.identifierare})`;
|
||||||
case "arbetsorder_skannad":
|
case "arbetsorder_skannad":
|
||||||
return `Arbetsorder skannad — ${h.falt.length} fält tolkade`;
|
return `Work order scanned — ${h.falt.length} fields interpreted`;
|
||||||
case "felbeskrivning":
|
case "felbeskrivning":
|
||||||
return `Felbeskrivning registrerad`;
|
return `Felbeskrivning registrerad`;
|
||||||
case "fraga_besvarad":
|
case "fraga_besvarad":
|
||||||
return `${h.fraga} — ${h.svar}`;
|
return `${h.fraga} — ${h.svar}`;
|
||||||
case "kontroll_utford":
|
case "kontroll_utford":
|
||||||
if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`;
|
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":
|
case "observation":
|
||||||
return `Observation: ${h.text}`;
|
return `Observation: ${h.text}`;
|
||||||
case "matvarde":
|
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":
|
case "hypotes":
|
||||||
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
|
return `Hypotes (${TILLFORLITLIGHET_LABEL[h.niva]}): ${h.text}`;
|
||||||
case "foto":
|
case "foto":
|
||||||
@@ -304,18 +304,18 @@ export function handelseRubrik(post: LoggPost): string {
|
|||||||
case "inaktivitet_forklarad":
|
case "inaktivitet_forklarad":
|
||||||
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
|
return `Komplettering (${h.minuter} min utan aktivitet): ${h.text}`;
|
||||||
case "overlamning":
|
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":
|
case "ansvarig_satt":
|
||||||
return `Ansvarig tekniker: ${h.ansvarig}`;
|
return `Ansvarig tekniker: ${h.ansvarig}`;
|
||||||
case "arendetyp_satt":
|
case "arendetyp_satt":
|
||||||
return `Ärendetyp: ${h.arendetyp}`;
|
return `Case type: ${h.arendetyp}`;
|
||||||
case "historik_kontrollerad":
|
case "historik_kontrollerad":
|
||||||
return h.kontrollerad
|
return h.kontrollerad
|
||||||
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
|
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
|
||||||
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
|
: `Fordonshistorik EJ kontrollerad — orsak: ${h.kommentar ?? "saknas"}`;
|
||||||
case "matarstallning":
|
case "matarstallning":
|
||||||
if (h.undantag) return `Mätarställning (${h.lage === "ingaende" ? "in" : "ut"}) kunde inte dokumenteras: ${h.undantag}`;
|
if (h.undantag) return `Odometer reading (${h.lage === "ingaende" ? "in" : "out"}) could not be documented: ${h.undantag}`;
|
||||||
return `Mätarställning ${h.lage === "ingaende" ? "in" : "ut"}: ${h.varde}`;
|
return `Odometer ${h.lage === "ingaende" ? "in" : "out"}: ${h.varde}`;
|
||||||
case "reproducering":
|
case "reproducering":
|
||||||
return h.status === "ja"
|
return h.status === "ja"
|
||||||
? `Symptomet reproducerat: ${h.beskrivning}`
|
? `Symptomet reproducerat: ${h.beskrivning}`
|
||||||
@@ -325,22 +325,22 @@ export function handelseRubrik(post: LoggPost): string {
|
|||||||
case "felorsak":
|
case "felorsak":
|
||||||
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse} — ${h.orsaker.join(", ")}`;
|
return `Felorsak (${TILLFORLITLIGHET_LABEL[h.sakerhet]}): ${h.avvikelse} — ${h.orsaker.join(", ")}`;
|
||||||
case "atgardsforslag":
|
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":
|
case "kundbeslut":
|
||||||
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? ` — ${h.kommentar}` : ""}`;
|
return `Kundens besked (${h.kanal}): ${KUNDBESLUT_LABEL[h.beslut]}${h.kommentar ? ` — ${h.kommentar}` : ""}`;
|
||||||
case "atgard_utford":
|
case "atgard_utford":
|
||||||
return h.utford
|
return h.utford
|
||||||
? `Åtgärd utförd: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
? `Action performed: ${h.beskrivning}${h.delar ? ` (delar: ${h.delar})` : ""}`
|
||||||
: `Ingen åtgärd utförd — ${h.motivering ?? "reason missing"}`;
|
: `No action performed — ${h.motivering ?? "reason missing"}`;
|
||||||
case "kvalitetskontroll":
|
case "kvalitetskontroll":
|
||||||
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
|
return `Kvalitetskontroll: ${KVALITETSKONTROLL_LABEL[h.resultat]} — ${h.beskrivning}`;
|
||||||
case "export_skapad":
|
case "export_skapad":
|
||||||
return `Export skapad: ${h.format}, version ${h.version}`;
|
return `Export skapad: ${h.format}, version ${h.version}`;
|
||||||
case "ai_svar": {
|
case "ai_svar": {
|
||||||
const forsta = h.rader[0];
|
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":
|
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",
|
rubrik: "Photographs present for checks that require them",
|
||||||
ok: fotoKravUtanFoto === 0,
|
ok: fotoKravUtanFoto === 0,
|
||||||
kravs: true,
|
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
|
// Compliance Engine: ärendetypens regler ur det aktiva regelpaketet
|
||||||
@@ -794,7 +794,7 @@ export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
|
|||||||
|
|
||||||
rader.push({
|
rader.push({
|
||||||
id: "evidensniva",
|
id: "evidensniva",
|
||||||
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
|
rubrik: `Evidence level: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
|
||||||
ok: evidensNiva(arende) !== "E0",
|
ok: evidensNiva(arende) !== "E0",
|
||||||
kravs: true,
|
kravs: true,
|
||||||
detalj: evidensNiva(arende) === "E0" ? "No evidence in the log yet." : undefined,
|
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 h = Math.floor(minuter / 60);
|
||||||
const m = minuter % 60;
|
const m = minuter % 60;
|
||||||
if (h === 0) return `${m} min`;
|
if (h === 0) return `${m} min`;
|
||||||
return `${h} tim ${m} min`;
|
return `${h} h ${m} min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TillforlitlighetsRad {
|
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 {
|
export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string): string {
|
||||||
const b = brief(arende, metodik, nu);
|
const b = brief(arende, metodik, nu);
|
||||||
const rader: string[] = [];
|
const rader: string[] = [];
|
||||||
rader.push(`ÖVERLÄMNING – Ärende #${arende.nummer}`);
|
rader.push(`HANDOVER – Case #${arende.nummer}`);
|
||||||
if (b.objekt) rader.push(`Objekt: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Kund: ${b.objekt.kund}` : ""}`);
|
if (b.objekt) rader.push(`Object: ${b.objekt.beskrivning} (${b.objekt.identifierare})${b.objekt.kund ? ` – Customer: ${b.objekt.kund}` : ""}`);
|
||||||
if (b.felbeskrivning) rader.push(`Felbeskrivning: ${b.felbeskrivning}`);
|
if (b.felbeskrivning) rader.push(`Fault description: ${b.felbeskrivning}`);
|
||||||
rader.push("");
|
rader.push("");
|
||||||
rader.push("Checks performed:");
|
rader.push("Checks performed:");
|
||||||
if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
|
if (b.utfordaKontroller.length === 0) rader.push(" (none yet)");
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
// Delade UI-komponenter för Guidad Felsökning.
|
// Delade UI-komponenter för Guidad Felsökning.
|
||||||
// Designfilosofi: industriellt verkstadsverktyg i ETKA-tradition —
|
//
|
||||||
// plana ytor, skarpa kanter, tät informationslayout, dämpad palett
|
// ---- Ett system, inte två -----------------------------------------------
|
||||||
// (ljusgrå ytor, djup marinblå som primärfärg), inga dekorationer.
|
//
|
||||||
// Verktygsrad ~44 px, rektangulära knappar (max 4 px radie), kompakta
|
// Det här filhuvudet beskrev tidigare en egen designfilosofi: "industriellt
|
||||||
// formulär med vänsterställda etiketter.
|
// 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 { Link } from "react-router-dom";
|
||||||
import { useRef, type ReactNode } from "react";
|
import { useRef, type ReactNode } from "react";
|
||||||
@@ -11,13 +34,8 @@ import type { Tillforlitlighet } from "./domain";
|
|||||||
import { TILLFORLITLIGHET_LABEL } from "./domain";
|
import { TILLFORLITLIGHET_LABEL } from "./domain";
|
||||||
import { MikrofonKnapp } from "./Mikrofon";
|
import { MikrofonKnapp } from "./Mikrofon";
|
||||||
import { IkonPunkt } from "./ikoner";
|
import { IkonPunkt } from "./ikoner";
|
||||||
|
import { Etikett, FARG } from "@/alva/komponenter";
|
||||||
// Industriell palett (inspirerad av tyska verkstadssystem):
|
import { ALVA } from "@/alva/system";
|
||||||
// bakgrund #ECECEC · paneler #F7F7F7 · kanter #C6C6C6
|
|
||||||
// primär (djup blå) #00437A · markerad rad #D6E4F2
|
|
||||||
// varning #9A6700 · fel #8B1A1A · ok #1E6B34
|
|
||||||
|
|
||||||
const TYPSNITT = "'Inter', 'IBM Plex Sans', 'Segoe UI', Roboto, system-ui, sans-serif";
|
|
||||||
|
|
||||||
export function FelsokningSkal({
|
export function FelsokningSkal({
|
||||||
rubrik,
|
rubrik,
|
||||||
@@ -34,32 +52,62 @@ export function FelsokningSkal({
|
|||||||
}) {
|
}) {
|
||||||
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
|
const maxBredd = bred ? "max-w-[1240px]" : "max-w-3xl";
|
||||||
return (
|
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
|
<div
|
||||||
className="felsokning-print min-h-screen bg-[#ECECEC] text-[13px] text-[#1A1A1A]"
|
className="alva-yta felsokning-print min-h-screen text-[13px]"
|
||||||
style={{ fontFamily: TYPSNITT }}
|
style={{ background: FARG.background, color: FARG.graphite }}
|
||||||
>
|
>
|
||||||
{/* Verktygsrad: låg, mörk marinblå — breadcrumb till vänster,
|
{/* Verktygsraden är vit med en enda linje under, precis som
|
||||||
status till höger. Inget stort sidhuvud. */}
|
portalens sidhuvud. Den var tidigare mörkt marinblå — ett eget
|
||||||
<header className="sticky top-0 z-20 border-b border-[#0A2A4A] bg-[#123A63] px-3 text-white print:static">
|
varumärke inuti varumärket. */}
|
||||||
<div className={`mx-auto flex min-h-11 ${maxBredd} items-center justify-between gap-3 py-1`}>
|
<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">
|
<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 && (
|
{tillbaka && (
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={tillbaka.till}
|
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}
|
← {tillbaka.text}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
{hoger}
|
{hoger}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,18 +127,22 @@ export function StorKnapp({
|
|||||||
type?: "button" | "submit";
|
type?: "button" | "submit";
|
||||||
className?: string;
|
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"
|
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"
|
: variant === "fara"
|
||||||
? "border border-[#6E1414] bg-[#8B1A1A] text-white hover:bg-[#A32020] active:bg-[#6E1414]"
|
? { background: FARG.stoppat, color: FARG.white, borderColor: FARG.stoppat }
|
||||||
: "border border-[#ADADAD] bg-white text-[#1A1A1A] hover:bg-[#EDF2F7] hover:border-[#00437A] active:bg-[#D6E4F2]";
|
: { background: FARG.white, color: FARG.graphite, borderColor: FARG.steel };
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
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}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
@@ -98,27 +150,44 @@ export function StorKnapp({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Panel({ rubrik, children }: { rubrik?: string; children: ReactNode }) {
|
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 (
|
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 && (
|
{rubrik && (
|
||||||
<h2 className="mb-2 border-b border-[#DDDDDD] pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
<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}
|
{rubrik}
|
||||||
</h2>
|
</h2>
|
||||||
|
</header>
|
||||||
)}
|
)}
|
||||||
{children}
|
<div className="px-4 py-2">{children}</div>
|
||||||
</section>
|
</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> = {
|
const NIVA_FARG: Record<Tillforlitlighet, string> = {
|
||||||
hog: "#1E6B34",
|
hog: FARG.blue,
|
||||||
medel: "#C08A00",
|
medel: FARG.varning,
|
||||||
lag: "#8B1A1A",
|
lag: FARG.stoppat,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
|
export function NivaBadge({ niva }: { niva: Tillforlitlighet }) {
|
||||||
return (
|
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]} />
|
<IkonPunkt farg={NIVA_FARG[niva]} />
|
||||||
{TILLFORLITLIGHET_LABEL[niva]}
|
{TILLFORLITLIGHET_LABEL[niva]}
|
||||||
</span>
|
</span>
|
||||||
@@ -146,12 +215,12 @@ export function TextFalt({
|
|||||||
// granskar och bekräftar alltid innan något sparas i loggen.
|
// granskar och bekräftar alltid innan något sparas i loggen.
|
||||||
const vardeRef = useRef(varde);
|
const vardeRef = useRef(varde);
|
||||||
vardeRef.current = varde;
|
vardeRef.current = varde;
|
||||||
const klass =
|
const klass = "w-full border px-2 py-2 text-[13px] focus:outline-none";
|
||||||
"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 stil = { borderColor: FARG.steel, background: FARG.white, color: FARG.graphite };
|
||||||
return (
|
return (
|
||||||
<label className="mb-2.5 block">
|
<label className="mb-4 block">
|
||||||
<span className="mb-1 flex min-h-6 items-center justify-between gap-2">
|
<span className="mb-2 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>
|
<Etikett>{label}</Etikett>
|
||||||
{rost && (
|
{rost && (
|
||||||
<MikrofonKnapp
|
<MikrofonKnapp
|
||||||
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
|
paText={(text) => satt(vardeRef.current ? `${vardeRef.current.trimEnd()} ${text}` : text)}
|
||||||
@@ -159,7 +228,14 @@ export function TextFalt({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{flerRad ? (
|
{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
|
<input
|
||||||
type={losenord ? "password" : "text"}
|
type={losenord ? "password" : "text"}
|
||||||
@@ -167,6 +243,7 @@ export function TextFalt({
|
|||||||
onChange={(e) => satt(e.target.value)}
|
onChange={(e) => satt(e.target.value)}
|
||||||
placeholder={platshallare}
|
placeholder={platshallare}
|
||||||
className={klass}
|
className={klass}
|
||||||
|
style={stil}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { KUNDBESLUT_LABEL, KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLI
|
|||||||
import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
||||||
import { nastaSteg } from "@/felsokning/metodik";
|
import { nastaSteg } from "@/felsokning/metodik";
|
||||||
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
|
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 { Slutsatspanel } from "@/felsokning/Slutsats";
|
||||||
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
|
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
|
||||||
import { grinda } from "../../../../services/gemensam/grind.mjs";
|
import { grinda } from "../../../../services/gemensam/grind.mjs";
|
||||||
@@ -145,7 +145,7 @@ export default function ArendeSida() {
|
|||||||
if (!arende || !id) {
|
if (!arende || !id) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Case not found" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<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>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -175,9 +175,24 @@ export default function ArendeSida() {
|
|||||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||||
hoger={
|
hoger={
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-[13px] font-semibold text-white">{total}</p>
|
{/* Vit text var rätt när verktygsraden var mörkt marinblå. När
|
||||||
<p className="text-[10px] font-semibold uppercase tracking-wide text-[#A9C3DE]">
|
raden blev vit försvann tiden helt och statusraden blev en
|
||||||
{avslutat ? "Closed" : "In progress"} · {SYNKSTATUS_LABEL[synkStatus]}
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -189,13 +204,13 @@ export default function ArendeSida() {
|
|||||||
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
|
{/* Klassisk trekolumnslayout på skrivbord: navigationsträd till
|
||||||
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
|
vänster, arbetsyta i mitten, kontextpanel till höger. På smala
|
||||||
skärmar: flikrad + en kolumn. */}
|
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) => (
|
{FLIKAR.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => setFlik(f.id)}
|
onClick={() => setFlik(f.id)}
|
||||||
className={`min-h-9 text-[13px] font-semibold transition-colors ${
|
className={`min-h-9 text-[13px] font-semibold ${
|
||||||
flik === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
flik === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
@@ -203,7 +218,7 @@ export default function ArendeSida() {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</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">
|
<aside className="sticky top-14 hidden lg:block print:hidden">
|
||||||
<VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} />
|
<VyTrad flik={flik} sattFlik={setFlik} arende={arende} metodik={metodik} />
|
||||||
</aside>
|
</aside>
|
||||||
@@ -232,8 +247,8 @@ function identitetsRader(rader: [string, string | undefined][]) {
|
|||||||
return rader
|
return rader
|
||||||
.filter(([, varde]) => varde)
|
.filter(([, varde]) => varde)
|
||||||
.map(([etikett, varde]) => (
|
.map(([etikett, varde]) => (
|
||||||
<div key={etikett} className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
|
<div key={etikett} className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[13px] last:border-0">
|
||||||
<span className="text-[#4A5560]">{etikett}</span>
|
<span className="text-[#4D5662]">{etikett}</span>
|
||||||
<span className="text-right font-medium">{varde}</span>
|
<span className="text-right font-medium">{varde}</span>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
@@ -248,12 +263,12 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
|
|||||||
const del = (etikett: string, varde?: string) =>
|
const del = (etikett: string, varde?: string) =>
|
||||||
varde ? (
|
varde ? (
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{etikett && <span className="text-[#707070]">{etikett} </span>}
|
{etikett && <span className="text-[#4D5662]">{etikett} </span>}
|
||||||
<span className="font-semibold text-[#1A1A1A]">{varde}</span>
|
<span className="font-semibold text-[#1B1E22]">{varde}</span>
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
return (
|
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("WO", idn.arbetsorder)}
|
||||||
{del("Claim", idn.claim)}
|
{del("Claim", idn.claim)}
|
||||||
{del("Claim no.", idn.skadenummer)}
|
{del("Claim no.", idn.skadenummer)}
|
||||||
@@ -262,13 +277,13 @@ function IdentitetsRad({ arende, skicka }: { arende: Arende; skicka: (h: Handels
|
|||||||
{del("VIN", idn.vin)}
|
{del("VIN", idn.vin)}
|
||||||
{del("Mileage", idn.miltal)}
|
{del("Mileage", idn.miltal)}
|
||||||
{del("Responsible", idn.ansvarig)}
|
{del("Responsible", idn.ansvarig)}
|
||||||
<label className="ml-auto flex items-center gap-1">
|
<label className="ml-auto flex items-center gap-2">
|
||||||
<span className="text-[#707070]">Case type</span>
|
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">Case type</span>
|
||||||
<select
|
<select
|
||||||
value={typ}
|
value={typ}
|
||||||
disabled={idn.avslutat}
|
disabled={idn.avslutat}
|
||||||
onChange={(e) => skicka({ typ: "arendetyp_satt", arendetyp: e.target.value })}
|
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) => (
|
{ARENDETYPER.map((t) => (
|
||||||
<option key={t}>{t}</option>
|
<option key={t}>{t}</option>
|
||||||
@@ -326,15 +341,15 @@ function MatarstallningSteg({ lage, skicka }: { lage: "ingaende" | "utgaende"; s
|
|||||||
<IkonKamera /> Photograph the instrument panel
|
<IkonKamera /> Photograph the instrument panel
|
||||||
</StorKnapp>
|
</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 && (
|
{foto && !laser && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{demo && (
|
{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.
|
Demo reading — image interpretation requires sign-in. Check the value.
|
||||||
</p>
|
</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" />
|
<TextFalt label={`Odometer reading (${lage === "ingaende" ? "incoming" : "outgoing"})`} varde={varde} satt={setVarde} platshallare="e.g. 84,320 km" />
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!varde.trim()}
|
disabled={!varde.trim()}
|
||||||
@@ -391,24 +406,24 @@ function TidigareArenden({ arende, skicka }: { arende: Arende; skicka: (h: Hande
|
|||||||
|
|
||||||
if (rader.length === 0) return null;
|
if (rader.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 border border-[#C6C6C6] bg-white p-2">
|
<div className="mb-2 border border-[#D7DCE2] bg-white p-2">
|
||||||
<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]">
|
||||||
Previous cases for {identifierare} — review before signing off the history
|
Previous cases for {identifierare} — review before signing off the history
|
||||||
</p>
|
</p>
|
||||||
{rader.map((rad) => (
|
{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>
|
<p>
|
||||||
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
<span className="font-semibold">Case #{rad.nummer}</span> · {tidDatum(rad.skapad)} ·{" "}
|
||||||
{rad.avslutat ? "closed" : "in progress"}
|
{rad.avslutat ? "closed" : "in progress"}
|
||||||
{rad.felbeskrivning && <span className="text-[#4A5560]"> — ”{rad.felbeskrivning}”</span>}
|
{rad.felbeskrivning && <span className="text-[#4D5662]"> — ”{rad.felbeskrivning}”</span>}
|
||||||
</p>
|
</p>
|
||||||
{rad.felorsaker.map((f, i) => (
|
{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(", ")})
|
Felorsak: {f.avvikelse} ({f.orsaker.join(", ")})
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<button
|
<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={() =>
|
onClick={() =>
|
||||||
skicka({
|
skicka({
|
||||||
typ: "kommentar",
|
typ: "kommentar",
|
||||||
@@ -439,15 +454,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
return (
|
return (
|
||||||
<Panel rubrik="Pre-diagnostics — before the work begins">
|
<Panel rubrik="Pre-diagnostics — before the work begins">
|
||||||
{rader.map((r) => (
|
{rader.map((r) => (
|
||||||
<p key={r.id} className="py-0.5 text-[13px]">
|
<p key={r.id} className="py-0 text-[13px]">
|
||||||
<span className={r.klar ? "text-[#1E6B34]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
<span className={r.klar ? "text-[#005CA9]" : "text-[#8B1A1A]"}>{r.klar ? <IkonCheck /> : "☐"}</span> {r.rubrik}
|
||||||
{r.varning && <span className="ml-1 text-[11px] font-semibold text-[#9A6700]"><IkonVarning /> {r.varning}</span>}
|
{r.varning && <span className="ml-2 text-[11px] font-semibold text-[#8A5A00]"><IkonVarning /> {r.varning}</span>}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!rad("historik").klar && (
|
{!rad("historik").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-4 border-t border-[#D7DCE2] pt-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]">
|
||||||
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
|
Has the vehicle's history been checked? (previous work, recurring faults, TSBs, campaigns)
|
||||||
</p>
|
</p>
|
||||||
<TidigareArenden arende={arende} skicka={skicka} />
|
<TidigareArenden arende={arende} skicka={skicka} />
|
||||||
@@ -484,8 +499,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!rad("matarstallning_in").klar && (
|
{!rad("matarstallning_in").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-4 border-t border-[#D7DCE2] pt-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]">
|
||||||
Incoming odometer reading — photograph the instrument panel
|
Incoming odometer reading — photograph the instrument panel
|
||||||
</p>
|
</p>
|
||||||
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
<MatarstallningSteg lage="ingaende" skicka={skicka} />
|
||||||
@@ -493,15 +508,15 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!rad("felbeskrivning").klar && (
|
{!rad("felbeskrivning").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-4 border-t border-[#D7DCE2] pt-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]">
|
||||||
Is the customer's fault description correctly recorded?
|
Is the customer's fault description correctly recorded?
|
||||||
</p>
|
</p>
|
||||||
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
{fb && <p className="mb-2 text-[13px]">”{fb}”</p>}
|
||||||
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
<StorKnapp variant="sekundar" onClick={() => skicka({ typ: "kommentar", text: `${MARKOR_FELBESKRIVNING_VERIFIERAD}.` })}>
|
||||||
Correct — verified
|
Correct — verified
|
||||||
</StorKnapp>
|
</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
|
Additional symptoms are documented as separate observations — do not mix them
|
||||||
with the customer's description.
|
with the customer's description.
|
||||||
</p>
|
</p>
|
||||||
@@ -509,8 +524,8 @@ function PreDiagnostikPanel({ arende, skicka }: { arende: Arende; skicka: (h: Ha
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!rad("tidiga_observationer").klar && (
|
{!rad("tidiga_observationer").klar && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-4 border-t border-[#D7DCE2] pt-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]">
|
||||||
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
|
Anything else on receipt? (traces of repair, modifications, damage, leaks, corrosion …)
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<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
|
The observations are documented
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</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.
|
Document with a photograph or an observation in the panel below — the button unlocks once something is logged.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -548,7 +563,7 @@ function ReproduceringPanel({ skicka }: { skicka: (h: Handelse) => void }) {
|
|||||||
: "Rationale (required) — why could the fault not be reproduced?";
|
: "Rationale (required) — why could the fault not be reproduced?";
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Symptom verification — reproduction">
|
<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?
|
Har kundens fel kunnat reproduceras?
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<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 (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.");
|
if (underlag.length === 0) return setFel("Link at least one evidence source to the assessment.");
|
||||||
const saknat = underlag.find((k) => !underlagFinns(arende, k));
|
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())
|
if (orsaker.includes("Unknown cause") && !motivering.trim())
|
||||||
return setFel("An unknown cause requires a reason why the cause could not be established.");
|
return setFel("An unknown cause requires a reason why the cause could not be established.");
|
||||||
if (sakerhet !== "hog" && !ytterligare.trim())
|
if (sakerhet !== "hog" && !ytterligare.trim())
|
||||||
@@ -636,9 +651,9 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "felorsak") return null;
|
if (h.typ !== "felorsak") return null;
|
||||||
return (
|
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="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]}
|
Cause: {h.orsaker.join(", ")} · Evidence: {h.underlag.join(", ")} · Confidence: {TILLFORLITLIGHET_LABEL[h.sakerhet]}
|
||||||
</p>
|
</p>
|
||||||
<p>Action: {h.atgard}</p>
|
<p>Action: {h.atgard}</p>
|
||||||
@@ -659,42 +674,42 @@ function FelorsaksPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handel
|
|||||||
rost
|
rost
|
||||||
platshallare="e.g. The starter motor does not engage despite correct supply voltage and a good earth connection."
|
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>
|
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">2. Most probable cause (one or more)</p>
|
||||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-3">
|
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||||
{aktivtRegelpaket().orsakskategorier.map((o) => (
|
{aktivtRegelpaket().orsakskategorier.map((o) => (
|
||||||
<button
|
<button
|
||||||
key={o}
|
key={o}
|
||||||
onClick={() => vaxla(orsaker, setOrsaker, o)}
|
onClick={() => vaxla(orsaker, setOrsaker, o)}
|
||||||
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
|
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||||
orsaker.includes(o) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
|
orsaker.includes(o) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{o}
|
{o}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">3. Evidence supporting the assessment</p>
|
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">3. Evidence supporting the assessment</p>
|
||||||
<div className="mb-3 grid grid-cols-2 gap-1 sm:grid-cols-4">
|
<div className="mb-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||||
{aktivtRegelpaket().underlagskallor.map((k) => (
|
{aktivtRegelpaket().underlagskallor.map((k) => (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={k}
|
||||||
onClick={() => vaxla(underlag, setUnderlag, k)}
|
onClick={() => vaxla(underlag, setUnderlag, k)}
|
||||||
className={`min-h-8 border px-1.5 text-[12px] font-medium ${
|
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||||
underlag.includes(k) ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
|
underlag.includes(k) ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{k}
|
{k}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">4. Confidence in the assessment</p>
|
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-[#4D5662]">4. Confidence in the assessment</p>
|
||||||
<div className="mb-3 grid grid-cols-3 gap-2">
|
<div className="mb-4 grid grid-cols-3 gap-2">
|
||||||
{(["hog", "medel", "lag"] as const).map((n) => (
|
{(["hog", "medel", "lag"] as const).map((n) => (
|
||||||
<button
|
<button
|
||||||
key={n}
|
key={n}
|
||||||
onClick={() => setSakerhet(n)}
|
onClick={() => setSakerhet(n)}
|
||||||
className={`min-h-9 border text-[12px] font-semibold ${
|
className={`min-h-9 border text-[12px] font-semibold ${
|
||||||
sakerhet === n ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
sakerhet === n ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{TILLFORLITLIGHET_LABEL[n]}
|
{TILLFORLITLIGHET_LABEL[n]}
|
||||||
@@ -768,20 +783,20 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgard_utford") return null;
|
if (h.typ !== "atgard_utford") return null;
|
||||||
return (
|
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 ? (
|
{h.utford ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-semibold">Action performed:</span> {h.beskrivning}
|
<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>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{kk && (
|
{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>
|
Kvalitetskontroll: {KVALITETSKONTROLL_LABEL[kk.resultat]} — <span className="font-normal">{kk.beskrivning}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -792,14 +807,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgardsforslag") return null;
|
if (h.typ !== "atgardsforslag") return null;
|
||||||
return (
|
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}
|
<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>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{beslut && (
|
{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]}
|
Kundens besked ({beslut.kanal}): {KUNDBESLUT_LABEL[beslut.beslut]}
|
||||||
{beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>}
|
{beslut.kommentar && <span className="font-normal"> — {beslut.kommentar}</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -826,7 +841,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{forslagsLage && (
|
{forslagsLage && (
|
||||||
<div className="mb-3 border border-[#C6C6C6] bg-white p-2">
|
<div className="mb-4 border border-[#D7DCE2] bg-white p-2">
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="Proposed action (shown to the customer in the shared view)"
|
label="Proposed action (shown to the customer in the shared view)"
|
||||||
varde={forslagText}
|
varde={forslagText}
|
||||||
@@ -860,8 +875,8 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{forslag.length > 0 && !beslut && (
|
{forslag.length > 0 && !beslut && (
|
||||||
<div className="mb-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
<div className="mb-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#9A6700]">
|
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#8A5A00]">
|
||||||
Record the customer's decision before the work begins
|
Record the customer's decision before the work begins
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<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}
|
key={val}
|
||||||
onClick={() => setBeslutsVal(val)}
|
onClick={() => setBeslutsVal(val)}
|
||||||
className={`min-h-9 border text-[12px] font-semibold ${
|
className={`min-h-9 border text-[12px] font-semibold ${
|
||||||
beslutsVal === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
beslutsVal === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{KUNDBESLUT_LABEL[val]}
|
{KUNDBESLUT_LABEL[val]}
|
||||||
@@ -879,14 +894,14 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
</div>
|
</div>
|
||||||
{beslutsVal && (
|
{beslutsVal && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">How was the decision given?</p>
|
<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-1">
|
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||||
{KUNDKANALER.map((k) => (
|
{KUNDKANALER.map((k) => (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={k}
|
||||||
onClick={() => setKanal(k)}
|
onClick={() => setKanal(k)}
|
||||||
className={`min-h-8 border px-1 text-[12px] font-medium ${
|
className={`min-h-8 border px-2 text-[12px] font-medium ${
|
||||||
kanal === k ? "border-[#00437A] bg-[#D6E4F2] text-[#00437A]" : "border-[#C6C6C6] bg-white text-[#333333]"
|
kanal === k ? "border-[#005CA9] bg-[#FFFFFF] text-[#005CA9]" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{k}
|
{k}
|
||||||
@@ -967,16 +982,16 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
|
|
||||||
{lage === "ingen" && (
|
{lage === "ingen" && (
|
||||||
<div className="mt-2">
|
<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)
|
Reason no action was performed (required)
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid gap-1">
|
<div className="mb-2 grid gap-2">
|
||||||
{INGEN_ATGARD_ORSAKER.map((val) => (
|
{INGEN_ATGARD_ORSAKER.map((val) => (
|
||||||
<button
|
<button
|
||||||
key={val}
|
key={val}
|
||||||
onClick={() => setOrsak(val)}
|
onClick={() => setOrsak(val)}
|
||||||
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
||||||
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
|
orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{val}
|
{val}
|
||||||
@@ -985,7 +1000,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
</div>
|
</div>
|
||||||
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
<TextFalt label="Or a cause of your own" varde={orsak && !INGEN_ATGARD_ORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
|
||||||
{beslut?.beslut === "avbojt" && (
|
{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.
|
The customer has declined the action via {beslut.kanal} — the decision is recorded in the log.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1008,12 +1023,12 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{utford && !kk && (
|
{utford && !kk && (
|
||||||
<div className="mt-3 border-t border-[#DDDDDD] pt-2">
|
<div className="mt-4 border-t border-[#D7DCE2] pt-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]">
|
||||||
Quality check — is the symptom gone after the action?
|
Quality check — is the symptom gone after the action?
|
||||||
</p>
|
</p>
|
||||||
{repro?.status === "ja" && (
|
{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.
|
The symptom was reproduced during the examination — recreate the same conditions to verify.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1023,7 +1038,7 @@ function AtgardsPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse
|
|||||||
key={val}
|
key={val}
|
||||||
onClick={() => setKkResultat(val)}
|
onClick={() => setKkResultat(val)}
|
||||||
className={`min-h-9 border px-2 text-[12px] font-semibold ${
|
className={`min-h-9 border px-2 text-[12px] font-semibold ${
|
||||||
kkResultat === val ? "border-[#00437A] bg-[#00437A] text-white" : "border-[#ADADAD] bg-white text-[#333333]"
|
kkResultat === val ? "border-[#005CA9] bg-[#005CA9] text-white" : "border-[#D7DCE2] bg-white text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{KVALITETSKONTROLL_LABEL[val]}
|
{KVALITETSKONTROLL_LABEL[val]}
|
||||||
@@ -1080,24 +1095,24 @@ function VyTrad({
|
|||||||
const steg = nastaSteg(arende, metodik);
|
const steg = nastaSteg(arende, metodik);
|
||||||
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
|
const aktuellIndex = metodik.steg.findIndex((s) => s.id === steg.steg.id);
|
||||||
return (
|
return (
|
||||||
<nav className="overflow-hidden border border-[#C6C6C6] bg-[#F7F7F7] shadow-[0_1px_2px_rgba(0,0,0,0.06)]">
|
<nav className="overflow-hidden border border-[#D7DCE2] bg-[#F6F7F8]">
|
||||||
<p className="border-b border-[#DDDDDD] bg-[#EFEFEF] px-2.5 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-[#4A5560]">
|
<p className="border-b border-[#D7DCE2] bg-[#D7DCE2] px-2 py-2 text-[11px] font-semibold uppercase tracking-wider text-[#4D5662]">
|
||||||
Case views
|
Case views
|
||||||
</p>
|
</p>
|
||||||
{FLIKAR.map((f) => (
|
{FLIKAR.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => sattFlik(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
|
flik === f.id
|
||||||
? "border-[#00437A] bg-[#D6E4F2] font-semibold text-[#00437A]"
|
? "border-[#005CA9] bg-[#FFFFFF] font-semibold text-[#005CA9]"
|
||||||
: "border-transparent text-[#333333] hover:bg-[#E4E9EE]"
|
: "border-transparent text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f.label}
|
{f.label}
|
||||||
</button>
|
</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}
|
{metodik.namn}
|
||||||
</p>
|
</p>
|
||||||
{metodik.steg.map((s, i) => {
|
{metodik.steg.map((s, i) => {
|
||||||
@@ -1106,8 +1121,8 @@ function VyTrad({
|
|||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
key={s.id}
|
key={s.id}
|
||||||
className={`px-3 py-1 text-[12px] ${
|
className={`px-4 py-2 text-[12px] ${
|
||||||
aktuell ? "bg-[#FFF8E1] font-semibold text-[#9A6700]" : klar ? "text-[#1E6B34]" : "text-[#707070]"
|
aktuell ? "bg-[#FFFFFF] font-semibold text-[#8A5A00]" : klar ? "text-[#005CA9]" : "text-[#4D5662]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
|
{klar ? "✓" : aktuell ? "▸" : "·"} {s.rubrik}
|
||||||
@@ -1135,9 +1150,9 @@ function KontextPanel({
|
|||||||
const senasteAi = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar");
|
const senasteAi = [...arende.handelser].reverse().find((p) => p.handelse.typ === "ai_svar");
|
||||||
const rad = (etikett: string, varde: string | undefined) =>
|
const rad = (etikett: string, varde: string | undefined) =>
|
||||||
varde ? (
|
varde ? (
|
||||||
<div className="flex justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[12px] last:border-0">
|
<div className="flex justify-between gap-2 border-b border-[#D7DCE2] py-2 text-[12px] last:border-0">
|
||||||
<span className="shrink-0 text-[#4A5560]">{etikett}</span>
|
<span className="shrink-0 text-[#4D5662]">{etikett}</span>
|
||||||
<span className="text-right font-medium text-[#1A1A1A]">{varde}</span>
|
<span className="text-right font-medium text-[#1B1E22]">{varde}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
@@ -1153,7 +1168,7 @@ function KontextPanel({
|
|||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Reliability">
|
<Panel rubrik="Reliability">
|
||||||
{b.tillforlitlighet.map((r, i) => (
|
{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}
|
<NivaBadge niva={r.niva} /> {r.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -1161,7 +1176,7 @@ function KontextPanel({
|
|||||||
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
|
{!b.avslutat && b.rekommenderatNastaSteg.length > 0 && (
|
||||||
<Panel rubrik="Recommended next step">
|
<Panel rubrik="Recommended next step">
|
||||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
{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}
|
{i + 1}. {s}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -1170,14 +1185,14 @@ function KontextPanel({
|
|||||||
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
|
{senasteAi && senasteAi.handelse.typ === "ai_svar" && (
|
||||||
<Panel rubrik="Technical recommendation">
|
<Panel rubrik="Technical recommendation">
|
||||||
{senasteAi.handelse.rader.map((r, i) => (
|
{senasteAi.handelse.rader.map((r, i) => (
|
||||||
<p key={i} className="py-0.5 text-[12px]">
|
<p key={i} className="py-0 text-[12px]">
|
||||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
|
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[r.typ]}:</span> {r.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-1 text-[12px]">
|
<p className="mt-2 text-[12px]">
|
||||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||||
</p>
|
</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
|
Decision support · never presented as an established fault
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -1202,11 +1217,11 @@ function InaktivitetsBanner({
|
|||||||
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
|
const minuter = Math.floor((new Date(nu).getTime() - new Date(sista.tidpunkt).getTime()) / 60000);
|
||||||
if (minuter < 20) return null;
|
if (minuter < 20) return null;
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 border border-[#00437A] bg-[#F7F7F7] p-4">
|
<div className="mb-4 border border-[#005CA9] bg-[#F6F7F8] p-4">
|
||||||
<p className="mb-2 text-[14px] font-semibold text-[#00437A]">
|
<p className="mb-2 text-[14px] font-semibold text-[#005CA9]">
|
||||||
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
Ingen aktivitet har registrerats de senaste {minuter} minuterna.
|
||||||
</p>
|
</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 />
|
<TextFalt label="What has been done?" varde={text} satt={setText} platshallare="e.g. Removed the instrument panel to reach the wiring harness." rost />
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
disabled={!text.trim()}
|
disabled={!text.trim()}
|
||||||
@@ -1286,7 +1301,7 @@ function GuideFlik({
|
|||||||
if (avslutat) {
|
if (avslutat) {
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="The diagnosis is closed">
|
<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.
|
The case is locked for new guided steps. The log, the brief and the report remain available for traceability and export.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -1339,8 +1354,8 @@ function GuideFlik({
|
|||||||
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
|
<Panel rubrik={`Methodology: ${metodik.namn} · Step: ${steg.steg.rubrik}`}>
|
||||||
{steg.klart ? (
|
{steg.klart ? (
|
||||||
<>
|
<>
|
||||||
<p className="mb-3 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
<p className="mb-4 text-[15px] font-semibold">Every step in the methodology is documented.</p>
|
||||||
<p className="mb-4 text-[#333333]">
|
<p className="mb-4 text-[#1B1E22]">
|
||||||
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
|
If the root cause is not verified: document a hypothesis and extend the diagnosis, or close the case
|
||||||
with recommended next steps.
|
with recommended next steps.
|
||||||
</p>
|
</p>
|
||||||
@@ -1353,11 +1368,11 @@ function GuideFlik({
|
|||||||
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
|
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
|
||||||
// hinder, inte en varning. Enda vägen framåt är att ändra
|
// hinder, inte en varning. Enda vägen framåt är att ändra
|
||||||
// förutsättningen, inte att klicka förbi.
|
// förutsättningen, inte att klicka förbi.
|
||||||
<div className="border-2 border-[#B42318] bg-[#FEF3F2] p-4">
|
<div className="border-2 border-[#8B1A1A] bg-[#FFFFFF] p-4">
|
||||||
<p className="mb-2 text-[15px] font-semibold text-[#B42318]">Work must not continue</p>
|
<p className="mb-2 text-[15px] font-semibold text-[#8B1A1A]">Work must not continue</p>
|
||||||
<p className="mb-2 text-[#333333]">{steg.sparr.orsak}</p>
|
<p className="mb-2 text-[#1B1E22]">{steg.sparr.orsak}</p>
|
||||||
<p className="mb-3 font-semibold text-[#333333]">{steg.sparr.atgard}</p>
|
<p className="mb-4 font-semibold text-[#1B1E22]">{steg.sparr.atgard}</p>
|
||||||
<p className="text-[12px] text-[#666666]">
|
<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
|
The answer is documented in the log. The methodology opens when the precondition is met and the question is answered
|
||||||
jakande.
|
jakande.
|
||||||
</p>
|
</p>
|
||||||
@@ -1377,19 +1392,19 @@ function GuideFlik({
|
|||||||
: "Guidance"
|
: "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" && (
|
{aiStatus === "fel" && (
|
||||||
<p className="mb-2 font-semibold text-[#8B1A1A]">The analysis could not be retrieved — try again. The methodology continues unaffected.</p>
|
<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 && senasteAiSvar.handelse.typ === "ai_svar" && (
|
||||||
<>
|
<>
|
||||||
{senasteAiSvar.handelse.rader.map((rad, i) => (
|
{senasteAiSvar.handelse.rader.map((rad, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">
|
<p key={i} className="py-0 text-[14px]">
|
||||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-2 text-[14px]">
|
<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}
|
{senasteAiSvar.handelse.nastaSteg}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -1447,15 +1462,15 @@ function GuideFlik({
|
|||||||
*/
|
*/
|
||||||
function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) {
|
function Avslutshinder({ hinder }: { hinder: { id: string; rubrik: string; detalj?: string }[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
|
<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-[#9A6700]">
|
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#8A5A00]">
|
||||||
The case cannot be closed yet
|
The case cannot be closed yet
|
||||||
</p>
|
</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) => (
|
{hinder.map((h) => (
|
||||||
<li key={h.id}>
|
<li key={h.id}>
|
||||||
{h.rubrik}
|
{h.rubrik}
|
||||||
{h.detalj && <span className="text-[#4A5560]"> — {h.detalj}</span>}
|
{h.detalj && <span className="text-[#4D5662]"> — {h.detalj}</span>}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1469,15 +1484,15 @@ function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse)
|
|||||||
if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori;
|
if (post.handelse.typ === "kategori_byte") aktiv = post.handelse.kategori;
|
||||||
}
|
}
|
||||||
return (
|
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) => (
|
{(Object.keys(TIDKATEGORI_LABEL) as TidKategori[]).map((k) => (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={k}
|
||||||
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
|
onClick={() => k !== aktiv && skicka({ typ: "kategori_byte", kategori: k })}
|
||||||
className={`whitespace-nowrap 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
|
k === aktiv
|
||||||
? "border-[#00437A] bg-[#00437A] text-white"
|
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{TIDKATEGORI_LABEL[k]}
|
{TIDKATEGORI_LABEL[k]}
|
||||||
@@ -1559,7 +1574,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
|||||||
|
|
||||||
return (
|
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>
|
<p className="mb-4 text-[17px] font-semibold leading-snug">{kontroll.text}</p>
|
||||||
{krav === "foto" ? (
|
{krav === "foto" ? (
|
||||||
<>
|
<>
|
||||||
@@ -1579,7 +1594,7 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
|
|||||||
/>
|
/>
|
||||||
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
|
<TextFalt label="Observation (optional)" varde={resultat} satt={setResultat} rost />
|
||||||
<StorKnapp onClick={() => filRef.current?.click()}><IkonKamera /> Take a photograph — verifies the check</StorKnapp>
|
<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
|
rost
|
||||||
/>
|
/>
|
||||||
{!kravUppfyllt && (
|
{!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{" "}
|
No {krav === "matvarde" ? "measurement" : "observation"} has been recorded — add a short{" "}
|
||||||
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
|
{krav === "matvarde" ? "measurement entry" : "comment"} before the check can be verified.
|
||||||
</p>
|
</p>
|
||||||
@@ -1633,24 +1648,24 @@ function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => setOppen(true)}
|
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 …
|
Evidence cannot be produced …
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mt-3 border border-[#E0C36A] bg-[#FFF8E1] p-2">
|
<div className="mt-4 border border-[#8A5A00] bg-[#FFFFFF] p-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
|
<p className="mb-2 text-[12px] font-semibold text-[#8A5A00]">
|
||||||
Kontrollen dokumenteras utan underlag — ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
|
Kontrollen dokumenteras utan underlag — ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
|
||||||
</p>
|
</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) => (
|
{aktivtRegelpaket().undantagsorsaker.map((val) => (
|
||||||
<button
|
<button
|
||||||
key={val}
|
key={val}
|
||||||
onClick={() => setOrsak(val)}
|
onClick={() => setOrsak(val)}
|
||||||
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
className={`min-h-8 border px-2 text-left text-[12px] font-medium ${
|
||||||
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
|
orsak === val ? "border-[#005CA9] bg-[#FFFFFF]" : "border-[#D7DCE2] bg-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{val}
|
{val}
|
||||||
@@ -1794,10 +1809,10 @@ function SnabbDokumentation({
|
|||||||
? instRef.current?.click()
|
? instRef.current?.click()
|
||||||
: setTyp(typ === d.id ? null : d.id)
|
: 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
|
typ === d.id
|
||||||
? "border-[#00437A] bg-[#00437A] text-white"
|
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333] hover:border-[#8FA8C0]"
|
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22] hover:border-[#4D5662]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
+ {d.label}
|
+ {d.label}
|
||||||
@@ -1850,8 +1865,8 @@ function SnabbDokumentation({
|
|||||||
/>
|
/>
|
||||||
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
|
{videoFel && <p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">{videoFel}</p>}
|
||||||
{video && (
|
{video && (
|
||||||
<div className="mt-3 border border-[#C6C6C6] bg-white p-2">
|
<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-[#C6C6C6]" />
|
<video src={video.dataUrl} controls className="mb-2 max-h-48 w-full border border-[#D7DCE2]" />
|
||||||
<TextFalt
|
<TextFalt
|
||||||
label="What does the video show? (required — what makes the noise or moves)"
|
label="What does the video show? (required — what makes the noise or moves)"
|
||||||
varde={video.beskrivning}
|
varde={video.beskrivning}
|
||||||
@@ -1876,23 +1891,23 @@ function SnabbDokumentation({
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{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 && (
|
{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.
|
Demo reading — image interpretation requires sign-in. The values are sample data.
|
||||||
</p>
|
</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
|
Read: {avlasning.tolkning.instrumenttyp} — confirm before anything is logged
|
||||||
</p>
|
</p>
|
||||||
{avlasning.tolkning.varden.map((v, i) => (
|
{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>
|
<span>
|
||||||
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span>
|
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span>
|
||||||
</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)} %`}
|
{v.konfidens >= 0.95 ? "✓" : `${Math.round(v.konfidens * 100)} %`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1906,7 +1921,7 @@ function SnabbDokumentation({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{typ && typ !== "foto" && typ !== "instrument" && (
|
{typ && typ !== "foto" && typ !== "instrument" && (
|
||||||
<div className="mt-3">
|
<div className="mt-4">
|
||||||
{typ === "hypotes" && (
|
{typ === "hypotes" && (
|
||||||
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
|
<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.
|
<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 (
|
return (
|
||||||
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
|
<div className="fixed inset-0 z-30 flex items-end justify-center bg-black/70 p-4 sm:items-center">
|
||||||
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#ADADAD] bg-[#F7F7F7] p-4">
|
<div className="max-h-full w-full max-w-2xl overflow-y-auto border border-[#D7DCE2] bg-[#F6F7F8] p-4">
|
||||||
<h2 className="mb-3 text-[15px] font-semibold">Handover report</h2>
|
<h2 className="mb-4 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]">
|
<pre className="mb-4 overflow-x-auto whitespace-pre-wrap bg-white p-4 text-[12px] text-[#1B1E22]">
|
||||||
{text}
|
{text}
|
||||||
</pre>
|
</pre>
|
||||||
{aiLage === "vilar" && (
|
{aiLage === "vilar" && (
|
||||||
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
|
<StorKnapp variant="sekundar" className="mb-4" onClick={komplettera}>
|
||||||
Extend the analysis: risks & uncertainties
|
Extend the analysis: risks & uncertainties
|
||||||
</StorKnapp>
|
</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" && (
|
{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 && (
|
{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) => (
|
{aiRader.map((rad, i) => (
|
||||||
<p key={i} className="py-0.5 text-[12px] text-[#333333]">
|
<p key={i} className="py-0 text-[12px] text-[#1B1E22]">
|
||||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2049,10 +2064,10 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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>}
|
{fel && <p className="mb-2 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
{delningar.map((delning) => (
|
{delningar.map((delning) => (
|
||||||
<div key={delning.kod} className="flex items-center justify-between gap-2 border-b border-[#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]">
|
<span className="text-[14px]">
|
||||||
{NIVA_LABEL[delning.niva]}{" "}
|
{NIVA_LABEL[delning.niva]}{" "}
|
||||||
{delning.aterkallad && <span className="text-[12px] font-semibold text-[#8B1A1A]">(revoked)</span>}
|
{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 && (
|
{!delning.aterkallad && (
|
||||||
<span className="flex gap-2">
|
<span className="flex gap-2">
|
||||||
<button
|
<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={() =>
|
onClick={() =>
|
||||||
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
navigator.clipboard.writeText(`${window.location.origin}/felsokning/delad/${delning.kod}`)
|
||||||
}
|
}
|
||||||
@@ -2068,7 +2083,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
<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 () => {
|
onClick={async () => {
|
||||||
await aterkallaDelning(delning.kod);
|
await aterkallaDelning(delning.kod);
|
||||||
skicka({ typ: "kommentar", text: `Share link (${NIVA_LABEL[delning.niva]}) revoked.` });
|
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) => (
|
{(Object.keys(NIVA_LABEL) as DelningsNiva[]).map((niva) => (
|
||||||
<button
|
<button
|
||||||
key={niva}
|
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 () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await skapaDelning(arende.id, niva);
|
await skapaDelning(arende.id, niva);
|
||||||
@@ -2100,7 +2115,7 @@ function DelningsHanterare({ arende, skicka }: { arende: Arende; skicka: (h: Han
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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) ·
|
Customer: what can be shared with the customer · External partner: also hypotheses (marked unverified) ·
|
||||||
Internal: full visibility. Filtering happens on the server.
|
Internal: full visibility. Filtering happens on the server.
|
||||||
</p>
|
</p>
|
||||||
@@ -2113,16 +2128,16 @@ function LoggFlik({ arende }: { arende: Arende }) {
|
|||||||
<Panel rubrik="Work log — append-only, nothing is overwritten">
|
<Panel rubrik="Work log — append-only, nothing is overwritten">
|
||||||
<ol>
|
<ol>
|
||||||
{arende.handelser.map((post) => (
|
{arende.handelser.map((post) => (
|
||||||
<li key={post.id} className="flex gap-3 border-b border-[#DDDDDD] py-2 last:border-0">
|
<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-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>
|
<span className="w-14 shrink-0 font-mono text-[12px] font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-[13px] text-[#1A1A1A]">{handelseRubrik(post)}</p>
|
<p className="text-[13px] text-[#1B1E22]">{handelseRubrik(post)}</p>
|
||||||
<p className="text-[11px] text-[#707070]">{post.anvandare}</p>
|
<p className="text-[11px] text-[#4D5662]">{post.anvandare}</p>
|
||||||
{post.handelse.typ === "foto" && (
|
{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" && (
|
{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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -2164,25 +2179,25 @@ function BriefFlik({
|
|||||||
<>
|
<>
|
||||||
{!b.avslutat && (
|
{!b.avslutat && (
|
||||||
<Panel rubrik="Review of the evidence">
|
<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
|
Have the system review the whole record before you hand it on: contradictions between
|
||||||
observations, gaps in the documentation, and conclusions that lack support.
|
observations, gaps in the documentation, and conclusions that lack support.
|
||||||
</p>
|
</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" && (
|
{granskning === "fel" && (
|
||||||
<p className="mb-2 font-semibold text-[#8B1A1A]">The review could not be retrieved — requires sign-in. Try again.</p>
|
<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" && (
|
{senasteAi && senasteAi.handelse.typ === "ai_svar" && granskning === "vilar" && (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
{senasteAi.handelse.rader.map((rad, i) => (
|
{senasteAi.handelse.rader.map((rad, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">
|
<p key={i} className="py-0 text-[14px]">
|
||||||
<span className="font-semibold text-[#4A5560]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
<span className="font-semibold text-[#4D5662]">{AI_RADTYP_LABEL[rad.typ]}:</span> {rad.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<p className="mt-1 text-[14px]">
|
<p className="mt-2 text-[14px]">
|
||||||
<span className="font-semibold text-[#00437A]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
<span className="font-semibold text-[#005CA9]">Next step:</span> {senasteAi.handelse.nastaSteg}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
|
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
|
||||||
@@ -2194,63 +2209,63 @@ function BriefFlik({
|
|||||||
{b.objekt ? (
|
{b.objekt ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
|
<p className="text-[15px] font-semibold">{b.objekt.beskrivning}</p>
|
||||||
<p className="font-semibold text-[#00437A]">{b.objekt.identifierare}</p>
|
<p className="font-semibold text-[#005CA9]">{b.objekt.identifierare}</p>
|
||||||
{b.objekt.kund && <p className="text-[#333333]">Kund: {b.objekt.kund}</p>}
|
{b.objekt.kund && <p className="text-[#1B1E22]">Kund: {b.objekt.kund}</p>}
|
||||||
{b.ansvarig && <p className="text-[#333333]">Ansvarig tekniker: {b.ansvarig}</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>
|
||||||
<Panel rubrik="Customer's description">
|
<Panel rubrik="Customer's description">
|
||||||
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
<p className="text-[14px]">{b.felbeskrivning ? `”${b.felbeskrivning}”` : "—"}</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Checks performed">
|
<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) =>
|
{b.utfordaKontroller.map((k, i) =>
|
||||||
k.undantag ? (
|
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}
|
<IkonVarning /> {k.text} — underlag saknas: {k.undantag}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p key={i} className="py-0.5 text-[14px]">
|
<p key={i} className="py-0 text-[14px]">
|
||||||
✓ {k.text}
|
✓ {k.text}
|
||||||
{k.resultat && <span className="text-[#4A5560]"> — {k.resultat}</span>}
|
{k.resultat && <span className="text-[#4D5662]"> — {k.resultat}</span>}
|
||||||
</p>
|
</p>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Observations">
|
<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) => (
|
{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>
|
</Panel>
|
||||||
{b.hypoteser.length > 0 && (
|
{b.hypoteser.length > 0 && (
|
||||||
<Panel rubrik="Hypotheses — require verification">
|
<Panel rubrik="Hypotheses — require verification">
|
||||||
{b.hypoteser.map((h, i) => (
|
{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}
|
<NivaBadge niva={h.niva} /> {h.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Not checked">
|
<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) => (
|
{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>
|
</Panel>
|
||||||
{!b.avslutat && (
|
{!b.avslutat && (
|
||||||
<Panel rubrik="Recommended next step">
|
<Panel rubrik="Recommended next step">
|
||||||
{b.rekommenderatNastaSteg.map((s, i) => (
|
{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>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Reliability">
|
<Panel rubrik="Reliability">
|
||||||
{b.tillforlitlighet.map((r, i) => (
|
{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}
|
<NivaBadge niva={r.niva} /> {r.text}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -2333,15 +2348,15 @@ function RapportFlik({
|
|||||||
undantag i händelseloggen. */}
|
undantag i händelseloggen. */}
|
||||||
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
|
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
|
||||||
{grind.map((rad) => (
|
{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]">
|
<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 />}
|
{rad.ok ? <IkonCheck /> : rad.kravs ? <IkonKryss /> : <IkonVarning />}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{rad.rubrik}
|
{rad.rubrik}
|
||||||
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (recommended)</span>}
|
{!rad.kravs && <span className="text-[11px] text-[#4D5662]"> (recommended)</span>}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!godkand && (
|
{!godkand && (
|
||||||
@@ -2352,7 +2367,7 @@ function RapportFlik({
|
|||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Customer report">
|
<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
|
A shareable summary of the work performed. Review the content before the report is shared — images may
|
||||||
contain details about other customers.
|
contain details about other customers.
|
||||||
</p>
|
</p>
|
||||||
@@ -2365,7 +2380,7 @@ function RapportFlik({
|
|||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</div>
|
||||||
<Link to={`/felsokning/dela/${arende.id}`} className="mt-2 block">
|
<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>
|
</Link>
|
||||||
{plattformAktiv() ? (
|
{plattformAktiv() ? (
|
||||||
<DelningsHanterare arende={arende} skicka={skicka} />
|
<DelningsHanterare arende={arende} skicka={skicka} />
|
||||||
@@ -2383,7 +2398,7 @@ function RapportFlik({
|
|||||||
</StorKnapp>
|
</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).
|
The share link requires the case to be synchronized (signed-in user).
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -2445,19 +2460,19 @@ function RapportFlik({
|
|||||||
{repro ? (
|
{repro ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-[14px] font-semibold">{reproduceringsText(repro.status)}</p>
|
<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>
|
||||||
<Panel rubrik="Root cause analysis">
|
<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) => {
|
{orsakerLista.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "felorsak") return null;
|
if (h.typ !== "felorsak") return null;
|
||||||
return (
|
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>
|
<p className="text-[14px] font-semibold">{h.avvikelse}</p>
|
||||||
{identitetsRader([
|
{identitetsRader([
|
||||||
["Assessed root cause", h.orsaker.join(", ")],
|
["Assessed root cause", h.orsaker.join(", ")],
|
||||||
@@ -2495,7 +2510,7 @@ function RapportFlik({
|
|||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<Panel rubrik="Action performed and verification">
|
<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) => {
|
{atgardsRader.map((p) => {
|
||||||
const h = p.handelse;
|
const h = p.handelse;
|
||||||
if (h.typ !== "atgard_utford") return null;
|
if (h.typ !== "atgard_utford") return null;
|
||||||
@@ -2507,7 +2522,7 @@ function RapportFlik({
|
|||||||
["Parts", h.delar],
|
["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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -2523,15 +2538,15 @@ function RapportFlik({
|
|||||||
{fordelning.length > 0 && (
|
{fordelning.length > 0 && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{fordelning.map((r) => (
|
{fordelning.map((r) => (
|
||||||
<p key={r.label} className="text-[#333333]">{r.label}: {r.tid}</p>
|
<p key={r.label} className="text-[#1B1E22]">{r.label}: {r.tid}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel rubrik="Timeline">
|
<Panel rubrik="Timeline">
|
||||||
{kundposter.map((post) => (
|
{kundposter.map((post) => (
|
||||||
<p key={post.id} className="py-0.5 text-[13px]">
|
<p key={post.id} className="py-0 text-[13px]">
|
||||||
<span className="font-mono font-semibold text-[#00437A]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
<span className="font-mono font-semibold text-[#005CA9]">{tidKlockslag(post.tidpunkt)}</span>{" "}
|
||||||
{handelseRubrik(post)}
|
{handelseRubrik(post)}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -2540,8 +2555,8 @@ function RapportFlik({
|
|||||||
<Panel rubrik="Documented video">
|
<Panel rubrik="Documented video">
|
||||||
{klipp.map((v, i) => (
|
{klipp.map((v, i) => (
|
||||||
<figure key={i} className="mb-2">
|
<figure key={i} className="mb-2">
|
||||||
<Klipp bilaga={v.bilaga} className="w-full border border-[#C6C6C6]" />
|
<Klipp bilaga={v.bilaga} className="w-full border border-[#D7DCE2]" />
|
||||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{v.beskrivning}</figcaption>
|
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{v.beskrivning}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -2551,8 +2566,8 @@ function RapportFlik({
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{bilder.map((bild, i) => (
|
{bilder.map((bild, i) => (
|
||||||
<figure key={i}>
|
<figure key={i}>
|
||||||
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#C6C6C6]" />
|
<Bild bilaga={bild.bilaga} alt={bild.beskrivning} className="border border-[#D7DCE2]" />
|
||||||
<figcaption className="mt-1 text-[11px] text-[#4A5560]">{bild.beskrivning}</figcaption>
|
<figcaption className="mt-2 text-[11px] text-[#4D5662]">{bild.beskrivning}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2561,11 +2576,11 @@ function RapportFlik({
|
|||||||
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
{!b.avslutat || b.ejKontrollerat.length > 0 ? (
|
||||||
<Panel rubrik="Recommended next steps">
|
<Panel rubrik="Recommended next steps">
|
||||||
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
|
{(b.avslutat ? b.ejKontrollerat : b.rekommenderatNastaSteg).map((s, i) => (
|
||||||
<p key={i} className="py-0.5 text-[14px]">{i + 1}. {s}</p>
|
<p key={i} className="py-0 text-[14px]">{i + 1}. {s}</p>
|
||||||
))}
|
))}
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : 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.
|
Generated from the case log · Observations and measurements are reported without conclusions that lack support.
|
||||||
</p>
|
</p>
|
||||||
<Ritningsstampel arende={arende} />
|
<Ritningsstampel arende={arende} />
|
||||||
@@ -2632,7 +2647,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOppen(true)}
|
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
|
Report a problem with the system
|
||||||
</button>
|
</button>
|
||||||
@@ -2642,7 +2657,7 @@ function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | nu
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Report a problem with the system">
|
<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.
|
Case, methodology and platform version are included automatically. Nothing about the vehicle or the customer is sent.
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
<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."
|
platshallare="Whoever answers cannot see your screen."
|
||||||
flerRad
|
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>}
|
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||||
@@ -2708,11 +2723,11 @@ function Ritningsstampel({ arende }: { arende: Arende }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
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">
|
<div className="grid grid-cols-2 sm:grid-cols-3">
|
||||||
{falt.map(([etikett, varde]) => (
|
{falt.map(([etikett, varde]) => (
|
||||||
<div key={etikett} className="border-b border-r border-[#DDDDDD] px-2 py-2 last:border-r-0">
|
<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-[#4A5560]">{etikett}</div>
|
<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 className="mt-2 font-mono text-[11px] text-[#1B1E22]">{varde}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ function PlattformInloggning() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel rubrik="Platform account">
|
<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]">
|
<p className="text-[14px]">
|
||||||
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
|
||||||
{konto.organisation} — synchronization and decision support active.
|
{konto.organisation} — synchronization and decision support active.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<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={() => {
|
onClick={() => {
|
||||||
loggaUtPlattform();
|
loggaUtPlattform();
|
||||||
setKonto(null);
|
setKonto(null);
|
||||||
@@ -60,7 +60,7 @@ function PlattformInloggning() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(konto.roll === "arbetsledare" || konto.roll === "admin") && (
|
{(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>
|
<StorKnapp variant="sekundar"><IkonDiagram /> Organization overview</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -79,7 +79,7 @@ function PlattformInloggning() {
|
|||||||
<Panel rubrik="Platform account">
|
<Panel rubrik="Platform account">
|
||||||
{lage === "stangd" ? (
|
{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.
|
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>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<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="E-mail" varde={epost} satt={setEpost} platshallare="anna@workshop.example" />
|
||||||
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
<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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
<StorKnapp variant="sekundar" onClick={() => setLage("stangd")}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -175,23 +175,23 @@ function AnvandarAdmin() {
|
|||||||
{lista.map((anv) => (
|
{lista.map((anv) => (
|
||||||
<div
|
<div
|
||||||
key={anv.id}
|
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]">
|
<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}
|
{anv.namn}
|
||||||
</span>{" "}
|
</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 && (
|
{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>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="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
|
anv.aktiv === false
|
||||||
? "border-[#1E6B34] bg-white text-[#1E6B34]"
|
? "border-[#005CA9] bg-white text-[#005CA9]"
|
||||||
: "border-[#6E1414] bg-white text-[#8B1A1A]"
|
: "border-[#8B1A1A] bg-white text-[#8B1A1A]"
|
||||||
}`}
|
}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setFel("");
|
setFel("");
|
||||||
@@ -207,11 +207,11 @@ function AnvandarAdmin() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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.
|
Disabling takes effect immediately — active sessions on that person's devices end at once.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
className="mt-3"
|
className="mt-4"
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setFel("");
|
setFel("");
|
||||||
@@ -229,7 +229,7 @@ function AnvandarAdmin() {
|
|||||||
<TextFalt label="Name" varde={namn} satt={setNamn} />
|
<TextFalt label="Name" varde={namn} satt={setNamn} />
|
||||||
<TextFalt label="E-mail" varde={epost} satt={setEpost} />
|
<TextFalt label="E-mail" varde={epost} satt={setEpost} />
|
||||||
<TextFalt label="Password (at least 8 characters)" varde={losenord} satt={setLosenord} losenord />
|
<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) => (
|
{(Object.keys(ROLL_LABEL) as PlattformRoll[]).map((valbar) => (
|
||||||
<button
|
<button
|
||||||
key={valbar}
|
key={valbar}
|
||||||
@@ -237,15 +237,15 @@ function AnvandarAdmin() {
|
|||||||
onClick={() => setRoll(valbar)}
|
onClick={() => setRoll(valbar)}
|
||||||
className={`min-h-9 border text-[12px] font-semibold ${
|
className={`min-h-9 border text-[12px] font-semibold ${
|
||||||
roll === valbar
|
roll === valbar
|
||||||
? "border-[#00437A] bg-[#00437A] text-white"
|
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#333333]"
|
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#1B1E22]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{ROLL_LABEL[valbar]}
|
{ROLL_LABEL[valbar]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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}>
|
<StorKnapp type="submit" disabled={!epost.trim() || !namn.trim() || losenord.length < 8}>
|
||||||
Create user
|
Create user
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
@@ -271,7 +271,7 @@ export default function Arendelista() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Guided Diagnostics">
|
<FelsokningSkal rubrik="Guided Diagnostics">
|
||||||
<Panel rubrik="Who is working?">
|
<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.
|
All work is logged with user and timestamp. Enter your name to begin.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
@@ -304,20 +304,20 @@ export default function Arendelista() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
rubrik="Guided Diagnostics"
|
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">
|
<Link to="/felsokning/nytt">
|
||||||
<StorKnapp className="mb-4">+ New case</StorKnapp>
|
<StorKnapp className="mb-4">+ New case</StorKnapp>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{alla.length > 0 && (
|
{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) => (
|
{FILTER.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.id}
|
key={f.id}
|
||||||
onClick={() => setFilter(f.id)}
|
onClick={() => setFilter(f.id)}
|
||||||
className={`min-h-9 text-[13px] font-semibold transition-colors ${
|
className={`min-h-9 text-[13px] font-semibold ${
|
||||||
filter === f.id ? "bg-[#00437A] text-white" : "text-[#333333] hover:bg-[#E4E9EE]"
|
filter === f.id ? "bg-[#005CA9] text-white" : "text-[#1B1E22] hover:bg-[#D7DCE2]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f.label} · {f.antal}
|
{f.label} · {f.antal}
|
||||||
@@ -328,7 +328,7 @@ export default function Arendelista() {
|
|||||||
|
|
||||||
{alla.length === 0 && (
|
{alla.length === 0 && (
|
||||||
<Panel rubrik="Get started">
|
<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.
|
No cases yet. Start by identifying an object — or explore a finished demo case with a complete work log, brief and customer report.
|
||||||
</p>
|
</p>
|
||||||
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
<StorKnapp variant="sekundar" onClick={() => laggInArende(byggDemoArende(nastaNummer, anvandare))}>
|
||||||
@@ -338,7 +338,7 @@ export default function Arendelista() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{lista.length === 0 && alla.length > 0 && (
|
{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 />}
|
{plattformAktiv() && <PlattformInloggning />}
|
||||||
@@ -350,7 +350,7 @@ export default function Arendelista() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Panel rubrik="Decision support">
|
<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.
|
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>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -363,24 +363,24 @@ export default function Arendelista() {
|
|||||||
const b = brief(arende, metodikForArende(arende), new Date().toISOString());
|
const b = brief(arende, metodikForArende(arende), new Date().toISOString());
|
||||||
return (
|
return (
|
||||||
<Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block">
|
<Link key={arende.id} to={`/felsokning/arende/${arende.id}`} className="block">
|
||||||
<div className="mb-3 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">
|
<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}
|
Case #{arende.nummer}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
|
||||||
avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{avslutat ? "Closed" : "In progress"}
|
{avslutat ? "Closed" : "In progress"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[15px] font-semibold">{o ? o.beskrivning : "Unknown object"}</p>
|
<p className="mt-2 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>}
|
{o && <p className="text-[12px] font-semibold text-[#4D5662]">{o.identifierare}{o.kund ? ` · ${o.kund}` : ""}</p>}
|
||||||
{fel && <p className="mt-2 text-[14px] text-[#333333]">”{fel}”</p>}
|
{fel && <p className="mt-2 text-[14px] text-[#1B1E22]">”{fel}”</p>}
|
||||||
<p className="mt-2 text-[12px] text-[#707070]">
|
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||||
{sista ? `Senaste aktivitet ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
|
{sista ? `Last activity ${tidDatum(sista.tidpunkt)} ${tidKlockslag(sista.tidpunkt)}` : ""}
|
||||||
{" · "}
|
{" · "}
|
||||||
{b.totalArbetstid}
|
{b.totalArbetstid}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function DelatArende() {
|
|||||||
if (!arende) {
|
if (!arende) {
|
||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Shared case">
|
<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>
|
</FelsokningSkal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ function Vallista({
|
|||||||
<button
|
<button
|
||||||
key={val}
|
key={val}
|
||||||
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
|
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
|
||||||
className={`min-h-10 border px-2 font-semibold transition-colors ${
|
className={`min-h-10 border px-2 font-semibold ${
|
||||||
aktiv
|
aktiv
|
||||||
? "border-[#00437A] bg-[#00437A] text-white"
|
? "border-[#005CA9] bg-[#005CA9] text-white"
|
||||||
: "border-[#ADADAD] bg-[#F7F7F7] text-[#4A5560]"
|
: "border-[#D7DCE2] bg-[#F6F7F8] text-[#4D5662]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{aktiv ? "✓ " : ""}
|
{aktiv ? "✓ " : ""}
|
||||||
@@ -85,11 +85,11 @@ function Integrationer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel rubrik="Brand-specific connectors">
|
<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.
|
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>
|
</p>
|
||||||
{!krypteringKlar && (
|
{!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.
|
Encryption is not configured in this deployment (INTEGRATION_NYCKEL) — details cannot be saved until the key exists.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -97,13 +97,13 @@ function Integrationer() {
|
|||||||
{leverantorer.map((lev) => {
|
{leverantorer.map((lev) => {
|
||||||
const finns = befintliga.find((i) => i.leverantor === lev.id);
|
const finns = befintliga.find((i) => i.leverantor === lev.id);
|
||||||
return (
|
return (
|
||||||
<div key={lev.id} className="border-b border-[#EBEBEB] py-2 last:border-0">
|
<div key={lev.id} className="border-b border-[#D7DCE2] py-2 last:border-0">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-[13px] font-semibold">{lev.namn}</p>
|
<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 && (
|
{finns && (
|
||||||
<p className="mt-1 text-[12px] text-[#4A5560]">
|
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||||
{Object.entries(finns.uppgifter)
|
{Object.entries(finns.uppgifter)
|
||||||
.map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`)
|
.map(([nyckel, varde]) => `${nyckel}: ${varde || "—"}`)
|
||||||
.join(" · ")}
|
.join(" · ")}
|
||||||
@@ -111,13 +111,13 @@ function Integrationer() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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"}
|
{finns ? "Configured" : "Not configured"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{oppen === lev.id ? (
|
{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) => (
|
{lev.falt.map((falt) => (
|
||||||
<TextFalt
|
<TextFalt
|
||||||
key={falt.nyckel}
|
key={falt.nyckel}
|
||||||
@@ -150,7 +150,7 @@ function Integrationer() {
|
|||||||
Save details
|
Save details
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
</div>
|
</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.
|
Secret fields are never shown again after saving — enter a new value to replace one.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +188,7 @@ function Integrationer() {
|
|||||||
</div>
|
</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>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -210,7 +210,7 @@ export default function Installningar() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<p className="text-[14px] text-[#1B1E22]">
|
||||||
Organization settings are managed by your system administrator.
|
Organization settings are managed by your system administrator.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -243,7 +243,7 @@ export default function Installningar() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Settings" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[#333333]">
|
<p className="text-[#1B1E22]">
|
||||||
Choose what is shown when a new case is started.{" "}
|
Choose what is shown when a new case is started.{" "}
|
||||||
{inloggad
|
{inloggad
|
||||||
? "The setting applies to everyone in the organization."
|
? "The setting applies to everyone in the organization."
|
||||||
@@ -269,8 +269,8 @@ export default function Installningar() {
|
|||||||
|
|
||||||
{inloggad && <Integrationer />}
|
{inloggad && <Integrationer />}
|
||||||
|
|
||||||
{status === "fel" && <p className="mb-3 font-semibold text-[#8B1A1A]">{felText}</p>}
|
{status === "fel" && <p className="mb-4 font-semibold text-[#8B1A1A]">{felText}</p>}
|
||||||
{status === "sparat" && <p className="mb-3 font-semibold text-[#1E6B34]">✓ Saved</p>}
|
{status === "sparat" && <p className="mb-4 font-semibold text-[#005CA9]">✓ Saved</p>}
|
||||||
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
||||||
{status === "sparar" ? "Saving …" : "Save settings"}
|
{status === "sparar" ? "Saving …" : "Save settings"}
|
||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export default function NyttArende() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Start case" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel rubrik="Scan work order">
|
<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.
|
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>
|
</p>
|
||||||
<input
|
<input
|
||||||
@@ -192,7 +192,7 @@ export default function NyttArende() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{skannar ? (
|
{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>
|
<StorKnapp onClick={() => skanRef.current?.click()}><IkonKamera /> Skanna arbetsorder</StorKnapp>
|
||||||
)}
|
)}
|
||||||
@@ -215,7 +215,7 @@ export default function NyttArende() {
|
|||||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
||||||
>
|
>
|
||||||
{tolkning.demo && (
|
{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.
|
Demo interpretation — the platform's document interpretation requires sign-in. The values below are sample data.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -253,7 +253,7 @@ export default function NyttArende() {
|
|||||||
</StorKnapp>
|
</StorKnapp>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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.
|
QR code, barcode and type plate can be read in the next step — or enter the identity manually.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -272,8 +272,8 @@ export default function NyttArende() {
|
|||||||
/>
|
/>
|
||||||
{skannarKod ? (
|
{skannarKod ? (
|
||||||
<div>
|
<div>
|
||||||
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#C6C6C6]" />
|
<video ref={videoRef} playsInline muted className="mb-2 w-full border border-[#D7DCE2]" />
|
||||||
<p className="mb-2 text-[12px] text-[#707070]">
|
<p className="mb-2 text-[12px] text-[#4D5662]">
|
||||||
Point the camera at the QR code, barcode or VIN label.
|
Point the camera at the QR code, barcode or VIN label.
|
||||||
</p>
|
</p>
|
||||||
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
<StorKnapp variant="sekundar" onClick={stangSkanning}>
|
||||||
@@ -293,7 +293,7 @@ export default function NyttArende() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{avlastKod && (
|
{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}
|
Read {avlastKod.typ}: {avlastKod.varde}
|
||||||
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
{avlastKod.format ? ` (${avlastKod.format})` : ""} — check the value below.
|
||||||
</p>
|
</p>
|
||||||
@@ -316,11 +316,11 @@ export default function NyttArende() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Confirm object" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4A5560]">{objektet.typ}</p>
|
<p className="text-[12px] font-semibold uppercase tracking-widest text-[#4D5662]">{objektet.typ}</p>
|
||||||
<p className="mt-1 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
<p className="mt-2 text-[20px] font-semibold">{objektet.beskrivning}</p>
|
||||||
<p className="mt-1 text-[15px] font-semibold text-[#00437A]">{objektet.identifierare}</p>
|
<p className="mt-2 text-[15px] font-semibold text-[#005CA9]">{objektet.identifierare}</p>
|
||||||
<p className="mt-1 text-[12px] text-[#4A5560]">Identifierad via: {objektet.identifieringsmetod}</p>
|
<p className="mt-2 text-[12px] text-[#4D5662]">Identifierad via: {objektet.identifieringsmetod}</p>
|
||||||
{objektet.kund && <p className="mt-1 text-[14px] text-[#333333]">Kund: {objektet.kund}</p>}
|
{objektet.kund && <p className="mt-2 text-[14px] text-[#1B1E22]">Kund: {objektet.kund}</p>}
|
||||||
</Panel>
|
</Panel>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
<StorKnapp variant="sekundar" onClick={() => setSteg("identifiera")}>
|
||||||
@@ -346,8 +346,8 @@ export default function NyttArende() {
|
|||||||
rost
|
rost
|
||||||
/>
|
/>
|
||||||
{metodik && (
|
{metodik && (
|
||||||
<p className="mb-3 text-[12px] font-semibold text-[#4A5560]">
|
<p className="mb-4 text-[12px] font-semibold text-[#4D5662]">
|
||||||
Vald metodik: <span className="text-[#00437A]">{metodik.namn}</span>
|
Vald metodik: <span className="text-[#005CA9]">{metodik.namn}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<StorKnapp
|
<StorKnapp
|
||||||
@@ -401,13 +401,13 @@ function GranskaTolkning({
|
|||||||
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
|
setRader((r) => r.map((rad) => (rad.id === id ? { ...rad, ...andring } : rad)));
|
||||||
|
|
||||||
return (
|
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">
|
<Panel rubrik="Scanned work order">
|
||||||
<div className="relative">
|
<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 && (
|
{vald?.omrade && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: `${vald.omrade.x * 100}%`,
|
left: `${vald.omrade.x * 100}%`,
|
||||||
top: `${vald.omrade.y * 100}%`,
|
top: `${vald.omrade.y * 100}%`,
|
||||||
@@ -417,7 +417,7 @@ function GranskaTolkning({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[11px] text-[#707070]">
|
<p className="mt-2 text-[11px] text-[#4D5662]">
|
||||||
{vald?.omrade
|
{vald?.omrade
|
||||||
? `Markerat: ${vald.etikett} (approximate position)`
|
? `Markerat: ${vald.etikett} (approximate position)`
|
||||||
: "Select a field to see where in the document the value was found."}
|
: "Select a field to see where in the document the value was found."}
|
||||||
@@ -436,27 +436,27 @@ function GranskaTolkning({
|
|||||||
<div
|
<div
|
||||||
key={rad.id}
|
key={rad.id}
|
||||||
onClick={() => setMarkerad(rad.id)}
|
onClick={() => setMarkerad(rad.id)}
|
||||||
className={`mb-1.5 cursor-pointer border p-2 ${
|
className={`mb-2 cursor-pointer border p-2 ${
|
||||||
markerad === rad.id ? "border-[#00437A]" : "border-transparent"
|
markerad === rad.id ? "border-[#005CA9]" : "border-transparent"
|
||||||
} ${niva === "gul" ? "bg-[#FFF8E1]" : niva === "rod" ? "bg-[#FBE9E9]" : "bg-white"}`}
|
} ${niva === "gul" ? "bg-[#FFFFFF]" : niva === "rod" ? "bg-[#FFFFFF]" : "bg-white"}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<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}
|
{rad.etikett}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`text-[11px] font-semibold ${
|
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)} %`}
|
{niva === "gron" ? "✓" : `${Math.round(rad.konfidens * 100)} %`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
value={rad.varde}
|
value={rad.varde}
|
||||||
onChange={(e) => uppdatera(rad.id, { varde: e.target.value, bekraftad: true })}
|
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 && (
|
{niva === "rod" && !rad.bekraftad && (
|
||||||
<button
|
<button
|
||||||
@@ -464,7 +464,7 @@ function GranskaTolkning({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
uppdatera(rad.id, { bekraftad: true });
|
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
|
Confirm
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="col-span-2 border border-[#C6C6C6] bg-white p-2">
|
<div className="col-span-2 border border-[#D7DCE2] bg-white p-2">
|
||||||
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4A5560]">New responsible technician</p>
|
<p className="mb-2 text-[12px] font-semibold uppercase text-[#4D5662]">New responsible technician</p>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{personer.map((person) => (
|
{personer.map((person) => (
|
||||||
<button
|
<button
|
||||||
key={person.id}
|
key={person.id}
|
||||||
className="min-h-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 () => {
|
onClick={async () => {
|
||||||
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
|
const post = nyLoggPost(konto?.namn ?? "arbetsledare", {
|
||||||
typ: "ansvarig_satt",
|
typ: "ansvarig_satt",
|
||||||
@@ -69,7 +69,7 @@ function Omfordela({ rad, vidKlar }: { rad: OversiktsRad; vidKlar: () => void })
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<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)}
|
onClick={() => setOppen(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -101,7 +101,7 @@ export default function Oversikt() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
<FelsokningSkal rubrik="Organization overview" tillbaka={{ till: "/felsokning", text: "Cases" }}>
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<p className="text-[14px] text-[#1B1E22]">
|
||||||
The overview requires supervisor or administrator rights on the platform.
|
The overview requires supervisor or administrator rights on the platform.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -153,30 +153,30 @@ export default function Oversikt() {
|
|||||||
<FelsokningSkal
|
<FelsokningSkal
|
||||||
rubrik="Organization overview"
|
rubrik="Organization overview"
|
||||||
tillbaka={{ till: "/felsokning", text: "Cases" }}
|
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>}
|
{fel && <p className="mb-4 font-semibold text-[#8B1A1A]">{fel}</p>}
|
||||||
{!rader && !fel && <p className="animate-pulse text-[14px] text-[#4A5560]">Retrieving …</p>}
|
{!rader && !fel && <p className="text-[14px] text-[#4D5662]">Retrieving …</p>}
|
||||||
|
|
||||||
{rader && (
|
{rader && (
|
||||||
<>
|
<>
|
||||||
<Panel rubrik="Statistics">
|
<Panel rubrik="Statistics">
|
||||||
<div className="grid grid-cols-3 gap-3 text-center">
|
<div className="grid grid-cols-3 gap-4 text-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold">{rader.length}</p>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold text-[#00437A]">{pagaende.length}</p>
|
<p className="text-[20px] font-semibold text-[#005CA9]">{pagaende.length}</p>
|
||||||
<p className="text-[12px] font-semibold uppercase text-[#4A5560]">In progress</p>
|
<p className="text-[12px] font-semibold uppercase text-[#4D5662]">In progress</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[20px] font-semibold">{avslutade.length}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{avslutade.length > 0 && (
|
{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>
|
Average lead time for closed cases: <span className="font-semibold">{formateraTid(medelLedtidMs)}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -185,41 +185,41 @@ export default function Oversikt() {
|
|||||||
{felorsaker.length > 0 && (
|
{felorsaker.length > 0 && (
|
||||||
<Panel rubrik="Root cause statistics — recurring faults in the organization">
|
<Panel rubrik="Root cause statistics — recurring faults in the organization">
|
||||||
{felorsaker.map((rad) => (
|
{felorsaker.map((rad) => (
|
||||||
<div key={rad.orsak} className="flex items-center gap-2 py-0.5 text-[13px]">
|
<div key={rad.orsak} className="flex items-center gap-2 py-0 text-[13px]">
|
||||||
<span className="w-52 shrink-0 text-[#333333]">{rad.orsak}</span>
|
<span className="w-52 shrink-0 text-[#1B1E22]">{rad.orsak}</span>
|
||||||
<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 }}
|
style={{ width: `${Math.min(100, (rad.antal / felorsaker[0].antal) * 100)}%`, minWidth: 6 }}
|
||||||
/>
|
/>
|
||||||
<span className="font-semibold">{rad.antal}</span>
|
<span className="font-semibold">{rad.antal}</span>
|
||||||
</div>
|
</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.
|
From documented root cause analyses — shows patterns such as wear, previous repairs or possible design problems.
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</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) => (
|
{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">
|
<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}
|
Case #{rad.nummer}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-0.5 text-[11px] font-semibold uppercase ${
|
className={`px-2 py-0 text-[11px] font-semibold uppercase ${
|
||||||
rad.avslutat ? "bg-[#8A94A0] text-white" : "bg-[#00437A] text-white"
|
rad.avslutat ? "bg-[#4D5662] text-white" : "bg-[#005CA9] text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{rad.avslutat ? "Closed" : "In progress"}
|
{rad.avslutat ? "Closed" : "In progress"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
|
<p className="mt-2 text-[15px] font-semibold">{rad.objekt ?? "Unknown object"}</p>
|
||||||
{rad.felbeskrivning && <p className="text-[14px] text-[#333333]">”{rad.felbeskrivning}”</p>}
|
{rad.felbeskrivning && <p className="text-[14px] text-[#1B1E22]">”{rad.felbeskrivning}”</p>}
|
||||||
<p className="mt-1 text-[12px] text-[#707070]">
|
<p className="mt-2 text-[12px] text-[#4D5662]">
|
||||||
Ansvarig: <span className="font-semibold text-[#333333]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
Ansvarig: <span className="font-semibold text-[#1B1E22]">{rad.ansvarig ?? rad.skapare ?? "—"}</span>
|
||||||
{` · ${rad.antal_handelser} events`}
|
{` · ${rad.antal_handelser} events`}
|
||||||
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
{rad.senaste ? ` · senast ${tidDatum(rad.senaste)} ${tidKlockslag(rad.senaste)}` : ""}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function PublikDelning() {
|
|||||||
return (
|
return (
|
||||||
<FelsokningSkal rubrik="Shared case">
|
<FelsokningSkal rubrik="Shared case">
|
||||||
<Panel>
|
<Panel>
|
||||||
<p className="text-[14px] text-[#333333]">
|
<p className="text-[14px] text-[#1B1E22]">
|
||||||
{status === "laddar"
|
{status === "laddar"
|
||||||
? "Retrieving the case …"
|
? "Retrieving the case …"
|
||||||
: "The case is not available. Check the link with the workshop — the share may have been revoked."}
|
: "The case is not available. Check the link with the workshop — the share may have been revoked."}
|
||||||
|
|||||||
Reference in New Issue
Block a user