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
This commit is contained in:
@@ -803,3 +803,27 @@ Keine Commits in dieser Session.
|
||||
- frontend/src/pages/admin/ObjekttypSection.tsx | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
|
||||
|
||||
---
|
||||
## 2026-09-08 22:25 – 22:30 (5m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** asb-material
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- .claude/agents/DEVLOG.md | 11 +
|
||||
- DEVLOG.md | 4565 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
- arbeitskacheln/00_index.md | 11 +-
|
||||
- arbeitskacheln/22_ui_redesign_v2.md | 107 +++
|
||||
- backend/DEVLOG.md | 50 +
|
||||
- backend/alembic/versions/0033_wartungsauftrag_teil.py | 33 +
|
||||
- backend/app/api/v1/endpoints/wartung.py | 49 +-
|
||||
- backend/app/models/wartung.py | 10 +
|
||||
- backend/app/schemas/wartung.py | 15 +
|
||||
- backend/app/services/wartung.py | 22 +
|
||||
- backend/tests/test_wartung.py | 70 ++
|
||||
- frontend/DEVLOG.md | 176 ++++
|
||||
- frontend/src/api/types.ts | 8 +
|
||||
- frontend/src/components/WartungSection.tsx | 111 ++-
|
||||
|
||||
---
|
||||
|
||||
@@ -408,6 +408,14 @@ export interface Benutzer {
|
||||
rollen: string[];
|
||||
einheit_id: number | null;
|
||||
einsatzfunktion_id: number | null;
|
||||
person_id: number | null;
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
id: number;
|
||||
name: string;
|
||||
geburtsdatum: string | null;
|
||||
kontakt: string | null;
|
||||
}
|
||||
|
||||
export interface Einsatzfunktion {
|
||||
|
||||
@@ -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, Nummernschema, Objekt, Objekttyp, Qualifikationstyp, Standort } from "../api/types";
|
||||
import type { Benutzer, Bereich, Beladungsvorlage, Einheit, Einsatzfunktion, Fach, Kategorie, Material, Nummernschema, Objekt, Objekttyp, Person, Qualifikationstyp, Standort } from "../api/types";
|
||||
import { BenutzerSection } from "./admin/BenutzerSection";
|
||||
import { BereichSection } from "./admin/BereichSection";
|
||||
import { EinheitSection } from "./admin/EinheitSection";
|
||||
@@ -19,6 +19,7 @@ import { KontrollverantwortungSection } from "./admin/KontrollverantwortungSecti
|
||||
import { MaterialSection } from "./admin/MaterialSection";
|
||||
import { ObjektSection } from "./admin/ObjektSection";
|
||||
import { ObjekttypSection } from "./admin/ObjekttypSection";
|
||||
import { PersonSection } from "./admin/PersonSection";
|
||||
import { QualifikationSection } from "./admin/QualifikationSection";
|
||||
import { StandortSection } from "./admin/StandortSection";
|
||||
import { VorlageSection } from "./admin/VorlageSection";
|
||||
@@ -110,6 +111,7 @@ export function AdminPage() {
|
||||
const [einheiten, setEinheiten] = useState<Einheit[]>([]);
|
||||
const [einsatzfunktionen, setEinsatzfunktionen] = useState<Einsatzfunktion[]>([]);
|
||||
const [nummernschemata, setNummernschemata] = useState<Nummernschema[]>([]);
|
||||
const [personen, setPersonen] = useState<Person[]>([]);
|
||||
const [qualifikationstypen, setQualifikationstypen] = useState<Qualifikationstyp[]>([]);
|
||||
const [fehler, setFehler] = useState<string | null>(null);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
@@ -129,7 +131,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, ns] = await Promise.allSettled([
|
||||
const [be, ka, ot, s, v, o, m, b, f, ei, qt, ef, ns, pe] = await Promise.allSettled([
|
||||
apiRequest<Bereich[]>("/bereiche"),
|
||||
apiRequest<Kategorie[]>("/kategorien"),
|
||||
apiRequest<Objekttyp[]>("/objekttypen"),
|
||||
@@ -143,6 +145,7 @@ export function AdminPage() {
|
||||
apiRequest<Qualifikationstyp[]>("/qualifikationstypen"),
|
||||
apiRequest<Einsatzfunktion[]>("/einsatzfunktionen"),
|
||||
apiRequest<Nummernschema[]>("/nummernschemata"),
|
||||
apiRequest<Person[]>("/personen"),
|
||||
]);
|
||||
const wert = <T,>(r: PromiseSettledResult<T[]>): T[] => (r.status === "fulfilled" ? r.value : []);
|
||||
setBereiche(wert(be));
|
||||
@@ -158,6 +161,7 @@ export function AdminPage() {
|
||||
setQualifikationstypen(wert(qt));
|
||||
setEinsatzfunktionen(wert(ef));
|
||||
setNummernschemata(wert(ns));
|
||||
setPersonen(wert(pe));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -249,6 +253,7 @@ export function AdminPage() {
|
||||
benutzer={benutzer}
|
||||
einheiten={einheiten}
|
||||
einsatzfunktionen={einsatzfunktionen}
|
||||
personen={personen}
|
||||
onGeaendert={ladeAlles}
|
||||
onFehler={setFehler}
|
||||
/>
|
||||
@@ -274,6 +279,7 @@ export function AdminPage() {
|
||||
{aktiverTab === "eskalation" && <EskalationSection onFehler={setFehler} />}
|
||||
{aktiverTab === "personal" && (
|
||||
<>
|
||||
<PersonSection personen={personen} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||
<EinheitSection einheiten={einheiten} standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||
<QualifikationSection
|
||||
qualifikationstypen={qualifikationstypen}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Benutzer, Einheit, Einsatzfunktion, Rolle } from "../../api/types";
|
||||
import type { Benutzer, Einheit, Einsatzfunktion, Person, Rolle } from "../../api/types";
|
||||
|
||||
interface Props {
|
||||
benutzer: Benutzer[];
|
||||
einheiten: Einheit[];
|
||||
einsatzfunktionen: Einsatzfunktion[];
|
||||
personen: Person[];
|
||||
onGeaendert: () => Promise<void>;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
@@ -18,13 +19,14 @@ const ALLE_ROLLEN: Rolle[] = [
|
||||
"administration",
|
||||
];
|
||||
|
||||
export function BenutzerSection({ benutzer, einheiten, einsatzfunktionen, onGeaendert, onFehler }: Props) {
|
||||
export function BenutzerSection({ benutzer, einheiten, einsatzfunktionen, personen, onGeaendert, onFehler }: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [login, setLogin] = useState("");
|
||||
const [passwort, setPasswort] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [einheitId, setEinheitId] = useState<number | "">("");
|
||||
const [einsatzfunktionId, setEinsatzfunktionId] = useState<number | "">("");
|
||||
const [personId, setPersonId] = useState<number | "">("");
|
||||
const [neueEinsatzfunktion, setNeueEinsatzfunktion] = useState("");
|
||||
const [rollen, setRollen] = useState<Rolle[]>(["mitarbeiter"]);
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
@@ -65,6 +67,7 @@ export function BenutzerSection({ benutzer, einheiten, einsatzfunktionen, onGeae
|
||||
email: email || null,
|
||||
einheit_id: einheitId === "" ? null : einheitId,
|
||||
einsatzfunktion_id: einsatzfunktionId === "" ? null : einsatzfunktionId,
|
||||
person_id: personId === "" ? null : personId,
|
||||
rollen,
|
||||
},
|
||||
});
|
||||
@@ -74,6 +77,7 @@ export function BenutzerSection({ benutzer, einheiten, einsatzfunktionen, onGeae
|
||||
setEmail("");
|
||||
setEinheitId("");
|
||||
setEinsatzfunktionId("");
|
||||
setPersonId("");
|
||||
setRollen(["mitarbeiter"]);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
@@ -177,6 +181,14 @@ export function BenutzerSection({ benutzer, einheiten, einsatzfunktionen, onGeae
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="input" value={personId} onChange={(e) => setPersonId(Number(e.target.value) || "")}>
|
||||
<option value="">Neue Person anlegen (Standard)</option>
|
||||
{personen.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
Bestehende Person: {p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
value={neueEinsatzfunktion}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Person } from "../../api/types";
|
||||
|
||||
interface Props {
|
||||
personen: Person[];
|
||||
onGeaendert: () => Promise<void>;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
export function PersonSection({ personen, onGeaendert, onFehler }: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [geburtsdatum, setGeburtsdatum] = useState("");
|
||||
const [kontakt, setKontakt] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [bearbeiteteId, setBearbeiteteId] = useState<number | null>(null);
|
||||
const [bearbeitungsName, setBearbeitungsName] = useState("");
|
||||
const [bearbeitungsGeburtsdatum, setBearbeitungsGeburtsdatum] = useState("");
|
||||
const [bearbeitungsKontakt, setBearbeitungsKontakt] = useState("");
|
||||
|
||||
async function anlegen() {
|
||||
setWirdAngelegt(true);
|
||||
try {
|
||||
await apiRequest("/personen", {
|
||||
method: "POST",
|
||||
body: { name, geburtsdatum: geburtsdatum || null, kontakt: kontakt || null },
|
||||
});
|
||||
setName("");
|
||||
setGeburtsdatum("");
|
||||
setKontakt("");
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Person konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setWirdAngelegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
function bearbeiten(p: Person) {
|
||||
setBearbeiteteId(p.id);
|
||||
setBearbeitungsName(p.name);
|
||||
setBearbeitungsGeburtsdatum(p.geburtsdatum ?? "");
|
||||
setBearbeitungsKontakt(p.kontakt ?? "");
|
||||
}
|
||||
|
||||
async function speichern(id: number) {
|
||||
try {
|
||||
await apiRequest(`/personen/${id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
name: bearbeitungsName,
|
||||
geburtsdatum: bearbeitungsGeburtsdatum || null,
|
||||
kontakt: bearbeitungsKontakt || null,
|
||||
},
|
||||
});
|
||||
setBearbeiteteId(null);
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Person konnte nicht geändert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Personen</h2>
|
||||
<p className="text-muted" style={{ marginTop: 0 }}>
|
||||
Fachliche Personen unabhängig vom Systemzugang – z. B. Jugendgruppe oder externe
|
||||
Helfer ohne Login. Ein Login (Tab „Benutzer") kann optional mit einer bestehenden
|
||||
Person verknüpft werden.
|
||||
</p>
|
||||
<div className="card">
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem", marginBottom: "0.75rem" }}>
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={geburtsdatum}
|
||||
onChange={(e) => setGeburtsdatum(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={kontakt}
|
||||
onChange={(e) => setKontakt(e.target.value)}
|
||||
placeholder="Kontakt (optional)"
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
|
||||
Anlegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{personen.length === 0 ? (
|
||||
<p className="text-muted">Keine Personen erfasst.</p>
|
||||
) : (
|
||||
<ul className="card-list">
|
||||
{personen.map((p) => (
|
||||
<li key={p.id} style={{ padding: "0.4rem 0" }}>
|
||||
{bearbeiteteId === p.id ? (
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.5rem" }}>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsName}
|
||||
onChange={(e) => setBearbeitungsName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={bearbeitungsGeburtsdatum}
|
||||
onChange={(e) => setBearbeitungsGeburtsdatum(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={bearbeitungsKontakt}
|
||||
onChange={(e) => setBearbeitungsKontakt(e.target.value)}
|
||||
placeholder="Kontakt"
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => speichern(p.id)}>
|
||||
Speichern
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setBearbeiteteId(null)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row-between">
|
||||
<span>
|
||||
{p.name}
|
||||
{p.geburtsdatum && <span className="text-muted"> · geb. {p.geburtsdatum}</span>}
|
||||
{p.kontakt && <span className="text-muted"> · {p.kontakt}</span>}
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => bearbeiten(p)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user