feat(fahrzeug): Fahrzeug-Spezialfelder + 5-stufiger Status (Roadmap Phase 3)
Neue Tabelle fahrzeugdetails (1:1 zu Objekt, additiv/optional): Kennzeichen, Funkrufname, Baujahr, Kilometerstand, Betriebsstunden, nächste HU/UVV. objekt_status um 'in_wartung' erweitert (Nutzer-Vorgabe: einsatzbereit/ eingeschränkt/nicht einsatzbereit/in Wartung/außer Dienst - die ersten drei bleiben weiterhin rein rechnerisch über dashboard.einsatzbereitschaft(), in_wartung/ausser_dienst sind die beiden manuell gesetzten Zustände). dashboard.einsatzbereitschaft(): ausser_dienst-Objekte zählen nicht mehr mit, in_wartung und überfällige HU/UVV sind neue Gründe für "nicht einsatzbereit". dashboard.bevorstehende_prueftermine() (Kachel 2) zeigt jetzt Geräte- UND Fahrzeugprüfungen (HU/UVV) in einer Liste (Nutzer-Vorgabe: ein Konzept für "welche Prüfungen bald fällig sind") - Schema dafür verallgemeinert (typ/bezeichnung/faelligkeitsdatum statt geräte-spezifischer Felder). Frontend: Status-Dropdown + Fahrzeugdetails-Formular in ObjektSection (nur für Objekttypen mit ist_zugfahrzeug=True), Dashboard-Kachel 2 zeigt gemischte Liste. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -3925,3 +3925,75 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/pages/admin/QualifikationSection.tsx | 204 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
- frontend/src/pages/admin/QualifikationSection.tsx | 204 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-05 13:20 – 13:24 (3m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- eb20aff feat(mangel): Mangel-Ticket-System (Roadmap Phase 2)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 27 +++++++++++++
|
||||||
|
- backend/alembic/versions/0013_mangel.py | 62 +++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/mangel.py | 85 +++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/models/mangel.py | 61 ++++++++++++++++++++++++++++
|
||||||
|
- backend/app/schemas/mangel.py | 42 ++++++++++++++++++++
|
||||||
|
- backend/app/services/dashboard.py | 5 +++
|
||||||
|
- backend/app/services/mangel.py | 33 ++++++++++++++++
|
||||||
|
- backend/tests/test_mangel.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/App.tsx | 9 +++++
|
||||||
|
- frontend/src/api/types.ts | 17 ++++++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 3 ++
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/MangelListePage.tsx | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 13:26 – 13:28 (2m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 27 +++++++++++++
|
||||||
|
- backend/alembic/versions/0013_mangel.py | 62 +++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/mangel.py | 85 +++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/models/mangel.py | 61 ++++++++++++++++++++++++++++
|
||||||
|
- backend/app/schemas/mangel.py | 42 ++++++++++++++++++++
|
||||||
|
- backend/app/services/dashboard.py | 5 +++
|
||||||
|
- backend/app/services/mangel.py | 33 ++++++++++++++++
|
||||||
|
- backend/tests/test_mangel.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/App.tsx | 9 +++++
|
||||||
|
- frontend/src/api/types.ts | 17 ++++++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 3 ++
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/MangelListePage.tsx | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 13:29 – 13:30 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 27 +++++++++++++
|
||||||
|
- backend/alembic/versions/0013_mangel.py | 62 +++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/mangel.py | 85 +++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/models/mangel.py | 61 ++++++++++++++++++++++++++++
|
||||||
|
- backend/app/schemas/mangel.py | 42 ++++++++++++++++++++
|
||||||
|
- backend/app/services/dashboard.py | 5 +++
|
||||||
|
- backend/app/services/mangel.py | 33 ++++++++++++++++
|
||||||
|
- backend/tests/test_mangel.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/App.tsx | 9 +++++
|
||||||
|
- frontend/src/api/types.ts | 17 ++++++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 3 ++
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/MangelListePage.tsx | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Fahrzeug-Spezialfelder (Roadmap Phase 3: BOS-Ressourcenplattform-Anforderung
|
||||||
|
Modul 2 "Fahrzeuge"). Kennzeichen/Funkrufname/Kilometerstand/Betriebsstunden/
|
||||||
|
Baujahr/HU/UVV als optionale 1:1-Erweiterung zu Objekt (nicht jeder Objekttyp
|
||||||
|
braucht das, nur Zugfahrzeuge - additiv, kein eigener Fahrzeug-Objekttyp
|
||||||
|
erzwungen, Karte 11 bleibt gültig).
|
||||||
|
|
||||||
|
Zusätzlich objekt_status um 'in_wartung' erweitert (Nutzer-Vorgabe: 5-stufiger
|
||||||
|
Fahrzeug-Status). ALTER TYPE ... ADD VALUE muss außerhalb der laufenden
|
||||||
|
Transaktion committen (Postgres-Einschränkung), daher AUTOCOMMIT-Block.
|
||||||
|
|
||||||
|
Revision ID: 0014_fahrzeugdetails
|
||||||
|
Revises: 0013_mangel
|
||||||
|
Create Date: 2026-09-05
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0014_fahrzeugdetails"
|
||||||
|
down_revision: Union[str, None] = "0013_mangel"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.execute("ALTER TYPE objekt_status ADD VALUE IF NOT EXISTS 'in_wartung'")
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE fahrzeugdetails (
|
||||||
|
objekt_id INTEGER PRIMARY KEY REFERENCES objekt(id),
|
||||||
|
kennzeichen TEXT,
|
||||||
|
funkrufname TEXT,
|
||||||
|
baujahr INTEGER,
|
||||||
|
kilometerstand INTEGER,
|
||||||
|
betriebsstunden NUMERIC,
|
||||||
|
naechste_hu DATE,
|
||||||
|
naechste_uvv DATE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP TABLE fahrzeugdetails")
|
||||||
|
# 'in_wartung' aus dem ENUM zu entfernen ist in Postgres nicht direkt möglich
|
||||||
|
# (kein ALTER TYPE ... DROP VALUE) - bewusst nicht rückgängig gemacht, siehe
|
||||||
|
# Muster in 0001_initial_schema (dort ebenfalls nur DROP TYPE als Ganzes).
|
||||||
@@ -8,12 +8,15 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.api.deps import get_current_user, require_roles
|
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 KnotenTyp, RolleTyp, Systemknoten
|
from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
|
||||||
|
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||||
from app.models.objekt import Objekt
|
from app.models.objekt import Objekt
|
||||||
from app.models.objektposition import Objektposition
|
from app.models.objektposition import Objektposition
|
||||||
from app.models.stammdaten import Material, Objekttyp
|
from app.models.stammdaten import Material, Objekttyp
|
||||||
from app.models.vorlage import Beladungsvorlage
|
from app.models.vorlage import Beladungsvorlage
|
||||||
from app.services.label import generiere_code128_label_pdf
|
from app.services.label import generiere_code128_label_pdf
|
||||||
from app.schemas.objekt import (
|
from app.schemas.objekt import (
|
||||||
|
FahrzeugdetailsRead,
|
||||||
|
FahrzeugdetailsUpsert,
|
||||||
ObjektCreate,
|
ObjektCreate,
|
||||||
ObjektDuplizierenRequest,
|
ObjektDuplizierenRequest,
|
||||||
ObjektpositionCreate,
|
ObjektpositionCreate,
|
||||||
@@ -391,3 +394,40 @@ async def fuege_zusatzposition_hinzu(
|
|||||||
code=position.code,
|
code=position.code,
|
||||||
pruefintervall_monate=position.pruefintervall_monate,
|
pruefintervall_monate=position.pruefintervall_monate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/objekte/{objekt_id}/fahrzeugdetails", response_model=FahrzeugdetailsRead)
|
||||||
|
async def hole_fahrzeugdetails(
|
||||||
|
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||||
|
) -> Fahrzeugdetails:
|
||||||
|
"""Roadmap Phase 3 (Modul Fahrzeuge): 404 statt leerem Default, wenn noch
|
||||||
|
keine Fahrzeugdetails erfasst sind - Frontend zeigt dann ein leeres
|
||||||
|
Formular statt scheinbar vorhandener Nullwerte."""
|
||||||
|
details = await db.get(Fahrzeugdetails, objekt_id)
|
||||||
|
if details is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Keine Fahrzeugdetails erfasst")
|
||||||
|
return details
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/objekte/{objekt_id}/fahrzeugdetails", response_model=FahrzeugdetailsRead)
|
||||||
|
async def setze_fahrzeugdetails(
|
||||||
|
objekt_id: int,
|
||||||
|
payload: FahrzeugdetailsUpsert,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_=Depends(_materialverantwortliche),
|
||||||
|
) -> Fahrzeugdetails:
|
||||||
|
"""Upsert (PUT statt PATCH): Fahrzeugdetails existieren typischerweise
|
||||||
|
genau einmal je Objekt, ein Formular ersetzt komplett statt Teil-Update -
|
||||||
|
einfacher als Anlegen/Ändern zu unterscheiden."""
|
||||||
|
objekt = await db.get(Objekt, objekt_id)
|
||||||
|
if objekt is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||||
|
|
||||||
|
details = await db.get(Fahrzeugdetails, objekt_id)
|
||||||
|
if details is None:
|
||||||
|
details = Fahrzeugdetails(objekt_id=objekt_id)
|
||||||
|
db.add(details)
|
||||||
|
for feld, wert in payload.model_dump().items():
|
||||||
|
setattr(details, feld, wert)
|
||||||
|
await db.flush()
|
||||||
|
return details
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import Date, ForeignKey, Integer, Numeric, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Fahrzeugdetails(Base):
|
||||||
|
"""Roadmap Phase 3 (Modul "Fahrzeuge"): optionale 1:1-Erweiterung zu Objekt
|
||||||
|
für Fahrzeug-Spezialfelder. Objekt bleibt generisch (Karte 11) - nicht jedes
|
||||||
|
Objekt braucht das, daher eigene Tabelle statt Spalten direkt an Objekt."""
|
||||||
|
|
||||||
|
__tablename__ = "fahrzeugdetails"
|
||||||
|
|
||||||
|
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), primary_key=True)
|
||||||
|
kennzeichen: Mapped[str | None] = mapped_column(String)
|
||||||
|
funkrufname: Mapped[str | None] = mapped_column(String)
|
||||||
|
baujahr: Mapped[int | None] = mapped_column(Integer)
|
||||||
|
kilometerstand: Mapped[int | None] = mapped_column(Integer)
|
||||||
|
betriebsstunden: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||||
|
naechste_hu: Mapped[date | None] = mapped_column(Date)
|
||||||
|
naechste_uvv: Mapped[date | None] = mapped_column(Date)
|
||||||
@@ -10,6 +10,7 @@ from app.db.base import Base
|
|||||||
class ObjektStatus(str, enum.Enum):
|
class ObjektStatus(str, enum.Enum):
|
||||||
aktiv = "aktiv"
|
aktiv = "aktiv"
|
||||||
ausser_dienst = "ausser_dienst"
|
ausser_dienst = "ausser_dienst"
|
||||||
|
in_wartung = "in_wartung"
|
||||||
|
|
||||||
|
|
||||||
objekt_status_pg = PgEnum(ObjektStatus, name="objekt_status", create_type=False)
|
objekt_status_pg = PgEnum(ObjektStatus, name="objekt_status", create_type=False)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ from datetime import date
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.models.geraet_instanz import GeraetStatus
|
|
||||||
|
|
||||||
|
|
||||||
class KennzahlenRead(BaseModel):
|
class KennzahlenRead(BaseModel):
|
||||||
anzahl_offener_fehlbestaende: int
|
anzahl_offener_fehlbestaende: int
|
||||||
@@ -39,13 +37,18 @@ class EinsatzbereitschaftRead(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PrueftermineintragRead(BaseModel):
|
class PrueftermineintragRead(BaseModel):
|
||||||
geraet_instanz_id: uuid.UUID
|
"""typ='geraet' (Karte 14) oder 'fahrzeug_hu'/'fahrzeug_uvv' (Roadmap Phase
|
||||||
|
3, Modul Fahrzeuge) - eine Kachel für beide (Nutzer-Vorgabe: "welche
|
||||||
|
Prüfungen bald fällig sind" ist ein Konzept). Die geraet-spezifischen
|
||||||
|
Felder sind bei Fahrzeug-Einträgen None."""
|
||||||
|
|
||||||
|
typ: str
|
||||||
objekt_id: int
|
objekt_id: int
|
||||||
objektposition_id: uuid.UUID
|
bezeichnung: str
|
||||||
material_id: int
|
faelligkeitsdatum: date
|
||||||
material_name: str
|
|
||||||
seriennummer: str
|
|
||||||
geraet_status: GeraetStatus
|
|
||||||
naechste_pruefung: date
|
|
||||||
verbleibende_tage: int
|
verbleibende_tage: int
|
||||||
status: str
|
status: str
|
||||||
|
geraet_instanz_id: uuid.UUID | None = None
|
||||||
|
objektposition_id: uuid.UUID | None = None
|
||||||
|
material_id: int | None = None
|
||||||
|
seriennummer: str | None = None
|
||||||
|
|||||||
@@ -32,6 +32,29 @@ class ObjektRead(BaseModel):
|
|||||||
|
|
||||||
class ObjektUpdate(BaseModel):
|
class ObjektUpdate(BaseModel):
|
||||||
fahrzeug_id: int | None = None
|
fahrzeug_id: int | None = None
|
||||||
|
status: ObjektStatus | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FahrzeugdetailsUpsert(BaseModel):
|
||||||
|
kennzeichen: str | None = None
|
||||||
|
funkrufname: str | None = None
|
||||||
|
baujahr: int | None = None
|
||||||
|
kilometerstand: int | None = None
|
||||||
|
betriebsstunden: Decimal | None = None
|
||||||
|
naechste_hu: date | None = None
|
||||||
|
naechste_uvv: date | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FahrzeugdetailsRead(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
objekt_id: int
|
||||||
|
kennzeichen: str | None
|
||||||
|
funkrufname: str | None
|
||||||
|
baujahr: int | None
|
||||||
|
kilometerstand: int | None
|
||||||
|
betriebsstunden: Decimal | None
|
||||||
|
naechste_hu: date | None
|
||||||
|
naechste_uvv: date | None
|
||||||
|
|
||||||
|
|
||||||
class ObjektDuplizierenRequest(BaseModel):
|
class ObjektDuplizierenRequest(BaseModel):
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from datetime import date, datetime, timedelta, timezone
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||||
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||||
from app.models.geraet_instanz import GeraetInstanz
|
from app.models.geraet_instanz import GeraetInstanz
|
||||||
from app.models.kontrolle import Kontrolle, KontrollStatus
|
from app.models.kontrolle import Kontrolle, KontrollStatus
|
||||||
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
|
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
|
||||||
from app.models.objekt import Objekt
|
from app.models.objekt import Objekt, ObjektStatus
|
||||||
from app.models.objektposition import Objektposition
|
from app.models.objektposition import Objektposition
|
||||||
from app.models.stammdaten import Material
|
from app.models.stammdaten import Material
|
||||||
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
|
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
|
||||||
@@ -104,7 +105,12 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
(Leitplanke U2/Karte 08) - das Objekt bleibt daher "eingeschränkt
|
(Leitplanke U2/Karte 08) - das Objekt bleibt daher "eingeschränkt
|
||||||
einsatzbereit", nicht "einsatzbereit"."""
|
einsatzbereit", nicht "einsatzbereit"."""
|
||||||
heute = datetime.now(timezone.utc).date()
|
heute = datetime.now(timezone.utc).date()
|
||||||
objekte = (await db.execute(select(Objekt))).scalars().all()
|
# ausser_dienst-Objekte zählen fachlich gar nicht mehr mit (Roadmap Phase 3,
|
||||||
|
# Modul Fahrzeuge: 5-stufiger Status) - anders als in_wartung, das als
|
||||||
|
# aktiver Grund weiter unten erscheint statt das Objekt komplett auszublenden.
|
||||||
|
objekte = (
|
||||||
|
(await db.execute(select(Objekt).where(Objekt.status != ObjektStatus.ausser_dienst))).scalars().all()
|
||||||
|
)
|
||||||
|
|
||||||
offene_fehlbestaende = (
|
offene_fehlbestaende = (
|
||||||
await db.execute(
|
await db.execute(
|
||||||
@@ -163,6 +169,25 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
|
|
||||||
objekt_ids_kritischer_mangel = await objekt_ids_mit_kritischem_offenem_mangel(db)
|
objekt_ids_kritischer_mangel = await objekt_ids_mit_kritischem_offenem_mangel(db)
|
||||||
|
|
||||||
|
objekt_ids_hu_uvv_ueberfaellig = set(
|
||||||
|
(
|
||||||
|
await db.execute(
|
||||||
|
select(func.distinct(Fahrzeugdetails.objekt_id)).where(
|
||||||
|
(
|
||||||
|
(Fahrzeugdetails.naechste_hu.is_not(None))
|
||||||
|
& (Fahrzeugdetails.naechste_hu < heute)
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
(Fahrzeugdetails.naechste_uvv.is_not(None))
|
||||||
|
& (Fahrzeugdetails.naechste_uvv < heute)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scalars()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
bereit = eingeschraenkt = 0
|
bereit = eingeschraenkt = 0
|
||||||
details: list[dict] = []
|
details: list[dict] = []
|
||||||
for objekt in objekte:
|
for objekt in objekte:
|
||||||
@@ -179,6 +204,10 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|||||||
gruende.append("kontrolle_laeuft")
|
gruende.append("kontrolle_laeuft")
|
||||||
if objekt.id in objekt_ids_kritischer_mangel:
|
if objekt.id in objekt_ids_kritischer_mangel:
|
||||||
gruende.append("mangel_kritisch_offen")
|
gruende.append("mangel_kritisch_offen")
|
||||||
|
if objekt.status == ObjektStatus.in_wartung:
|
||||||
|
gruende.append("in_wartung")
|
||||||
|
if objekt.id in objekt_ids_hu_uvv_ueberfaellig:
|
||||||
|
gruende.append("hu_uvv_ueberfaellig")
|
||||||
|
|
||||||
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
||||||
eingeschraenkt += 1
|
eingeschraenkt += 1
|
||||||
@@ -205,30 +234,67 @@ async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int =
|
|||||||
Endpoint /dashboard/ablaufdaten) - Prüfung ist Wartungsintervall, Ablauf ist
|
Endpoint /dashboard/ablaufdaten) - Prüfung ist Wartungsintervall, Ablauf ist
|
||||||
Verbrauchsgut-Verfall, fachlich unterschiedlich (Nutzer-Entscheidung)."""
|
Verbrauchsgut-Verfall, fachlich unterschiedlich (Nutzer-Entscheidung)."""
|
||||||
heute = datetime.now(timezone.utc).date()
|
heute = datetime.now(timezone.utc).date()
|
||||||
|
eintraege = []
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(GeraetInstanz, Objektposition, Material)
|
select(GeraetInstanz, Objektposition, Material)
|
||||||
.join(Objektposition, GeraetInstanz.objektposition_id == Objektposition.id)
|
.join(Objektposition, GeraetInstanz.objektposition_id == Objektposition.id)
|
||||||
.join(Material, Objektposition.material_id == Material.id)
|
.join(Material, Objektposition.material_id == Material.id)
|
||||||
.where(GeraetInstanz.naechste_pruefung.is_not(None))
|
.where(GeraetInstanz.naechste_pruefung.is_not(None))
|
||||||
)
|
)
|
||||||
eintraege = []
|
|
||||||
for geraet, objektposition, material in result.all():
|
for geraet, objektposition, material in result.all():
|
||||||
status_wert, verbleibende_tage = _ablauf_status(geraet.naechste_pruefung, warnzeitraum_tage, heute)
|
status_wert, verbleibende_tage = _ablauf_status(geraet.naechste_pruefung, warnzeitraum_tage, heute)
|
||||||
if status_wert == "gueltig":
|
if status_wert == "gueltig":
|
||||||
continue
|
continue
|
||||||
eintraege.append(
|
eintraege.append(
|
||||||
{
|
{
|
||||||
"geraet_instanz_id": geraet.id,
|
"typ": "geraet",
|
||||||
"objekt_id": objektposition.objekt_id,
|
"objekt_id": objektposition.objekt_id,
|
||||||
"objektposition_id": objektposition.id,
|
"bezeichnung": f"{material.name} (SN {geraet.seriennummer})",
|
||||||
"material_id": material.id,
|
"faelligkeitsdatum": geraet.naechste_pruefung,
|
||||||
"material_name": material.name,
|
|
||||||
"seriennummer": geraet.seriennummer,
|
|
||||||
"geraet_status": geraet.status,
|
|
||||||
"naechste_pruefung": geraet.naechste_pruefung,
|
|
||||||
"verbleibende_tage": verbleibende_tage,
|
"verbleibende_tage": verbleibende_tage,
|
||||||
"status": status_wert,
|
"status": status_wert,
|
||||||
|
"geraet_instanz_id": geraet.id,
|
||||||
|
"objektposition_id": objektposition.id,
|
||||||
|
"material_id": material.id,
|
||||||
|
"seriennummer": geraet.seriennummer,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fahrzeug-HU/UVV (Roadmap Phase 3, Modul Fahrzeuge) - gleiche Kachel wie
|
||||||
|
# Geräteprüfungen (Nutzer-Vorgabe: "welche Prüfungen bald fällig sind" ist
|
||||||
|
# EIN Konzept, egal ob Gerät oder Fahrzeug).
|
||||||
|
result = await db.execute(
|
||||||
|
select(Fahrzeugdetails, Objekt)
|
||||||
|
.join(Objekt, Fahrzeugdetails.objekt_id == Objekt.id)
|
||||||
|
.where(
|
||||||
|
(Fahrzeugdetails.naechste_hu.is_not(None)) | (Fahrzeugdetails.naechste_uvv.is_not(None))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for details, objekt in result.all():
|
||||||
|
for feld, typ, label in (
|
||||||
|
(details.naechste_hu, "fahrzeug_hu", "HU"),
|
||||||
|
(details.naechste_uvv, "fahrzeug_uvv", "UVV"),
|
||||||
|
):
|
||||||
|
if feld is None:
|
||||||
|
continue
|
||||||
|
status_wert, verbleibende_tage = _ablauf_status(feld, warnzeitraum_tage, heute)
|
||||||
|
if status_wert == "gueltig":
|
||||||
|
continue
|
||||||
|
eintraege.append(
|
||||||
|
{
|
||||||
|
"typ": typ,
|
||||||
|
"objekt_id": objekt.id,
|
||||||
|
"bezeichnung": f"{label} {objekt.name} ({details.kennzeichen or objekt.code})",
|
||||||
|
"faelligkeitsdatum": feld,
|
||||||
|
"verbleibende_tage": verbleibende_tage,
|
||||||
|
"status": status_wert,
|
||||||
|
"geraet_instanz_id": None,
|
||||||
|
"objektposition_id": None,
|
||||||
|
"material_id": None,
|
||||||
|
"seriennummer": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
eintraege.sort(key=lambda e: e["verbleibende_tage"])
|
eintraege.sort(key=lambda e: e["verbleibende_tage"])
|
||||||
return eintraege
|
return eintraege
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import auth_header, login
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fahrzeugdetails_upsert(client, objekt_mit_position, admin_user):
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "admin1")
|
||||||
|
|
||||||
|
fehlend = await client.get(f"/api/v1/objekte/{objekt.id}/fahrzeugdetails", headers=auth_header(token))
|
||||||
|
assert fehlend.status_code == 404
|
||||||
|
|
||||||
|
gesetzt = await client.put(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/fahrzeugdetails",
|
||||||
|
json={"kennzeichen": "XX-YY 123", "kilometerstand": 42315, "naechste_hu": "2027-05-01"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert gesetzt.status_code == 200
|
||||||
|
assert gesetzt.json()["kennzeichen"] == "XX-YY 123"
|
||||||
|
assert gesetzt.json()["kilometerstand"] == 42315
|
||||||
|
|
||||||
|
aktualisiert = await client.put(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/fahrzeugdetails",
|
||||||
|
json={"kilometerstand": 42500},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert aktualisiert.json()["kilometerstand"] == 42500
|
||||||
|
assert aktualisiert.json()["kennzeichen"] is None # PUT ersetzt komplett, kein Merge
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mitarbeiter_darf_fahrzeugdetails_nicht_setzen(client, objekt_mit_position, mitarbeiter_user):
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "mitarbeiter1")
|
||||||
|
response = await client.put(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/fahrzeugdetails", json={"kennzeichen": "X"}, headers=auth_header(token)
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ausser_dienst_objekt_zaehlt_nicht_in_einsatzbereitschaft(
|
||||||
|
client, objekt_mit_position, admin_user, materialverantwortlicher_user
|
||||||
|
):
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "admin1")
|
||||||
|
await client.patch(f"/api/v1/objekte/{objekt.id}", json={"status": "ausser_dienst"}, headers=auth_header(token))
|
||||||
|
|
||||||
|
verantwortlicher_token = await login(client, "materialverantwortlicher1")
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(verantwortlicher_token)
|
||||||
|
)
|
||||||
|
body = response.json()
|
||||||
|
assert body["gesamt"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_in_wartung_macht_objekt_nicht_einsatzbereit(
|
||||||
|
client, objekt_mit_position, admin_user, materialverantwortlicher_user
|
||||||
|
):
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "admin1")
|
||||||
|
await client.patch(f"/api/v1/objekte/{objekt.id}", json={"status": "in_wartung"}, headers=auth_header(token))
|
||||||
|
|
||||||
|
verantwortlicher_token = await login(client, "materialverantwortlicher1")
|
||||||
|
response = await client.get(
|
||||||
|
"/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(verantwortlicher_token)
|
||||||
|
)
|
||||||
|
body = response.json()
|
||||||
|
assert body["nicht_einsatzbereit"] == 1
|
||||||
|
assert body["details"][0]["gruende"] == ["in_wartung"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ueberfaellige_hu_macht_objekt_nicht_einsatzbereit_und_erscheint_in_prueftermine(
|
||||||
|
client, objekt_mit_position, admin_user, materialverantwortlicher_user
|
||||||
|
):
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
token = await login(client, "admin1")
|
||||||
|
await client.put(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/fahrzeugdetails",
|
||||||
|
json={"naechste_hu": (date.today() - timedelta(days=10)).isoformat()},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
|
||||||
|
verantwortlicher_token = await login(client, "materialverantwortlicher1")
|
||||||
|
bereitschaft = await client.get(
|
||||||
|
"/api/v1/dashboard/einsatzbereitschaft", headers=auth_header(verantwortlicher_token)
|
||||||
|
)
|
||||||
|
assert bereitschaft.json()["details"][0]["gruende"] == ["hu_uvv_ueberfaellig"]
|
||||||
|
|
||||||
|
prueftermine = await client.get("/api/v1/dashboard/prueftermine", headers=auth_header(verantwortlicher_token))
|
||||||
|
body = prueftermine.json()
|
||||||
|
assert len(body) == 1
|
||||||
|
assert body[0]["typ"] == "fahrzeug_hu"
|
||||||
|
assert body[0]["status"] == "abgelaufen"
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
export interface Fahrzeugdetails {
|
||||||
|
objekt_id: number;
|
||||||
|
kennzeichen: string | null;
|
||||||
|
funkrufname: string | null;
|
||||||
|
baujahr: number | null;
|
||||||
|
kilometerstand: number | null;
|
||||||
|
betriebsstunden: string | null;
|
||||||
|
naechste_hu: string | null;
|
||||||
|
naechste_uvv: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Objekt {
|
export interface Objekt {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ interface Einsatzbereitschaft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Prueftermineintrag {
|
interface Prueftermineintrag {
|
||||||
geraet_instanz_id: string;
|
typ: string;
|
||||||
objekt_id: number;
|
objekt_id: number;
|
||||||
material_name: string;
|
bezeichnung: string;
|
||||||
seriennummer: string;
|
faelligkeitsdatum: string;
|
||||||
naechste_pruefung: string;
|
|
||||||
verbleibende_tage: number;
|
verbleibende_tage: number;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
@@ -42,6 +41,8 @@ const GRUND_TEXT: Record<string, string> = {
|
|||||||
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
||||||
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
||||||
mangel_kritisch_offen: "Kritischer Mangel offen",
|
mangel_kritisch_offen: "Kritischer Mangel offen",
|
||||||
|
in_wartung: "In Wartung",
|
||||||
|
hu_uvv_ueberfaellig: "HU/UVV überfällig",
|
||||||
mindermenge_genehmigt: "Mindermenge genehmigt",
|
mindermenge_genehmigt: "Mindermenge genehmigt",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,13 +125,11 @@ export function DashboardPage() {
|
|||||||
<p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>
|
<p className="text-muted">Keine anstehenden oder überfälligen Geräteprüfungen.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="card-list">
|
<ul className="card-list">
|
||||||
{prueftermine.map((p) => (
|
{prueftermine.map((p, i) => (
|
||||||
<li key={p.geraet_instanz_id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
<li key={i} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||||
<span>
|
<span>{p.bezeichnung}</span>
|
||||||
{p.material_name} <span className="text-muted">(SN {p.seriennummer})</span>
|
<span style={{ color: p.status === "abgelaufen" ? "#b00020" : undefined }}>
|
||||||
</span>
|
{p.faelligkeitsdatum} ({p.verbleibende_tage} Tage)
|
||||||
<span className={p.status === "abgelaufen" ? "text-muted" : "text-muted"} style={{ color: p.status === "abgelaufen" ? "#b00020" : undefined }}>
|
|
||||||
{p.naechste_pruefung} ({p.verbleibende_tage} Tage)
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { apiRequest } from "../../api/client";
|
import { apiRequest } from "../../api/client";
|
||||||
import type { Beladungsvorlage, Material, Objekt, Objekttyp, Standort } from "../../api/types";
|
import type { Beladungsvorlage, Fahrzeugdetails, Material, Objekt, Objekttyp, Standort } from "../../api/types";
|
||||||
import { ObjektPositionenPanel } from "./ObjektPositionenPanel";
|
import { ObjektPositionenPanel } from "./ObjektPositionenPanel";
|
||||||
|
|
||||||
|
const STATUS_OPTIONEN = [
|
||||||
|
{ wert: "aktiv", label: "Aktiv" },
|
||||||
|
{ wert: "in_wartung", label: "In Wartung" },
|
||||||
|
{ wert: "ausser_dienst", label: "Außer Dienst" },
|
||||||
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
objekte: Objekt[];
|
objekte: Objekt[];
|
||||||
objekttypen: Objekttyp[];
|
objekttypen: Objekttyp[];
|
||||||
@@ -46,6 +52,52 @@ export function ObjektSection({
|
|||||||
const [fahrzeugBearbeitenId, setFahrzeugBearbeitenId] = useState<number | null>(null);
|
const [fahrzeugBearbeitenId, setFahrzeugBearbeitenId] = useState<number | null>(null);
|
||||||
const [fahrzeugAuswahl, setFahrzeugAuswahl] = useState<number | "">("");
|
const [fahrzeugAuswahl, setFahrzeugAuswahl] = useState<number | "">("");
|
||||||
const [suche, setSuche] = useState("");
|
const [suche, setSuche] = useState("");
|
||||||
|
const [detailsOffenId, setDetailsOffenId] = useState<number | null>(null);
|
||||||
|
const [details, setDetails] = useState<Partial<Fahrzeugdetails>>({});
|
||||||
|
|
||||||
|
async function statusAendern(o: Objekt, status: string) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/objekte/${o.id}`, { method: "PATCH", body: { status } });
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Status konnte nicht geändert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fahrzeugdetailsOeffnen(o: Objekt) {
|
||||||
|
if (detailsOffenId === o.id) {
|
||||||
|
setDetailsOffenId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDetailsOffenId(o.id);
|
||||||
|
try {
|
||||||
|
const daten = await apiRequest<Fahrzeugdetails>(`/objekte/${o.id}/fahrzeugdetails`);
|
||||||
|
setDetails(daten);
|
||||||
|
} catch {
|
||||||
|
setDetails({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fahrzeugdetailsSpeichern(objektId: number) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/objekte/${objektId}/fahrzeugdetails`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
kennzeichen: details.kennzeichen || null,
|
||||||
|
funkrufname: details.funkrufname || null,
|
||||||
|
baujahr: details.baujahr || null,
|
||||||
|
kilometerstand: details.kilometerstand || null,
|
||||||
|
betriebsstunden: details.betriebsstunden || null,
|
||||||
|
naechste_hu: details.naechste_hu || null,
|
||||||
|
naechste_uvv: details.naechste_uvv || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setDetailsOffenId(null);
|
||||||
|
onFehler("");
|
||||||
|
} catch {
|
||||||
|
onFehler("Fahrzeugdetails konnten nicht gespeichert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Option A+C (Karte 11 Nachtrag): Dropdown zeigt nur Zugfahrzeuge (Objekttyp-
|
// Option A+C (Karte 11 Nachtrag): Dropdown zeigt nur Zugfahrzeuge (Objekttyp-
|
||||||
// Flag ist_zugfahrzeug) ohne eigene Kopplung - serverseitig zusätzlich per
|
// Flag ist_zugfahrzeug) ohne eigene Kopplung - serverseitig zusätzlich per
|
||||||
@@ -286,6 +338,24 @@ export function ObjektSection({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
|
{!nurPflegen && (
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={o.status}
|
||||||
|
onChange={(e) => statusAendern(o, e.target.value)}
|
||||||
|
>
|
||||||
|
{STATUS_OPTIONEN.map((s) => (
|
||||||
|
<option key={s.wert} value={s.wert}>
|
||||||
|
{s.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{objekttypen.find((t) => t.id === o.objekttyp_id)?.ist_zugfahrzeug && (
|
||||||
|
<button className="btn btn-secondary" onClick={() => fahrzeugdetailsOeffnen(o)}>
|
||||||
|
{detailsOffenId === o.id ? "Schließen" : "Fahrzeugdetails"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
onClick={() => setAusgeklapptId(ausgeklapptId === o.id ? null : o.id)}
|
||||||
@@ -304,6 +374,63 @@ export function ObjektSection({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{detailsOffenId === o.id && (
|
||||||
|
<div className="stack" style={{ marginTop: "0.5rem", maxWidth: "24rem" }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Kennzeichen"
|
||||||
|
value={details.kennzeichen ?? ""}
|
||||||
|
onChange={(e) => setDetails((d) => ({ ...d, kennzeichen: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Funkrufname"
|
||||||
|
value={details.funkrufname ?? ""}
|
||||||
|
onChange={(e) => setDetails((d) => ({ ...d, funkrufname: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<div className="row">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
placeholder="Baujahr"
|
||||||
|
value={details.baujahr ?? ""}
|
||||||
|
onChange={(e) => setDetails((d) => ({ ...d, baujahr: Number(e.target.value) || undefined }))}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
placeholder="Kilometerstand"
|
||||||
|
value={details.kilometerstand ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDetails((d) => ({ ...d, kilometerstand: Number(e.target.value) || undefined }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="field">
|
||||||
|
<label>Nächste HU</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="date"
|
||||||
|
value={details.naechste_hu ?? ""}
|
||||||
|
onChange={(e) => setDetails((d) => ({ ...d, naechste_hu: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Nächste UVV</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="date"
|
||||||
|
value={details.naechste_uvv ?? ""}
|
||||||
|
onChange={(e) => setDetails((d) => ({ ...d, naechste_uvv: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={() => fahrzeugdetailsSpeichern(o.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{fahrzeugBearbeitenId === o.id && (
|
{fahrzeugBearbeitenId === o.id && (
|
||||||
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||||
<select
|
<select
|
||||||
|
|||||||
Reference in New Issue
Block a user