Sammanfattning och slutsats i rapport och Live Share; API dokumenterat

Sammanfattningen ligger först i både kundrapporten och Live Share.
Mottagaren är oftast inte tekniker — det är kunden, en
försäkringshandläggare eller en flottansvarig — och de ska få bilden på
fem sekunder och sedan kunna gå djupare, inte tvärtom.

I Live Share härleds den ur det NIVÅFILTRERADE underlaget. Det betyder
att sammanfattningen aldrig kan avslöja något som delningsnivån döljer:
filtret ligger före projektionen, inte efter.

Slutsatsen visas som ett eget avsnitt före underlaget. En handläggare
läser skälet först och kontrollerar det sedan — den ordningen är hela
poängen med att fältet finns.

Sju nya vägar dokumenterade i OpenAPI och låsta av paritetstestet:
sammanfattning, protokollinläsning, statistik, integrationskategorier,
prenumerationer, radering och mätdon. Ett API som inte är dokumenterat
är inte ett API någon kan koppla in sig mot — och specen valideras
maskinellt så dokumentationen inte kan glida från servern.

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 15:55:31 +00:00
parent 3ec25dea9c
commit cd6b2caca5
4 changed files with 307 additions and 0 deletions
@@ -10,6 +10,7 @@ import { KUNDBESLUT_LABEL, handelseRubrik } from "./domain";
import { arAvslutat, arendeidentitet, brief, foton, tidsfordelningsRader, videor } from "./projektioner";
import { metodikForArende } from "./store";
import { Bild, Klipp } from "./Bilagevisning";
import { sammanfatta } from "../../../services/gemensam/sammanfattning.mjs";
import { tidDatum, tidKlockslag } from "./format";
import { FelsokningSkal, Panel, StorKnapp } from "./ui";
import { IkonCheck, IkonKlocka, IkonUppdatera } from "./ikoner";
@@ -157,6 +158,41 @@ export function DelatArendeVy({
ärende vyn avser. Härledd ur det nivåfiltrerade underlaget. */}
<IdentitetsPanel arende={arende} avslutat={avslutat} />
{/* ALVA-PROC-0030 · Sammanfattningen först. Mottagaren är oftast
inte tekniker: kunden, en försäkringshandläggare, en
flottansvarig. De ska få bilden på fem sekunder och sedan kunna
gå djupare — inte tvärtom. Texten härleds ur det nivåfiltrerade
underlaget, så den kan aldrig avslöja något som nivån döljer. */}
<Panel rubrik="Sammanfattning">
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
</Panel>
{/* ALVA-RULE-200 · Teknikerns varför. Den enda rad en
försäkringsbedömare egentligen behöver, kunddelbar med avsikt. */}
{(() => {
const post = [...arende.handelser].reverse().find((p) => p.handelse.typ === "slutsats");
if (!post || post.handelse.typ !== "slutsats") return null;
const h = post.handelse;
const rader: [string, string][] = [
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
["Uteslutna alternativ", h.uteslutet],
...(h.atgardsval ? ([["Val av åtgärd", h.atgardsval]] as [string, string][]) : []),
["Kvarstående osäkerhet", h.kvarstaende],
];
return (
<Panel rubrik="Slutsats och motivering">
<dl className="text-[14px] leading-[22px]">
{rader.map(([etikett, text]) => (
<div key={etikett} className="border-t border-[#D7DCE2] py-2 first:border-t-0">
<dt className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[#4D5662]">{etikett}</dt>
<dd className="mt-1 text-[#1B1E22]">{text}</dd>
</div>
))}
</dl>
</Panel>
);
})()}
<p className="mb-4 rounded border border-[#C6C6C6] bg-[#F7F7F7] p-3 text-[12px] text-[#4A5560] print:hidden">
{notis}
</p>
@@ -101,6 +101,15 @@ describe("AI-orkestern", () => {
["/api/arenden/{arendeId}/bilagor", "laddaUppVag"],
["/api/bilagor/{bilagaId}", "bilagaVag"],
["/api/delad/{delningskod}/bilagor/{bilagaId}", "delatBilaga"],
// ALVA: nya vägar. Ett API som inte är dokumenterat är inte ett API
// någon kan koppla in sig mot.
["/api/arenden/{arendeId}/sammanfattning", "sammanfattningVag"],
["/api/arenden/{arendeId}/protokoll", "protokollVag"],
["/api/statistik/oversikt", "/api/statistik/oversikt"],
["/api/integration/kategorier", "/api/integration/kategorier"],
["/api/integration/prenumerationer", "/api/integration/prenumerationer"],
["/api/radering", "/api/radering"],
["/api/matdon", "/api/matdon"],
];
for (const [iSpec, iServer] of vagar) {
expect(spec).toContain(`${iSpec}:`);
@@ -7,6 +7,7 @@ import { nastaSteg } from "@/felsokning/metodik";
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 { arendebeteckning, fasDefinition } from "@/alva/system";
import {
arAvslutat,
@@ -2317,6 +2318,33 @@ function RapportFlik({
Delningslänken kräver att ärendet är synkat mot molnet (inloggad användare).
</p>
</Panel>
{/* ALVA-PROC-0030 · Sammanfattningen först. Den som öppnar
rapporten — kund, handläggare, nästa tekniker — ska få bilden
på fem sekunder och sedan kunna gå djupare. Texten är härledd
ur loggen, så den kan inte säga något som inte står där. */}
<Panel rubrik="Sammanfattning">
<p className="text-[15px] leading-[24px] text-[#1B1E22]">{sammanfatta(arende).text}</p>
<p className="mt-2 font-mono text-[11px] text-[#4D5662]">ALVA-PROC-0030 · härledd ur händelseloggen</p>
</Panel>
{/* ALVA-RULE-200 · Teknikerns varför. Placerad före underlaget:
en handläggare läser skälet först och kontrollerar det sedan. */}
{(() => {
const s = [...arende.handelser].reverse().find((p) => p.handelse.typ === "slutsats");
if (!s || s.handelse.typ !== "slutsats") return null;
const h = s.handelse;
return (
<Panel rubrik="Slutsats och motivering">
{identitetsRader([
[h.orsakFastställd === false ? "Skäl till att orsaken inte fastställts" : "Motivering", h.motivering],
["Uteslutna alternativ", h.uteslutet],
["Val av åtgärd", h.atgardsval ?? "—"],
["Kvarstående osäkerhet", h.kvarstaende],
])}
</Panel>
);
})()}
{/* Rapportens första sida: ärendeidentiteten — registrerad en gång,
återanvänd här automatiskt. */}
<Panel rubrik="Ärendeinformation">