feat(wartung): MAINT-005 Ersatzteile/Kosten je Wartungsauftrag
CI / backend-tests (push) Successful in 2m22s
CI / frontend-build (push) Successful in 25s

Neue Tabelle wartungsauftrag_teil (Migration 0033), CRUD-Endpunkte
/wartungsauftraege/{id}/teile, UI-Panel in WartungSection.tsx mit
Summenanzeige. Backlog Epic 22 (UI-Redesign Runde 2, Kacheln UI2-001..005)
als geplant ergänzt, Umsetzung folgt separat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-08 22:06:02 +02:00
co-authored by Claude Sonnet 5
parent 3b5cf8feab
commit 847c303a97
14 changed files with 5232 additions and 6 deletions
+11
View File
@@ -18,3 +18,14 @@ Keine Commits in dieser Session.
- frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 11 ++++----- - frontend/src/pages/dashboard/tiles/MaengelTile.tsx | 11 ++++-----
--- ---
## 2026-09-08 01:27 01:29 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- arbeitskacheln/00_index.md | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------------
---
+4565
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -195,7 +195,7 @@ vergeben statt geraten.
| MAINT-002 | Maintenance | Wartungsintervalle | P2 | S | MEDIUM | MAINT-001, ASSET-009 | ✅ (`services/wartung.py` referenziert MAINT-002, Zeit-/km-/Betriebsstunden-Intervalle) | | MAINT-002 | Maintenance | Wartungsintervalle | P2 | S | MEDIUM | MAINT-001, ASSET-009 | ✅ (`services/wartung.py` referenziert MAINT-002, Zeit-/km-/Betriebsstunden-Intervalle) |
| MAINT-003 | Maintenance | Wartungsauftrag | P2 | M | MEDIUM | MAINT-002 | ✅ (`Wartungsauftrag`, `wartungsauftraege`-Endpunkte) | | MAINT-003 | Maintenance | Wartungsauftrag | P2 | M | MEDIUM | MAINT-002 | ✅ (`Wartungsauftrag`, `wartungsauftraege`-Endpunkte) |
| MAINT-004 | Maintenance | Wartungshistorie | P2 | S | LOW | MAINT-003, FOUND-006 | ✅ (Wartungsauftrag-Status/Erledigen-Verlauf, `WartungsplanSection.tsx`) | | MAINT-004 | Maintenance | Wartungshistorie | P2 | S | LOW | MAINT-003, FOUND-006 | ✅ (Wartungsauftrag-Status/Erledigen-Verlauf, `WartungsplanSection.tsx`) |
| MAINT-005 | Maintenance | Ersatzteile/Kosten | P3 | M | MEDIUM | MAINT-003 | (kein Ersatzteil-/Kosten-Feld gefunden) | | MAINT-005 | Maintenance | Ersatzteile/Kosten | P3 | M | MEDIUM | MAINT-003 | (`WartungsauftragTeil`, Migration 0033, `/wartungsauftraege/{id}/teile`-CRUD, UI in `WartungSection.tsx`) |
| MAINT-006 | Maintenance | Wartungsdokument-Anbindung | P2 | XS | LOW | MAINT-003, DOC-001 | ✅ (Dokument polymorph, deckt Wartungsauftrag ab) | | MAINT-006 | Maintenance | Wartungsdokument-Anbindung | P2 | XS | LOW | MAINT-003, DOC-001 | ✅ (Dokument polymorph, deckt Wartungsauftrag ab) |
| DEFECT-001 | Defects | Mangelmeldung + Foto | P1 | M | LOW | ASSET-003, DOC-001 | ✅ (`mangel.py`, `MangelListePage.tsx`, Dokument-Anhang möglich) | | DEFECT-001 | Defects | Mangelmeldung + Foto | P1 | M | LOW | ASSET-003, DOC-001 | ✅ (`mangel.py`, `MangelListePage.tsx`, Dokument-Anhang möglich) |
| DEFECT-002 | Defects | Priorität & Status-Workflow | P1 | S | LOW | DEFECT-001 | ✅ (`MangelStatus`, `MangelPrioritaet` Enums) | | DEFECT-002 | Defects | Priorität & Status-Workflow | P1 | S | LOW | DEFECT-001 | ✅ (`MangelStatus`, `MangelPrioritaet` Enums) |
@@ -307,10 +307,13 @@ eigenes Modul (MAINT-\*, MABEA hat nur Prüfung, keine Wartung)**.
| `18_notifications.md` | NOTIF-001 … NOTIF-002 (vollständig, nachträglich ergänzt) | | `18_notifications.md` | NOTIF-001 … NOTIF-002 (vollständig, nachträglich ergänzt) |
| `19_satelliten_server.md` | SAT-001 (Platzhalter, nachträglich ergänzt, bewusst zurückgestellt) | | `19_satelliten_server.md` | SAT-001 (Platzhalter, nachträglich ergänzt, bewusst zurückgestellt) |
| `20_ui_redesign.md` | UI-001 … UI-008 (vollständig umgesetzt, 2026-09-06) | | `20_ui_redesign.md` | UI-001 … UI-008 (vollständig umgesetzt, 2026-09-06) |
| `21_akte_redesign.md` | AKTE-001 … AKTE-003 (vollständig umgesetzt, 2026-09-06) |
| `22_ui_redesign_v2.md` | UI2-001 … UI2-005 (offen, ergänzt 2026-09-08, Grundlage: Subagenten-Analyse ai-coding-starter-kit-v2) |
**Alle 20 Epics durchgegangen** (16 ursprüngliche + 17 Zuständigkeit/18 Notifications/19 **Alle 22 Epics durchgegangen** (16 ursprüngliche + 17 Zuständigkeit/18 Notifications/19
Satelliten-Server/20 UI-Redesign, nachträglich ergänzt). Detaillierungsphase Satelliten-Server/20 UI-Redesign/21 Akte-Redesign/22 UI-Redesign Runde 2, nachträglich
abgeschlossen (2026-09-05), UI-Redesign-Epic ergänzt (2026-09-06). ergänzt). Detaillierungsphase abgeschlossen (2026-09-05), UI-Redesign-Epic ergänzt
(2026-09-06), Runde 2 ergänzt (2026-09-08).
## Nachtrag: Externe Roadmap-Bewertung Shelf/Resgrid/Emergency Mgmt (2026-09-05) ## Nachtrag: Externe Roadmap-Bewertung Shelf/Resgrid/Emergency Mgmt (2026-09-05)
+107
View File
@@ -0,0 +1,107 @@
# Epic 22 — UI-Redesign Runde 2 (Patterns aus ai-coding-starter-kit-v2)
Ausgangspunkt: Subagenten-Analyse von `gitea.perlbach24.de/scripte/ai-coding-starter-kit-v2`
(2026-09-08), reines shadcn/ui-Komponenten-Skelett (Vite/React/TS, Tailwind v3, Radix,
react-hook-form+zod). Kein Framework-Wechsel, kein Neubau — Epic 20 (UI-001..008) bleibt
Grundlage, hier werden zusätzliche, im Code konkret nachgewiesene Lücken geschlossen.
**Kein kompletter Frontend-Neubau** — bewusste Entscheidung, siehe Chat 2026-09-08.
---
## UI2-001 — Granularere Design-Tokens (Card/Popover/Sidebar)
- **Ziel:** Card-/Popover-/Sidebar-Hintergründe über eigene Tokens statt generischem
`--color-primary`/`bg-white`.
- **Beschreibung:** `--color-card`, `--color-card-foreground`, `--color-popover` etc.
in `frontend/src/styles/global/tokens.css` ergänzen (Light+Dark), bestehende
Komponenten schrittweise umstellen.
- **Benutzerwert:** Konsistentere Flächenhierarchie, einfacheres Dark-Mode-Feintuning.
- **Abhängigkeiten:** UI-001 (bereits erledigt)
- **Backend:** keins
- **Frontend:** `styles/global/tokens.css`
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** neue Tokens vorhanden, mind. Card-Komponente nutzt sie statt
Hardcoded-Werten.
- **Tests:** keine (reines Styling).
- **DoD:** offen.
## UI2-002 — AdminPage Sidebar/Sheet-Umschaltung
- **Ziel:** Touch-taugliche Navigation für AdminPage-Tabs — Desktop Sidebar, Mobile
Sheet-Overlay statt aktuellem Tab-Leisten-Umbruch.
- **Beschreibung:** Pattern aus Starter-Kit (`useIsMobile`-Hook + Radix-Dialog-Sheet),
MABEA hat bereits `useIsDesktop` — Umschaltung analog, kein natives Drag&Drop.
- **Benutzerwert:** Bessere Bedienbarkeit auf Tablet im Feld.
- **Abhängigkeiten:** UI2-001
- **Backend:** keins
- **Frontend:** `AdminPage.tsx`, ggf. neue `components/ui/sheet.tsx`
- **Mobile:** Priorität — Zielgerät Tablet
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** AdminPage-Tabs auf schmalem Viewport über Sheet erreichbar,
bestehende Tab-Inhalte unverändert.
- **Tests:** keine neuen (reines UI/Routing).
- **DoD:** offen.
## UI2-003 — Formular-Feld-Komposition
- **Ziel:** Wiederverwendbarer Feld-Baustein (Label+Input+Fehlermeldung) statt
individueller State-Verdrahtung pro Feld in Formularen.
- **Beschreibung:** Leichtgewichtiges `FormField`-Pattern (kein react-hook-form/zod
nötig, MABEA nutzt bereits einfache `useState`-Formulare — nur Layout/Fehler-
Darstellung vereinheitlichen), zuerst in `ObjektAnlegenFormular.tsx` und
`ObjekttypSection.tsx` angewendet.
- **Benutzerwert:** Einheitliche Fehlerdarstellung, weniger Boilerplate bei neuen
Formularen.
- **Abhängigkeiten:** UI2-001
- **Backend:** keins
- **Frontend:** neue `components/ui/form-field.tsx`, `ObjektAnlegenFormular.tsx`,
`ObjekttypSection.tsx` darauf umgestellt
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** beide Formulare nutzen den neuen Baustein, Verhalten
unverändert.
- **Tests:** bestehende Vitest-Tests bleiben grün.
- **DoD:** offen.
## UI2-004 — Skeleton-Loading für Dashboard-Kacheln
- **Ziel:** Skeleton-Platzhalter statt Spinner/nichts beim Nachladen von
Dashboard-Kacheln.
- **Beschreibung:** Einfache `animate-pulse`-Komponente (`components/ui/skeleton.tsx`),
in Dashboard-Tiles während Ladezustand eingesetzt.
- **Benutzerwert:** Wahrgenommene Ladezeit sinkt, weniger Layout-Sprung.
- **Abhängigkeiten:** UI2-001
- **Backend:** keins
- **Frontend:** neue `components/ui/skeleton.tsx`, Dashboard-Tiles (`*Tile.tsx`)
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** sichtbarer Skeleton-Zustand vor Dateneintreffen, kein
Layout-Sprung beim Einblenden der echten Daten.
- **Tests:** keine neuen.
- **DoD:** offen.
## UI2-005 — Scrollbarer Table-Wrapper für Materiallisten
- **Ziel:** Materiallisten/Kontroll-Tabellen brechen auf schmalem Tablet-Viewport
nicht das Seiten-Layout, sondern scrollen innerhalb eines eigenen Containers.
- **Beschreibung:** Gemeinsamer Table-Wrapper (`overflow-x: auto`) statt Ad-hoc-Lösung
je Seite, zuerst in `KontrollPage.tsx`-Listen angewendet.
- **Benutzerwert:** Kein horizontales Seiten-Scrollen mehr auf kleinen Screens.
- **Abhängigkeiten:** UI2-001
- **Backend:** keins
- **Frontend:** `KontrollPage.tsx` bzw. betroffene Listen-Komponenten
- **Mobile:** Priorität — Tablet-Ziel
- **Rechte:** keine Änderung
- **Akzeptanzkriterien:** Tabelle scrollt intern, Seite selbst nie horizontal.
- **Tests:** keine neuen.
- **DoD:** offen.
---
**Reihenfolge (empfohlen, nicht blockierend zwischen 002-005):** UI2-001 zuerst
(Basis für alle), danach UI2-002..005 in beliebiger Reihenfolge nach Nutzerpriorität.
**Nicht-Ziele:** kein Wechsel auf react-hook-form/zod/Radix, kein kompletter
Frontend-Neubau, keine Breaking Changes an bestehender Fachlogik/API.
## Referenzen
Subagenten-Analyse `ai-coding-starter-kit-v2`, Chat 2026-09-08 (kein Frontend-Neubau,
Patterns schrittweise als Kacheln übernehmen).
+50
View File
@@ -388,3 +388,53 @@ Keine Commits in dieser Session.
- frontend/src/pages/AktePage.tsx | 11 +++++++++++ - frontend/src/pages/AktePage.tsx | 11 +++++++++++
--- ---
## 2026-09-08 10:35 10:39 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- arbeitskacheln/00_index.md | 2 +-
- backend/alembic/versions/0026_dokumenttyp.py | 33 +++++++++++++++++++++++++++++++++
- backend/app/api/v1/endpoints/dokument.py | 9 +++++++--
- backend/app/models/dokument.py | 22 +++++++++++++++++++++-
- backend/app/schemas/dokument.py | 3 +++
- backend/app/services/dokument.py | 17 ++++++++++-------
- backend/tests/test_dokument.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
- frontend/src/api/client.ts | 2 ++
- frontend/src/api/types.ts | 9 +++++++++
- frontend/src/components/DokumentePanel.tsx | 52 ++++++++++++++++++++++++++++++++++++++++++++++++----
---
## 2026-09-08 10:43 10:44 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .gitignore | 1 +
- Handball Rucksack.xlsx | Bin 12700 -> 0 bytes
- Rettungsrucksack.xlsx | Bin 12322 -> 0 bytes
- Rucksack PAX SEG.xlsx | Bin 36047 -> 0 bytes
- backend/tests/test_dokument.py | 10 +++++++---
---
## 2026-09-08 10:45 10:46 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** backend
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .gitignore | 1 +
- Handball Rucksack.xlsx | Bin 12700 -> 0 bytes
- Rettungsrucksack.xlsx | Bin 12322 -> 0 bytes
- Rucksack PAX SEG.xlsx | Bin 36047 -> 0 bytes
- backend/tests/test_dokument.py | 10 +++++++---
---
@@ -0,0 +1,33 @@
"""MAINT-005: Ersatzteile/Kosten je Wartungsauftrag.
Revision ID: 0033_wartungsauftrag_teil
Revises: 0032_nummernschema
Create Date: 2026-09-08
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0033_wartungsauftrag_teil"
down_revision: Union[str, None] = "0032_nummernschema"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"""
CREATE TABLE wartungsauftrag_teil (
id UUID PRIMARY KEY,
auftrag_id UUID NOT NULL REFERENCES wartungsauftrag(id),
bezeichnung TEXT NOT NULL,
menge NUMERIC NOT NULL DEFAULT 1,
kosten NUMERIC NOT NULL
)
"""
)
op.execute("CREATE INDEX ix_wartungsauftrag_teil_auftrag_id ON wartungsauftrag_teil (auftrag_id)")
def downgrade() -> None:
op.execute("DROP TABLE wartungsauftrag_teil")
+48 -1
View File
@@ -8,26 +8,31 @@ from app.api.deps import get_current_user, require_roles
from app.db.session import get_db from app.db.session import get_db
from app.models.auth import Benutzer, RolleTyp from app.models.auth import Benutzer, RolleTyp
from app.models.objekt import Objekt from app.models.objekt import Objekt
from app.models.wartung import Wartungsauftrag, Wartungsplan, WartungsauftragStatus from app.models.wartung import Wartungsauftrag, WartungsauftragTeil, Wartungsplan, WartungsauftragStatus
from app.schemas.wartung import ( from app.schemas.wartung import (
FaelligeWartungRead, FaelligeWartungRead,
WartungsauftragCreate, WartungsauftragCreate,
WartungsauftragErledigen, WartungsauftragErledigen,
WartungsauftragRead, WartungsauftragRead,
WartungsauftragStatusUpdate, WartungsauftragStatusUpdate,
WartungsauftragTeilCreate,
WartungsauftragTeilRead,
WartungsplanCreate, WartungsplanCreate,
WartungsplanPositionRead, WartungsplanPositionRead,
WartungsplanRead, WartungsplanRead,
) )
from app.services.wartung import ( from app.services.wartung import (
aendere_status, aendere_status,
entferne_wartungsauftrag_teil,
erledige_wartungsauftrag, erledige_wartungsauftrag,
erstelle_wartungsauftrag, erstelle_wartungsauftrag,
erstelle_wartungsauftrag_teil,
erstelle_wartungsplan, erstelle_wartungsplan,
faellige_wartungen_fuer_objekt, faellige_wartungen_fuer_objekt,
hole_intervalle, hole_intervalle,
hole_positionen_mit_intervallen, hole_positionen_mit_intervallen,
liste_wartungsauftraege, liste_wartungsauftraege,
liste_wartungsauftrag_teile,
) )
router = APIRouter() router = APIRouter()
@@ -151,3 +156,45 @@ async def route_erledigen(
kilometerstand_bei_erledigung=payload.kilometerstand_bei_erledigung, kilometerstand_bei_erledigung=payload.kilometerstand_bei_erledigung,
betriebsstunden_bei_erledigung=payload.betriebsstunden_bei_erledigung, betriebsstunden_bei_erledigung=payload.betriebsstunden_bei_erledigung,
) )
@router.get("/wartungsauftraege/{auftrag_id}/teile", response_model=list[WartungsauftragTeilRead])
async def liste_teile(
auftrag_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> list[WartungsauftragTeil]:
auftrag = await db.get(Wartungsauftrag, auftrag_id)
if auftrag is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wartungsauftrag nicht gefunden")
return await liste_wartungsauftrag_teile(db, auftrag_id=auftrag_id)
@router.post(
"/wartungsauftraege/{auftrag_id}/teile",
response_model=WartungsauftragTeilRead,
status_code=status.HTTP_201_CREATED,
)
async def erstelle_teil(
auftrag_id: uuid.UUID,
payload: WartungsauftragTeilCreate,
db: AsyncSession = Depends(get_db),
_=Depends(_verantwortliche),
) -> WartungsauftragTeil:
auftrag = await db.get(Wartungsauftrag, auftrag_id)
if auftrag is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Wartungsauftrag nicht gefunden")
return await erstelle_wartungsauftrag_teil(
db, auftrag_id=auftrag_id, bezeichnung=payload.bezeichnung, menge=payload.menge, kosten=payload.kosten
)
@router.delete("/wartungsauftraege/{auftrag_id}/teile/{teil_id}", status_code=status.HTTP_204_NO_CONTENT)
async def loesche_teil(
auftrag_id: uuid.UUID,
teil_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
_=Depends(_verantwortliche),
) -> None:
teil = await db.get(WartungsauftragTeil, teil_id)
if teil is None or teil.auftrag_id != auftrag_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Ersatzteil nicht gefunden")
await entferne_wartungsauftrag_teil(db, teil=teil)
+10
View File
@@ -78,3 +78,13 @@ class Wartungsauftrag(Base):
kosten: Mapped[Decimal | None] = mapped_column(Numeric) kosten: Mapped[Decimal | None] = mapped_column(Numeric)
kilometerstand_bei_erledigung: Mapped[int | None] = mapped_column(Integer) kilometerstand_bei_erledigung: Mapped[int | None] = mapped_column(Integer)
betriebsstunden_bei_erledigung: Mapped[Decimal | None] = mapped_column(Numeric) betriebsstunden_bei_erledigung: Mapped[Decimal | None] = mapped_column(Numeric)
class WartungsauftragTeil(Base):
__tablename__ = "wartungsauftrag_teil"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
auftrag_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("wartungsauftrag.id"), nullable=False)
bezeichnung: Mapped[str] = mapped_column(String, nullable=False)
menge: Mapped[Decimal] = mapped_column(Numeric, nullable=False, default=1)
kosten: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
+15
View File
@@ -91,6 +91,21 @@ class WartungsauftragRead(BaseModel):
betriebsstunden_bei_erledigung: Decimal | None betriebsstunden_bei_erledigung: Decimal | None
class WartungsauftragTeilCreate(BaseModel):
bezeichnung: str
menge: Decimal = Decimal(1)
kosten: Decimal
class WartungsauftragTeilRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
auftrag_id: uuid.UUID
bezeichnung: str
menge: Decimal
kosten: Decimal
class FaelligeWartungRead(BaseModel): class FaelligeWartungRead(BaseModel):
wartungsplan_position_id: int wartungsplan_position_id: int
wartungsart: str wartungsart: str
+22
View File
@@ -10,6 +10,7 @@ from app.models.wartung import (
IntervallTyp, IntervallTyp,
Wartungsauftrag, Wartungsauftrag,
WartungsauftragStatus, WartungsauftragStatus,
WartungsauftragTeil,
Wartungsintervall, Wartungsintervall,
Wartungsplan, Wartungsplan,
WartungsplanPosition, WartungsplanPosition,
@@ -135,6 +136,27 @@ async def liste_wartungsauftraege(
return list(result.scalars().all()) return list(result.scalars().all())
async def erstelle_wartungsauftrag_teil(
db: AsyncSession, *, auftrag_id, bezeichnung: str, menge: Decimal, kosten: Decimal
) -> WartungsauftragTeil:
teil = WartungsauftragTeil(auftrag_id=auftrag_id, bezeichnung=bezeichnung, menge=menge, kosten=kosten)
db.add(teil)
await db.flush()
return teil
async def liste_wartungsauftrag_teile(db: AsyncSession, *, auftrag_id) -> list[WartungsauftragTeil]:
result = await db.execute(
select(WartungsauftragTeil).where(WartungsauftragTeil.auftrag_id == auftrag_id)
)
return list(result.scalars().all())
async def entferne_wartungsauftrag_teil(db: AsyncSession, *, teil: WartungsauftragTeil) -> None:
await db.delete(teil)
await db.flush()
async def faellige_wartungen_fuer_objekt(db: AsyncSession, objekt: Objekt) -> list[dict]: async def faellige_wartungen_fuer_objekt(db: AsyncSession, objekt: Objekt) -> list[dict]:
"""MAINT-002: kombiniert Zeit-/km-/Betriebsstunden-Intervalle je Position - """MAINT-002: kombiniert Zeit-/km-/Betriebsstunden-Intervalle je Position -
fällig, sobald das erste Kriterium erreicht ist. Nie durchgeführt gilt fällig, sobald das erste Kriterium erreicht ist. Nie durchgeführt gilt
+70
View File
@@ -212,3 +212,73 @@ async def test_ad_hoc_reparatur_ohne_wartungsplan_mit_externer_werkstatt(
historie = await client.get(f"/api/v1/wartungsauftraege?objekt_id={objekt_id}", headers=auth_header(token)) historie = await client.get(f"/api/v1/wartungsauftraege?objekt_id={objekt_id}", headers=auth_header(token))
assert len(historie.json()) == 1 assert len(historie.json()) == 1
@pytest.mark.anyio
async def test_wartungsauftrag_teile_erfassen_und_summieren(
client, admin_user, standort_factory, objekt_factory
):
token = await login(client, "admin1")
standort = await standort_factory("Standort-W6")
await objekt_factory(name="RTW W6", code="RTW-W6", standort=standort)
objekte = await client.get("/api/v1/objekte", headers=auth_header(token))
objekt_id = [o for o in objekte.json() if o["code"] == "RTW-W6"][0]["id"]
auftrag = await client.post(
"/api/v1/wartungsauftraege",
json={"objekt_id": objekt_id, "wartungsart": "Ölwechsel"},
headers=auth_header(token),
)
auftrag_id = auftrag.json()["id"]
teil1 = await client.post(
f"/api/v1/wartungsauftraege/{auftrag_id}/teile",
json={"bezeichnung": "Ölfilter", "menge": "1", "kosten": "12.50"},
headers=auth_header(token),
)
assert teil1.status_code == 201
teil2 = await client.post(
f"/api/v1/wartungsauftraege/{auftrag_id}/teile",
json={"bezeichnung": "Motoröl 5L", "menge": "2", "kosten": "45.00"},
headers=auth_header(token),
)
assert teil2.status_code == 201
liste = await client.get(f"/api/v1/wartungsauftraege/{auftrag_id}/teile", headers=auth_header(token))
positionen = liste.json()
assert len(positionen) == 2
gesamt = sum(float(p["kosten"]) for p in positionen)
assert gesamt == 57.50
loeschen = await client.delete(
f"/api/v1/wartungsauftraege/{auftrag_id}/teile/{teil1.json()['id']}", headers=auth_header(token)
)
assert loeschen.status_code == 204
liste_danach = await client.get(f"/api/v1/wartungsauftraege/{auftrag_id}/teile", headers=auth_header(token))
assert len(liste_danach.json()) == 1
@pytest.mark.anyio
async def test_mitarbeiter_darf_wartungsauftrag_teil_nicht_anlegen(
client, admin_user, mitarbeiter_user, standort_factory, objekt_factory
):
admin_token = await login(client, "admin1")
standort = await standort_factory("Standort-W7")
await objekt_factory(name="RTW W7", code="RTW-W7", standort=standort)
objekte = await client.get("/api/v1/objekte", headers=auth_header(admin_token))
objekt_id = [o for o in objekte.json() if o["code"] == "RTW-W7"][0]["id"]
auftrag = await client.post(
"/api/v1/wartungsauftraege",
json={"objekt_id": objekt_id, "wartungsart": "Ölwechsel"},
headers=auth_header(admin_token),
)
auftrag_id = auftrag.json()["id"]
mitarbeiter_token = await login(client, "mitarbeiter1")
antwort = await client.post(
f"/api/v1/wartungsauftraege/{auftrag_id}/teile",
json={"bezeichnung": "Ölfilter", "menge": "1", "kosten": "12.50"},
headers=auth_header(mitarbeiter_token),
)
assert antwort.status_code == 403
+176
View File
@@ -627,3 +627,179 @@ Keine Commits in dieser Session.
- frontend/src/styles/global.css | 2 ++ - frontend/src/styles/global.css | 2 ++
--- ---
## 2026-09-07 22:33 22:35 (2m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .claude/agents/ui-designer.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-09-07 23:25 23:25 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .claude/agents/accessibility-expert.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- .claude/agents/ui-visual-validator.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-09-07 23:26 23:26 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** frontend
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .claude/agents/accessibility-expert.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- .claude/agents/ui-visual-validator.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-09-07 23:27 23:29 (2m)
**Beschreibung:** Claude Code Session
**Projekt:** frontend
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .claude/agents/accessibility-expert.md | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- .claude/agents/ui-visual-validator.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
---
## 2026-09-08 00:39 00:43 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- frontend/src/pages/KontrollPage.tsx | 27 ++++++++++++++++-----------
- frontend/src/pages/kontrolle/NachfuellDialog.tsx | 36 +++++++++++++++++++++++++++++++++---
- frontend/src/pages/kontrolle/PositionCard.tsx | 35 +++++++++++++++++++++++++++++++++--
- frontend/src/styles/global.css | 19 ++++++++++++++++++-
---
## 2026-09-08 00:52 00:58 (6m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- .gitignore | 1 +
- backend/alembic/versions/0023_suchindex.py | 26 ++++++++++++++++++++++++++
- backend/app/api/v1/api.py | 2 ++
- backend/app/api/v1/endpoints/suche.py | 38 ++++++++++++++++++++++++++++++++++++++
- backend/app/schemas/suche.py | 7 +++++++
- backend/tests/test_suche.py | 32 ++++++++++++++++++++++++++++++++
- frontend/src/components/CommandPalette.tsx | 59 +++++++++++++++++++++++++++++++++++++++++------------------
---
## 2026-09-08 01:03 01:06 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- backend/alembic/versions/0024_dokument_hash.py | 30 ++++++++++++++++++++++++++++++
- backend/app/api/v1/endpoints/dokument.py | 10 ++++++++++
- backend/app/models/dokument.py | 6 ++++++
- backend/app/services/dokument.py | 22 ++++++++++++++++++++++
- backend/tests/test_dokument.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
- frontend/src/api/client.ts | 36 +++++++++++++++++++++++++++---------
- frontend/src/components/DokumentePanel.tsx | 11 ++++++++---
---
## 2026-09-08 01:09 01:10 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- frontend/src/pages/ObjektListPage.tsx | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
- frontend/src/styles/global.css | 26 +++++++++++++++++++++++++
---
## 2026-09-08 01:14 01:16 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- frontend/src/hooks/useIsDesktop.ts | 20 ++++++++++++++++++++
- frontend/src/pages/ObjektListPage.tsx | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
- frontend/src/styles/global.css | 18 ++----------------
---
## 2026-09-08 10:13 10:15 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- backend/tests/test_stammdaten.py | 17 +++++++++++------
---
## 2026-09-08 10:19 10:25 (5m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- frontend/package-lock.json | 64 ++++++++++++++++++++++++++++++++++++++++------------------------
- frontend/package.json | 2 +-
---
## 2026-09-08 10:49 11:02 (12m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- arbeitskacheln/00_index.md | 2 +-
---
## 2026-09-08 22:01 22:04 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- arbeitskacheln/00_index.md | 2 +-
- backend/alembic/versions/0032_nummernschema.py | 31 +++++++++++++++++
- backend/app/api/v1/endpoints/objekte.py | 29 +++++++++++++---
- backend/app/api/v1/endpoints/stammdaten.py | 52 +++++++++++++++++++++++++++-
- backend/app/models/stammdaten.py | 20 ++++++++++-
- backend/app/schemas/stammdaten.py | 16 +++++++++
- backend/app/services/objekte.py | 10 +++---
- backend/tests/test_objekte.py | 65 +++++++++++++++++++++++++++++++++++
- frontend/src/api/types.ts | 7 ++++
- frontend/src/pages/AdminPage.tsx | 8 +++--
- frontend/src/pages/admin/ObjektAnlegenFormular.tsx | 23 +++++++++++--
- frontend/src/pages/admin/ObjekttypSection.tsx | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
---
+8
View File
@@ -80,6 +80,14 @@ export interface FaelligeWartung {
faellig_bei_betriebsstunden: string | null; faellig_bei_betriebsstunden: string | null;
} }
export interface WartungsauftragTeil {
id: string;
auftrag_id: string;
bezeichnung: string;
menge: string;
kosten: string;
}
export interface Wartungsauftrag { export interface Wartungsauftrag {
id: string; id: string;
objekt_id: number; objekt_id: number;
+110 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { apiRequest } from "../api/client"; import { apiRequest } from "../api/client";
import { useAuth } from "../auth/AuthContext"; import { useAuth } from "../auth/AuthContext";
import type { FaelligeWartung, Wartungsauftrag } from "../api/types"; import type { FaelligeWartung, Wartungsauftrag, WartungsauftragTeil } from "../api/types";
import { DokumentePanel } from "./DokumentePanel"; import { DokumentePanel } from "./DokumentePanel";
interface Props { interface Props {
@@ -27,6 +27,11 @@ export function WartungSection({ objektId, onFehler }: Props) {
const [faellig, setFaellig] = useState<FaelligeWartung[]>([]); const [faellig, setFaellig] = useState<FaelligeWartung[]>([]);
const [auftraege, setAuftraege] = useState<Wartungsauftrag[]>([]); const [auftraege, setAuftraege] = useState<Wartungsauftrag[]>([]);
const [dokumenteOffenId, setDokumenteOffenId] = useState<string | null>(null); const [dokumenteOffenId, setDokumenteOffenId] = useState<string | null>(null);
const [teileOffenId, setTeileOffenId] = useState<string | null>(null);
const [teile, setTeile] = useState<WartungsauftragTeil[]>([]);
const [teilBezeichnung, setTeilBezeichnung] = useState("");
const [teilMenge, setTeilMenge] = useState("1");
const [teilKosten, setTeilKosten] = useState("");
const [neuArt, setNeuArt] = useState(""); const [neuArt, setNeuArt] = useState("");
const [neuExtern, setNeuExtern] = useState(false); const [neuExtern, setNeuExtern] = useState(false);
@@ -96,6 +101,52 @@ export function WartungSection({ objektId, onFehler }: Props) {
} }
} }
async function teileLaden(auftragId: string) {
try {
const t = await apiRequest<WartungsauftragTeil[]>(`/wartungsauftraege/${auftragId}/teile`);
setTeile(t);
} catch {
onFehler("Ersatzteile konnten nicht geladen werden.");
}
}
async function teileUmschalten(auftragId: string) {
if (teileOffenId === auftragId) {
setTeileOffenId(null);
return;
}
setTeileOffenId(auftragId);
await teileLaden(auftragId);
}
async function teilAnlegen(auftragId: string) {
if (!teilBezeichnung || !teilKosten) {
onFehler("Bezeichnung und Kosten angeben.");
return;
}
try {
await apiRequest(`/wartungsauftraege/${auftragId}/teile`, {
method: "POST",
body: { bezeichnung: teilBezeichnung, menge: teilMenge, kosten: teilKosten },
});
setTeilBezeichnung("");
setTeilMenge("1");
setTeilKosten("");
await teileLaden(auftragId);
} catch {
onFehler("Ersatzteil konnte nicht angelegt werden.");
}
}
async function teilLoeschen(auftragId: string, teilId: string) {
try {
await apiRequest(`/wartungsauftraege/${auftragId}/teile/${teilId}`, { method: "DELETE" });
await teileLaden(auftragId);
} catch {
onFehler("Ersatzteil konnte nicht gelöscht werden.");
}
}
const offeneAuftraege = auftraege.filter((a) => a.status !== "erledigt"); const offeneAuftraege = auftraege.filter((a) => a.status !== "erledigt");
const erledigteAuftraege = auftraege.filter((a) => a.status === "erledigt"); const erledigteAuftraege = auftraege.filter((a) => a.status === "erledigt");
const faelligeOhneOffenenAuftrag = faellig.filter((f) => f.faellig); const faelligeOhneOffenenAuftrag = faellig.filter((f) => f.faellig);
@@ -176,12 +227,70 @@ export function WartungSection({ objektId, onFehler }: Props) {
> >
{dokumenteOffenId === a.id ? "Dokumente schließen" : "Dokumente/Rechnung"} {dokumenteOffenId === a.id ? "Dokumente schließen" : "Dokumente/Rechnung"}
</button> </button>
<button className="btn btn-secondary" onClick={() => teileUmschalten(a.id)}>
{teileOffenId === a.id ? "Ersatzteile schließen" : "Ersatzteile/Kosten"}
</button>
</div> </div>
{dokumenteOffenId === a.id && ( {dokumenteOffenId === a.id && (
<div style={{ marginTop: "0.5rem" }}> <div style={{ marginTop: "0.5rem" }}>
<DokumentePanel entitaetTyp="wartungsauftrag" entitaetId={a.id} onFehler={onFehler} /> <DokumentePanel entitaetTyp="wartungsauftrag" entitaetId={a.id} onFehler={onFehler} />
</div> </div>
)} )}
{teileOffenId === a.id && (
<div style={{ marginTop: "0.5rem" }}>
{teile.length === 0 ? (
<p className="text-muted" style={{ fontSize: "0.85rem" }}>Keine Ersatzteile erfasst.</p>
) : (
<ul className="card-list">
{teile.map((t) => (
<li key={t.id} className="row-between" style={{ padding: "0.2rem 0" }}>
<span>
{t.bezeichnung} × {t.menge} {t.kosten}
</span>
{istVerantwortlich && (
<button className="btn btn-secondary" onClick={() => teilLoeschen(a.id, t.id)}>
Löschen
</button>
)}
</li>
))}
</ul>
)}
{teile.length > 0 && (
<div className="text-muted" style={{ fontSize: "0.85rem", marginTop: "0.2rem" }}>
Summe: {teile.reduce((s, t) => s + Number(t.menge) * Number(t.kosten), 0).toFixed(2)}
</div>
)}
{istVerantwortlich && (
<div className="row" style={{ gap: "0.3rem", marginTop: "0.4rem", flexWrap: "wrap" }}>
<input
className="input"
placeholder="Bezeichnung"
value={teilBezeichnung}
onChange={(e) => setTeilBezeichnung(e.target.value)}
style={{ maxWidth: "10rem" }}
/>
<input
className="input"
placeholder="Menge"
value={teilMenge}
onChange={(e) => setTeilMenge(e.target.value)}
style={{ maxWidth: "5rem" }}
/>
<input
className="input"
placeholder="Kosten (€)"
value={teilKosten}
onChange={(e) => setTeilKosten(e.target.value)}
style={{ maxWidth: "6rem" }}
/>
<button className="btn btn-primary" onClick={() => teilAnlegen(a.id)}>
Hinzufügen
</button>
</div>
)}
</div>
)}
</li> </li>
))} ))}
</ul> </ul>