diff --git a/DEVLOG.md b/DEVLOG.md index e2529b8..42531e4 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -15677,3 +15677,48 @@ Keine Commits in dieser Session. - arbeitskacheln/10_maintenance.md | 6 +----- --- +## 2026-09-13 23:05 – 23:08 (3m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +- e5bb213 fix(akte): Grid-Overlap bei Nicht-Fahrzeug-Objekten auf Desktop-Breite + +### Geänderte Dateien +- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/components/ZaehlerstandPanel.tsx | 63 +++++++++++++++++++++++++++++++++++++++------------------------ +- frontend/src/pages/AktePage.tsx | 12 +++++++----- +- frontend/src/styles/global/akte-grid.css | 4 ++++ +- frontend/src/styles/global/responsive.css | 12 ++++++++++++ + +--- +## 2026-09-13 23:10 – 23:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/components/ZaehlerstandPanel.tsx | 63 +++++++++++++++++++++++++++++++++++++++------------------------ +- frontend/src/pages/AktePage.tsx | 12 +++++++----- +- frontend/src/styles/global/akte-grid.css | 4 ++++ +- frontend/src/styles/global/responsive.css | 12 ++++++++++++ + +--- +## 2026-09-13 23:11 – 23:11 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/components/ZaehlerstandPanel.tsx | 63 +++++++++++++++++++++++++++++++++++++++------------------------ +- frontend/src/pages/AktePage.tsx | 12 +++++++----- +- frontend/src/styles/global/akte-grid.css | 4 ++++ +- frontend/src/styles/global/responsive.css | 12 ++++++++++++ + +--- diff --git a/arbeitskacheln/04_assets.md b/arbeitskacheln/04_assets.md index 1bcfeee..f3ddee5 100644 --- a/arbeitskacheln/04_assets.md +++ b/arbeitskacheln/04_assets.md @@ -386,12 +386,26 @@ Details zu ASSET-001 … ASSET-010. ASSET-010 ergänzt am 2026-09-09 aus Open-So Rückwärtslauf mit 409 ab außer bei `RolleTyp.administration`, legt `Fahrzeugdetails`-Zeile bei Bedarf automatisch an (auch für Nicht-Fahrzeuge). Audit-Log-Eintrag `zaehlerstand_aktualisiert`. Frontend: - neue eigenständige Komponente `ZaehlerstandPanel.tsx` in `AktePage.tsx`, - bewusst OHNE `istFahrzeug`-Gate (für jedes Objekt sichtbar). Tests: - Monoton-Validierung, Admin-Override, funktioniert nachweislich mit + neue eigenständige Komponente `ZaehlerstandPanel.tsx` in `AktePage.tsx`. + Tests: Monoton-Validierung, Admin-Override, funktioniert nachweislich mit `objekt_mit_position` (KEIN Fahrzeugtyp) — erfüllt exakt das Akzeptanzkriterium "Fahrzeug UND Nicht-Fahrzeug nutzen denselben Mechanismus". `backend/tests/test_fahrzeugdetails.py`. + **Bugfix 2026-09-13 (Nutzer-Fund per Screenshot):** "bewusst OHNE + istFahrzeug-Gate" oben war ein Fehler in der Praxis — das Panel war + dadurch in JEDER Akte sichtbar, auch bei einem Rettungsrucksack ohne + Kilometerstand/Betriebsstunden. Auf Opt-in je Objekttyp umgestellt + (gleiches Muster wie `ressourcenbuchung_aktiv`, ASSET-012): neues Feld + `Objekttyp.zaehler_typ` (Enum `keiner`/`kilometer`/`betriebsstunden`/ + `beide`, Migration `0058_objekttyp_zaehler_typ.py`, bestehende + Fahrzeug-Objekttypen automatisch auf `beide` migriert). `AkteRead. + zaehler_typ` steuert die bedingte Anzeige in `AktePage.tsx` UND welche(s) + Feld(er) `ZaehlerstandPanel.tsx` überhaupt zeigt (nicht mehr immer beide). + Admin-UI: Auswahlfeld „Zählerstand in der Akte" in `ObjekttypSection.tsx`. + Der `PATCH /objekte/{id}/zaehlerstand`-Endpunkt selbst bleibt ungegatet + (reine Anzeige-Einschränkung, kein Zugriffsschutz nötig). Test: + `test_akte_zaehler_typ_folgt_objekttyp_und_ist_default_keiner` + (`backend/tests/test_akte.py`). ## ASSET-011 — Externer Verleih mit Rückgabe-Tracking (P2, neu, Referenz: HiOrg-Server) diff --git a/backend/alembic/versions/0058_objekttyp_zaehler_typ.py b/backend/alembic/versions/0058_objekttyp_zaehler_typ.py new file mode 100644 index 0000000..f9bb6c7 --- /dev/null +++ b/backend/alembic/versions/0058_objekttyp_zaehler_typ.py @@ -0,0 +1,34 @@ +"""ASSET-009-Bugfix: Zählerstand-Panel war ungeachtet des Objekttyps in +jeder Akte sichtbar (auch bei Rettungsrucksäcken ohne km/Betriebsstunden). +Opt-in je Objekttyp, gleiches Muster wie ressourcenbuchung_aktiv. + +Revision ID: 0058_objekttyp_zaehler_typ +Revises: 0057_benutzer_standort_zugriff +Create Date: 2026-09-13 +""" + +from typing import Sequence, Union + +from alembic import op + +revision: str = "0058_objekttyp_zaehler_typ" +down_revision: Union[str, None] = "0057_benutzer_standort_zugriff" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("CREATE TYPE zaehler_typ AS ENUM ('keiner', 'kilometer', 'betriebsstunden', 'beide')") + op.execute( + "ALTER TABLE objekttyp ADD COLUMN zaehler_typ zaehler_typ NOT NULL DEFAULT 'keiner'" + ) + # Bestehende Fahrzeug-Objekttypen hatten das Zähler-Panel faktisch schon + # genutzt (Fahrzeugdetails.kilometerstand/betriebsstunden existieren seit + # Modul Fahrzeuge) - Migration setzt sie automatisch auf "beide", damit + # sich für diese produktiv genutzten Fälle nichts sichtbar ändert. + op.execute("UPDATE objekttyp SET zaehler_typ = 'beide' WHERE ist_zugfahrzeug = true") + + +def downgrade() -> None: + op.execute("ALTER TABLE objekttyp DROP COLUMN zaehler_typ") + op.execute("DROP TYPE zaehler_typ") diff --git a/backend/app/models/stammdaten.py b/backend/app/models/stammdaten.py index 4e4f233..8392b4b 100644 --- a/backend/app/models/stammdaten.py +++ b/backend/app/models/stammdaten.py @@ -7,6 +7,16 @@ from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base +class ZaehlerTyp(str, enum.Enum): + keiner = "keiner" + kilometer = "kilometer" + betriebsstunden = "betriebsstunden" + beide = "beide" + + +zaehler_typ_pg = PgEnum(ZaehlerTyp, name="zaehler_typ", create_type=False) + + class MaterialTyp(str, enum.Enum): standard = "standard" ablauf_charge = "ablauf_charge" @@ -57,6 +67,12 @@ class Objekttyp(Base): # (Verbrauchsmaterial-Rucksäcke etc.) ergibt eine Buchungskalender-Ansicht # keinen Sinn. Jetzt Opt-in je Objekttyp statt global erzwungen. ressourcenbuchung_aktiv: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + # ASSET-009 Bugfix (Nutzer-Fund 2026-09-13): Zählerstand-Panel war in + # JEDER Akte sichtbar, unabhängig vom Objekttyp - ein Rettungsrucksack + # hat weder Kilometerstand noch Betriebsstunden. Gleiches Opt-in-Muster + # 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) class Nummernschema(Base): diff --git a/backend/app/schemas/akte.py b/backend/app/schemas/akte.py index 5c7a378..ecd893b 100644 --- a/backend/app/schemas/akte.py +++ b/backend/app/schemas/akte.py @@ -41,6 +41,7 @@ class AkteRead(BaseModel): einsatzbereitschaft_gruende: list[str] objekttyp_name: str ressourcenbuchung_aktiv: bool + zaehler_typ: str standort_name: str fahrzeugdetails: FahrzeugdetailsRead | None zustaendige_benutzer: list[ZustaendigerBenutzer] diff --git a/backend/app/schemas/stammdaten.py b/backend/app/schemas/stammdaten.py index 3f93bb2..f9e5e20 100644 --- a/backend/app/schemas/stammdaten.py +++ b/backend/app/schemas/stammdaten.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, ConfigDict -from app.models.stammdaten import MaterialTyp +from app.models.stammdaten import MaterialTyp, ZaehlerTyp class BereichCreate(BaseModel): @@ -91,6 +91,7 @@ class ObjekttypCreate(BaseModel): name: str ist_zugfahrzeug: bool = False ressourcenbuchung_aktiv: bool = False + zaehler_typ: ZaehlerTyp = ZaehlerTyp.keiner class ObjekttypRead(ObjekttypCreate): @@ -104,6 +105,7 @@ class ObjekttypUpdate(BaseModel): name: str | None = None ist_zugfahrzeug: bool | None = None ressourcenbuchung_aktiv: bool | None = None + zaehler_typ: ZaehlerTyp | None = None class FachCreate(BaseModel): diff --git a/backend/app/services/akte.py b/backend/app/services/akte.py index f37c421..440d495 100644 --- a/backend/app/services/akte.py +++ b/backend/app/services/akte.py @@ -140,6 +140,7 @@ async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None: einsatzbereitschaft_gruende=readiness["gruende"], 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", standort_name=standort.name if standort else "", fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None, zustaendige_benutzer=zustaendige, diff --git a/backend/tests/test_akte.py b/backend/tests/test_akte.py index c61e3ce..6b0be1c 100644 --- a/backend/tests/test_akte.py +++ b/backend/tests/test_akte.py @@ -79,6 +79,30 @@ async def test_akte_ressourcenbuchung_aktiv_folgt_objekttyp_flag(client, objekt_ assert antwort.json()["ressourcenbuchung_aktiv"] is True +@pytest.mark.asyncio +async def test_akte_zaehler_typ_folgt_objekttyp_und_ist_default_keiner(client, objekt_mit_position, admin_user): + """ASSET-009-Bugfix (Nutzer-Fund 2026-09-13): Zählerstand-Panel war + ungeachtet des Objekttyps immer sichtbar (auch bei einem + Rettungsrucksack) - jetzt Opt-in je Objekttyp wie ressourcenbuchung_aktiv.""" + 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()["zaehler_typ"] == "keiner" + + objekttyp_id = objekt.objekttyp_id + aendern = await client.patch( + f"/api/v1/objekttypen/{objekttyp_id}", + json={"zaehler_typ": "betriebsstunden"}, + headers=auth_header(token), + ) + assert aendern.status_code == 200 + assert aendern.json()["zaehler_typ"] == "betriebsstunden" + + antwort = await client.get(f"/api/v1/akte/objekt/{objekt.id}", headers=auth_header(token)) + assert antwort.json()["zaehler_typ"] == "betriebsstunden" + + @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, diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 2dba658..0ff37b8 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -413,6 +413,8 @@ export interface Modell { name: string; } +export type ZaehlerTyp = "keiner" | "kilometer" | "betriebsstunden" | "beide"; + export interface Objekttyp { id: number; bereich_id: number; @@ -420,6 +422,7 @@ export interface Objekttyp { name: string; ist_zugfahrzeug: boolean; ressourcenbuchung_aktiv: boolean; + zaehler_typ: ZaehlerTyp; } export interface Nummernschema { @@ -478,6 +481,7 @@ export interface Akte { einsatzbereitschaft_gruende: string[]; objekttyp_name: string; ressourcenbuchung_aktiv: boolean; + zaehler_typ: ZaehlerTyp; standort_name: string; fahrzeugdetails: Fahrzeugdetails | null; zustaendige_benutzer: ZustaendigerBenutzer[]; diff --git a/frontend/src/components/ZaehlerstandPanel.tsx b/frontend/src/components/ZaehlerstandPanel.tsx index 5e5e040..95f6aac 100644 --- a/frontend/src/components/ZaehlerstandPanel.tsx +++ b/frontend/src/components/ZaehlerstandPanel.tsx @@ -1,19 +1,24 @@ import { useState } from "react"; import { apiRequest, ApiError } from "../api/client"; -import type { Fahrzeugdetails } from "../api/types"; +import type { Fahrzeugdetails, ZaehlerTyp } from "../api/types"; interface Props { objektId: number; + zaehlerTyp: ZaehlerTyp; fahrzeugdetails: Fahrzeugdetails | null; onAktualisiert: () => Promise; } -// ASSET-009: generischer Nutzungszähler - bewusst NICHT auf Fahrzeuge -// beschränkt (siehe Kachel-Nachtrag), deshalb hier ohne istFahrzeug-Prüfung -// immer angezeigt. Kilometerstand bleibt bei einem Nicht-Fahrzeug (z.B. -// Stromerzeuger) einfach ungenutzt/leer. -export function ZaehlerstandPanel({ objektId, fahrzeugdetails, onAktualisiert }: Props) { +// ASSET-009-Bugfix (Nutzer-Fund 2026-09-13): war ungeachtet des Objekttyps +// IMMER sichtbar, z.B. auch bei einem Rettungsrucksack ohne Kilometerstand/ +// Betriebsstunden - jetzt Opt-in über Objekttyp.zaehler_typ (gleiches +// Muster wie ressourcenbuchung_aktiv), AktePage rendert diese Komponente +// nur noch bei zaehler_typ !== "keiner". Zeigt zusätzlich nur das/die +// tatsächlich konfigurierte(n) Feld(er) statt immer beide. +export function ZaehlerstandPanel({ objektId, zaehlerTyp, fahrzeugdetails, onAktualisiert }: Props) { + const zeigtKilometer = zaehlerTyp === "kilometer" || zaehlerTyp === "beide"; + const zeigtBetriebsstunden = zaehlerTyp === "betriebsstunden" || zaehlerTyp === "beide"; const [kilometerstand, setKilometerstand] = useState(""); const [betriebsstunden, setBetriebsstunden] = useState(""); const [wirdGespeichert, setWirdGespeichert] = useState(false); @@ -49,44 +54,52 @@ export function ZaehlerstandPanel({ objektId, fahrzeugdetails, onAktualisiert }:

