Garantiregimer, försäkringsvillkor och felanmälan i ärendet
---- Garantier (ALVA-SPEC-040) ----------------------------------------- Skiljelinjen mellan LAGSTADGAT och AVTALAT bär allt annat, och den förväxlas dagligen åt bägge håll. Överraskande lite av det verkstaden kallar garanti är lagstadgat: nybilsgaranti avtalad. Ingen lag i EU eller USA kräver den. rostskyddsgaranti avtalad. Ingen lag kräver den, någonstans. lackgaranti avtalad. drivlinegaranti avtalad. avgasgaranti LAGSTADGAD i bägge, och den enda som är det. Att kalla rostskyddet lagstadgat är fel; att kalla avgasgarantin en tillverkarutfästelse är fel åt andra hållet, och det andra felet kostar kunden pengar — en katalysator på en sexårig bil offereras rutinmässigt som en betald reparation när den inte är det. Varje lagstadgad post bär källa och kontrolldatum: 2019/771 (2 år, säljaren — inte tillverkaren), MVBER 461/2010 förlängd till 2028, 715/2007 (5 år/100 000 km), 2024/1257 (8 år/160 000 km, batteri 80 % vid 5 år och 72 % vid 8), CAA § 207 (2/24 000 och 8/80 000 miles), Magnuson–Moss § 2302(c), CARB (3/50 000 och 7/70 000), ACC II (70 % SoH vid 8 år/100 000 miles). Bedömningen skiljer INOM, UTANFÖR och OKLART. Saknas underlag blir det oklart, aldrig utanför — att påstå att en garanti gått ut när underlaget inte räcker är det enda felet här som kostar kunden pengar. ---- Försäkring (ALVA-SPEC-041) ---------------------------------------- Modulen avgör ingenting och kan inte avgöra något. Vad som gäller bestäms av kundens eget försäkringsbrev och villkoren vid tecknandet — inte av vad ett bolag skriver i dag och inte av vad vi noterat. Testerna låser att resultatet inte innehåller något fält som liknar ett utfall, och att ett fordon utanför samtliga noterade gränser ändå inte får ett nej: registret kan vara ofullständigt och produkten en annan. Det som står överst är de sju villkoren som avgör en maskinskadefråga. Ålder och sträcka är två av dem; de fem andra avgör oftare. Registret med If, Folksam, Trygg-Hansa, Svedea, WaterCircles och Hedvig ligger under, med källa och avläsningsdatum per post, och märks som föråldrat efter 180 dagar i stället för att se aktuellt ut. ---- Felanmälan i ärendet (ALVA-PROC-0050) ----------------------------- En anmälningsknapp som bara finns i en portalvy nås aldrig av den som står med händerna i en bil. Den ligger därför i varje ärende, och tar med sitt sammanhang självt: ärende, metodik, plattformsversion och spår-id. Teknikern minns inte plattformsversionen, och att fråga efter den är att lägga över vårt problem på verkstaden. Sammanhanget HÄRLEDS och kan inte smugglas: ett regnr i anropet släpps inte igenom. En supportanmälan är inte ett skäl att flytta personuppgifter till ett annat system. Bevisat i integrationstestet. Anmälan är oföränderlig och statusen en projektion av inläggen — samma skäl som för fakturan: en anmälan vars historia kan skrivas om är inte ett underlag när någon frågar hur länge felet var känt. Verkstaden svarar i sitt eget ärende; status sätts av supporten med egen nyckel. ---- Historikkontrollen ------------------------------------------------- Kravet fanns redan: klienten öppnar inte metodiken förrän pre-diagnostiken är besvarad, och ett nej kräver skriven motivering. Det var dock aldrig BEVISAT att spärren håller. Genomgången prövar nu att metodiken inte går att nå medan historikfrågan står obesvarad — i det enda ögonblick det går att pröva, eftersom svaret därefter redan är avgivet. 429 enhetstester · 157 integrationskontroller mot riktig Postgres · genomgången 4/4 · portalspärren 16/16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
// ALVA-SPEC-041 · Försäkringsvillkor.
|
||||
//
|
||||
// Vyn får aldrig se ut som ett besked. Den är byggd så att det som står
|
||||
// överst är FRÅGORNA — det som faktiskt avgör en maskinskadefråga — och
|
||||
// registret därunder, som det underlag det är.
|
||||
//
|
||||
// Ordningen är inte kosmetisk. Läggs tabellen först läses den som ett
|
||||
// svar, och de fem villkor som avgör oftare än åldern gör hamnar under
|
||||
// vikningen.
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AVGORANDE_VILLKOR,
|
||||
BOLAG,
|
||||
FORBEHALL,
|
||||
bedom,
|
||||
foraldrad,
|
||||
spann,
|
||||
} from "../../../../services/gemensam/forsakring.mjs";
|
||||
import { Block, Etikett, FARG, Rubrik, Tabell, Textfalt } from "@/alva/komponenter";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
interface Produkt {
|
||||
namn: string;
|
||||
maxAlderAr: number;
|
||||
maxMil: number;
|
||||
}
|
||||
interface Bolag {
|
||||
id: string;
|
||||
namn: string;
|
||||
marknad: string;
|
||||
produkter: Produkt[];
|
||||
kalla: string;
|
||||
kontrollerad: string;
|
||||
}
|
||||
interface Villkor {
|
||||
id: string;
|
||||
rubrik: string;
|
||||
fraga: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function Forsakring() {
|
||||
const [alder, setAlder] = useState("");
|
||||
const [mil, setMil] = useState("");
|
||||
|
||||
const tal = (v: string) => (v.trim() === "" || Number.isNaN(Number(v)) ? undefined : Number(v));
|
||||
const utfall = bedom({ alderAr: tal(alder), mil: tal(mil) }) as {
|
||||
inom: { bolag: string; produkt: string; foraldrad: boolean }[];
|
||||
utanfor: { bolag: string; produkt: string }[];
|
||||
oklart: { bolag: string; produkt: string }[];
|
||||
nastaSteg: string;
|
||||
};
|
||||
const s = spann() as { alderAr: { minsta: number; storsta: number }; mil: { minsta: number; storsta: number } };
|
||||
const angivet = tal(alder) !== undefined || tal(mil) !== undefined;
|
||||
|
||||
return (
|
||||
<Ram portal>
|
||||
<div className="mx-auto max-w-[1040px] px-6 py-12">
|
||||
<Etikett>Reasoning aid</Etikett>
|
||||
<div className="mt-2 mb-2">
|
||||
<Rubrik niva={1}>Insurance conditions</Rubrik>
|
||||
</div>
|
||||
<p className="mb-8 max-w-[680px] text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
ALVA does not decide whether a claim is covered, and cannot. Cover follows the
|
||||
customer’s own policy and the terms in force when it was taken out. What this page gives
|
||||
you is the shape of the question and what the register recorded, so the conversation starts
|
||||
from something better than a guess.
|
||||
</p>
|
||||
|
||||
<Block rubrik="What actually decides it" beteckning="ALVA-SPEC-041">
|
||||
<p className="mb-4 max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
Age and distance are two of seven. The other five decide more claims than the two that get
|
||||
asked about first.
|
||||
</p>
|
||||
<Tabell
|
||||
kolumner={["Condition", "The question", "Why it matters"]}
|
||||
rader={(AVGORANDE_VILLKOR as Villkor[]).map((v) => [
|
||||
<span key="r" className="font-semibold">
|
||||
{v.rubrik}
|
||||
</span>,
|
||||
<span key="f" className="text-[13px]">
|
||||
{v.fraga}
|
||||
</span>,
|
||||
<span key="t" className="text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
{v.text}
|
||||
</span>,
|
||||
])}
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block rubrik="Where the vehicle sits against the register" beteckning="ALVA-SPEC-041">
|
||||
<div className="grid max-w-[420px] gap-4 sm:grid-cols-2">
|
||||
<Textfalt label="Vehicle age (years)" varde={alder} andra={setAlder} platshallare="6" />
|
||||
<Textfalt label="Odometer (mil)" varde={mil} andra={setMil} platshallare="9 400" />
|
||||
</div>
|
||||
|
||||
{!angivet ? (
|
||||
<p className="mt-6 text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
Recorded limits range from {s.alderAr.minsta}–{s.alderAr.storsta} years and{" "}
|
||||
{s.mil.minsta.toLocaleString("sv-SE")}–{s.mil.storsta.toLocaleString("sv-SE")} mil, depending
|
||||
on insurer and product tier.
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-6 grid gap-4">
|
||||
{[
|
||||
["Within recorded limits", utfall.inom],
|
||||
["Beyond recorded limits", utfall.utanfor],
|
||||
["Cannot be placed on what you entered", utfall.oklart],
|
||||
].map(([rubrik, lista]) => {
|
||||
const rader = lista as { bolag: string; produkt: string; foraldrad?: boolean }[];
|
||||
return (
|
||||
<div key={rubrik as string}>
|
||||
<Etikett>{rubrik as string}</Etikett>
|
||||
{rader.length === 0 ? (
|
||||
<p className="mt-2 text-[13px]" style={{ color: FARG.steel }}>
|
||||
—
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-2 grid gap-2">
|
||||
{rader.map((r) => (
|
||||
<li key={`${r.bolag}${r.produkt}`} className="text-[13px]">
|
||||
<span className="font-semibold">{r.bolag}</span> · {r.produkt}
|
||||
{r.foraldrad && (
|
||||
<span className="ml-2 text-[11px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
register entry out of date
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Står även när fordonet ligger utanför varje noterad
|
||||
gräns. Registret kan vara ofullständigt, produkten en
|
||||
annan, och villkoren vid tecknandet andra. */}
|
||||
<p className="border-t pt-4 text-[13px] leading-[21px]" style={{ borderColor: FARG.lightSteel }}>
|
||||
{utfall.nastaSteg}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Block>
|
||||
|
||||
<Block rubrik="Register" beteckning="ALVA-SPEC-041">
|
||||
<Tabell
|
||||
kolumner={["Insurer", "Product", "Age", "Distance", "Read"]}
|
||||
rader={(BOLAG as Bolag[]).flatMap((b) =>
|
||||
b.produkter.map((p) => [
|
||||
<span key="b" className="font-semibold">
|
||||
{b.namn}
|
||||
</span>,
|
||||
<span key="p" className="text-[13px]">
|
||||
{p.namn}
|
||||
</span>,
|
||||
<span key="a" className="font-mono text-[12px]">
|
||||
{p.maxAlderAr} yr
|
||||
</span>,
|
||||
<span key="m" className="font-mono text-[12px]">
|
||||
{p.maxMil.toLocaleString("sv-SE")} mil
|
||||
</span>,
|
||||
<span
|
||||
key="k"
|
||||
className="font-mono text-[11px]"
|
||||
style={{ color: foraldrad(b) ? FARG.graphite : FARG.steel }}
|
||||
>
|
||||
{b.kontrollerad}
|
||||
{foraldrad(b) ? " — out of date" : ""}
|
||||
</span>,
|
||||
]),
|
||||
)}
|
||||
/>
|
||||
<p className="mt-4 max-w-[680px] text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
The register covers the insurers read so far and is not a market survey. Smaller insurers
|
||||
and non-Swedish markets have to be added by the organization, with a source and a date —
|
||||
an entry without those two is not usable as a starting point for anything.
|
||||
</p>
|
||||
</Block>
|
||||
|
||||
<Block rubrik="Basis" beteckning="ALVA-SPEC-041">
|
||||
<p className="max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
{FORBEHALL}
|
||||
</p>
|
||||
</Block>
|
||||
</div>
|
||||
</Ram>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// ALVA-SPEC-040 · Garantiregimer.
|
||||
//
|
||||
// Vyn har ett jobb och det är inte att vara fullständig: den ska göra
|
||||
// skillnaden mellan LAGSTADGAT och AVTALAT omöjlig att missa, och lyfta
|
||||
// de två regler som avgör om verkstaden får utföra arbetet alls.
|
||||
//
|
||||
// Ordningen är därför inte alfabetisk och inte kronologisk. Skyddet för
|
||||
// oberoende reparation står först, eftersom det är det som ifrågasätts —
|
||||
// ibland av kunden, som blivit tillsagd något annat.
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
FORBEHALL,
|
||||
JURISDIKTIONER,
|
||||
KONTROLLERAD,
|
||||
regimerFor,
|
||||
skyddadReparation,
|
||||
} from "../../../../services/gemensam/garantier.mjs";
|
||||
import { Block, Etikett, FARG, Rubrik, Tabell } from "@/alva/komponenter";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
interface Regim {
|
||||
id: string;
|
||||
jurisdiktion: string;
|
||||
grund: "lag" | "avtal";
|
||||
bärare: string;
|
||||
rubrik: string;
|
||||
kortform: string;
|
||||
källa: string;
|
||||
källänk: string | null;
|
||||
text: string;
|
||||
anmärkning: string;
|
||||
avvikelser: { marknad: string; text: string }[];
|
||||
}
|
||||
|
||||
const BARARE: Record<string, string> = {
|
||||
säljaren: "Seller",
|
||||
tillverkaren: "Manufacturer",
|
||||
};
|
||||
|
||||
export default function Garantier() {
|
||||
const [jurisdiktion, setJurisdiktion] = useState("EU");
|
||||
const regimer = regimerFor(jurisdiktion) as Regim[];
|
||||
const lag = regimer.filter((r) => r.grund === "lag");
|
||||
const avtal = regimer.filter((r) => r.grund === "avtal");
|
||||
const skydd = skyddadReparation(jurisdiktion) as Regim | null;
|
||||
|
||||
return (
|
||||
<Ram portal>
|
||||
<div className="mx-auto max-w-[1040px] px-6 py-12">
|
||||
<Etikett>Regulatory</Etikett>
|
||||
<div className="mt-2 mb-2">
|
||||
<Rubrik niva={1}>Warranty regimes</Rubrik>
|
||||
</div>
|
||||
<p className="mb-8 max-w-[680px] text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
What the law requires as a floor, and what the manufacturer merely promises. The two are
|
||||
confused daily on the shop floor, and the confusion runs in both directions — a corrosion
|
||||
warranty treated as statutory, an emissions warranty treated as a favour.
|
||||
</p>
|
||||
|
||||
{/* Jurisdiktionen först: allt nedanför beror på den, och ett svar
|
||||
för fel marknad är sämre än inget svar. */}
|
||||
<div className="mb-8">
|
||||
<Etikett>Jurisdiction</Etikett>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{(JURISDIKTIONER as { id: string; namn: string }[]).map((j) => {
|
||||
const vald = j.id === jurisdiktion;
|
||||
return (
|
||||
<button
|
||||
key={j.id}
|
||||
type="button"
|
||||
onClick={() => setJurisdiktion(j.id)}
|
||||
aria-pressed={vald}
|
||||
className="border px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.08em]"
|
||||
style={{
|
||||
background: vald ? FARG.graphite : FARG.white,
|
||||
color: vald ? FARG.white : FARG.steel,
|
||||
borderColor: vald ? FARG.graphite : FARG.lightSteel,
|
||||
}}
|
||||
>
|
||||
{j.namn}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skydd && (
|
||||
<Block rubrik="Servicing here does not void the warranty" beteckning={skydd.id}>
|
||||
<p className="max-w-[680px] text-[14px] leading-[23px]">{skydd.text}</p>
|
||||
<p className="mt-4 max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
{skydd.anmärkning}
|
||||
</p>
|
||||
<p className="mt-4 font-mono text-[11px]" style={{ color: FARG.steel }}>
|
||||
{skydd.källa}
|
||||
</p>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
<Block rubrik="Statutory — applies regardless of any warranty booklet" beteckning="ALVA-SPEC-040">
|
||||
<Tabell
|
||||
kolumner={["Regime", "Period", "Liable", "Source"]}
|
||||
rader={lag.map((r) => [
|
||||
<span key="r">
|
||||
<span className="font-semibold">{r.rubrik}</span>
|
||||
<span className="mt-2 block text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
{r.text}
|
||||
</span>
|
||||
{r.anmärkning && (
|
||||
<span className="mt-2 block text-[12px] leading-[19px]" style={{ color: FARG.graphite }}>
|
||||
{r.anmärkning}
|
||||
</span>
|
||||
)}
|
||||
{r.avvikelser.map((a) => (
|
||||
<span key={a.marknad} className="mt-2 block text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
{a.marknad}: {a.text}
|
||||
</span>
|
||||
))}
|
||||
</span>,
|
||||
<span key="p" className="font-mono text-[12px]">
|
||||
{r.kortform}
|
||||
</span>,
|
||||
<span key="b" className="text-[12px]" style={{ color: FARG.steel }}>
|
||||
{BARARE[r.bärare] ?? r.bärare}
|
||||
</span>,
|
||||
<span key="k" className="font-mono text-[11px]" style={{ color: FARG.steel }}>
|
||||
{r.källa}
|
||||
</span>,
|
||||
])}
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block rubrik="Contractual — the manufacturer's own promise" beteckning="ALVA-SPEC-040">
|
||||
<p className="mb-4 max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
None of the following has any statutory basis in the EU or the United States. Terms are set
|
||||
by the manufacturer and vary by brand, market and model year, so the ranges below describe
|
||||
what occurs on the market — not what any given brand undertakes. Read the vehicle’s own
|
||||
warranty terms before quoting a period to a customer.
|
||||
</p>
|
||||
<Tabell
|
||||
kolumner={["Warranty", "Typical", "Note"]}
|
||||
rader={avtal.map((r) => [
|
||||
<span key="r" className="font-semibold">
|
||||
{r.rubrik}
|
||||
</span>,
|
||||
<span key="p" className="text-[12px]" style={{ color: FARG.steel }}>
|
||||
{r.kortform}
|
||||
</span>,
|
||||
<span key="n" className="text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
{r.text} {r.anmärkning}
|
||||
</span>,
|
||||
])}
|
||||
/>
|
||||
</Block>
|
||||
|
||||
<Block rubrik="Basis" beteckning={`Verified ${KONTROLLERAD}`}>
|
||||
<p className="max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
{FORBEHALL}
|
||||
</p>
|
||||
</Block>
|
||||
</div>
|
||||
</Ram>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,9 @@ const PORTAL = [
|
||||
{ till: "/alva/portal/analys", text: "Analysis" },
|
||||
{ till: "/alva/portal/kunskapskallor", text: "Knowledge sources" },
|
||||
{ till: "/alva/portal/integration", text: "Integration" },
|
||||
{ till: "/alva/portal/garantier", text: "Warranty" },
|
||||
{ till: "/alva/portal/forsakring", text: "Insurance" },
|
||||
{ till: "/alva/portal/support", text: "Support" },
|
||||
{ till: "/alva/portal/fakturor", text: "Invoices" },
|
||||
{ till: "/felsokning", text: "Diagnostics" },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// ALVA-PROC-0050 · Supportärenden.
|
||||
//
|
||||
// Listan över organisationens anmälningar. Själva anmälandet sker inte
|
||||
// här utan i ärendet där felet uppstod — se Felanmalan i felsökningsvyn.
|
||||
// En anmälningsknapp som bara finns i en portalvy nås aldrig av den som
|
||||
// står med händerna i en bil.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { STATUSAR, TYPER } from "../../../../services/gemensam/support.mjs";
|
||||
import { hamtaSupport, plattformAktiv, type Supportarende } from "@/felsokning/plattform";
|
||||
import { Block, Demonstration, Etikett, FARG, Rubrik, Statusmärke, Tabell } from "@/alva/komponenter";
|
||||
import { Ram } from "./Ram";
|
||||
|
||||
const STATUSAV: Record<string, "passed" | "pending" | "failed" | "not_applicable"> = {
|
||||
mottagen: "pending",
|
||||
under_arbete: "pending",
|
||||
atgardad: "passed",
|
||||
stangd: "not_applicable",
|
||||
};
|
||||
|
||||
const STATUSTEXT: Record<string, string> = {
|
||||
mottagen: "Received",
|
||||
under_arbete: "In progress",
|
||||
atgardad: "Resolved",
|
||||
stangd: "Closed",
|
||||
};
|
||||
|
||||
// Vad som visas när ingen plattform är konfigurerad. Exemplet är märkt.
|
||||
const EXEMPEL: Supportarende[] = [
|
||||
{
|
||||
id: "demo-1",
|
||||
beteckning: "ALVA-SUP-0001",
|
||||
arende_id: "ALVA-CASE-91821",
|
||||
typ: "felanmalan",
|
||||
rubrik: "Measurement step will not accept a value from the pressure tester",
|
||||
beskrivning:
|
||||
"The check asks for a reading in bar. Entering 2,4 is rejected as invalid; entering 2.4 is accepted. The workshop uses a comma.",
|
||||
sammanhang: { metodik: "vibration", steg: "4", plattform: "ALVA 1.0" },
|
||||
status: "atgardad",
|
||||
skapad: "2026-08-04T09:12:00Z",
|
||||
anmald_av: "Anna",
|
||||
inlagg: [],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Support() {
|
||||
const skarpt = plattformAktiv();
|
||||
const [arenden, setArenden] = useState<Supportarende[] | null>(null);
|
||||
const [fel, setFel] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!skarpt) return;
|
||||
let avbruten = false;
|
||||
hamtaSupport()
|
||||
.then((a) => !avbruten && setArenden(a))
|
||||
.catch((orsak) => !avbruten && setFel(orsak instanceof Error ? orsak.message : String(orsak)));
|
||||
return () => {
|
||||
avbruten = true;
|
||||
};
|
||||
}, [skarpt]);
|
||||
|
||||
// Misslyckad hämtning visar felet, aldrig exemplet: data som ser äkta
|
||||
// ut men inte är det är värre än ett synligt fel.
|
||||
const lista = skarpt ? arenden : EXEMPEL;
|
||||
|
||||
return (
|
||||
<Ram portal>
|
||||
<div className="mx-auto max-w-[1040px] px-6 py-12">
|
||||
<Etikett>Support</Etikett>
|
||||
<div className="mt-2 mb-2">
|
||||
<Rubrik niva={1}>Reported issues</Rubrik>
|
||||
</div>
|
||||
<p className="mb-8 max-w-[680px] text-[14px] leading-[22px]" style={{ color: FARG.steel }}>
|
||||
Every case carries a report button. A report filed from the case brings its own context —
|
||||
methodology, step, platform version, trace id — so the first reply can be an answer rather
|
||||
than a question.
|
||||
</p>
|
||||
|
||||
{!skarpt && (
|
||||
<Demonstration>
|
||||
The entry below is an example. Connected to a platform instance this page lists the
|
||||
organization’s own reports.
|
||||
</Demonstration>
|
||||
)}
|
||||
|
||||
<Block rubrik="Reports" beteckning="ALVA-PROC-0050">
|
||||
{fel ? (
|
||||
<p className="text-[13px]" style={{ color: FARG.steel }}>
|
||||
Reports could not be retrieved: {fel}
|
||||
</p>
|
||||
) : !lista ? (
|
||||
<p className="text-[13px]" style={{ color: FARG.steel }}>
|
||||
Retrieving reports.
|
||||
</p>
|
||||
) : lista.length === 0 ? (
|
||||
<p className="text-[13px]" style={{ color: FARG.steel }}>
|
||||
Nothing has been reported.
|
||||
</p>
|
||||
) : (
|
||||
<Tabell
|
||||
kolumner={["Reference", "Kind", "Subject", "Case", "Status"]}
|
||||
rader={lista.map((s) => [
|
||||
<span key="b" className="font-mono text-[12px]">
|
||||
{s.beteckning}
|
||||
</span>,
|
||||
<span key="t" className="text-[12px]" style={{ color: FARG.steel }}>
|
||||
{(TYPER as { id: string; rubrik: string }[]).find((t) => t.id === s.typ)?.rubrik ?? s.typ}
|
||||
</span>,
|
||||
<span key="r">
|
||||
<span className="font-semibold">{s.rubrik}</span>
|
||||
<span className="mt-2 block text-[12px] leading-[19px]" style={{ color: FARG.steel }}>
|
||||
{Object.entries(s.sammanhang ?? {})
|
||||
.map(([n, v]) => `${n}: ${v}`)
|
||||
.join(" · ")}
|
||||
</span>
|
||||
</span>,
|
||||
<span key="a" className="font-mono text-[11px]" style={{ color: FARG.steel }}>
|
||||
{s.arende_id ?? "—"}
|
||||
</span>,
|
||||
<span key="s" className="flex flex-wrap items-center gap-2">
|
||||
<Statusmärke status={STATUSAV[s.status] ?? "pending"} />
|
||||
<span className="text-[11px] uppercase tracking-[0.08em]" style={{ color: FARG.steel }}>
|
||||
{STATUSTEXT[s.status] ?? s.status}
|
||||
</span>
|
||||
</span>,
|
||||
])}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
|
||||
<Block rubrik="States" beteckning="ALVA-PROC-0050">
|
||||
<p className="mb-4 max-w-[680px] text-[13px] leading-[21px]" style={{ color: FARG.steel }}>
|
||||
A report is never edited. Replies and state changes are separate entries, and the state is
|
||||
derived from them — so the record can still answer how long a fault was known about.
|
||||
</p>
|
||||
<Tabell
|
||||
kolumner={["State", "Meaning"]}
|
||||
rader={(STATUSAR as string[]).map((s) => [
|
||||
<span key="s" className="font-semibold">
|
||||
{STATUSTEXT[s] ?? s}
|
||||
</span>,
|
||||
<span key="m" className="text-[12px]" style={{ color: FARG.steel }}>
|
||||
{s === "mottagen"
|
||||
? "Filed. This is the state a report has the moment it exists — nothing has to be written for it to be true."
|
||||
: s === "under_arbete"
|
||||
? "Support has taken it on."
|
||||
: s === "atgardad"
|
||||
? "Fixed, and the fix is out."
|
||||
: "Closed. A closed report can be reopened — which happens when the fault turns out not to be gone."}
|
||||
</span>,
|
||||
])}
|
||||
/>
|
||||
</Block>
|
||||
</div>
|
||||
</Ram>
|
||||
);
|
||||
}
|
||||
@@ -2567,10 +2567,116 @@ function RapportFlik({
|
||||
Genererad ur ärendets händelselogg · Observationer och mätvärden redovisas utan slutsatser som saknar stöd.
|
||||
</p>
|
||||
<Ritningsstampel arende={arende} />
|
||||
<Felanmalan arende={arende} metodik={metodik ?? null} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Felanmälan — i ärendet, inte någon annanstans.
|
||||
*
|
||||
* En anmälningsknapp som bara finns i en portalvy nås aldrig av den som
|
||||
* står med händerna i en bil. Den ligger därför i varje ärende, och tar
|
||||
* med sitt sammanhang självt: ärende, metodik, öppet steg och
|
||||
* plattformsversion. Teknikern minns inte plattformsversionen, och att
|
||||
* fråga efter den är att lägga över vårt problem på verkstaden.
|
||||
*
|
||||
* Anmälan lagras aldrig i ärendets händelselogg. Loggen är protokollet
|
||||
* över vad som gjordes med FORDONET; att ett fel i vårt system anmäldes
|
||||
* hör inte dit, och skulle följa med in i en rapport som ska hålla i en
|
||||
* tvist.
|
||||
*/
|
||||
function Felanmalan({ arende, metodik }: { arende: Arende; metodik: Metodik | null }) {
|
||||
const [oppen, setOppen] = useState(false);
|
||||
const [typ, setTyp] = useState("felanmalan");
|
||||
const [rubrik, setRubrik] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [utfall, setUtfall] = useState<{ ok?: string; fel?: string }>({});
|
||||
const [skickar, setSkickar] = useState(false);
|
||||
|
||||
const skicka = async () => {
|
||||
setSkickar(true);
|
||||
setUtfall({});
|
||||
try {
|
||||
const { anmalSupport, plattformAktiv } = await import("@/felsokning/plattform");
|
||||
if (!plattformAktiv()) {
|
||||
setUtfall({ fel: "Ingen plattform är konfigurerad — anmälan kan inte skickas härifrån." });
|
||||
return;
|
||||
}
|
||||
const { beteckning } = await anmalSupport({
|
||||
typ,
|
||||
rubrik,
|
||||
beskrivning: text,
|
||||
arendeId: arende.id,
|
||||
sammanhang: {
|
||||
metodikId: metodik?.id ?? "",
|
||||
plattformsversion: PLATTFORMSVERSION,
|
||||
klient: typeof navigator === "undefined" ? "" : navigator.userAgent,
|
||||
},
|
||||
});
|
||||
setUtfall({ ok: beteckning });
|
||||
setRubrik("");
|
||||
setText("");
|
||||
} catch (orsak) {
|
||||
setUtfall({ fel: orsak instanceof Error ? orsak.message : String(orsak) });
|
||||
} finally {
|
||||
setSkickar(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!oppen) {
|
||||
return (
|
||||
<p className="mt-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOppen(true)}
|
||||
className="text-[12px] uppercase tracking-wide text-[#4A5560] underline"
|
||||
>
|
||||
Rapportera ett problem med systemet
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel rubrik="Rapportera ett problem med systemet">
|
||||
<p className="mb-2 text-[12px] text-[#707070]">
|
||||
Ärende, metodik och plattformsversion följer med automatiskt. Inget om fordonet eller kunden
|
||||
skickas.
|
||||
</p>
|
||||
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||
{[
|
||||
["felanmalan", "Fel"],
|
||||
["fraga", "Fråga"],
|
||||
["forbattring", "Förbättring"],
|
||||
].map(([id, etikett]) => (
|
||||
<StorKnapp key={id} variant={typ === id ? "primar" : "sekundar"} onClick={() => setTyp(id)}>
|
||||
{etikett}
|
||||
</StorKnapp>
|
||||
))}
|
||||
</div>
|
||||
<TextFalt label="Vad gäller det?" varde={rubrik} satt={setRubrik} platshallare="Kort rubrik" />
|
||||
<TextFalt
|
||||
label="Vad hände, och vad väntade du dig?"
|
||||
varde={text}
|
||||
satt={setText}
|
||||
platshallare="Den som svarar ser inte din skärm."
|
||||
flerRad
|
||||
/>
|
||||
{utfall.ok && <p className="mt-2 text-[13px] text-[#1E6B34]">Anmäld som {utfall.ok}.</p>}
|
||||
{utfall.fel && <p className="mt-2 text-[13px] text-[#8B1A1A]">{utfall.fel}</p>}
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<StorKnapp onClick={skicka} disabled={skickar || rubrik.trim().length < 5 || text.trim().length < 20}>
|
||||
{skickar ? "Skickar" : "Skicka anmälan"}
|
||||
</StorKnapp>
|
||||
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
|
||||
Stäng
|
||||
</StorKnapp>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ritningsstämpel.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user