fix(maintenance): MAINT-003 Wartung als Opt-in je Objekttyp
CI / backend-tests (push) Successful in 3m41s
CI / frontend-build (push) Successful in 23s

Nutzer-Fund: Wartungsauftrag war für JEDES Objekt anlegbar/sichtbar, auch
reine Bestandsobjekte (Rettungsrucksack), die nur Inventur/Kontrolle
brauchen. Neues Feld Objekttyp.wartung_aktiv (Migration 0059, bestehende
Objekttypen mit Wartungsplan oder ist_zugfahrzeug automatisch aktiviert)
steuert jetzt, ob WartungSection in der Akte überhaupt erscheint - gleiches
Opt-in-Muster wie ressourcenbuchung_aktiv/zaehler_typ.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-14 09:40:26 +02:00
1 parent 29b334578f
commit d661354d69
11 files changed
+176 -3

No files matched your search

+66
View File
@@ -15722,3 +15722,69 @@ Keine Commits in dieser Session.
- frontend/src/styles/global/responsive.css | 12 ++++++++++++
---
## 2026-09-13 23:14 – 23:20 (6m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- 29b3345 fix(assets): ASSET-009 Zählerstand-Panel als Opt-in je Objekttyp
### Geänderte Dateien
- DEVLOG.md | 45 ++++++++++++++++++++++++++++++++
- arbeitskacheln/04_assets.md | 20 +++++++++++---
- backend/alembic/versions/0058_objekttyp_zaehler_typ.py | 34 ++++++++++++++++++++++++
- backend/app/models/stammdaten.py | 16 ++++++++++++
- backend/app/schemas/akte.py | 1 +
- backend/app/schemas/stammdaten.py | 4 ++-
- backend/app/services/akte.py | 1 +
- backend/tests/test_akte.py | 24 +++++++++++++++++
- frontend/src/api/types.ts | 4 +++
- frontend/src/components/ZaehlerstandPanel.tsx | 91 ++++++++++++++++++++++++++++++++++++----------------------------
- frontend/src/pages/AktePage.tsx | 17 +++++++-----
- frontend/src/pages/admin/ObjekttypSection.tsx | 45 +++++++++++++++++++++++++++++++-
---
## 2026-09-13 23:23 – 23:23 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 45 ++++++++++++++++++++++++++++++++
- arbeitskacheln/04_assets.md | 20 +++++++++++---
- backend/alembic/versions/0058_objekttyp_zaehler_typ.py | 34 ++++++++++++++++++++++++
- backend/app/models/stammdaten.py | 16 ++++++++++++
- backend/app/schemas/akte.py | 1 +
- backend/app/schemas/stammdaten.py | 4 ++-
- backend/app/services/akte.py | 1 +
- backend/tests/test_akte.py | 24 +++++++++++++++++
- frontend/src/api/types.ts | 4 +++
- frontend/src/components/ZaehlerstandPanel.tsx | 91 ++++++++++++++++++++++++++++++++++++----------------------------
- frontend/src/pages/AktePage.tsx | 17 +++++++-----
- frontend/src/pages/admin/ObjekttypSection.tsx | 45 +++++++++++++++++++++++++++++++-
---
## 2026-09-13 23:26 – 23:26 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 45 ++++++++++++++++++++++++++++++++
- arbeitskacheln/04_assets.md | 20 +++++++++++---
- backend/alembic/versions/0058_objekttyp_zaehler_typ.py | 34 ++++++++++++++++++++++++
- backend/app/models/stammdaten.py | 16 ++++++++++++
- backend/app/schemas/akte.py | 1 +
- backend/app/schemas/stammdaten.py | 4 ++-
- backend/app/services/akte.py | 1 +
- backend/tests/test_akte.py | 24 +++++++++++++++++
- frontend/src/api/types.ts | 4 +++
- frontend/src/components/ZaehlerstandPanel.tsx | 91 ++++++++++++++++++++++++++++++++++++----------------------------
- frontend/src/pages/AktePage.tsx | 17 +++++++-----
- frontend/src/pages/admin/ObjekttypSection.tsx | 45 +++++++++++++++++++++++++++++++-
---
+14
View File
@@ -85,6 +85,20 @@ ebenfalls P3/offen.
+ werkstatt_name** (beantwortet die Nutzerfrage nach externer Werkstatt),
Kosten optional. Zyklus offen→in_arbeit→erledigt getestet, inkl. ad-hoc-
Auftrag ohne Plan.
**Korrektur 2026-09-13 (Nutzer-Fund):** "jedes Objekt kann einen Wartungs-
auftrag bekommen" war in der Praxis unerwünscht — ein reines
Bestandsobjekt (Rettungsrucksack) braucht nur Inventur/Kontrolle, keine
Wartung. Auf Opt-in je Objekttyp umgestellt (`Objekttyp.wartung_aktiv`,
Migration `0059_objekttyp_wartung_aktiv.py`, gleiches Muster wie
`ressourcenbuchung_aktiv`/`zaehler_typ`) — bestehende Objekttypen mit
Wartungsplan ODER `ist_zugfahrzeug=true` automatisch aktiviert, damit sich
für produktiv genutzte Fälle nichts ändert. `AktePage.tsx` zeigt
`WartungSection` nur noch bei `akte.wartung_aktiv`. Backend-Endpunkte
selbst bleiben ungegatet (reine Anzeige-Einschränkung, wie bei den anderen
Opt-in-Flags). Admin-UI: Checkbox „Wartung in der Akte anzeigen" in
`ObjekttypSection.tsx`. Test:
`test_akte_wartung_aktiv_folgt_objekttyp_und_ist_default_false`
(`backend/tests/test_akte.py`).
## MAINT-004 — Wartungshistorie
@@ -0,0 +1,36 @@
"""Nutzer-Korrektur zu MAINT-003: Wartung war als universelles Ad-hoc-
Reparatur-Ticket für JEDES Objekt gedacht - in der Praxis unerwünscht für
reine Bestandsobjekte (Rucksäcke etc.), die nur Inventur/Kontrolle
brauchen. Opt-in je Objekttyp, gleiches Muster wie ressourcenbuchung_aktiv.
Revision ID: 0059_objekttyp_wartung_aktiv
Revises: 0058_objekttyp_zaehler_typ
Create Date: 2026-09-13
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0059_objekttyp_wartung_aktiv"
down_revision: Union[str, None] = "0058_objekttyp_zaehler_typ"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute("ALTER TABLE objekttyp ADD COLUMN wartung_aktiv BOOLEAN NOT NULL DEFAULT false")
# Objekttypen mit bereits bestehendem Wartungsplan ODER Fahrzeuge nutzen
# Wartung nachweislich produktiv - automatisch aktiviert, damit sich für
# diese Fälle nichts sichtbar ändert.
op.execute(
"""
UPDATE objekttyp SET wartung_aktiv = true
WHERE ist_zugfahrzeug = true
OR id IN (SELECT DISTINCT objekttyp_id FROM wartungsplan)
"""
)
def downgrade() -> None:
op.execute("ALTER TABLE objekttyp DROP COLUMN wartung_aktiv")
+6
View File
@@ -73,6 +73,12 @@ class Objekttyp(Base):
# wie ressourcenbuchung_aktiv, hier als Enum statt bool, da die Kachel
# selbst zwischen km/Betriebsstunden/beidem/keinem unterscheidet.
zaehler_typ: Mapped[ZaehlerTyp] = mapped_column(zaehler_typ_pg, nullable=False, default=ZaehlerTyp.keiner)
# Nutzer-Entscheidung 2026-09-13 (Korrektur zu MAINT-003): Wartung war
# bewusst als universelles Ad-hoc-Reparatur-Ticket für JEDES Objekt
# gedacht - in der Praxis aber unerwünscht für reine Bestandsobjekte
# (Rucksäcke etc.), die nur Inventur/Kontrolle brauchen, keine Wartung.
# Gleiches Opt-in-Muster wie ressourcenbuchung_aktiv/zaehler_typ.
wartung_aktiv: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class Nummernschema(Base):
+1
View File
@@ -42,6 +42,7 @@ class AkteRead(BaseModel):
objekttyp_name: str
ressourcenbuchung_aktiv: bool
zaehler_typ: str
wartung_aktiv: bool
standort_name: str
fahrzeugdetails: FahrzeugdetailsRead | None
zustaendige_benutzer: list[ZustaendigerBenutzer]
+2
View File
@@ -92,6 +92,7 @@ class ObjekttypCreate(BaseModel):
ist_zugfahrzeug: bool = False
ressourcenbuchung_aktiv: bool = False
zaehler_typ: ZaehlerTyp = ZaehlerTyp.keiner
wartung_aktiv: bool = False
class ObjekttypRead(ObjekttypCreate):
@@ -106,6 +107,7 @@ class ObjekttypUpdate(BaseModel):
ist_zugfahrzeug: bool | None = None
ressourcenbuchung_aktiv: bool | None = None
zaehler_typ: ZaehlerTyp | None = None
wartung_aktiv: bool | None = None
class FachCreate(BaseModel):
+1
View File
@@ -141,6 +141,7 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
objekttyp_name=objekttyp.name if objekttyp else "",
ressourcenbuchung_aktiv=objekttyp.ressourcenbuchung_aktiv if objekttyp else False,
zaehler_typ=objekttyp.zaehler_typ.value if objekttyp else "keiner",
wartung_aktiv=objekttyp.wartung_aktiv if objekttyp else False,
standort_name=standort.name if standort else "",
fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None,
zustaendige_benutzer=zustaendige,
+24
View File
@@ -103,6 +103,30 @@ async def test_akte_zaehler_typ_folgt_objekttyp_und_ist_default_keiner(client, o
assert antwort.json()["zaehler_typ"] == "betriebsstunden"
@pytest.mark.asyncio
async def test_akte_wartung_aktiv_folgt_objekttyp_und_ist_default_false(client, objekt_mit_position, admin_user):
"""Nutzer-Korrektur zu MAINT-003 (2026-09-13): Wartung war universell für
jedes Objekt sichtbar - jetzt Opt-in je Objekttyp, ein reines
Bestandsobjekt (Rucksack) braucht nur Inventur/Kontrolle, keine Wartung."""
objekt, _material = objekt_mit_position
token = await login(client, "admin1")
ohne_aenderung = await client.get(f"/api/v1/akte/objekt/{objekt.id}", headers=auth_header(token))
assert ohne_aenderung.json()["wartung_aktiv"] is False
objekttyp_id = objekt.objekttyp_id
aendern = await client.patch(
f"/api/v1/objekttypen/{objekttyp_id}",
json={"wartung_aktiv": True},
headers=auth_header(token),
)
assert aendern.status_code == 200
assert aendern.json()["wartung_aktiv"] is True
antwort = await client.get(f"/api/v1/akte/objekt/{objekt.id}", headers=auth_header(token))
assert antwort.json()["wartung_aktiv"] is True
@pytest.mark.asyncio
async def test_akte_readiness_unbekannt_bei_nie_kontrolliert(client, objekt_mit_position, admin_user):
"""UI-005: ein Objekt ohne jede Auffälligkeit, aber noch nie kontrolliert,
+2
View File
@@ -423,6 +423,7 @@ export interface Objekttyp {
ist_zugfahrzeug: boolean;
ressourcenbuchung_aktiv: boolean;
zaehler_typ: ZaehlerTyp;
wartung_aktiv: boolean;
}
export interface Nummernschema {
@@ -482,6 +483,7 @@ export interface Akte {
objekttyp_name: string;
ressourcenbuchung_aktiv: boolean;
zaehler_typ: ZaehlerTyp;
wartung_aktiv: boolean;
standort_name: string;
fahrzeugdetails: Fahrzeugdetails | null;
zustaendige_benutzer: ZustaendigerBenutzer[];
+5 -3
View File
@@ -279,9 +279,11 @@ export function AktePage() {
</div>
)}
<div className="akte-wartung">
<WartungSection objektId={objekt.id} onFehler={setFehler} />
</div>
{akte.wartung_aktiv && (
<div className="akte-wartung">
<WartungSection objektId={objekt.id} onFehler={setFehler} />
</div>
)}
<div className="akte-maengel card">
<h3 style={{ marginTop: 0 }}>Mängel</h3>
@@ -40,6 +40,8 @@ export function ObjekttypSection({
const [bearbeitungsRessourcenbuchung, setBearbeitungsRessourcenbuchung] = useState(false);
const [zaehlerTyp, setZaehlerTyp] = useState<ZaehlerTyp>("keiner");
const [bearbeitungsZaehlerTyp, setBearbeitungsZaehlerTyp] = useState<ZaehlerTyp>("keiner");
const [wartungAktiv, setWartungAktiv] = useState(false);
const [bearbeitungsWartung, setBearbeitungsWartung] = useState(false);
const [schemaBearbeitenId, setSchemaBearbeitenId] = useState<number | null>(null);
const [schemaPraefix, setSchemaPraefix] = useState("");
const [schemaStellenzahl, setSchemaStellenzahl] = useState(5);
@@ -55,6 +57,7 @@ export function ObjekttypSection({
setBearbeitungsZugfahrzeug(o.ist_zugfahrzeug);
setBearbeitungsRessourcenbuchung(o.ressourcenbuchung_aktiv);
setBearbeitungsZaehlerTyp(o.zaehler_typ);
setBearbeitungsWartung(o.wartung_aktiv);
}
async function speichern(id: number) {
@@ -66,6 +69,7 @@ export function ObjekttypSection({
ist_zugfahrzeug: bearbeitungsZugfahrzeug,
ressourcenbuchung_aktiv: bearbeitungsRessourcenbuchung,
zaehler_typ: bearbeitungsZaehlerTyp,
wartung_aktiv: bearbeitungsWartung,
},
});
setBearbeiteteId(null);
@@ -92,12 +96,14 @@ export function ObjekttypSection({
ist_zugfahrzeug: istZugfahrzeug,
ressourcenbuchung_aktiv: ressourcenbuchungAktiv,
zaehler_typ: zaehlerTyp,
wartung_aktiv: wartungAktiv,
},
});
setName("");
setIstZugfahrzeug(false);
setRessourcenbuchungAktiv(false);
setZaehlerTyp("keiner");
setWartungAktiv(false);
await onGeaendert();
} catch {
onFehler("Objekttyp konnte nicht angelegt werden.");
@@ -202,6 +208,10 @@ export function ObjekttypSection({
/>
Ressourcenbuchung in der Akte anzeigen
</label>
<label className="row" style={{ alignItems: "center", gap: "0.4rem", marginTop: "1.5rem" }}>
<input type="checkbox" checked={wartungAktiv} onChange={(e) => setWartungAktiv(e.target.checked)} />
Wartung in der Akte anzeigen
</label>
<FormField label="Zählerstand in der Akte">
{(feldProps) => (
<select
@@ -259,6 +269,14 @@ export function ObjekttypSection({
/>
Ressourcenbuchung
</label>
<label className="row" style={{ alignItems: "center", gap: "0.4rem" }}>
<input
type="checkbox"
checked={bearbeitungsWartung}
onChange={(e) => setBearbeitungsWartung(e.target.checked)}
/>
Wartung
</label>
<select
className="input"
value={bearbeitungsZaehlerTyp}
@@ -283,6 +301,7 @@ export function ObjekttypSection({
{o.name}
{o.ist_zugfahrzeug && <span className="text-muted"> (Zugfahrzeug)</span>}
{o.ressourcenbuchung_aktiv && <span className="text-muted"> (Buchungen)</span>}
{o.wartung_aktiv && <span className="text-muted"> (Wartung)</span>}
{o.zaehler_typ !== "keiner" && (
<span className="text-muted"> ({ZAEHLER_TYP_LABEL[o.zaehler_typ]})</span>
)}