Zählerstand

-
- Kilometerstand: - {fahrzeugdetails?.kilometerstand ?? "–"} km -
-
- Betriebsstunden: - {fahrzeugdetails?.betriebsstunden ?? "–"} h -
+ {zeigtKilometer && ( +
+ Kilometerstand: + {fahrzeugdetails?.kilometerstand ?? "–"} km +
+ )} + {zeigtBetriebsstunden && ( +
+ Betriebsstunden: + {fahrzeugdetails?.betriebsstunden ?? "–"} h +
+ )}
{/* Card kann schmal sein (Sidebar-Layout) - Felder untereinander statt in einer Reihe, sonst schneidet der Platzhaltertext ab bzw. überlappt der Zahlen-Spinner den Text (Bug-Fund per Screenshot). */}
-
- - setKilometerstand(e.target.value)} - /> -
-
- - setBetriebsstunden(e.target.value)} - /> -
+ {zeigtKilometer && ( +
+ + setKilometerstand(e.target.value)} + /> +
+ )} + {zeigtBetriebsstunden && ( +
+ + setBetriebsstunden(e.target.value)} + /> +
+ )} @@ -243,6 +283,9 @@ export function ObjekttypSection({ {o.name} {o.ist_zugfahrzeug && (Zugfahrzeug)} {o.ressourcenbuchung_aktiv && (Buchungen)} + {o.zaehler_typ !== "keiner" && ( + ({ZAEHLER_TYP_LABEL[o.zaehler_typ]}) + )}