Neue Tabelle person (Migration 0034, Backfill bestehender Benutzer 1:1 per Zeilennummer, keine Datenverluste). Benutzer.person_id optional - Login kann bestehende Person verknüpfen oder legt automatisch eine neue an (Rückwärtskompatibilität zum bisherigen Anlegen-Flow). /personen-CRUD (admin-only), neue PersonSection.tsx im Personal-Tab, BenutzerSection um Personen-Auswahl erweitert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
import enum
|
|
import uuid
|
|
from datetime import date
|
|
|
|
from sqlalchemy import CheckConstraint, Date, ForeignKey, String, UniqueConstraint
|
|
from sqlalchemy.dialects.postgresql import ENUM as PgEnum, UUID
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.db.base import Base
|
|
|
|
|
|
class Qualifikationskategorie(str, enum.Enum):
|
|
fuehrerschein = "fuehrerschein"
|
|
lehrgang = "lehrgang"
|
|
berechtigung = "berechtigung"
|
|
|
|
|
|
qualifikationskategorie_pg = PgEnum(
|
|
Qualifikationskategorie, name="qualifikationskategorie", create_type=False
|
|
)
|
|
|
|
|
|
class Person(Base):
|
|
"""PERS-001: fachliches Konzept einer Person, getrennt vom technischen Login
|
|
(Benutzer). Kann ohne Benutzerkonto existieren (z.B. Jugendgruppe, externe
|
|
Helfer ohne Systemzugang) - Benutzer.person_id verweist optional hierher."""
|
|
|
|
__tablename__ = "person"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
geburtsdatum: Mapped[date | None] = mapped_column(Date)
|
|
kontakt: Mapped[str | None] = mapped_column(String)
|
|
|
|
|
|
class Einheit(Base):
|
|
"""Roadmap-Erweiterung (Personal-Modul): Organisationseinheit (Zug/Gruppe),
|
|
self-referenzierend für Hierarchie (analog Kategorie.ueberkategorie_id).
|
|
Bewusst eigenständig statt an Standort gekoppelt - eine Einheit kann
|
|
ortsübergreifend sein, ein Standort mehrere Einheiten beherbergen."""
|
|
|
|
__tablename__ = "einheit"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
uebergeordnete_einheit_id: Mapped[int | None] = mapped_column(ForeignKey("einheit.id"))
|
|
standort_id: Mapped[int | None] = mapped_column(ForeignKey("standort.id"))
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("uebergeordnete_einheit_id <> id", name="einheit_kein_selbstbezug"),
|
|
)
|
|
|
|
|
|
class Qualifikationstyp(Base):
|
|
"""Führerschein/Lehrgang/Berechtigung als ein Stammdaten-Konzept, damit
|
|
"welche Qualifikation braucht Objekttyp X" generisch bleibt (kein separates
|
|
Führerschein-Feld am Fahrzeug nötig, Karte-Nachtrag Personal-Modul)."""
|
|
|
|
__tablename__ = "qualifikationstyp"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
kategorie: Mapped[Qualifikationskategorie] = mapped_column(
|
|
qualifikationskategorie_pg, nullable=False
|
|
)
|
|
# NULL = unbefristet (z.B. manche Berechtigungen), sonst Gültigkeitsdauer in
|
|
# Monaten ab erworben_am - Ablauf wird nicht hier, sondern je
|
|
# BenutzerQualifikation über gueltig_bis geführt (individuelles Datum je
|
|
# Nachweis, nicht rein rechnerisch aus dem Typ abgeleitet).
|
|
gueltigkeit_monate: Mapped[int | None] = mapped_column()
|
|
|
|
|
|
class BenutzerQualifikation(Base):
|
|
__tablename__ = "benutzer_qualifikation"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
|
|
qualifikationstyp_id: Mapped[int] = mapped_column(ForeignKey("qualifikationstyp.id"), nullable=False)
|
|
erworben_am: Mapped[date] = mapped_column(Date, nullable=False)
|
|
gueltig_bis: Mapped[date | None] = mapped_column(Date)
|
|
nachweis_nummer: Mapped[str | None] = mapped_column(String)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"benutzer_id", "qualifikationstyp_id", "erworben_am", name="uq_benutzer_qualifikation"
|
|
),
|
|
)
|
|
|
|
|
|
class Einsatzfunktion(Base):
|
|
"""PERS-004: Funktion im Einsatzkontext (z.B. Gruppenführer, Truppführer,
|
|
Melder, Maschinist, Atemschutzgeräteträger) - unabhängig von den
|
|
technischen App-Rollen (RolleTyp: mitarbeiter/materialverantwortlicher/
|
|
...), die nur steuern, was jemand in MABEA tun darf, nicht welche Funktion
|
|
er im Einsatz hat. Bewusst analog zu Benutzer.einheit_id als einzelnes
|
|
optionales Feld modelliert, nicht als M:N - eine Person hat im Regelfall
|
|
genau eine primäre Einsatzfunktion, keine Historie/Zeitraum nötig."""
|
|
|
|
__tablename__ = "einsatzfunktion"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
|
|
|
|
|
class ObjekttypQualifikationsanforderung(Base):
|
|
"""M:N: welche Qualifikationstypen braucht man, um Objekte dieses Objekttyps
|
|
zu bedienen/fahren (Nutzer-Beispiel: "wer darf GW-San 01 fahren?")."""
|
|
|
|
__tablename__ = "objekttyp_qualifikationsanforderung"
|
|
|
|
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), primary_key=True)
|
|
qualifikationstyp_id: Mapped[int] = mapped_column(
|
|
ForeignKey("qualifikationstyp.id"), primary_key=True
|
|
)
|