feat(identity): IDENT-003 konfigurierbare Nummernschemata je Objekttyp
CI / backend-tests (push) Successful in 2m24s
CI / frontend-build (push) Successful in 27s

Neue Stammdaten-Tabelle nummernschema (Migration 0032): Präfix + Stellenzahl
einmal je Objekttyp festlegen statt bei jeder Objekt-Anlage manuell
einzutippen (Fehlerquelle - Tippfehler, uneinheitliche Schreibweise,
falsches Präfix für den Typ). GET /objekte/naechster-code akzeptiert jetzt
objekttyp_id als Alternative zu praefix; ohne konfiguriertes Schema bleibt
der bisherige manuelle Präfix-Weg nutzbar (Rückwärtskompatibilität).

Admin-UI: Nummernschema-Pflege direkt in der Objekttyp-Verwaltung
(ObjekttypSection). ObjektAnlegenFormular versucht zuerst das Schema des
gewählten Objekttyps, fällt bei 404 auf manuelles Präfix zurück.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-08 20:23:14 +02:00
co-authored by Claude Sonnet 5
parent 15f783fb5e
commit 3b5cf8feab
12 changed files with 380 additions and 53 deletions
+7
View File
@@ -302,6 +302,13 @@ export interface Objekttyp {
ist_zugfahrzeug: boolean;
}
export interface Nummernschema {
id: number;
objekttyp_id: number;
praefix: string;
stellenzahl: number;
}
export interface Fach {
id: number;
objekttyp_id: number;
+6 -2
View File
@@ -4,7 +4,7 @@ import { useSearchParams } from "react-router-dom";
import { apiRequest } from "../api/client";
import { AusgabeSection } from "./admin/AusgabeSection";
import { useAuth } from "../auth/AuthContext";
import type { Benutzer, Bereich, Beladungsvorlage, Einheit, Einsatzfunktion, Fach, Kategorie, Material, Objekt, Objekttyp, Qualifikationstyp, Standort } from "../api/types";
import type { Benutzer, Bereich, Beladungsvorlage, Einheit, Einsatzfunktion, Fach, Kategorie, Material, Nummernschema, Objekt, Objekttyp, Qualifikationstyp, Standort } from "../api/types";
import { BenutzerSection } from "./admin/BenutzerSection";
import { BereichSection } from "./admin/BereichSection";
import { EinheitSection } from "./admin/EinheitSection";
@@ -109,6 +109,7 @@ export function AdminPage() {
const [faecher, setFaecher] = useState<Fach[]>([]);
const [einheiten, setEinheiten] = useState<Einheit[]>([]);
const [einsatzfunktionen, setEinsatzfunktionen] = useState<Einsatzfunktion[]>([]);
const [nummernschemata, setNummernschemata] = useState<Nummernschema[]>([]);
const [qualifikationstypen, setQualifikationstypen] = useState<Qualifikationstyp[]>([]);
const [fehler, setFehler] = useState<string | null>(null);
const [laedt, setLaedt] = useState(true);
@@ -128,7 +129,7 @@ export function AdminPage() {
// Seite als "nicht geladen" zeigen, obwohl deren Tabs (z.B. Kontroll-
// verantwortung) trotzdem funktionieren sollen. allSettled + leerer
// Fallback statt hartem Fehler.
const [be, ka, ot, s, v, o, m, b, f, ei, qt, ef] = await Promise.allSettled([
const [be, ka, ot, s, v, o, m, b, f, ei, qt, ef, ns] = await Promise.allSettled([
apiRequest<Bereich[]>("/bereiche"),
apiRequest<Kategorie[]>("/kategorien"),
apiRequest<Objekttyp[]>("/objekttypen"),
@@ -141,6 +142,7 @@ export function AdminPage() {
apiRequest<Einheit[]>("/einheiten"),
apiRequest<Qualifikationstyp[]>("/qualifikationstypen"),
apiRequest<Einsatzfunktion[]>("/einsatzfunktionen"),
apiRequest<Nummernschema[]>("/nummernschemata"),
]);
const wert = <T,>(r: PromiseSettledResult<T[]>): T[] => (r.status === "fulfilled" ? r.value : []);
setBereiche(wert(be));
@@ -155,6 +157,7 @@ export function AdminPage() {
setEinheiten(wert(ei));
setQualifikationstypen(wert(qt));
setEinsatzfunktionen(wert(ef));
setNummernschemata(wert(ns));
}
useEffect(() => {
@@ -226,6 +229,7 @@ export function AdminPage() {
bereiche={bereiche}
kategorien={kategorien}
objekttypen={objekttypen}
nummernschemata={nummernschemata}
onGeaendert={ladeAlles}
onFehler={setFehler}
/>
@@ -1,6 +1,6 @@
import { useState } from "react";
import { apiRequest } from "../../api/client";
import { apiRequest, ApiError } from "../../api/client";
import type { Beladungsvorlage, Objekt, Objekttyp, Standort } from "../../api/types";
interface Props {
@@ -30,8 +30,27 @@ export function ObjektAnlegenFormular({ objekte, objekttypen, vorlagen, standort
});
async function codeVorschlagen() {
const objekttypId = vorlagen.find((v) => v.id === vorlageId)?.objekttyp_id;
// IDENT-003: erst versuchen, das für den Objekttyp konfigurierte
// Nummernschema zu nutzen (kein manuelles Präfix nötig) - nur wenn keins
// konfiguriert ist (404), auf das Präfix-Feld zurückfallen.
if (objekttypId !== undefined) {
try {
const antwort = await apiRequest<{ code: string }>(
`/objekte/naechster-code?objekttyp_id=${objekttypId}`
);
setCode(antwort.code);
return;
} catch (fehler) {
if (!(fehler instanceof ApiError && fehler.status === 404)) {
onFehler("Code-Vorschlag fehlgeschlagen.");
return;
}
// kein Nummernschema für diesen Objekttyp -> weiter mit manuellem Präfix
}
}
if (!praefix) {
onFehler("Präfix eingeben (z. B. RK, FZ).");
onFehler("Kein Nummernschema für diesen Objekttyp konfiguriert - Präfix eingeben (z. B. RK, FZ).");
return;
}
try {
+132 -38
View File
@@ -1,17 +1,25 @@
import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Bereich, Kategorie, Objekttyp } from "../../api/types";
import type { Bereich, Kategorie, Nummernschema, Objekttyp } from "../../api/types";
interface Props {
bereiche: Bereich[];
kategorien: Kategorie[];
objekttypen: Objekttyp[];
nummernschemata: Nummernschema[];
onGeaendert: () => Promise<void>;
onFehler: (text: string) => void;
}
export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaendert, onFehler }: Props) {
export function ObjekttypSection({
bereiche,
kategorien,
objekttypen,
nummernschemata,
onGeaendert,
onFehler,
}: Props) {
const [name, setName] = useState("");
const [bereichId, setBereichId] = useState<number | "">("");
const [kategorieId, setKategorieId] = useState<number | "">("");
@@ -20,6 +28,13 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
const [bearbeitungsName, setBearbeitungsName] = useState("");
const [istZugfahrzeug, setIstZugfahrzeug] = useState(false);
const [bearbeitungsZugfahrzeug, setBearbeitungsZugfahrzeug] = useState(false);
const [schemaBearbeitenId, setSchemaBearbeitenId] = useState<number | null>(null);
const [schemaPraefix, setSchemaPraefix] = useState("");
const [schemaStellenzahl, setSchemaStellenzahl] = useState(5);
function schemaFuer(objekttypId: number): Nummernschema | undefined {
return nummernschemata.find((n) => n.objekttyp_id === objekttypId);
}
function bearbeiten(o: Objekttyp) {
setBearbeiteteId(o.id);
@@ -61,6 +76,38 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
}
}
function schemaBearbeiten(objekttypId: number) {
const bestehendes = schemaFuer(objekttypId);
setSchemaBearbeitenId(objekttypId);
setSchemaPraefix(bestehendes?.praefix ?? "");
setSchemaStellenzahl(bestehendes?.stellenzahl ?? 5);
}
async function schemaSpeichern(objekttypId: number) {
if (!schemaPraefix.trim()) {
onFehler("Präfix eingeben.");
return;
}
const bestehendes = schemaFuer(objekttypId);
try {
if (bestehendes) {
await apiRequest(`/nummernschemata/${bestehendes.id}`, {
method: "PATCH",
body: { praefix: schemaPraefix.toUpperCase(), stellenzahl: schemaStellenzahl },
});
} else {
await apiRequest("/nummernschemata", {
method: "POST",
body: { objekttyp_id: objekttypId, praefix: schemaPraefix.toUpperCase(), stellenzahl: schemaStellenzahl },
});
}
setSchemaBearbeitenId(null);
await onGeaendert();
} catch {
onFehler("Nummernschema konnte nicht gespeichert werden.");
}
}
return (
<section className="section">
<h2>Objekttyp anlegen</h2>
@@ -105,43 +152,90 @@ export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaender
)}
{objekttypen.length > 0 && (
<ul className="card-list">
{objekttypen.map((o) => (
<li key={o.id} className="row-between" style={{ padding: "0.4rem 0" }}>
{bearbeiteteId === o.id ? (
<div className="row" style={{ flex: 1 }}>
<input
className="input"
value={bearbeitungsName}
onChange={(e) => setBearbeitungsName(e.target.value)}
/>
<label className="row" style={{ alignItems: "center", gap: "0.4rem" }}>
<input
type="checkbox"
checked={bearbeitungsZugfahrzeug}
onChange={(e) => setBearbeitungsZugfahrzeug(e.target.checked)}
/>
Zugfahrzeug
</label>
<button className="btn btn-primary" onClick={() => speichern(o.id)}>
Speichern
</button>
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
Abbrechen
</button>
{objekttypen.map((o) => {
const schema = schemaFuer(o.id);
return (
<li key={o.id} style={{ padding: "0.4rem 0" }}>
<div className="row-between">
{bearbeiteteId === o.id ? (
<div className="row" style={{ flex: 1 }}>
<input
className="input"
value={bearbeitungsName}
onChange={(e) => setBearbeitungsName(e.target.value)}
/>
<label className="row" style={{ alignItems: "center", gap: "0.4rem" }}>
<input
type="checkbox"
checked={bearbeitungsZugfahrzeug}
onChange={(e) => setBearbeitungsZugfahrzeug(e.target.checked)}
/>
Zugfahrzeug
</label>
<button className="btn btn-primary" onClick={() => speichern(o.id)}>
Speichern
</button>
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
Abbrechen
</button>
</div>
) : (
<>
<span>
{o.name}
{o.ist_zugfahrzeug && <span className="text-muted"> (Zugfahrzeug)</span>}
</span>
<button className="btn btn-secondary" onClick={() => bearbeiten(o)}>
Bearbeiten
</button>
</>
)}
</div>
) : (
<>
<span>
{o.name}
{o.ist_zugfahrzeug && <span className="text-muted"> (Zugfahrzeug)</span>}
</span>
<button className="btn btn-secondary" onClick={() => bearbeiten(o)}>
Bearbeiten
</button>
</>
)}
</li>
))}
<div className="row" style={{ alignItems: "center", gap: "0.4rem", marginTop: "0.25rem" }}>
{schemaBearbeitenId === o.id ? (
<>
<label htmlFor={`schema-praefix-${o.id}`} className="sr-only">
Präfix für {o.name}
</label>
<input
id={`schema-praefix-${o.id}`}
className="input"
style={{ maxWidth: "6rem" }}
value={schemaPraefix}
onChange={(e) => setSchemaPraefix(e.target.value)}
placeholder="Präfix"
/>
<input
className="input"
type="number"
min={1}
max={8}
style={{ maxWidth: "5rem" }}
value={schemaStellenzahl}
onChange={(e) => setSchemaStellenzahl(Number(e.target.value) || 5)}
/>
<button className="btn btn-primary" onClick={() => schemaSpeichern(o.id)}>
Speichern
</button>
<button className="btn btn-secondary" onClick={() => setSchemaBearbeitenId(null)}>
Abbrechen
</button>
</>
) : (
<>
<span className="text-muted" style={{ fontSize: "0.85rem" }}>
Nummernschema:{" "}
{schema ? `${schema.praefix}-${"0".repeat(schema.stellenzahl)}` : "keins konfiguriert"}
</span>
<button className="btn btn-secondary" onClick={() => schemaBearbeiten(o.id)}>
{schema ? "Ändern" : "Festlegen"}
</button>
</>
)}
</div>
</li>
);
})}
</ul>
)}
</div>