Publikt kundgodkännande i delningslänken

Kunden kan nu godkänna eller avböja åtgärdsförslaget direkt i sin
Live Share-länk — API:ts enda skrivande publika väg, med sex spärrar
som var och en verifieras i integrationstestet:

1. endast delningar på kundnivå (partner-/internlänkar får aldrig svara
   åt kunden) och aldrig återkallade
2. ärendets ursprungliga delningskod saknar registrerad nivå och kan
   inte heller svara
3. det måste finnas ett åtgärdsförslag att svara på
4. ett besked per ärende — svaret kan inte ändras i efterhand
5. endast godkant/avbojt/delvis plus kommentar på högst 500 tecken;
   inget annat kan skrivas till loggen den vägen
6. takt-begränsning per delningskod (429)

Beskedet loggas som kundbeslut med kanal "Delningslänk" och avsändaren
"Kund via delningslänk"; verkstadens egna registreringar (telefon, på
plats …) fungerar precis som förut, och kvalitetsgrindens krav gäller
oförändrat. Beslutspanelen visas bara på den publika kundlänken —
interna och partnervyer förblir helt skrivskyddade.

73 vitest-tester, integrationstest (åtta nya kontroller för spärrarna)
och OpenAPI-validering gröna. Därmed är även den sista dokumenterade
avgränsningen utöver externa fordonsdatabaser avklarad.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-03 12:25:14 +00:00
parent e22e0262e4
commit 64b2124cd8
8 changed files with 295 additions and 6 deletions
+1 -2
View File
@@ -38,7 +38,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
| Pre-diagnostik | ✅ Ingen felsökning förrän grundkontrollerna är gjorda eller motiverade: fordonshistorik — tidigare ärenden på samma fordon hämtas automatiskt med sina felorsaker (server i inloggat läge, lokala storen annars) och orsakskedjan kopplas med ett tryck; Ja/Nej med obligatorisk orsak → kvalitetsvarning, **ingående mätarställning** (foto av instrumentpanelen, bildtolkningen föreslår värdet), kundens felbeskrivning verifierad och tidiga observationer hanterade. Metodiken låses upp först därefter. **Utgående mätarställning** fotograferas inför avslut och blir obligatorisk i grinden när ärendet stängs. |
| Symptomverifiering (SVP) | ✅ Kundens beskrivning ≠ konstaterat fel: beskrivningen dokumenteras ordagrant, förtydligas via metodikens symptomfrågor (generiska metodiken har SVP-setet när/var/hur) och reproduceras — Ja (hur/förhållanden), Delvis (vad kunde/kunde inte) eller Nej (obligatorisk motivering). Rapportens beviskedja skiljer kundens beskrivning, verifierad observation, felorsaksanalys och rekommenderad åtgärd; formuleringen "kunde inte reproduceras under de förhållanden som rådde" används i stället för "felet konstaterat" (kodat även i orkesterns grundprompt). |
| Felorsaksanalys | ✅ Obligatorisk före avslut: konstaterad avvikelse (kvalitetsregeln avvisar "trasig/defekt/sliten" utan förklaring), orsakskategorier (inkl. Okänd orsak med krav på motivering), minst en evidenskälla som valideras mot loggen, säkerhetsnivå (medel/låg kräver stärkande kontroller) och rekommenderad åtgärd. Avslutsknappen spärrad tills SVP + felorsak finns; kvalitetsgrinden gör båda obligatoriska vid stängning. Eget avsnitt i slutrapporten. |
| Kundgodkännande | ✅ Åtgärdsförslag lämnas till kund innan arbetet påbörjas (förifyllt ur felorsaksanalysen, med uppskattad kostnad) och **visas i Live Share** — kunden ser vad som föreslås. Kundens besked registreras med utfall, kanal (telefon/på plats/e-post/SMS/delningslänk) och motivering vid avböjt. Åtgärdsknappen är låst tills beskedet finns och förblir låst vid avböjt; kvalitetsgrinden flaggar hårt om arbete utförts trots avböjt förslag. Publikt godkännande direkt i delningslänken är en medveten avgränsning. |
| Kundgodkännande | ✅ Åtgärdsförslag lämnas till kund innan arbetet påbörjas (förifyllt ur felorsaksanalysen, med uppskattad kostnad) och **visas i Live Share** — kunden ser vad som föreslås. Kundens besked registreras med utfall, kanal (telefon/på plats/e-post/SMS/delningslänk) och motivering vid avböjt. Åtgärdsknappen är låst tills beskedet finns och förblir låst vid avböjt; kvalitetsgrinden flaggar hårt om arbete utförts trots avböjt förslag. **Kunden kan även svara direkt i sin delningslänk** — den enda skrivande publika vägen, med sex spärrar (endast kundnivå, ej återkallad, förslag måste finnas, ett besked per ärende, begränsat innehåll, takt-begränsning), samtliga verifierade i integrationstestet. |
| Åtgärd och kvalitetskontroll | ✅ Arbetsflödets sista led: åtgärden dokumenteras (vad som gjordes + delar) eller motiveras varför den uteblev (kunden avböjde, väntar på reservdel …). Har en åtgärd utförts krävs **kvalitetskontroll** — symptomet borta / kvarstår / delvis / kunde inte verifieras, med beskrivning av hur det verifierades under samma förhållanden som symptomet reproducerades. Kvarstående symptom flaggas i grinden i stället för att döljas. Avslutsknappen är spärrad tills hela kedjan symptomverifiering → felorsak → åtgärd → kvalitetskontroll är komplett; rapporten har ett eget avsnitt ”Utförd åtgärd och verifiering”. |
| Ärendeidentitet | ✅ Fordonsobjektet som röd tråd: identiteten (AO-nummer, claim-/garantinummer, skadenummer, regnr, VIN, miltal, kund) registreras en gång — normalt via arbetsorderskanningen — och återanvänds i identitetsraden i arbetsytan (med ärendetypsval), låst panel överst i Live Share, slutrapportens första sida (Ärendeinformation + Fordonsinformation) och exporten. |
| Instrumentavläsning (visual-first) | ✅ Kameran som universellt gränssnitt: `📷 Instrument` i Dokumentera-panelen fotograferar multimetrar, diagnosskärmar, batteritestare m.m. — bildtolkningen identifierar instrumenttyp och extraherar värden/enheter/felkoder med konfidens per värde; teknikern bekräftar innan något loggas. Originalbilden loggas alltid tillsammans med de strukturerade mätvärdena — strukturerad data ersätter aldrig originalevidensen. Ingen integration mot diagnossystem krävs. |
@@ -58,4 +58,3 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
## Medvetna avgränsningar
- VIN-avkodning mot fordonsdatabaser (utrustningsnivå, återkallelser, TSB:er) och tillverkarintegrationer ingår inte ännu — arbetsorderskanningen och QR-/VIN-avläsningen ger strukturen de kopplas in i.
- Publikt kundgodkännande direkt i delningslänken (kunden klickar själv) kräver en skrivande publik endpoint med egen säkerhetsmodell; i dag registrerar verkstaden kundens besked med kanal och tidpunkt.
+18 -3
View File
@@ -181,9 +181,24 @@ registrerat och spårbart:
flaggar konflikten *"Utfört arbete trots avböjt åtgärdsförslag"* som
ett hårt fel.
Avgränsning: kunden godkänner i dagsläget genom kontakt med verkstaden —
plattformen registrerar beskedet. Publikt godkännande direkt i
delningslänken kräver en skrivande publik endpoint och hanteras separat.
**Kunden kan svara direkt i sin delningslänk** (`POST /api/delad/{kod}/beslut`)
— den enda skrivande publika vägen i hela API:t, med sex spärrar som var
och en verifieras i integrationstestet:
1. Endast delningar på **kundnivå** (partner-/internlänkar får aldrig
svara åt kunden) och aldrig återkallade.
2. Ärendets ursprungliga delningskod saknar registrerad nivå och kan inte
heller svara.
3. Det måste finnas ett åtgärdsförslag att svara på.
4. **Ett besked per ärende** — svaret kan inte ändras i efterhand
(kontakta verkstaden i stället).
5. Endast `godkant`/`avbojt`/`delvis` plus en kommentar på högst 500
tecken; inget annat kan skrivas till loggen den vägen.
6. Takt-begränsning per delningskod.
Beskedet loggas som `kundbeslut` med kanal `Delningslänk` och avsändaren
"Kund via delningslänk" — verkstadens egna registreringar (telefon, på
plats …) fungerar precis som förut.
## Åtgärdsfasen (Repair & Verification)
+59
View File
@@ -194,6 +194,65 @@ REGLER=$(curl -s "$BAS/api/ecm/regler" -H "Authorization: Bearer $TOKEN_J")
kontroll "regelpaketet serveras" "$(echo "$REGLER" | falt .version)" "2.0"
kontroll "regelpaketet innehåller garantiregler" "$(echo "$REGLER" | falt '.arendetypRegler.Garanti.length')" "3"
# 10c. Publikt kundgodkännande — den enda skrivande publika vägen
# Ärendets ursprungliga delningskod saknar registrerad nivå och får
# därför aldrig svara (bara riktiga kundlänkar duger).
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/delkod123/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
kontroll "legacy-delningskod kan inte svara" "$KOD" "404"
# Kundlänk utan åtgärdsförslag: inget att svara på
KUNDKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
-H 'Content-Type: application/json' -d '{"niva":"kund"}' | falt .kod)
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
kontroll "beslut utan åtgärdsförslag avvisas" "$KOD" "409"
# Lägg in ett åtgärdsförslag
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
-d '{"handelser":[{"id":"h-forslag","tidpunkt":"2026-08-03T08:06:00Z","anvandare":"Anna","handelse":{"typ":"atgardsforslag","beskrivning":"Byt reläet i kupémodulen.","uppskattadKostnad":"1 450 kr"}}]}' >/dev/null
# Partnerlänk får INTE svara åt kunden
PKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
-H 'Content-Type: application/json' -d '{"niva":"partner"}' | falt .kod)
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$PKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
kontroll "partnerlänk kan inte svara åt kunden" "$KOD" "404"
# Ogiltigt beslutsvärde avvisas
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"kanske"}')
kontroll "ogiltigt beslutsvärde avvisas" "$KOD" "400"
# Kunden godkänner via sin länk
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant","kommentar":"Kör på."}')
kontroll "kunden kan godkänna via sin länk" "$KOD" "200"
BESLUT=$(curl -s "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" \
| falt '.handelser.filter(h=>h.handelse.typ==="kundbeslut").map(h=>h.handelse.kanal+"/"+h.anvandare).join(",")')
kontroll "beskedet loggas med kanal och avsändare" "$BESLUT" "Delningslänk/Kund via delningslänk"
# Ett beslut per ärende — svaret kan inte ändras i efterhand
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"avbojt"}')
kontroll "beskedet kan inte ändras i efterhand" "$KOD" "409"
# Återkallad delning kan inte svara
curl -s -X POST "$BAS/api/delningar/$KUNDKOD/aterkalla" -H "Authorization: Bearer $TOKEN_A" >/dev/null
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$KUNDKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
kontroll "återkallad länk kan inte svara" "$KOD" "404"
# Takt-begränsning slår till efter upprepade försök
NYKOD=$(curl -s -X POST "$BAS/api/arenden/arende-test1/delningar" -H "Authorization: Bearer $TOKEN_A" \
-H 'Content-Type: application/json' -d '{"niva":"kund"}' | falt .kod)
SISTA=""
for i in 1 2 3 4 5 6 7; do
SISTA=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/delad/$NYKOD/beslut" \
-H 'Content-Type: application/json' -d '{"beslut":"godkant"}')
done
kontroll "takt-begränsning stoppar upprepade försök" "$SISTA" "429"
# 11. API-first: OpenAPI-specen serveras live, utan inloggning
SPEC=$(curl -s "$BAS/api/openapi.yaml")
case "$SPEC" in
+40
View File
@@ -229,6 +229,46 @@ paths:
"401": { $ref: "#/components/responses/Fel" }
"403": { $ref: "#/components/responses/Fel" }
/api/delad/{delningskod}/beslut:
post:
tags: [Delning]
summary: Kundens besked på ett åtgärdsförslag
description: >
Den enda skrivande publika vägen. Endast delningar på **kundnivå**
som inte återkallats får svara, det måste finnas ett
åtgärdsförslag, och **ett besked per ärende** — svaret kan inte
ändras i efterhand. Takt-begränsad per delningskod. Beskedet
loggas som `kundbeslut` med kanal `Delningslänk`.
security: []
parameters:
- name: delningskod
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [beslut]
properties:
beslut: { type: string, enum: [godkant, avbojt, delvis] }
kommentar: { type: string, maxLength: 500 }
responses:
"200":
description: Beskedet registrerat.
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean }
"400": { $ref: "#/components/responses/Fel" }
"404": { $ref: "#/components/responses/Fel" }
"409": { $ref: "#/components/responses/Fel" }
"429": { $ref: "#/components/responses/Fel" }
/api/organisation:
get:
tags: [Organisation]
+80
View File
@@ -40,6 +40,23 @@ const ROLLER = ["tekniker", "arbetsledare", "admin"];
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
// Enkel takt-begränsning för den publika beslutsendpointen (per
// delningskod, i minnet). Räcker för en enda pod; bakom flera repliker
// kompletteras den av databasspärren "ett beslut per förslag".
const BESLUT_TAK = 5;
const BESLUT_FONSTER_MS = 60_000;
const beslutsForsok = new Map();
function forTataForsok(kod) {
const nu = Date.now();
const forsok = (beslutsForsok.get(kod) ?? []).filter((t) => nu - t < BESLUT_FONSTER_MS);
forsok.push(nu);
beslutsForsok.set(kod, forsok);
// Enkel städning så kartan inte växer obegränsat.
if (beslutsForsok.size > 5000) beslutsForsok.clear();
return forsok.length > BESLUT_TAK;
}
// ---- JWT (HS256, utan beroenden) --------------------------------------
const b64url = (data) => Buffer.from(data).toString("base64url");
@@ -241,6 +258,69 @@ export function skapaServer() {
return svara(res, 200, { arende: arende.rows[0], handelser: handelser.rows, niva });
}
// -- Publikt kundgodkännande (den enda skrivande publika vägen) --
//
// Kunden svarar på ett åtgärdsförslag via sin delningslänk. Spärrar:
// 1. endast delningar på kundnivå (partner/intern får inte svara
// åt kunden), och aldrig återkallade
// 2. det måste finnas ett åtgärdsförslag att svara på
// 3. ett beslut per ärende — svaret kan inte ändras i efterhand
// 4. takt-begränsning per kod
// 5. beslutet får bara vara godkant/avbojt/delvis + kort kommentar;
// inget annat kan skrivas till loggen den här vägen
const beslutVag = vag.match(/^\/api\/delad\/([A-Za-z0-9_-]+)\/beslut$/);
if (req.method === "POST" && beslutVag) {
const kod = beslutVag[1];
if (forTataForsok(kod)) return svara(res, 429, { error: "För många försök — vänta en stund." });
const delning = await pool.query(
`select arende_id, niva from delningar where kod = $1 and aterkallad is null`,
[kod],
);
if (delning.rowCount === 0 || delning.rows[0].niva !== "kund") {
return svara(res, 404, { error: "Delningen är inte tillgänglig." });
}
const arendeId = delning.rows[0].arende_id;
const { beslut, kommentar } = await lasKropp(req);
if (!["godkant", "avbojt", "delvis"].includes(beslut)) {
return svara(res, 400, { error: "Ogiltigt beslut." });
}
if (kommentar !== undefined && (typeof kommentar !== "string" || kommentar.length > 500)) {
return svara(res, 400, { error: "Kommentaren är för lång." });
}
const forslag = await pool.query(
`select 1 from felsokning_handelser
where arende_id = $1 and handelse->>'typ' = 'atgardsforslag' limit 1`,
[arendeId],
);
if (forslag.rowCount === 0) {
return svara(res, 409, { error: "Det finns inget åtgärdsförslag att svara på." });
}
const tidigare = await pool.query(
`select 1 from felsokning_handelser
where arende_id = $1 and handelse->>'typ' = 'kundbeslut' limit 1`,
[arendeId],
);
if (tidigare.rowCount > 0) {
return svara(res, 409, { error: "Ett besked är redan registrerat — kontakta verkstaden." });
}
const handelse = {
typ: "kundbeslut",
beslut,
kanal: "Delningslänk",
...(kommentar?.trim() ? { kommentar: kommentar.trim() } : {}),
};
await pool.query(
`insert into felsokning_handelser (id, arende_id, tidpunkt, anvandare, handelse)
values ($1, $2, now(), $3, $4)`,
[`kb-${nyKod()}`, arendeId, "Kund via delningslänk", handelse],
);
return svara(res, 200, { ok: true });
}
// -- Skyddade endpoints (organisationsknutna) --
const anspr = kravAuth(req, hemlighet);
if (!anspr?.org) return svara(res, 401, { error: "Inloggning krävs." });
+68 -1
View File
@@ -4,14 +4,75 @@
// hypoteser) ingår aldrig i underlaget som når hit externt, och filtreras
// bort även lokalt.
import { useState } from "react";
import type { Arende } from "./domain";
import { KUNDBESLUT_LABEL, handelseRubrik } from "./domain";
import { arAvslutat, arendeidentitet, brief, foton, tidsfordelningsRader, videor } from "./projektioner";
import { metodikForArende } from "./store";
import { tidDatum, tidKlockslag } from "./format";
import { FelsokningSkal, Panel } from "./ui";
import { FelsokningSkal, Panel, StorKnapp } from "./ui";
import { IkonCheck, IkonKlocka, IkonUppdatera } from "./ikoner";
// Kundens svar på åtgärdsförslaget — den enda skrivande åtgärden i hela
// delningsvyn. Beskedet kan lämnas en gång och kan inte ändras här.
function BeslutsKnappar({
vidBeslut,
}: {
vidBeslut: (beslut: "godkant" | "avbojt", kommentar: string) => Promise<string | null>;
}) {
const [val, setVal] = useState<"" | "godkant" | "avbojt">("");
const [kommentar, setKommentar] = useState("");
const [skickar, setSkickar] = useState(false);
const [fel, setFel] = useState("");
return (
<div className="mt-2 border-t border-[#DDDDDD] pt-2 print:hidden">
<p className="mb-2 text-[12px] font-semibold uppercase tracking-wide text-[#4A5560]">
Ditt besked till verkstaden
</p>
<div className="mb-2 grid grid-cols-2 gap-2">
<StorKnapp variant={val === "godkant" ? "primar" : "sekundar"} onClick={() => setVal("godkant")}>
Godkänn åtgärden
</StorKnapp>
<StorKnapp variant={val === "avbojt" ? "fara" : "sekundar"} onClick={() => setVal("avbojt")}>
Avböj
</StorKnapp>
</div>
{val && (
<>
<label className="mb-2 block">
<span className="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
{val === "avbojt" ? "Kort motivering (valfritt)" : "Kommentar (valfritt)"}
</span>
<input
value={kommentar}
maxLength={500}
onChange={(e) => setKommentar(e.target.value)}
className="w-full rounded border border-[#ADADAD] bg-white px-2.5 py-1.5 text-[13px] focus:border-[#00437A] focus:outline-none"
/>
</label>
{fel && <p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">{fel}</p>}
<StorKnapp
disabled={skickar}
onClick={async () => {
setSkickar(true);
setFel("");
const felmeddelande = await vidBeslut(val, kommentar);
if (felmeddelande) setFel(felmeddelande);
setSkickar(false);
}}
>
{skickar ? "Skickar …" : "Skicka besked"}
</StorKnapp>
<p className="mt-1 text-[11px] text-[#707070]">
Beskedet registreras i ärendet och kan inte ändras här kontakta verkstaden om du ändrar dig.
</p>
</>
)}
</div>
);
}
function IdentitetsPanel({ arende, avslutat }: { arende: Arende; avslutat: boolean }) {
const idn = arendeidentitet(arende);
const falt: [string, string | undefined][] = [
@@ -45,10 +106,14 @@ export function DelatArendeVy({
nu,
notis,
redanFiltrerad = false,
vidBeslut,
}: {
arende: Arende;
nu: string;
notis: string;
// Satt endast på den publika kundlänken: kunden kan svara på ett
// åtgärdsförslag. Utan den är vyn helt skrivskyddad.
vidBeslut?: (beslut: "godkant" | "avbojt", kommentar: string) => Promise<string | null>;
// Sant när servern redan filtrerat per behörighetsnivå (publik delning) —
// då renderas händelserna som de kom, inklusive t.ex. hypoteser på
// partner-/internnivå.
@@ -116,6 +181,8 @@ export function DelatArendeVy({
Ditt besked: {KUNDBESLUT_LABEL[beslut.handelse.beslut]}{" "}
<span className="font-normal text-[#4A5560]">(registrerat via {beslut.handelse.kanal})</span>
</p>
) : vidBeslut ? (
<BeslutsKnappar vidBeslut={vidBeslut} />
) : (
<p className="mt-2 border-t border-[#DDDDDD] pt-2 text-[12px] text-[#707070]">
Inväntar ditt besked kontakta verkstaden för att godkänna eller avböja.
+1
View File
@@ -81,6 +81,7 @@ describe("AI-orkestern", () => {
["/api/statistik/felorsaker", "/api/statistik/felorsaker"],
["/api/oversikt", "/api/oversikt"],
["/api/delad/{delningskod}", "delad"],
["/api/delad/{delningskod}/beslut", "beslut"],
];
for (const [iSpec, iServer] of vagar) {
expect(spec).toContain(`${iSpec}:`);
+28
View File
@@ -99,12 +99,40 @@ export default function PublikDelning() {
);
}
// Kunden kan svara på ett åtgärdsförslag direkt i sin länk. Endast
// kundnivån får svara — servern gör samma kontroll igen.
const skickaBeslut =
niva === "kund" && kod
? async (beslut: "godkant" | "avbojt", kommentar: string) => {
const { plattformAktiv, PLATTFORM_URL } = await import("@/felsokning/plattform");
if (!plattformAktiv()) return "Beskedet kan inte lämnas här — kontakta verkstaden.";
try {
const res = await fetch(`${PLATTFORM_URL}/api/delad/${kod}/beslut`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ beslut, kommentar }),
});
if (!res.ok) {
const data = (await res.json().catch(() => ({}))) as { error?: string };
return data.error ?? "Beskedet kunde inte registreras — försök igen.";
}
// Hämta om direkt så kvittensen syns.
const uppdaterat = await hamtaDelat(kod);
if (uppdaterat) setArende(uppdaterat.arende);
return null;
} catch {
return "Beskedet kunde inte skickas — kontrollera anslutningen.";
}
}
: undefined;
return (
<DelatArendeVy
arende={arende}
nu={nu}
notis={NIVA_NOTIS[niva] ?? NIVA_NOTIS.kund}
redanFiltrerad
vidBeslut={skickaBeslut}
/>
);
}