feat(identity): IDENT-003 konfigurierbare Nummernschemata je Objekttyp
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:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user