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:
@@ -135,7 +135,7 @@ vergeben statt geraten.
|
||||
| FOUND-012 | Foundation | Dokumentationsgerüst | P1 | XS | LOW | FOUND-001 | ✅ (`README.md` im Projektwurzelverzeichnis) |
|
||||
| IDENT-001 | Identity | Objekt-ID-Schema | P0 | S | LOW | FOUND-002 | ✅ (Objekt-Code-Vergabe, `/objekte/naechster-code`) |
|
||||
| IDENT-002 | Identity | Inventarnummer manuell | P0 | S | LOW | IDENT-001 | ✅ (Objektposition-Code, `objektposition.py`) |
|
||||
| IDENT-003 | Identity | Inventarnummer-Nummernschemata | P1 | M | MEDIUM | IDENT-002 | ⬜ (kein konfigurierbares Schema gefunden, nur fester Code-Zähler) |
|
||||
| IDENT-003 | Identity | Inventarnummer-Nummernschemata | P1 | M | MEDIUM | IDENT-002 | ✅ (`Nummernschema` je Objekttyp, Migration 0032, `/objekte/naechster-code?objekttyp_id=` nutzt es, Admin-UI in ObjekttypSection) |
|
||||
| IDENT-004 | Identity | Seriennummer + Eindeutigkeitsprüfung | P0 | S | LOW | IDENT-001 | ✅ (`geraet_instanz.py` Seriennummer-Feld/Unique) |
|
||||
| IDENT-005 | Identity | Hersteller/Modell-Stammdaten | P1 | S | LOW | IDENT-004 | ✅ (`Hersteller`/`Modell`-Tabellen, Migration 0030, `geraet_instanz.modell_id` optional; kein eigenes Admin-UI, nur API) |
|
||||
| IDENT-006 | Identity | QR-Code-Erzeugung | P0 | S | LOW | IDENT-001 | ✅ (`label.pdf`-Endpunkte für Objekt/Objektposition) |
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""IDENT-003: Nummernschema je Objekttyp (Präfix + Stellenzahl konfigurierbar).
|
||||
|
||||
Revision ID: 0032_nummernschema
|
||||
Revises: 0031_einsatzfunktion
|
||||
Create Date: 2026-09-08
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0032_nummernschema"
|
||||
down_revision: Union[str, None] = "0031_einsatzfunktion"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE nummernschema (
|
||||
id SERIAL PRIMARY KEY,
|
||||
objekttyp_id INTEGER NOT NULL UNIQUE REFERENCES objekttyp(id),
|
||||
praefix TEXT NOT NULL,
|
||||
stellenzahl INTEGER NOT NULL DEFAULT 5
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE nummernschema")
|
||||
@@ -12,7 +12,7 @@ from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
|
||||
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.objektposition import Objektposition
|
||||
from app.models.stammdaten import Material, Objekttyp
|
||||
from app.models.stammdaten import Material, Nummernschema, Objekttyp
|
||||
from app.models.vorlage import Beladungsvorlage
|
||||
from app.services import historie as historie_service
|
||||
from app.services.label import generiere_code128_label_pdf, generiere_qr_label_pdf
|
||||
@@ -105,11 +105,32 @@ async def hole_objekt_per_code(
|
||||
|
||||
@router.get("/objekte/naechster-code")
|
||||
async def naechster_objekt_code(
|
||||
praefix: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||
praefix: str | None = None,
|
||||
objekttyp_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
) -> dict[str, str]:
|
||||
"""Code-Vorschlag beim Anlegen/Duplizieren (Konzept-Ergänzung Karte 10/Prompt 09).
|
||||
Vorschlag ist überschreibbar, Eindeutigkeit erzwingt weiterhin objekt.code UNIQUE."""
|
||||
return {"code": await naechster_freier_code(db, Objekt.code, praefix)}
|
||||
Vorschlag ist überschreibbar, Eindeutigkeit erzwingt weiterhin objekt.code UNIQUE.
|
||||
|
||||
IDENT-003: `objekttyp_id` statt `praefix` nutzt das konfigurierte
|
||||
Nummernschema (Präfix + Mindest-Stellenzahl) - kein manuelles Eintippen/
|
||||
Merken des richtigen Präfix je Objekttyp mehr nötig. `praefix` bleibt für
|
||||
Objekttypen ohne konfiguriertes Schema nutzbar (Rückwärtskompatibilität)."""
|
||||
stellenzahl = 5
|
||||
if objekttyp_id is not None:
|
||||
result = await db.execute(select(Nummernschema).where(Nummernschema.objekttyp_id == objekttyp_id))
|
||||
schema = result.scalar_one_or_none()
|
||||
if schema is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Kein Nummernschema für diesen Objekttyp konfiguriert - Präfix manuell angeben",
|
||||
)
|
||||
praefix = schema.praefix
|
||||
stellenzahl = schema.stellenzahl
|
||||
if not praefix:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Präfix oder Objekttyp angeben")
|
||||
return {"code": await naechster_freier_code(db, Objekt.code, praefix, stellenzahl)}
|
||||
|
||||
|
||||
@router.get("/objektpositionen/naechster-code")
|
||||
|
||||
@@ -6,7 +6,7 @@ from app.api.deps import get_current_user, require_roles, require_roles_or_permi
|
||||
from app.db.session import get_db
|
||||
from app.models.auth import RolleTyp
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.stammdaten import Bereich, Fach, Hersteller, Kategorie, Material, Modell, Objekttyp, Standort
|
||||
from app.models.stammdaten import Bereich, Fach, Hersteller, Kategorie, Material, Modell, Nummernschema, Objekttyp, Standort
|
||||
from app.models.vorlage import Beladungsvorlage, Vorlagenposition
|
||||
from app.services.material_merge import finde_konflikte, fuehre_zusammen, hole_verwendung
|
||||
from app.schemas.stammdaten import (
|
||||
@@ -25,6 +25,9 @@ from app.schemas.stammdaten import (
|
||||
KategorieUpdate,
|
||||
ModellCreate,
|
||||
ModellRead,
|
||||
NummernschemaCreate,
|
||||
NummernschemaRead,
|
||||
NummernschemaUpdate,
|
||||
MaterialCreate,
|
||||
MaterialRead,
|
||||
MaterialUpdate,
|
||||
@@ -141,6 +144,53 @@ async def erstelle_modell(
|
||||
return modell
|
||||
|
||||
|
||||
# -- Nummernschema (IDENT-003) ---------------------------------------------------
|
||||
|
||||
@router.get("/nummernschemata", response_model=list[NummernschemaRead])
|
||||
async def liste_nummernschemata(
|
||||
db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||
) -> list[Nummernschema]:
|
||||
result = await db.execute(select(Nummernschema))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.post("/nummernschemata", response_model=NummernschemaRead, status_code=status.HTTP_201_CREATED)
|
||||
async def erstelle_nummernschema(
|
||||
payload: NummernschemaCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Nummernschema:
|
||||
if await db.get(Objekttyp, payload.objekttyp_id) is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekttyp nicht gefunden")
|
||||
bestehendes = await db.execute(
|
||||
select(Nummernschema).where(Nummernschema.objekttyp_id == payload.objekttyp_id)
|
||||
)
|
||||
if bestehendes.scalar_one_or_none() is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Für diesen Objekttyp existiert bereits ein Nummernschema"
|
||||
)
|
||||
schema = Nummernschema(**payload.model_dump())
|
||||
db.add(schema)
|
||||
await db.flush()
|
||||
return schema
|
||||
|
||||
|
||||
@router.patch("/nummernschemata/{schema_id}", response_model=NummernschemaRead)
|
||||
async def aendere_nummernschema(
|
||||
schema_id: int,
|
||||
payload: NummernschemaUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_=Depends(require_roles(RolleTyp.administration)),
|
||||
) -> Nummernschema:
|
||||
schema = await db.get(Nummernschema, schema_id)
|
||||
if schema is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nummernschema nicht gefunden")
|
||||
for feld, wert in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(schema, feld, wert)
|
||||
await db.flush()
|
||||
return schema
|
||||
|
||||
|
||||
# -- Kategorie ------------------------------------------------------------------
|
||||
|
||||
@router.get("/kategorien", response_model=list[KategorieRead])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import ENUM as PgEnum
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@@ -54,6 +54,24 @@ class Objekttyp(Base):
|
||||
ist_zugfahrzeug: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class Nummernschema(Base):
|
||||
"""IDENT-003: statt bei jeder Objekt-Anlage das Präfix manuell einzutippen
|
||||
(Fehlerquelle - Tippfehler, uneinheitliche Groß-/Kleinschreibung, falsches
|
||||
Präfix für den Objekttyp) legt der Admin je Objekttyp einmal Präfix +
|
||||
Stellenzahl fest. `naechster_freier_code` schlägt darauf aufbauend den
|
||||
nächsten Code vor (PRAEFIX-NNN...), wie bisher, nur nicht mehr frei
|
||||
eingebbar. Ein Objekttyp OHNE Schema kann weiterhin mit explizitem Präfix
|
||||
angelegt werden (Rückwärtskompatibilität, kein Zwang zur Migration
|
||||
bestehender Objekte)."""
|
||||
|
||||
__tablename__ = "nummernschema"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), unique=True, nullable=False)
|
||||
praefix: Mapped[str] = mapped_column(String, nullable=False)
|
||||
stellenzahl: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
|
||||
|
||||
|
||||
class Fach(Base):
|
||||
"""Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp, damit beim Anlegen einer
|
||||
Vorlagenposition nicht jedes Mal Freitext getippt werden muss. `sortierung`
|
||||
|
||||
@@ -54,6 +54,22 @@ class ModellRead(ModellCreate):
|
||||
id: int
|
||||
|
||||
|
||||
class NummernschemaCreate(BaseModel):
|
||||
objekttyp_id: int
|
||||
praefix: str
|
||||
stellenzahl: int = 5
|
||||
|
||||
|
||||
class NummernschemaRead(NummernschemaCreate):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
|
||||
|
||||
class NummernschemaUpdate(BaseModel):
|
||||
praefix: str | None = None
|
||||
stellenzahl: int | None = None
|
||||
|
||||
|
||||
class StandortCreate(BaseModel):
|
||||
name: str
|
||||
adresse: str | None = None
|
||||
|
||||
@@ -13,14 +13,16 @@ from app.models.vorlage import Beladungsvorlage, Vorlagenposition
|
||||
_CODE_MUSTER = re.compile(r"^-(\d+)$")
|
||||
|
||||
|
||||
async def naechster_freier_code(db: AsyncSession, spalte, praefix: str) -> str:
|
||||
async def naechster_freier_code(db: AsyncSession, spalte, praefix: str, mindestbreite: int = 5) -> str:
|
||||
"""Konzept-Ergänzung Karte 10/Prompt 09: Code-Vorschlag PRAEFIX-NNN mit
|
||||
fortlaufender Nummer je Präfix. Zahlenbreite folgt der bisher größten
|
||||
vergebenen Nummer (mind. 5 Stellen bei Erstvergabe), damit die Sortierung im
|
||||
Regal stabil bleibt, solange nicht mehr als 10^Breite Objekte existieren."""
|
||||
vergebenen Nummer (mindestens `mindestbreite` bei Erstvergabe - IDENT-003:
|
||||
kommt aus dem konfigurierten Nummernschema, falls vorhanden, sonst 5 wie
|
||||
bisher), damit die Sortierung im Regal stabil bleibt, solange nicht mehr
|
||||
als 10^Breite Objekte existieren."""
|
||||
result = await db.execute(select(spalte).where(spalte.like(f"{praefix}-%")))
|
||||
hoechste = 0
|
||||
breite = 5
|
||||
breite = mindestbreite
|
||||
for (code,) in result.all():
|
||||
treffer = _CODE_MUSTER.match(code[len(praefix) :])
|
||||
if treffer is None:
|
||||
|
||||
@@ -262,6 +262,71 @@ async def test_naechster_objekt_code_zaehlt_hoch(
|
||||
assert response.json()["code"] == "RK-00018"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nummernschema_liefert_code_ohne_manuelles_praefix(
|
||||
client, objekttyp_factory, admin_user
|
||||
):
|
||||
"""IDENT-003: Präfix + Stellenzahl kommen aus dem konfigurierten Schema,
|
||||
nicht mehr manuell eingegeben."""
|
||||
objekttyp = await objekttyp_factory("Typ-Nummernschema")
|
||||
token = await login(client, "admin1")
|
||||
|
||||
schema = await client.post(
|
||||
"/api/v1/nummernschemata",
|
||||
json={"objekttyp_id": objekttyp.id, "praefix": "SEG", "stellenzahl": 3},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert schema.status_code == 201
|
||||
|
||||
duplikat = await client.post(
|
||||
"/api/v1/nummernschemata",
|
||||
json={"objekttyp_id": objekttyp.id, "praefix": "ANDERES"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert duplikat.status_code == 409
|
||||
|
||||
vorschlag = await client.get(
|
||||
f"/api/v1/objekte/naechster-code?objekttyp_id={objekttyp.id}", headers=auth_header(token)
|
||||
)
|
||||
assert vorschlag.status_code == 200
|
||||
assert vorschlag.json()["code"] == "SEG-001"
|
||||
|
||||
aenderung = await client.patch(
|
||||
f"/api/v1/nummernschemata/{schema.json()['id']}",
|
||||
json={"stellenzahl": 4},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert aenderung.json()["stellenzahl"] == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nummernschema_ohne_konfiguration_verlangt_praefix(
|
||||
client, objekttyp_factory, admin_user
|
||||
):
|
||||
objekttyp = await objekttyp_factory("Typ-Ohne-Schema")
|
||||
token = await login(client, "admin1")
|
||||
|
||||
ohne_schema = await client.get(
|
||||
f"/api/v1/objekte/naechster-code?objekttyp_id={objekttyp.id}", headers=auth_header(token)
|
||||
)
|
||||
assert ohne_schema.status_code == 404
|
||||
|
||||
ohne_beides = await client.get("/api/v1/objekte/naechster-code", headers=auth_header(token))
|
||||
assert ohne_beides.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mitarbeiter_darf_nummernschema_nicht_anlegen(client, objekttyp_factory, mitarbeiter_user):
|
||||
objekttyp = await objekttyp_factory("Typ-Nummernschema-Verboten")
|
||||
token = await login(client, "mitarbeiter1")
|
||||
response = await client.post(
|
||||
"/api/v1/nummernschemata",
|
||||
json={"objekttyp_id": objekttyp.id, "praefix": "X"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_naechster_objektposition_code(
|
||||
client, db_session, objekt_mit_position, materialverantwortlicher_user
|
||||
|
||||
@@ -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