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:
@@ -32,6 +32,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
|
||||
| Metodiker | ✅ Tre: vibration, elsystem/strömförsörjning (relä-exemplet ur visionen) och generisk — vald automatiskt utifrån felbeskrivningen. |
|
||||
| Live Share | ✅ Skrivskyddad livevy per ärende (`/felsokning/dela/:id`): status ✔/🔄/⏳, bilder, mätvärdestabell, tidslinje, rekommenderat nästa steg. Uppdateras automatiskt, interna poster filtreras bort. Publik delningssida (`/felsokning/delad/:kod`) läser via `hamta_delat_arende` utan inloggning och pollar för liveuppdatering; "Kopiera delningslänk" finns i rapportfliken. **Behörighetsnivåer**: återkallbara delningslänkar per nivå — kund (det kunddelbara), extern partner (även hypoteser, märkta ej verifierade), intern (full insyn) — med serverstyrd filtrering, hanterade från rapportfliken i självhostat läge. |
|
||||
| Dashboard | ✅ Enligt direktivet: räknare och filter för Alla/Pågående/Klara plus Starta nytt ärende. |
|
||||
| Inställningar | ✅ Systemadministratören väljer vilka objekttyper och identifieringsmetoder som visas när ett ärende startas (`/felsokning/installningar`). På plattformen gäller valet hela organisationen (sparas på organisationen, endast admin får ändra — verifierat i integrationstestet); i lokalt läge gäller valet enheten. Okända värden filtreras och tomma listor faller tillbaka till standard. |
|
||||
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. |
|
||||
| Öppet API | ✅ Plattforms-API:t är dokumenterat med OpenAPI 3.0 (`services/plattform/openapi.yaml`) — auth, användare, ärenden/händelser (append-only), översikt, publik delning och AI-orkestern, med scheman för alla händelsetyper. Specen valideras maskinellt, paritetstestas mot serverns rutter och serveras live på `GET /api/openapi.yaml`. |
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@ create extension if not exists pgcrypto;
|
||||
create table if not exists organisationer (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
namn text not null,
|
||||
-- Vad som visas när ett ärende startas (objekttyper,
|
||||
-- identifieringsmetoder) — sätts av systemadministratören.
|
||||
installningar jsonb not null default '{}'::jsonb,
|
||||
skapad timestamptz not null default now()
|
||||
);
|
||||
alter table organisationer add column if not exists installningar jsonb not null default '{}'::jsonb;
|
||||
|
||||
create table if not exists anvandare (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
|
||||
@@ -122,6 +122,23 @@ curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bea
|
||||
-d '{"handelser":[{"id":"h-omf","tidpunkt":"2026-08-03T08:05:00Z","anvandare":"Lisa","handelse":{"typ":"ansvarig_satt","ansvarig":"Johan"}}]}' >/dev/null
|
||||
kontroll "översikten visar ny ansvarig" "$(curl -s "$BAS/api/oversikt" -H "Authorization: Bearer $TOKEN_L" | falt '.arenden[0].ansvarig')" "Johan"
|
||||
|
||||
# 9c. Organisationsinställningar: alla läser, bara admin ändrar
|
||||
INST=$(curl -s "$BAS/api/organisation" -H "Authorization: Bearer $TOKEN_J")
|
||||
kontroll "inloggad läser organisationen" "$(echo "$INST" | falt .namn)" "Verkstad A"
|
||||
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/organisation/installningar" \
|
||||
-H "Authorization: Bearer $TOKEN_J" -H 'Content-Type: application/json' \
|
||||
-d '{"objekttyper":["Fordon"],"identifieringsmetoder":["VIN"]}')
|
||||
kontroll "tekniker nekas ändra inställningar" "$KOD" "403"
|
||||
curl -s -X POST "$BAS/api/organisation/installningar" -H "Authorization: Bearer $TOKEN_A" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"objekttyper":["Fordon","Hydraulik"],"identifieringsmetoder":["VIN","Manuell inmatning"]}' >/dev/null
|
||||
INST=$(curl -s "$BAS/api/organisation" -H "Authorization: Bearer $TOKEN_J")
|
||||
kontroll "inställningarna gäller hela organisationen" "$(echo "$INST" | falt '.installningar.objekttyper.join(",")')" "Fordon,Hydraulik"
|
||||
KOD=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BAS/api/organisation/installningar" \
|
||||
-H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||
-d '{"objekttyper":[],"identifieringsmetoder":["VIN"]}')
|
||||
kontroll "tomma listor avvisas" "$KOD" "400"
|
||||
|
||||
# 10. Live Share-behörighetsnivåer: kund/partner/intern + återkallelse
|
||||
curl -s -X POST "$BAS/api/arenden/arende-test1/handelser" -H "Authorization: Bearer $TOKEN_A" -H 'Content-Type: application/json' \
|
||||
-d '{"handelser":[{"id":"h3","tidpunkt":"2026-08-03T08:03:00Z","anvandare":"Anna","handelse":{"typ":"hypotes","text":"Trasigt relä","niva":"lag"}}]}' >/dev/null
|
||||
|
||||
@@ -147,6 +147,48 @@ paths:
|
||||
"403": { $ref: "#/components/responses/Fel" }
|
||||
"409": { $ref: "#/components/responses/Fel" }
|
||||
|
||||
/api/organisation:
|
||||
get:
|
||||
tags: [Organisation]
|
||||
summary: Hämta organisationens namn och inställningar
|
||||
description: >
|
||||
Inställningarna styr vad som visas när ett ärende startas
|
||||
(objekttyper och identifieringsmetoder). Alla inloggade läser.
|
||||
responses:
|
||||
"200":
|
||||
description: Organisationen.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
namn: { type: string }
|
||||
installningar: { $ref: "#/components/schemas/OrganisationsInstallningar" }
|
||||
"401": { $ref: "#/components/responses/Fel" }
|
||||
|
||||
/api/organisation/installningar:
|
||||
post:
|
||||
tags: [Organisation]
|
||||
summary: Uppdatera organisationens inställningar
|
||||
description: Kräver rollen `admin`. Minst ett alternativ per lista.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/OrganisationsInstallningar" }
|
||||
responses:
|
||||
"200":
|
||||
description: Sparat.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ok: { type: boolean }
|
||||
"400": { $ref: "#/components/responses/Fel" }
|
||||
"401": { $ref: "#/components/responses/Fel" }
|
||||
"403": { $ref: "#/components/responses/Fel" }
|
||||
|
||||
/api/arenden:
|
||||
get:
|
||||
tags: [Ärenden]
|
||||
@@ -456,6 +498,18 @@ components:
|
||||
Roll:
|
||||
type: string
|
||||
enum: [tekniker, arbetsledare, admin]
|
||||
OrganisationsInstallningar:
|
||||
type: object
|
||||
required: [objekttyper, identifieringsmetoder]
|
||||
properties:
|
||||
objekttyper:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { type: string }
|
||||
identifieringsmetoder:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { type: string }
|
||||
DelningsNiva:
|
||||
type: string
|
||||
enum: [kund, partner, intern]
|
||||
|
||||
@@ -266,6 +266,34 @@ export function skapaServer() {
|
||||
}
|
||||
}
|
||||
|
||||
// Organisationens inställningar: vad som visas när ett ärende
|
||||
// startas (objekttyper, identifieringsmetoder). Alla inloggade
|
||||
// läser; endast systemadministratören ändrar.
|
||||
if (req.method === "GET" && vag === "/api/organisation") {
|
||||
const rader = await pool.query(
|
||||
`select namn, installningar from organisationer where id = $1`,
|
||||
[anspr.org],
|
||||
);
|
||||
if (rader.rowCount === 0) return svara(res, 404, { error: "Organisationen finns inte." });
|
||||
return svara(res, 200, rader.rows[0]);
|
||||
}
|
||||
|
||||
if (req.method === "POST" && vag === "/api/organisation/installningar") {
|
||||
if (anspr.roll !== "admin") return svara(res, 403, { error: "Kräver administratörsbehörighet." });
|
||||
const { objekttyper, identifieringsmetoder } = await lasKropp(req);
|
||||
const giltigLista = (lista) =>
|
||||
Array.isArray(lista) && lista.length > 0 && lista.length <= 50 &&
|
||||
lista.every((v) => typeof v === "string" && v.length <= 100);
|
||||
if (!giltigLista(objekttyper) || !giltigLista(identifieringsmetoder)) {
|
||||
return svara(res, 400, { error: "Ange minst en objekttyp och en identifieringsmetod." });
|
||||
}
|
||||
await pool.query(
|
||||
`update organisationer set installningar = $2 where id = $1`,
|
||||
[anspr.org, JSON.stringify({ objekttyper, identifieringsmetoder })],
|
||||
);
|
||||
return svara(res, 200, { ok: true });
|
||||
}
|
||||
|
||||
// Organisationsöversikt för arbetsledare/admin: alla ärenden med
|
||||
// status, deltagande tekniker och sammanfattning — härlett ur
|
||||
// händelseloggen, aldrig lagrat separat.
|
||||
|
||||
@@ -20,6 +20,7 @@ import ArendeSida from "./pages/felsokning/ArendeSida";
|
||||
import DelatArende from "./pages/felsokning/DelatArende";
|
||||
import PublikDelning from "./pages/felsokning/PublikDelning";
|
||||
import Oversikt from "./pages/felsokning/Oversikt";
|
||||
import Installningar from "./pages/felsokning/Installningar";
|
||||
import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -49,6 +50,7 @@ function AppContent() {
|
||||
<Route path="/felsokning/dela/:id" element={<DelatArende />} />
|
||||
<Route path="/felsokning/delad/:kod" element={<PublikDelning />} />
|
||||
<Route path="/felsokning/oversikt" element={<Oversikt />} />
|
||||
<Route path="/felsokning/installningar" element={<Installningar />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
|
||||
@@ -71,6 +71,8 @@ describe("AI-orkestern", () => {
|
||||
["/api/auth/registrera", "/api/auth/registrera"],
|
||||
["/api/auth/logga-in", "/api/auth/logga-in"],
|
||||
["/api/anvandare", "/api/anvandare"],
|
||||
["/api/organisation", "/api/organisation"],
|
||||
["/api/organisation/installningar", "/api/organisation/installningar"],
|
||||
["/api/arenden", "/api/arenden"],
|
||||
["/api/arenden/{arendeId}/handelser", "handelser"],
|
||||
["/api/oversikt", "/api/oversikt"],
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
ALLA_IDENTIFIERINGSMETODER,
|
||||
ALLA_OBJEKTTYPER,
|
||||
lastaInstallningar,
|
||||
normalisera,
|
||||
} from "../installningar";
|
||||
|
||||
describe("organisationsinställningar", () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
|
||||
it("utan sparade val visas allt", () => {
|
||||
const inst = lastaInstallningar();
|
||||
expect(inst.objekttyper).toEqual(ALLA_OBJEKTTYPER);
|
||||
expect(inst.identifieringsmetoder).toEqual(ALLA_IDENTIFIERINGSMETODER);
|
||||
});
|
||||
|
||||
it("okända värden filtreras bort och ordningen följer standardlistan", () => {
|
||||
const inst = normalisera({
|
||||
objekttyper: ["Hydraulik", "Påhittad typ", "Fordon"],
|
||||
identifieringsmetoder: ["VIN"],
|
||||
});
|
||||
expect(inst.objekttyper).toEqual(["Fordon", "Hydraulik"]);
|
||||
expect(inst.identifieringsmetoder).toEqual(["VIN"]);
|
||||
});
|
||||
|
||||
it("en tom eller trasig lista faller tillbaka till hela standardlistan", () => {
|
||||
expect(normalisera({ objekttyper: [] }).objekttyper).toEqual(ALLA_OBJEKTTYPER);
|
||||
expect(normalisera({ objekttyper: "fel typ" }).objekttyper).toEqual(ALLA_OBJEKTTYPER);
|
||||
expect(normalisera(null).identifieringsmetoder).toEqual(ALLA_IDENTIFIERINGSMETODER);
|
||||
});
|
||||
|
||||
it("sparade val på enheten läses tillbaka", () => {
|
||||
localStorage.setItem(
|
||||
"gf-installningar",
|
||||
JSON.stringify({ objekttyper: ["Elsystem"], identifieringsmetoder: ["Serienummer", "Manuell inmatning"] }),
|
||||
);
|
||||
const inst = lastaInstallningar();
|
||||
expect(inst.objekttyper).toEqual(["Elsystem"]);
|
||||
expect(inst.identifieringsmetoder).toEqual(["Serienummer", "Manuell inmatning"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// Organisationsinställningar: systemadministratören väljer vilka
|
||||
// objekttyper och identifieringsmetoder som visas när ett ärende startas.
|
||||
// På plattformen gäller valet hela organisationen (sparas på servern);
|
||||
// i lokalt läge sparas valet på den här enheten. En cache i localStorage
|
||||
// gör att vyerna kan rendera direkt och uppdateras när servern svarat.
|
||||
|
||||
import { plattformAktiv, plattformFetch, plattformToken } from "./plattform";
|
||||
|
||||
export const ALLA_OBJEKTTYPER = [
|
||||
"Fordon",
|
||||
"Lastbil/Buss",
|
||||
"Entreprenadmaskin",
|
||||
"Industrimaskin",
|
||||
"Elsystem",
|
||||
"Hydraulik",
|
||||
"Övrigt",
|
||||
];
|
||||
|
||||
export const ALLA_IDENTIFIERINGSMETODER = [
|
||||
"Registreringsnummer",
|
||||
"VIN",
|
||||
"Serienummer",
|
||||
"Maskinnummer",
|
||||
"Manuell inmatning",
|
||||
];
|
||||
|
||||
export interface Installningar {
|
||||
objekttyper: string[];
|
||||
identifieringsmetoder: string[];
|
||||
}
|
||||
|
||||
const NYCKEL = "gf-installningar";
|
||||
|
||||
// Okända värden filtreras bort och en tom lista faller tillbaka till
|
||||
// hela standardlistan — det går aldrig att låsa ute alla alternativ.
|
||||
export function normalisera(data: unknown): Installningar {
|
||||
const rad = (varden: unknown, alla: string[]): string[] => {
|
||||
const valda = Array.isArray(varden) ? alla.filter((v) => varden.includes(v)) : [];
|
||||
return valda.length > 0 ? valda : [...alla];
|
||||
};
|
||||
const d = (data ?? {}) as Partial<Installningar>;
|
||||
return {
|
||||
objekttyper: rad(d.objekttyper, ALLA_OBJEKTTYPER),
|
||||
identifieringsmetoder: rad(d.identifieringsmetoder, ALLA_IDENTIFIERINGSMETODER),
|
||||
};
|
||||
}
|
||||
|
||||
export function lastaInstallningar(): Installningar {
|
||||
try {
|
||||
return normalisera(JSON.parse(localStorage.getItem(NYCKEL) ?? "{}"));
|
||||
} catch {
|
||||
return normalisera({});
|
||||
}
|
||||
}
|
||||
|
||||
export async function hamtaInstallningar(): Promise<Installningar> {
|
||||
if (plattformAktiv() && plattformToken()) {
|
||||
try {
|
||||
const res = await plattformFetch("/api/organisation");
|
||||
if (res.ok) {
|
||||
const { installningar } = (await res.json()) as { installningar: unknown };
|
||||
const inst = normalisera(installningar);
|
||||
localStorage.setItem(NYCKEL, JSON.stringify(inst));
|
||||
return inst;
|
||||
}
|
||||
} catch {
|
||||
// Nätverksfel: cachen/enhetens val gäller tills servern nås igen.
|
||||
}
|
||||
}
|
||||
return lastaInstallningar();
|
||||
}
|
||||
|
||||
export async function sparaInstallningar(inst: Installningar): Promise<void> {
|
||||
const ren = normalisera(inst);
|
||||
localStorage.setItem(NYCKEL, JSON.stringify(ren));
|
||||
if (plattformAktiv() && plattformToken()) {
|
||||
const res = await plattformFetch("/api/organisation/installningar", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(ren),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(data.error ?? `Fel ${res.status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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