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) {