Spärra avslut utan slutsats även i klienten (ALVA-RULE-200)

Kvalitetsgrinden på servern krävde en slutsats, men klientens
avslutsknapp gjorde inte det. Ett ärende gick därför att stänga på
skärmen utan ett varför, och teknikern fick beskedet först vid synk —
den sämsta tänkbara tidpunkten, eftersom bilen då har lämnat verkstaden.

Klienten anropar nu granskaSlutsats() — samma funktion som grinden, inte
en klientkopia av regeln — och avslutsknappen är spärrad tills den är
ren. Slutsatspanelen visas så fort underlaget är helt, inte bara när
metodikens sista steg är avbockat, så att frågan kommer när den går att
besvara och inte som ett hinder efteråt.

Hindret säger vad som fattas i stället för att bara konstatera att kraven
inte är uppfyllda: först underlaget, sedan de enskilda bristerna i
slutsatsen.

Fyra tester jämför verdikt mellan klient och grind för samma logg, så att
de två ändarna inte kan glida isär igen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-05 16:20:27 +00:00
parent cd6b2caca5
commit 590fab3a70
2 changed files with 101 additions and 16 deletions
@@ -8,6 +8,7 @@ import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
import { Fasrad } from "@/alva/komponenter";
import { Slutsatspanel } from "@/felsokning/Slutsats";
import { sammanfatta } from "../../../../services/gemensam/sammanfattning.mjs";
import { granskaSlutsats } from "../../../../services/gemensam/motivering.mjs";
import { arendebeteckning, fasDefinition } from "@/alva/system";
import {
arAvslutat,
@@ -1252,12 +1253,26 @@ function GuideFlik({
// åtgärd faktiskt utförts — kvalitetskontroll av att symptomet är borta.
const atgardsposter = atgarder(arende);
const utfordAtgard = atgardsposter.some((p) => p.handelse.typ === "atgard_utford" && p.handelse.utford);
const kanAvslutas =
// Underlaget: det som gör att en slutsats över huvud taget går att
// motivera. Innan detta är på plats vore varför-frågan en begäran om
// en gissning.
const underlagKlart =
!!reproducering(arende) &&
felorsaker(arende).length > 0 &&
atgardsposter.length > 0 &&
(!utfordAtgard || !!kvalitetskontroll(arende));
// ALVA-RULE-200. Samma granskning som kvalitetsgrinden på servern kör
// — avsiktligt samma funktion, inte en klientkopia av regeln. En
// tekniker ska aldrig få veta vid synk att ärendet inte gick att
// stänga; hindret ska synas på skärmen där arbetet utförs.
const slutsatsbrister = granskaSlutsats(
[...arende.handelser].reverse().find((p) => p.handelse.typ === "slutsats")?.handelse,
arende.handelser.map((p) => p.handelse),
);
const kanAvslutas = underlagKlart && slutsatsbrister.length === 0;
if (avslutat) {
return (
<Panel rubrik="Felsökningen är avslutad">
@@ -1304,10 +1319,12 @@ function GuideFlik({
</p>
</div>
{/* ALVA-RULE-200. Panelen visas när metodiken är genomförd — det
är då varför-frågan har ett underlag att vila på. Att fråga
tidigare vore att be om en gissning. */}
{steg.klart && <Slutsatspanel arende={arende} skicka={skicka} />}
{/* ALVA-RULE-200. Panelen visas när det finns ett underlag att
vila på — antingen för att metodiken är genomförd eller för att
kedjan reproducering → felorsak → åtgärd → kvalitetskontroll är
hel. Att fråga tidigare vore att be om en gissning; att fråga
först vid avslutsknappen vore att neka någon som redan är klar. */}
{(steg.klart || underlagKlart) && <Slutsatspanel arende={arende} skicka={skicka} />}
<Panel rubrik={`Metodik: ${metodik.namn} · Steg: ${steg.steg.rubrik}`}>
{steg.klart ? (
@@ -1320,11 +1337,7 @@ function GuideFlik({
<StorKnapp variant="fara" disabled={!kanAvslutas} onClick={() => skicka({ typ: "arende_avslutat", signatur: anvandare })}>
Avsluta felsökning
</StorKnapp>
{!kanAvslutas && (
<p className="mt-2 text-[12px] font-semibold text-[#9A6700]">
Avslut kräver symptomverifiering, felorsaksanalys, åtgärd och kvalitetskontroll se panelerna nedan.
</p>
)}
{!kanAvslutas && <Avslutshinder underlagKlart={underlagKlart} brister={slutsatsbrister} />}
</>
) : steg.sparr ? (
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
@@ -1397,12 +1410,7 @@ function GuideFlik({
</Panel>
)}
{!kanAvslutas && (
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
Avslut kräver hela kedjan: symptomverifiering, felorsaksanalys, dokumenterad åtgärd och vid utförd åtgärd
kvalitetskontroll av att symptomet är borta.
</p>
)}
{!kanAvslutas && <Avslutshinder underlagKlart={underlagKlart} brister={slutsatsbrister} />}
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
Lämna över arbete
@@ -1419,6 +1427,36 @@ function GuideFlik({
);
}
/**
* Varför avslut är spärrat — i klartext, på skärmen, medan arbetet pågår.
*
* Ett hinder som bara säger "kraven är inte uppfyllda" tvingar teknikern
* att leta. Här står vad som fattas, i den ordning det behöver åtgärdas:
* först underlaget, sedan varför-frågan.
*/
function Avslutshinder({ underlagKlart, brister }: { underlagKlart: boolean; brister: { falt: string; text: string }[] }) {
return (
<div className="mb-2 border-l-2 border-[#9A6700] bg-[#FFFBF0] px-4 py-4">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-[0.08em] text-[#9A6700]">
Ärendet kan inte avslutas ännu
</p>
{!underlagKlart && (
<p className="mb-2 text-[13px] leading-[20px] text-[#333333]">
Underlaget är ofullständigt: avslut kräver symptomverifiering, felorsaksanalys, dokumenterad åtgärd och
vid utförd åtgärd kvalitetskontroll av att symptomet är borta. Se panelerna nedan.
</p>
)}
{brister.length > 0 && (
<ul className="ml-4 list-disc text-[13px] leading-[20px] text-[#333333]">
{brister.map((b) => (
<li key={b.falt}>{b.text}</li>
))}
</ul>
)}
</div>
);
}
function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
let aktiv: TidKategori = "aktiv_felsokning";
for (const post of arende.handelser) {