diff --git a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts
index 94d8b9a..8b4f135 100644
--- a/felsokning/app/src/felsokning/__tests__/slutsats.test.ts
+++ b/felsokning/app/src/felsokning/__tests__/slutsats.test.ts
@@ -178,3 +178,50 @@ describe("grinden spärrar avslutet utan slutsats", () => {
expect(hinder).toContain("slutsats_slutsats");
});
});
+
+// ---- Klient och grind måste ge samma svar --------------------------------
+//
+// Bakgrunden till det här testet är en riktig bugg: kvalitetsgrinden på
+// servern krävde en slutsats, men klientens avslutsknapp gjorde inte det.
+// Demoärendet gick därför att stänga på skärmen utan ett varför, och
+// teknikern fick beskedet först vid synk — vilket är den sämsta tänkbara
+// tidpunkten, eftersom bilen då har lämnat verkstaden.
+//
+// Testet jämför inte texter utan verdikt: samma logg ska ge samma svar på
+// frågan "får detta ärende stängas?" i båda ändar.
+describe("avslutsvillkoret är detsamma i klienten och i grinden", () => {
+ const slutsatsHinder = async (handelser: unknown[]) => {
+ const { grinda } = await import("../../../../services/gemensam/grind.mjs");
+ const { ALLA_METODIKER } = await import("../../../../services/gemensam/metodiker.mjs");
+ return grinda(handelser, ALLA_METODIKER.at(-1))
+ .map((h) => h.id)
+ .filter((id) => id.startsWith("slutsats_"));
+ };
+
+ // Precis det uttryck klienten använder i ArendeSida.kanAvslutas.
+ const klientBrister = (handelser: Record[]) =>
+ granskaSlutsats([...handelser].reverse().find((h) => h.typ === "slutsats"), handelser);
+
+ it("en logg utan slutsats spärras i båda ändar", async () => {
+ expect(klientBrister(ARBETE).length).toBeGreaterThan(0);
+ expect(await slutsatsHinder(ARBETE)).not.toHaveLength(0);
+ });
+
+ it("en logg med fullständig slutsats släpps igenom i båda ändar", async () => {
+ const logg = [...ARBETE, GILTIG];
+ expect(klientBrister(logg)).toEqual([]);
+ expect(await slutsatsHinder(logg)).toEqual([]);
+ });
+
+ it("en slutsats med tunn motivering spärras i båda ändar", async () => {
+ const logg = [...ARBETE, { ...GILTIG, motivering: "Klart." }];
+ expect(klientBrister(logg).length).toBeGreaterThan(0);
+ expect(await slutsatsHinder(logg)).not.toHaveLength(0);
+ });
+
+ it("en obemött hypotes spärras i båda ändar", async () => {
+ const logg = [...ARBETE, { typ: "hypotes", text: "Misstänkt glapp i hjullagret höger fram", niva: "medel" }, GILTIG];
+ expect(klientBrister(logg).length).toBeGreaterThan(0);
+ expect(await slutsatsHinder(logg)).not.toHaveLength(0);
+ });
+});
diff --git a/felsokning/app/src/pages/felsokning/ArendeSida.tsx b/felsokning/app/src/pages/felsokning/ArendeSida.tsx
index 829214e..73b47fc 100644
--- a/felsokning/app/src/pages/felsokning/ArendeSida.tsx
+++ b/felsokning/app/src/pages/felsokning/ArendeSida.tsx
@@ -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 (
@@ -1304,10 +1319,12 @@ function GuideFlik({
- {/* 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 && }
+ {/* 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) && }
{steg.klart ? (
@@ -1320,11 +1337,7 @@ function GuideFlik({
skicka({ typ: "arende_avslutat", signatur: anvandare })}>
Avsluta felsökning
- {!kanAvslutas && (
-
- Avslut kräver symptomverifiering, felorsaksanalys, åtgärd och kvalitetskontroll — se panelerna nedan.
-
- )}
+ {!kanAvslutas && }
>
) : steg.sparr ? (
// Säkerhetsspärr. Metodiken går inte vidare — svaret är ett
@@ -1397,12 +1410,7 @@ function GuideFlik({
)}
- {!kanAvslutas && (
-
- Avslut kräver hela kedjan: symptomverifiering, felorsaksanalys, dokumenterad åtgärd och — vid utförd åtgärd
- — kvalitetskontroll av att symptomet är borta.
-
- )}
+ {!kanAvslutas && }
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 (
+
+
+ Ärendet kan inte avslutas ännu
+
+ {!underlagKlart && (
+
+ 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.
+
+ )}
+ {brister.length > 0 && (
+
+ {brister.map((b) => (
+ - {b.text}
+ ))}
+
+ )}
+
+ );
+}
+
function KategoriRad({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
let aktiv: TidKategori = "aktiv_felsokning";
for (const post of arende.handelser) {