Betalarspåret: beviskraven följer den som betalar — ALVA 3.6
Garantistandarden FGS-1.0 (fabriksgaranti, vagnskadegaranti, försäkring, extern garantigivare, leasing/fleet, goodwill) inarbetad i regelmotorn och bokförd i docs/GARANTISTANDARD-FGS.md (ALVA-DOC-0011): vad som redan var uppfyllt, vad som införs nu, och den ärliga gränsen — ODIS/DISS/SAGA2 är tillverkarsystem; ALVA bär bevispaketet och eskaleringsspåret, den ersätter dem inte. Tre nya händelsetyper i det stängda schemat: betalare (spår, namn, claim-/skadereferens, godkännande), eskalering (öppnad/besvarad, kanal, referens) och reservdel (artikelnummer, serienummer, batch, sparad del). Grinden kräver fastställd betalare för de ärendetyper där någon annan än kunden betalar, godkännande för goodwill och externa garantigivare, och spärrar avslut när en eskalering är öppnad utan dokumenterat svar — matchad per referens. Claim- och skadenummer kan komma ur betalaren, inte bara ur skannad arbetsorder. Tre nya ärendetyper med regelpaketskrav: Body damage warranty, Extended warranty, Leasing or fleet. Betalare, eskalering och reservdel är interna i delningsfiltret — kunden ser åtgärden, inte förhandlingen. Fyndet på vägen, åtgärdat med regressionstest: serverns ärendetypskrav var en tyst no-op. Servern matade den distribuerade paketformen (arendetypRegler) in i en grindfunktion som bara läste den slimmade (arendetyper[typ].krav) — garanti- och försäkringskraven upprätthölls bara som råd i klienten. C-2-mönstret, återuppstånden genom ett filformat. Grinden läser nu båda formerna, och regressionstestet låser att den distribuerade formen spärrar på servern. UI: betalar- och eskaleringspaneler i ärendevyn (panelen visas när ärendetypen kräver den), grindnyckeln grind.eskalering på alla tio språken, regelpaket 2.1 speglat klient/server med likhetstestet intakt. 781 enhetstester, genomgång 4/4 inom budget, portalspärr, integrationstest mot Postgres, typkontroll, lint på baslinjen, artefaktmätning och språkrunda gröna. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -181,7 +181,7 @@ describe("Traceability Engine + ärendeidentitet", () => {
|
||||
it("spårbarhetspaketet innehåller version, grindstatus och evidensposter", () => {
|
||||
const arende = byggArende([OBJEKT, { typ: "foto", beskrivning: "x", dataUrl: "data:" }]);
|
||||
const paket = sparbarhetspaket(arende, VIBRATION_METODIK);
|
||||
expect(paket.ecmVersion).toBe("2.0");
|
||||
expect(paket.ecmVersion).toBe("2.1");
|
||||
expect(paket.arendetyp).toBe("Private customer");
|
||||
expect(paket.evidensposter).toHaveLength(1);
|
||||
expect(paket.kvalitetsgrind.some((r) => r.id === "objekt" && r.ok)).toBe(true);
|
||||
|
||||
@@ -572,3 +572,74 @@ describe("mätarställningen ska vara fotograferad, inte bara inskriven", () =>
|
||||
expect(grinda(tomt, GENERISK).map((h) => h.id)).toContain("matarstallning_ingaende");
|
||||
});
|
||||
});
|
||||
|
||||
// ---- FGS-1.0: betalarspåret och eskaleringen ----------------------------
|
||||
//
|
||||
// Betalaren avgör beviskraven, och en öppnad teknisk eskalering utan
|
||||
// dokumenterat svar spärrar avslutet — bulletinernas "invänta svar innan
|
||||
// ytterligare åtgärder" som grindregel, inte som råd.
|
||||
describe("FGS · betalare och eskalering", () => {
|
||||
it("en öppnad eskalering spärrar avslutet, ett dokumenterat svar släpper", () => {
|
||||
const oppen = [...komplettLogg(), { typ: "eskalering", status: "oppnad", beskrivning: "DISS query", referens: "D-1" }];
|
||||
expect(grinda(oppen, GENERISK).map((h) => h.id)).toContain("eskalering");
|
||||
const besvarad = [...oppen, { typ: "eskalering", status: "besvarad", beskrivning: "Factory answer", referens: "D-1" }];
|
||||
expect(grinda(besvarad, GENERISK)).toEqual([]);
|
||||
});
|
||||
|
||||
it("svaret matchas per referens — ett svar på fel förfrågan släpper inte", () => {
|
||||
const logg = [
|
||||
...komplettLogg(),
|
||||
{ typ: "eskalering", status: "oppnad", beskrivning: "Battery query", referens: "D-1" },
|
||||
{ typ: "eskalering", status: "oppnad", beskrivning: "HV query", referens: "D-2" },
|
||||
{ typ: "eskalering", status: "besvarad", beskrivning: "Answer", referens: "D-1" },
|
||||
];
|
||||
const hinder = grinda(logg, GENERISK).filter((h) => h.id === "eskalering");
|
||||
expect(hinder).toHaveLength(1);
|
||||
expect(hinder[0].detalj).toContain("D-2");
|
||||
});
|
||||
|
||||
it("betalarkravet uppfylls av en registrerad betalare med spår och namn", () => {
|
||||
const paket = { arendetypRegler: { Warranty: [{ id: "w_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the payer." }] } };
|
||||
const utan = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }];
|
||||
expect(grindaArendetyp(utan, paket).map((h) => h.id)).toEqual(["arendetyp_betalare"]);
|
||||
const med = [...utan, { typ: "betalare", spar: "fabriksgaranti", namn: "Volkswagen AG" }];
|
||||
expect(grindaArendetyp(med, paket)).toEqual([]);
|
||||
});
|
||||
|
||||
it("claim-referensen kan komma ur betalaren, inte bara ur skannad arbetsorder", () => {
|
||||
const paket = { arendetyper: { Warranty: { krav: ["claim"] } } };
|
||||
const utan = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }];
|
||||
expect(grindaArendetyp(utan, paket).map((h) => h.id)).toEqual(["arendetyp_claim"]);
|
||||
const med = [...utan, { typ: "betalare", spar: "fabriksgaranti", namn: "VW", referens: "CL-4711" }];
|
||||
expect(grindaArendetyp(med, paket)).toEqual([]);
|
||||
});
|
||||
|
||||
it("goodwill kräver godkännande — en betalare utan klartecken räcker inte", () => {
|
||||
const paket = { arendetypRegler: { Goodwill: [{ id: "g", rubrik: "Approval", krav: "godkannande", detaljVidBrist: "Approval required." }] } };
|
||||
const utanKlartecken = [
|
||||
{ typ: "arendetyp_satt", arendetyp: "Goodwill" },
|
||||
{ typ: "betalare", spar: "goodwill", namn: "Importören" },
|
||||
];
|
||||
expect(grindaArendetyp(utanKlartecken, paket).map((h) => h.id)).toEqual(["arendetyp_godkannande"]);
|
||||
const med = [...utanKlartecken, { typ: "betalare", spar: "goodwill", namn: "Importören", godkannande: "GW-9" }];
|
||||
expect(grindaArendetyp(med, paket)).toEqual([]);
|
||||
});
|
||||
|
||||
it("den distribuerade paketformen (arendetypRegler) utvärderas på servern", () => {
|
||||
// Regression: servern matade in filens rika form i en funktion som
|
||||
// bara läste den slimmade — varje ärendetypskrav blev en tyst no-op
|
||||
// och spärren fanns bara som råd i klienten. C-2, återuppstånden
|
||||
// genom ett format.
|
||||
const rikt = {
|
||||
arendetypRegler: {
|
||||
Warranty: [{ id: "garanti_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Odometer reading required." }],
|
||||
},
|
||||
};
|
||||
const utan = [{ typ: "arendetyp_satt", arendetyp: "Warranty" }];
|
||||
const hinder = grindaArendetyp(utan, rikt);
|
||||
expect(hinder.map((h) => h.id)).toEqual(["arendetyp_miltal"]);
|
||||
expect(hinder[0].rubrik).toBe("Mileage documented");
|
||||
expect(hinder[0].detalj).toBe("Odometer reading required.");
|
||||
expect(grindaArendetyp([...utan, { typ: "matarstallning", lage: "ingaende", varde: "12000" }], rikt)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,6 +87,27 @@ export interface Bilaga {
|
||||
dataUrl?: string;
|
||||
}
|
||||
|
||||
// FGS-1.0 §3: betalarspåren. Datavärdena är invarianta (svenska tokens i
|
||||
// loggen, precis som "godkant"/"ingaende"); etiketterna är gränssnitt.
|
||||
export type Betalarspar =
|
||||
| "fabriksgaranti"
|
||||
| "vagnskadegaranti"
|
||||
| "forsakring"
|
||||
| "extern_garanti"
|
||||
| "leasing"
|
||||
| "goodwill"
|
||||
| "kund";
|
||||
|
||||
export const BETALARSPAR_LABEL: Record<Betalarspar, string> = {
|
||||
fabriksgaranti: "Factory warranty",
|
||||
vagnskadegaranti: "Body damage warranty",
|
||||
forsakring: "Insurance",
|
||||
extern_garanti: "External warranty",
|
||||
leasing: "Leasing or fleet",
|
||||
goodwill: "Goodwill",
|
||||
kund: "Customer pays",
|
||||
};
|
||||
|
||||
export type Handelse =
|
||||
| { typ: "objekt_identifierat"; objekt: Objekt }
|
||||
| ({ typ: "arbetsorder_skannad"; falt: ArbetsorderFalt[] } & Bilaga)
|
||||
@@ -137,6 +158,19 @@ export type Handelse =
|
||||
// Ärendetypen styr vilka dokumentationskrav ECM ställer (garanti,
|
||||
// försäkring, reklamation …).
|
||||
| { typ: "arendetyp_satt"; arendetyp: string }
|
||||
// FGS-1.0: betalarspåret. Vem som ersätter arbetet avgör beviskraven —
|
||||
// fabrik, vagnskadegaranti, försäkringsbolag, extern garantigivare,
|
||||
// leasing/fleet, goodwill eller kunden själv. Referensen är claim-,
|
||||
// skade- eller policynummer; godkännandet är betalarens klartecken.
|
||||
| { typ: "betalare"; spar: Betalarspar; namn: string; referens?: string; godkannande?: string }
|
||||
// Teknisk eskalering (DISS-mönstret): öppnad förfrågan till tillverkare
|
||||
// eller garantigivare. En öppnad eskalering utan besvarad spärrar
|
||||
// avslutet — svaret ska inväntas och dokumenteras.
|
||||
| { typ: "eskalering"; status: "oppnad" | "besvarad"; beskrivning: string; referens?: string; kanal?: string }
|
||||
// Reservdelsdokumentation: artikelnummer och spårbarhet för utbytt del.
|
||||
// `sparad` betyder att den demonterade delen behålls i väntan på
|
||||
// garantibeslut eller revision.
|
||||
| { typ: "reservdel"; artikelnummer: string; beskrivning: string; serienummer?: string; batch?: string; sparad?: boolean }
|
||||
// Pre-diagnostik: fordonshistoriken kontrollerad — eller motiverat
|
||||
// varför inte (kvalitetsvarning).
|
||||
| { typ: "historik_kontrollerad"; kontrollerad: boolean; kommentar?: string }
|
||||
@@ -312,6 +346,14 @@ export function handelseRubrik(post: LoggPost): string {
|
||||
return `Ansvarig tekniker: ${h.ansvarig}`;
|
||||
case "arendetyp_satt":
|
||||
return `Case type: ${h.arendetyp}`;
|
||||
case "betalare":
|
||||
return `Payer: ${BETALARSPAR_LABEL[h.spar]} — ${h.namn}${h.referens ? ` (ref ${h.referens})` : ""}${h.godkannande ? ` — approval ${h.godkannande}` : ""}`;
|
||||
case "eskalering":
|
||||
return h.status === "oppnad"
|
||||
? `Escalation opened${h.kanal ? ` via ${h.kanal}` : ""}${h.referens ? ` (${h.referens})` : ""}: ${h.beskrivning}`
|
||||
: `Escalation answered${h.referens ? ` (${h.referens})` : ""}: ${h.beskrivning}`;
|
||||
case "reservdel":
|
||||
return `Part documented: ${h.artikelnummer} — ${h.beskrivning}${h.serienummer ? `, s/n ${h.serienummer}` : ""}${h.batch ? `, batch ${h.batch}` : ""}${h.sparad ? " — old part retained" : ""}`;
|
||||
case "historik_kontrollerad":
|
||||
return h.kontrollerad
|
||||
? `Fordonshistorik kontrollerad${h.kommentar ? `: ${h.kommentar}` : ""}`
|
||||
|
||||
@@ -22,7 +22,7 @@ import type { Arende, LoggPost } from "./domain";
|
||||
import type { Metodik } from "./metodik";
|
||||
import { arendetypNu } from "../../../services/gemensam/sprak/index.mjs";
|
||||
|
||||
export const ECM_VERSION = "2.0";
|
||||
export const ECM_VERSION = "2.1";
|
||||
|
||||
// ---- 1. Evidence Engine -----------------------------------------------
|
||||
|
||||
@@ -345,6 +345,9 @@ export const ARENDETYPER = [
|
||||
"Insurance",
|
||||
"Complaint",
|
||||
"Used-vehicle warranty",
|
||||
"Body damage warranty",
|
||||
"Extended warranty",
|
||||
"Leasing or fleet",
|
||||
"Internal quality check",
|
||||
"Technical investigation",
|
||||
] as const;
|
||||
@@ -372,7 +375,7 @@ export function arendetyp(arende: Arende): Arendetyp {
|
||||
// Klienten cachar senast hämtade paket och faller tillbaka till det
|
||||
// inbyggda standardpaketet offline/i lokalt läge.
|
||||
|
||||
export type ComplianceKravTyp = "miltal" | "historik" | "claim" | "skadenummer" | "foto";
|
||||
export type ComplianceKravTyp = "miltal" | "historik" | "claim" | "skadenummer" | "foto" | "betalare" | "godkannande";
|
||||
|
||||
export interface ComplianceRegel {
|
||||
id: string;
|
||||
@@ -403,11 +406,21 @@ const KRAV_KONTROLLER: Record<ComplianceKravTyp, (arende: Arende) => boolean> =
|
||||
a.handelser.some((p) => p.handelse.typ === "matarstallning" && !p.handelse.undantag) ||
|
||||
!!objektFalt(a, "miltal"),
|
||||
historik: (a) => a.handelser.some((p) => p.handelse.typ === "historik_kontrollerad" && p.handelse.kontrollerad),
|
||||
claim: (a) => !!objektFalt(a, "claim"),
|
||||
skadenummer: (a) => !!objektFalt(a, "skadenummer"),
|
||||
// Referensen kan komma ur den skannade arbetsordern ELLER ur en
|
||||
// registrerad betalare — alla verkstäder skannar inte (FGS-1.0 §4).
|
||||
claim: (a) => !!objektFalt(a, "claim") || betalarReferens(a),
|
||||
skadenummer: (a) => !!objektFalt(a, "skadenummer") || betalarReferens(a),
|
||||
foto: (a) => a.handelser.some((p) => p.handelse.typ === "foto"),
|
||||
betalare: (a) =>
|
||||
a.handelser.some((p) => p.handelse.typ === "betalare" && p.handelse.namn.trim() !== "" && p.handelse.spar.trim() !== ""),
|
||||
godkannande: (a) =>
|
||||
a.handelser.some((p) => p.handelse.typ === "betalare" && (p.handelse.godkannande ?? "").trim() !== ""),
|
||||
};
|
||||
|
||||
function betalarReferens(a: Arende): boolean {
|
||||
return a.handelser.some((p) => p.handelse.typ === "betalare" && (p.handelse.referens ?? "").trim() !== "");
|
||||
}
|
||||
|
||||
// Inbyggt standardpaket = fallback när servern inte nåtts ännu.
|
||||
export const STANDARD_REGELPAKET: RegelPaket = {
|
||||
version: ECM_VERSION,
|
||||
@@ -415,15 +428,18 @@ export const STANDARD_REGELPAKET: RegelPaket = {
|
||||
"Warranty": [
|
||||
{ id: "garanti_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Warranty cases require a documented odometer reading." },
|
||||
{ id: "garanti_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Warranty cases require a checked service history." },
|
||||
{ id: "garanti_claim", rubrik: "Claim or warranty number recorded", krav: "claim", detaljVidBrist: "State the claim or warranty number (read from the work order)." },
|
||||
{ id: "garanti_claim", rubrik: "Claim or warranty number recorded", krav: "claim", detaljVidBrist: "State the claim or warranty number — from the scanned work order or the recorded payer." },
|
||||
{ id: "garanti_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the payer — track, name and reference — before closing." },
|
||||
],
|
||||
Goodwill: [
|
||||
{ id: "goodwill_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Goodwill cases require a documented odometer reading." },
|
||||
{ id: "goodwill_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Goodwill cases require a checked service history." },
|
||||
{ id: "goodwill_godkannande", rubrik: "Grantor's approval recorded", krav: "godkannande", detaljVidBrist: "Goodwill requires the grantor's documented approval before closing." },
|
||||
],
|
||||
"Insurance": [
|
||||
{ id: "forsakring_skadenummer", rubrik: "Claim number recorded", krav: "skadenummer", detaljVidBrist: "Insurance cases require a claim number (read from the work order)." },
|
||||
{ id: "forsakring_bildbevis", rubrik: "Photographic evidence present", krav: "foto", detaljVidBrist: "Insurance cases require photographic documentation." },
|
||||
{ id: "forsakring_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the insurer — name and claim reference — before closing." },
|
||||
],
|
||||
"Complaint": [
|
||||
{ id: "reklamation_historik", rubrik: "History and previous attempts checked", krav: "historik", detaljVidBrist: "Complaint cases require a checked history (previous repairs and attempts)." },
|
||||
@@ -431,6 +447,21 @@ export const STANDARD_REGELPAKET: RegelPaket = {
|
||||
"Used-vehicle warranty": [
|
||||
{ id: "begagnat_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Used-vehicle warranty cases require a documented odometer reading." },
|
||||
],
|
||||
"Body damage warranty": [
|
||||
{ id: "vagnskada_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the warranty administrator — name and reference — before closing." },
|
||||
{ id: "vagnskada_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Body damage warranty cases require a documented odometer reading." },
|
||||
{ id: "vagnskada_bildbevis", rubrik: "Photographic evidence present", krav: "foto", detaljVidBrist: "Body damage warranty cases require photographs of the damage." },
|
||||
],
|
||||
"Extended warranty": [
|
||||
{ id: "forlangd_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the warranty provider — name and reference — before closing." },
|
||||
{ id: "forlangd_claim", rubrik: "Claim authorization recorded", krav: "claim", detaljVidBrist: "External warranties require a claim or authorization number before repair is reimbursed." },
|
||||
{ id: "forlangd_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Extended warranty cases require a documented odometer reading." },
|
||||
{ id: "forlangd_historik", rubrik: "Service history checked", krav: "historik", detaljVidBrist: "Extended warranty terms often condition cover on service history — check and document it." },
|
||||
],
|
||||
"Leasing or fleet": [
|
||||
{ id: "leasing_betalare", rubrik: "Payer established", krav: "betalare", detaljVidBrist: "Record the leasing or fleet company — name and reference — before closing." },
|
||||
{ id: "leasing_miltal", rubrik: "Mileage documented", krav: "miltal", detaljVidBrist: "Leasing and fleet cases require a documented odometer reading." },
|
||||
],
|
||||
},
|
||||
undantagsorsaker: UNDANTAGSORSAKER,
|
||||
orsakskategorier: ORSAKSKATEGORIER,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import type { Arende, Handelse, TidKategori, Tillforlitlighet } from "@/felsokning/domain";
|
||||
import { KUNDBESLUT_LABEL, KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLITLIGHET_LABEL, handelseRubrik } from "@/felsokning/domain";
|
||||
import type { Arende, Betalarspar, Handelse, TidKategori, Tillforlitlighet } from "@/felsokning/domain";
|
||||
import { BETALARSPAR_LABEL, KUNDBESLUT_LABEL, KVALITETSKONTROLL_LABEL, TIDKATEGORI_LABEL, TILLFORLITLIGHET_LABEL, handelseRubrik } from "@/felsokning/domain";
|
||||
import type { Metodik, NastaSteg } from "@/felsokning/metodik";
|
||||
import { nastaSteg } from "@/felsokning/metodik";
|
||||
import { fasFor, klaraFaser } from "../../../../services/gemensam/faser.mjs";
|
||||
@@ -1444,6 +1444,19 @@ function GuideFlik({
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{/* FGS-1.0: betalaren avgör beviskraven — grinden kräver spår, namn
|
||||
och referens för de ärendetyper där någon annan än kunden
|
||||
betalar. Panelen visas när ärendetypen kräver den eller när en
|
||||
betalare redan registrerats. */}
|
||||
{(BETALARTYPER.includes(arendetyp(arende)) ||
|
||||
arende.handelser.some((p) => p.handelse.typ === "betalare")) && (
|
||||
<BetalarePanel arende={arende} skicka={skicka} />
|
||||
)}
|
||||
|
||||
{/* Teknisk eskalering (DISS-mönstret): en öppnad förfrågan utan
|
||||
dokumenterat svar spärrar avslutet. */}
|
||||
<EskaleringPanel arende={arende} skicka={skicka} />
|
||||
|
||||
{!kanAvslutas && <Avslutshinder hinder={hinder} />}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" onClick={() => setVisaOverlamning(true)}>
|
||||
@@ -1461,6 +1474,125 @@ function GuideFlik({
|
||||
);
|
||||
}
|
||||
|
||||
// Ärendetyper där någon annan än kunden ersätter arbetet — betalaren ska
|
||||
// vara fastställd innan grinden släpper avslutet (FGS-1.0 §1/§18).
|
||||
const BETALARTYPER: readonly string[] = [
|
||||
"Warranty",
|
||||
"Goodwill",
|
||||
"Insurance",
|
||||
"Used-vehicle warranty",
|
||||
"Body damage warranty",
|
||||
"Extended warranty",
|
||||
"Leasing or fleet",
|
||||
];
|
||||
|
||||
function BetalarePanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
|
||||
const [spar, setSpar] = useState<Betalarspar>("fabriksgaranti");
|
||||
const [namn, setNamn] = useState("");
|
||||
const [referens, setReferens] = useState("");
|
||||
const [godkannande, setGodkannande] = useState("");
|
||||
const senaste = [...arende.handelser].reverse().find((p) => p.handelse.typ === "betalare")?.handelse;
|
||||
return (
|
||||
<Panel rubrik="Payer and claim — who reimburses the work">
|
||||
{senaste?.typ === "betalare" && (
|
||||
<p className="mb-2 text-[13px]">
|
||||
<span className="font-semibold text-[#4D5662]">Recorded:</span> {BETALARSPAR_LABEL[senaste.spar]} — {senaste.namn}
|
||||
{senaste.referens ? ` (ref ${senaste.referens})` : ""}
|
||||
{senaste.godkannande ? ` — approval ${senaste.godkannande}` : ""}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-2 grid gap-2 sm:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.08em] text-[#4D5662]">Track</span>
|
||||
<select
|
||||
value={spar}
|
||||
onChange={(e) => setSpar(e.target.value as Betalarspar)}
|
||||
className="w-full border border-[#4D5662] bg-white px-2 py-2 text-[13px] focus:outline-none"
|
||||
>
|
||||
{(Object.keys(BETALARSPAR_LABEL) as Betalarspar[]).map((k) => (
|
||||
<option key={k} value={k}>
|
||||
{BETALARSPAR_LABEL[k]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<TextFalt label="Payer name" varde={namn} satt={setNamn} platshallare="e.g. the manufacturer, insurer or warranty provider" />
|
||||
<TextFalt label="Claim / policy / damage number" varde={referens} satt={setReferens} platshallare="Claim, policy or damage reference" />
|
||||
<TextFalt label="Approval reference" varde={godkannande} satt={setGodkannande} platshallare="Authorization — goodwill and external warranties" />
|
||||
</div>
|
||||
<StorKnapp
|
||||
variant="sekundar"
|
||||
disabled={!namn.trim()}
|
||||
onClick={() => {
|
||||
skicka({
|
||||
typ: "betalare",
|
||||
spar,
|
||||
namn: namn.trim(),
|
||||
...(referens.trim() ? { referens: referens.trim() } : {}),
|
||||
...(godkannande.trim() ? { godkannande: godkannande.trim() } : {}),
|
||||
});
|
||||
setNamn("");
|
||||
setReferens("");
|
||||
setGodkannande("");
|
||||
}}
|
||||
>
|
||||
Record payer
|
||||
</StorKnapp>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function EskaleringPanel({ arende, skicka }: { arende: Arende; skicka: (h: Handelse) => void }) {
|
||||
const [text, setText] = useState("");
|
||||
const [kanal, setKanal] = useState("");
|
||||
const [referens, setReferens] = useState("");
|
||||
// Samma matchning som grinden: öppnade per referens, besvarade tar bort.
|
||||
const oppna = new Map<string, string>();
|
||||
for (const post of arende.handelser) {
|
||||
const h = post.handelse;
|
||||
if (h.typ !== "eskalering") continue;
|
||||
const nyckel = (h.referens ?? "").trim();
|
||||
if (h.status === "oppnad") oppna.set(nyckel, h.beskrivning);
|
||||
else if (oppna.has(nyckel)) oppna.delete(nyckel);
|
||||
else if (nyckel === "" && oppna.size === 1) oppna.clear();
|
||||
}
|
||||
const skickaMed = (status: "oppnad" | "besvarad") => {
|
||||
skicka({
|
||||
typ: "eskalering",
|
||||
status,
|
||||
beskrivning: text.trim(),
|
||||
...(referens.trim() ? { referens: referens.trim() } : {}),
|
||||
...(kanal.trim() ? { kanal: kanal.trim() } : {}),
|
||||
});
|
||||
setText("");
|
||||
setKanal("");
|
||||
setReferens("");
|
||||
};
|
||||
return (
|
||||
<Panel rubrik="Technical escalation — manufacturer or warranty provider">
|
||||
{oppna.size > 0 && (
|
||||
<p className="mb-2 border-l-2 border-[#8A5A00] pl-4 text-[13px]">
|
||||
<span className="font-semibold text-[#8A5A00]">Open, awaiting answer:</span>{" "}
|
||||
{[...oppna.entries()].map(([r, b]) => (r ? `${r} — ${b}` : b)).join(" · ")}
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-2 grid gap-2 sm:grid-cols-3">
|
||||
<TextFalt label="Description" varde={text} satt={setText} platshallare="What was asked, or what the answer says" />
|
||||
<TextFalt label="Channel" varde={kanal} satt={setKanal} platshallare="e.g. DISS, technical support" />
|
||||
<TextFalt label="Reference" varde={referens} satt={setReferens} platshallare="Ticket or query number" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<StorKnapp variant="sekundar" disabled={!text.trim()} onClick={() => skickaMed("oppnad")}>
|
||||
Open escalation
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" disabled={!text.trim() || oppna.size === 0} onClick={() => skickaMed("besvarad")}>
|
||||
Document the answer
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Varför avslut är spärrat — i klartext, på skärmen, medan arbetet pågår.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user