Files
MABEA/backend/app/models/personal.py
T
patrickandClaude Sonnet 5 7dc6446a56
CI / backend-tests (push) Successful in 2m29s
CI / frontend-build (push) Successful in 28s
feat(personal): PERS-001/002 Person fachlich von Benutzer getrennt
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
2026-09-08 22:31:46 +02:00

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
)