fix(maintenance): MAINT-003 Wartung als Opt-in je Objekttyp
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:
1 parent
29b334578f
commit
d661354d69
11 files changed
+176
-3
No files matched your search
@@ -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 +++++++++++++++++++++++++++++++-
|
||||
|
||||
---
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user