diff --git a/DEVLOG.md b/DEVLOG.md index d5bc21c..1ac9d5b 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -15245,3 +15245,67 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/VerleihSection.tsx | 25 +++++++++--- --- +## 2026-09-12 22:55 – 23:04 (9m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- c0c484c feat(pruefung): INSP-002 Phase 3 - Objekttyp-Intervall als Fallback + +### Geänderte Dateien +- DEVLOG.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- arbeitskacheln/07_warehouse.md | 12 +++---- +- arbeitskacheln/09_inspections.md | 12 +++++++ +- backend/app/services/geraet_instanz.py | 13 +++++--- +- backend/app/services/pruefung.py | 20 ++++++++++++ +- backend/tests/test_geraet_instanz.py | 67 +++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-12 23:09 – 23:09 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- arbeitskacheln/07_warehouse.md | 12 +++---- +- arbeitskacheln/09_inspections.md | 12 +++++++ +- backend/app/services/geraet_instanz.py | 13 +++++--- +- backend/app/services/pruefung.py | 20 ++++++++++++ +- backend/tests/test_geraet_instanz.py | 67 +++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-12 23:11 – 23:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- arbeitskacheln/07_warehouse.md | 12 +++---- +- arbeitskacheln/09_inspections.md | 12 +++++++ +- backend/app/services/geraet_instanz.py | 13 +++++--- +- backend/app/services/pruefung.py | 20 ++++++++++++ +- backend/tests/test_geraet_instanz.py | 67 +++++++++++++++++++++++++++++++++++++++ + +--- +## 2026-09-12 23:13 – 23:13 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- arbeitskacheln/07_warehouse.md | 12 +++---- +- arbeitskacheln/09_inspections.md | 12 +++++++ +- backend/app/services/geraet_instanz.py | 13 +++++--- +- backend/app/services/pruefung.py | 20 ++++++++++++ +- backend/tests/test_geraet_instanz.py | 67 +++++++++++++++++++++++++++++++++++++++ + +--- diff --git a/arbeitskacheln/12_personnel.md b/arbeitskacheln/12_personnel.md index f61a407..093c68d 100644 --- a/arbeitskacheln/12_personnel.md +++ b/arbeitskacheln/12_personnel.md @@ -91,10 +91,22 @@ Details zu PERS-001 … PERS-007 (vollständig). - **Audit:** Änderung geloggt - **Akzeptanzkriterien:** Person mit Funktion in Einheit zuordnen. - **Tests:** CRUD-Test. -- **DoD:** komplett neu in MABEA — Einheit-Zuordnung existiert (`Benutzer.einheit_id`), - aber keine fachliche Funktion/Rolle INNERHALB der Einheit (nur System-Rollen wie - „Materialverantwortlicher" existieren — das ist etwas anderes als „Zugführer von - Zug 3"). +- **DoD:** ✅ war bereits umgesetzt, DoD-Text war veraltet (Fund 2026-09-12, + gleicher Fall wie WH-006). `Einsatzfunktion` (Stammdaten, `app/models/ + personal.py`) + `Benutzer.einsatzfunktion_id` (nicht `funktion`/ + `einheit_mitgliedschaft` wie ursprünglich skizziert — einfachere + Einzelfeld-Modellierung statt M:N, siehe Docstring: eine Person hat i.d.R. + genau eine primäre Funktion, keine Historie nötig). CRUD unter + `/einsatzfunktionen`, Zuordnung im Admin-Tab „Benutzer" + (`BenutzerSection.tsx`, zeigt „Name – Einheit – Einsatzfunktion"). + **Lücke geschlossen (2026-09-12):** `einheit_id`/`einsatzfunktion_id` + gab es bisher NUR an `Benutzer` — eine `Person` OHNE Login (Jugendgruppe, + externe Helfer, siehe PERS-001) konnte keiner Einheit/Funktion zugeordnet + werden. Migration `0055_person_einheit_funktion.py` ergänzt beide Felder + an `Person`, Admin-Tab „Personen" (`PersonSection.tsx`) hat jetzt dieselben + zwei Auswahlfelder wie „Benutzer". Tests: + `test_person_ohne_login_einheit_und_funktion_zuordnen` + (`backend/tests/test_personal.py`). ## PERS-005 — Qualifikationen/Lehrgänge diff --git a/backend/alembic/versions/0055_person_einheit_funktion.py b/backend/alembic/versions/0055_person_einheit_funktion.py new file mode 100644 index 0000000..9afa187 --- /dev/null +++ b/backend/alembic/versions/0055_person_einheit_funktion.py @@ -0,0 +1,28 @@ +"""PERS-004: Person (Login-lose Helfer) kann jetzt einer Einheit und +Einsatzfunktion zugeordnet werden - bisher gab es das nur an Benutzer. + +Revision ID: 0055_person_einheit_funktion +Revises: 0054_ressourcenbuchung_optin +Create Date: 2026-09-12 +""" + +from typing import Sequence, Union + +from alembic import op + +revision: str = "0055_person_einheit_funktion" +down_revision: Union[str, None] = "0054_ressourcenbuchung_optin" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("ALTER TABLE person ADD COLUMN einheit_id INTEGER REFERENCES einheit(id)") + op.execute( + "ALTER TABLE person ADD COLUMN einsatzfunktion_id INTEGER REFERENCES einsatzfunktion(id)" + ) + + +def downgrade() -> None: + op.execute("ALTER TABLE person DROP COLUMN einsatzfunktion_id") + op.execute("ALTER TABLE person DROP COLUMN einheit_id") diff --git a/backend/app/api/v1/endpoints/personal.py b/backend/app/api/v1/endpoints/personal.py index a87afd8..d9ee946 100644 --- a/backend/app/api/v1/endpoints/personal.py +++ b/backend/app/api/v1/endpoints/personal.py @@ -57,7 +57,12 @@ async def erstelle_person_endpunkt( payload: PersonCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only) ) -> Person: return await erstelle_person( - db, name=payload.name, geburtsdatum=payload.geburtsdatum, kontakt=payload.kontakt + db, + name=payload.name, + geburtsdatum=payload.geburtsdatum, + kontakt=payload.kontakt, + einheit_id=payload.einheit_id, + einsatzfunktion_id=payload.einsatzfunktion_id, ) @@ -74,6 +79,10 @@ async def aendere_person( person.geburtsdatum = payload.geburtsdatum if "kontakt" in payload.model_fields_set: person.kontakt = payload.kontakt + if "einheit_id" in payload.model_fields_set: + person.einheit_id = payload.einheit_id + if "einsatzfunktion_id" in payload.model_fields_set: + person.einsatzfunktion_id = payload.einsatzfunktion_id await db.flush() return person diff --git a/backend/app/models/personal.py b/backend/app/models/personal.py index d955ba0..570710d 100644 --- a/backend/app/models/personal.py +++ b/backend/app/models/personal.py @@ -23,7 +23,16 @@ qualifikationskategorie_pg = PgEnum( 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.""" + Helfer ohne Systemzugang) - Benutzer.person_id verweist optional hierher. + + PERS-004-Lücke geschlossen (2026-09-12): einheit_id/einsatzfunktion_id + gab es bisher NUR an Benutzer - eine Person ohne Login (Jugendgruppe, + externe Helfer) konnte damit keiner Einheit/Funktion zugeordnet werden, + "wer ist Zugführer von Zug 3" war für sie unbeantwortbar. Bewusst + dieselbe einfache Einzelfeld-Modellierung wie bei Benutzer (siehe + Einsatzfunktion-Docstring) - keine getrennte Historie/Zeitraum-Logik, + Konsistenz zwischen beiden Entitäten wichtiger als Symmetrie zu + vermeiden.""" __tablename__ = "person" @@ -31,6 +40,8 @@ class Person(Base): name: Mapped[str] = mapped_column(String, nullable=False) geburtsdatum: Mapped[date | None] = mapped_column(Date) kontakt: Mapped[str | None] = mapped_column(String) + einheit_id: Mapped[int | None] = mapped_column(ForeignKey("einheit.id")) + einsatzfunktion_id: Mapped[int | None] = mapped_column(ForeignKey("einsatzfunktion.id")) class Einheit(Base): diff --git a/backend/app/schemas/personal.py b/backend/app/schemas/personal.py index 512aa07..a929605 100644 --- a/backend/app/schemas/personal.py +++ b/backend/app/schemas/personal.py @@ -10,6 +10,8 @@ class PersonCreate(BaseModel): name: str geburtsdatum: date | None = None kontakt: str | None = None + einheit_id: int | None = None + einsatzfunktion_id: int | None = None class PersonRead(BaseModel): @@ -18,12 +20,16 @@ class PersonRead(BaseModel): name: str geburtsdatum: date | None kontakt: str | None + einheit_id: int | None + einsatzfunktion_id: int | None class PersonUpdate(BaseModel): name: str | None = None geburtsdatum: date | None = None kontakt: str | None = None + einheit_id: int | None = None + einsatzfunktion_id: int | None = None class EinheitCreate(BaseModel): diff --git a/backend/app/services/personal.py b/backend/app/services/personal.py index 82d6859..6f2d4f2 100644 --- a/backend/app/services/personal.py +++ b/backend/app/services/personal.py @@ -12,8 +12,22 @@ from app.models.personal import ( ) -async def erstelle_person(db: AsyncSession, *, name: str, geburtsdatum, kontakt: str | None) -> Person: - person = Person(name=name, geburtsdatum=geburtsdatum, kontakt=kontakt) +async def erstelle_person( + db: AsyncSession, + *, + name: str, + geburtsdatum, + kontakt: str | None, + einheit_id: int | None = None, + einsatzfunktion_id: int | None = None, +) -> Person: + person = Person( + name=name, + geburtsdatum=geburtsdatum, + kontakt=kontakt, + einheit_id=einheit_id, + einsatzfunktion_id=einsatzfunktion_id, + ) db.add(person) await db.flush() return person diff --git a/backend/tests/test_personal.py b/backend/tests/test_personal.py index 458b48f..36f787e 100644 --- a/backend/tests/test_personal.py +++ b/backend/tests/test_personal.py @@ -266,6 +266,39 @@ async def test_person_ohne_benutzer_anlegbar(client, admin_user): assert geaendert.json()["kontakt"] == "Erziehungsberechtigter: 0170..." +@pytest.mark.asyncio +async def test_person_ohne_login_einheit_und_funktion_zuordnen(client, admin_user): + """PERS-004-Lücke: Person OHNE Benutzerkonto (Jugendgruppe/externe Helfer) + konnte bisher keiner Einheit/Einsatzfunktion zugeordnet werden - nur + Benutzer hatte diese Felder.""" + token = await login(client, "admin1") + + einheit = await client.post("/api/v1/einheiten", json={"name": "Jugendgruppe"}, headers=auth_header(token)) + einheit_id = einheit.json()["id"] + funktion = await client.post( + "/api/v1/einsatzfunktionen", json={"name": "Jugendgruppenleiter"}, headers=auth_header(token) + ) + funktion_id = funktion.json()["id"] + + person = await client.post( + "/api/v1/personen", + json={"name": "Erika Musterfrau", "einheit_id": einheit_id, "einsatzfunktion_id": funktion_id}, + headers=auth_header(token), + ) + assert person.status_code == 201 + assert person.json()["einheit_id"] == einheit_id + assert person.json()["einsatzfunktion_id"] == funktion_id + + person_id = person.json()["id"] + geloest = await client.patch( + f"/api/v1/personen/{person_id}", + json={"einheit_id": None, "einsatzfunktion_id": None}, + headers=auth_header(token), + ) + assert geloest.json()["einheit_id"] is None + assert geloest.json()["einsatzfunktion_id"] is None + + @pytest.mark.asyncio async def test_mitarbeiter_darf_person_nicht_anlegen(client, mitarbeiter_user): token = await login(client, "mitarbeiter1") diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 0ff3eb8..2dba658 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -529,6 +529,8 @@ export interface Person { name: string; geburtsdatum: string | null; kontakt: string | null; + einheit_id: number | null; + einsatzfunktion_id: number | null; } export interface Einsatzfunktion { diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index b0addd0..1de9eb1 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -369,7 +369,13 @@ export function AdminPage() { {aktiverTab === "eskalation" && } {aktiverTab === "personal" && ( <> - + Promise; onFehler: (text: string) => void; } -export function PersonSection({ personen, onGeaendert, onFehler }: Props) { +export function PersonSection({ personen, einheiten, einsatzfunktionen, onGeaendert, onFehler }: Props) { const [name, setName] = useState(""); const [geburtsdatum, setGeburtsdatum] = useState(""); const [kontakt, setKontakt] = useState(""); @@ -18,6 +20,18 @@ export function PersonSection({ personen, onGeaendert, onFehler }: Props) { const [bearbeitungsName, setBearbeitungsName] = useState(""); const [bearbeitungsGeburtsdatum, setBearbeitungsGeburtsdatum] = useState(""); const [bearbeitungsKontakt, setBearbeitungsKontakt] = useState(""); + const [bearbeitungsEinheitId, setBearbeitungsEinheitId] = useState(""); + const [bearbeitungsEinsatzfunktionId, setBearbeitungsEinsatzfunktionId] = useState(""); + + function einheitName(id: number | null): string { + if (id === null) return "–"; + return einheiten.find((e) => e.id === id)?.name ?? `#${id}`; + } + + function einsatzfunktionName(id: number | null): string { + if (id === null) return "–"; + return einsatzfunktionen.find((f) => f.id === id)?.name ?? `#${id}`; + } async function anlegen() { setWirdAngelegt(true); @@ -42,6 +56,8 @@ export function PersonSection({ personen, onGeaendert, onFehler }: Props) { setBearbeitungsName(p.name); setBearbeitungsGeburtsdatum(p.geburtsdatum ?? ""); setBearbeitungsKontakt(p.kontakt ?? ""); + setBearbeitungsEinheitId(p.einheit_id ?? ""); + setBearbeitungsEinsatzfunktionId(p.einsatzfunktion_id ?? ""); } async function speichern(id: number) { @@ -52,6 +68,8 @@ export function PersonSection({ personen, onGeaendert, onFehler }: Props) { name: bearbeitungsName, geburtsdatum: bearbeitungsGeburtsdatum || null, kontakt: bearbeitungsKontakt || null, + einheit_id: bearbeitungsEinheitId === "" ? null : bearbeitungsEinheitId, + einsatzfunktion_id: bearbeitungsEinsatzfunktionId === "" ? null : bearbeitungsEinsatzfunktionId, }, }); setBearbeiteteId(null); @@ -114,6 +132,30 @@ export function PersonSection({ personen, onGeaendert, onFehler }: Props) { onChange={(e) => setBearbeitungsKontakt(e.target.value)} placeholder="Kontakt" /> + + @@ -127,6 +169,10 @@ export function PersonSection({ personen, onGeaendert, onFehler }: Props) { {p.name} {p.geburtsdatum && · geb. {p.geburtsdatum}} {p.kontakt && · {p.kontakt}} + {p.einheit_id !== null && · {einheitName(p.einheit_id)}} + {p.einsatzfunktion_id !== null && ( + · {einsatzfunktionName(p.einsatzfunktion_id)} + )}