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)}
+ )}