feat(wartung): MAINT-005 Ersatzteile/Kosten je Wartungsauftrag
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:
@@ -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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------------------------------------
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -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")
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user