Inställningar: admin väljer objekttyper och identifieringsmetoder
Ny inställningssida (/felsokning/installningar) där systemadmin väljer vad som visas när ett ärende startas. På plattformen sparas valet på organisationen (alla läser via GET /api/organisation, bara admin ändrar via POST /api/organisation/installningar); i lokalt läge gäller valet enheten. Nytt ärende-vyn filtrerar knapparna efter valet. - Klientmodul med normalisering: okända värden filtreras, tomma listor faller tillbaka till standard (går aldrig att låsa ute allt) - installningar-kolumn (jsonb) på organisationer, idempotent migrering - OpenAPI-specen utökad; paritets- och enhetstester - Integrationstest: läsning för alla, 403 för tekniker, org-bred effekt, 400 för tomma listor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
@@ -62,6 +62,11 @@ function PlattformInloggning() {
|
||||
<StorKnapp variant="sekundar">📊 Organisationsöversikt</StorKnapp>
|
||||
</Link>
|
||||
)}
|
||||
{konto.roll === "admin" && (
|
||||
<Link to="/felsokning/installningar" className="mt-2 block">
|
||||
<StorKnapp variant="sekundar">⚙️ Inställningar för organisationen</StorKnapp>
|
||||
</Link>
|
||||
)}
|
||||
</Panel>
|
||||
{konto.roll === "admin" && <AnvandarAdmin />}
|
||||
</>
|
||||
@@ -300,6 +305,12 @@ export default function Arendelista() {
|
||||
|
||||
{plattformAktiv() && <PlattformInloggning />}
|
||||
|
||||
{!plattformKonto() && (
|
||||
<Link to="/felsokning/installningar" className="mb-4 block">
|
||||
<StorKnapp variant="sekundar">⚙️ Inställningar</StorKnapp>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Panel rubrik="AI-stöd">
|
||||
<p className="text-zinc-300">
|
||||
När du är inloggad hjälper AI:n dig medan du arbetar: föreslår nästa steg utifrån det du
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// Inställningar: vilka objekttyper och identifieringsmetoder som visas
|
||||
// när ett ärende startas. På plattformen får bara systemadministratören
|
||||
// ändra (valet gäller hela organisationen); i lokalt läge gäller valet
|
||||
// den här enheten.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ALLA_IDENTIFIERINGSMETODER,
|
||||
ALLA_OBJEKTTYPER,
|
||||
hamtaInstallningar,
|
||||
lastaInstallningar,
|
||||
sparaInstallningar,
|
||||
type Installningar as Inst,
|
||||
} from "@/felsokning/installningar";
|
||||
import { plattformAktiv, plattformKonto } from "@/felsokning/plattform";
|
||||
import { FelsokningSkal, Panel, StorKnapp } from "@/felsokning/ui";
|
||||
|
||||
function Vallista({
|
||||
alla,
|
||||
valda,
|
||||
vidByte,
|
||||
}: {
|
||||
alla: string[];
|
||||
valda: string[];
|
||||
vidByte: (nya: string[]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{alla.map((val) => {
|
||||
const aktiv = valda.includes(val);
|
||||
return (
|
||||
<button
|
||||
key={val}
|
||||
onClick={() => vidByte(aktiv ? valda.filter((v) => v !== val) : [...valda, val])}
|
||||
className={`min-h-14 rounded-lg border-2 px-2 font-extrabold transition-colors ${
|
||||
aktiv
|
||||
? "border-amber-400 bg-amber-400 text-zinc-950"
|
||||
: "border-zinc-600 bg-zinc-900 text-zinc-400"
|
||||
}`}
|
||||
>
|
||||
{aktiv ? "✓ " : ""}
|
||||
{val}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Installningar() {
|
||||
const konto = plattformKonto();
|
||||
const inloggad = plattformAktiv() && !!konto;
|
||||
const farAndra = !inloggad || konto?.roll === "admin";
|
||||
|
||||
const [inst, setInst] = useState<Inst>(lastaInstallningar());
|
||||
const [status, setStatus] = useState<"" | "sparar" | "sparat" | "fel">("");
|
||||
const [felText, setFelText] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
hamtaInstallningar().then(setInst);
|
||||
}, []);
|
||||
|
||||
if (!farAndra) {
|
||||
return (
|
||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-lg text-zinc-300">
|
||||
Organisationens inställningar hanteras av er systemadministratör.
|
||||
</p>
|
||||
</Panel>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
}
|
||||
|
||||
const spara = async () => {
|
||||
if (inst.objekttyper.length === 0 || inst.identifieringsmetoder.length === 0) {
|
||||
setStatus("fel");
|
||||
setFelText("Minst ett alternativ måste vara valt i varje lista.");
|
||||
return;
|
||||
}
|
||||
setStatus("sparar");
|
||||
setFelText("");
|
||||
try {
|
||||
await sparaInstallningar(inst);
|
||||
setStatus("sparat");
|
||||
} catch (misslyckande) {
|
||||
setStatus("fel");
|
||||
setFelText(misslyckande instanceof Error ? misslyckande.message : "Kunde inte spara.");
|
||||
}
|
||||
};
|
||||
|
||||
const byt = (nya: Partial<Inst>) => {
|
||||
setInst((f) => ({ ...f, ...nya }));
|
||||
setStatus("");
|
||||
};
|
||||
|
||||
return (
|
||||
<FelsokningSkal rubrik="Inställningar" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel>
|
||||
<p className="text-zinc-300">
|
||||
Välj vad som visas när ett nytt ärende startas.{" "}
|
||||
{inloggad
|
||||
? "Valet gäller alla i organisationen."
|
||||
: "I lokalt läge gäller valet den här enheten; inloggad systemadministratör styr hela organisationen."}
|
||||
</p>
|
||||
</Panel>
|
||||
|
||||
<Panel rubrik="Objekttyper">
|
||||
<Vallista
|
||||
alla={ALLA_OBJEKTTYPER}
|
||||
valda={inst.objekttyper}
|
||||
vidByte={(objekttyper) => byt({ objekttyper })}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
<Panel rubrik="Identifieringsmetoder">
|
||||
<Vallista
|
||||
alla={ALLA_IDENTIFIERINGSMETODER}
|
||||
valda={inst.identifieringsmetoder}
|
||||
vidByte={(identifieringsmetoder) => byt({ identifieringsmetoder })}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
{status === "fel" && <p className="mb-3 font-bold text-red-400">{felText}</p>}
|
||||
{status === "sparat" && <p className="mb-3 font-bold text-green-400">✓ Sparat</p>}
|
||||
<StorKnapp disabled={status === "sparar"} onClick={spara}>
|
||||
{status === "sparar" ? "Sparar …" : "Spara inställningar"}
|
||||
</StorKnapp>
|
||||
</FelsokningSkal>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { Objekt } from "@/felsokning/domain";
|
||||
import { useFelsokning } from "@/felsokning/store";
|
||||
import { valjMetodik } from "@/felsokning/metodik";
|
||||
import { valjMetodikMedAi } from "@/felsokning/ai";
|
||||
import { hamtaInstallningar, lastaInstallningar } from "@/felsokning/installningar";
|
||||
import { FelsokningSkal, Panel, StorKnapp, TextFalt } from "@/felsokning/ui";
|
||||
|
||||
const IDENTIFIERINGSMETODER = ["Registreringsnummer", "VIN", "Serienummer", "Maskinnummer", "Manuell inmatning"];
|
||||
const OBJEKTTYPER = ["Fordon", "Lastbil/Buss", "Entreprenadmaskin", "Industrimaskin", "Elsystem", "Hydraulik", "Övrigt"];
|
||||
|
||||
// Ingen felsökning börjar innan objektet identifierats och bekräftats.
|
||||
// Vilka objekttyper och identifieringsmetoder som visas styrs av
|
||||
// organisationens inställningar (systemadmin) — enhetens val i lokalt läge.
|
||||
export default function NyttArende() {
|
||||
const navigate = useNavigate();
|
||||
const skapaArende = useFelsokning((s) => s.skapaArende);
|
||||
|
||||
const [inst, setInst] = useState(lastaInstallningar());
|
||||
const [steg, setSteg] = useState<"identifiera" | "bekrafta" | "felbeskrivning">("identifiera");
|
||||
const [metod, setMetod] = useState(IDENTIFIERINGSMETODER[0]);
|
||||
const [typ, setTyp] = useState(OBJEKTTYPER[0]);
|
||||
const [metod, setMetod] = useState(inst.identifieringsmetoder[0]);
|
||||
const [typ, setTyp] = useState(inst.objekttyper[0]);
|
||||
|
||||
useEffect(() => {
|
||||
hamtaInstallningar().then((farsk) => {
|
||||
setInst(farsk);
|
||||
setTyp((t) => (farsk.objekttyper.includes(t) ? t : farsk.objekttyper[0]));
|
||||
setMetod((m) => (farsk.identifieringsmetoder.includes(m) ? m : farsk.identifieringsmetoder[0]));
|
||||
});
|
||||
}, []);
|
||||
const [identifierare, setIdentifierare] = useState("");
|
||||
const [beskrivning, setBeskrivning] = useState("");
|
||||
const [kund, setKund] = useState("");
|
||||
@@ -36,7 +45,7 @@ export default function NyttArende() {
|
||||
<FelsokningSkal rubrik="Identifiera objekt" tillbaka={{ till: "/felsokning", text: "Ärenden" }}>
|
||||
<Panel rubrik="Objekttyp">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{OBJEKTTYPER.map((t) => (
|
||||
{inst.objekttyper.map((t) => (
|
||||
<StorKnapp key={t} variant={t === typ ? "primar" : "sekundar"} onClick={() => setTyp(t)}>
|
||||
{t}
|
||||
</StorKnapp>
|
||||
@@ -45,7 +54,7 @@ export default function NyttArende() {
|
||||
</Panel>
|
||||
<Panel rubrik="Identifieringsmetod">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{IDENTIFIERINGSMETODER.map((m) => (
|
||||
{inst.identifieringsmetoder.map((m) => (
|
||||
<StorKnapp key={m} variant={m === metod ? "primar" : "sekundar"} onClick={() => setMetod(m)}>
|
||||
{m}
|
||||
</StorKnapp>
|
||||
|
||||
Reference in New Issue
Block a user