Karte 14 Schritt 2 (Admin-Teil) + Frontend für Anhänger-Kopplung
Backend: geraet_instanz-CRUD (Liste/Anlegen/Status ändern inkl. optionalem Fehlbestand pro Gerät/Löschen), pruefintervall_monate in Objektposition- Schemas verdrahtet (Prüfdatum -> naechste_pruefung wird automatisch berechnet, Monatsrechnung ohne neue Dependency). Frontend: GeraeteInstanzenListe ersetzt das Einzel-SN-Feld im Admin-Portal (ObjektPositionenPanel) durch eine Instanz-Liste mit Status/Prüfdatum/ Bemerkung je Exemplar - löst das Karte-14-Kernproblem (mehrere Geräte pro Position). ObjektSection: Anhänger-Kopplung bei Anlage und nachträglich änderbar. Kontroll-Erfassung (PositionCard/KontrollPage) bleibt unverändert - die komplett neue Erfassungs-UI für geraet_sn (Umsetzungsschritt 4, Karte 14) ist bewusst nicht Teil dieses Commits. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -3191,3 +3191,37 @@ Keine Commits in dieser Session.
|
|||||||
- .../pages/admin/KontrollverantwortungSection.tsx | 185 +++++++++++++++++++++
|
- .../pages/admin/KontrollverantwortungSection.tsx | 185 +++++++++++++++++++++
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-04 20:47 – 20:48 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** backend
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- eff9fe4 Fahrzeug/Anhänger-Kopplung (objekt.gezogen_von_objekt_id)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- backend/DEVLOG.md | 36 ++++++++
|
||||||
|
- backend/alembic/versions/0009_gezogen_von.py | 42 +++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 38 ++++++++
|
||||||
|
- backend/app/models/objekt.py | 6 ++
|
||||||
|
- backend/app/schemas/objekt.py | 6 ++
|
||||||
|
- backend/app/services/objekte.py | 6 +-
|
||||||
|
- backend/tests/test_objekte.py | 131 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 20:48 – 20:49 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- backend/DEVLOG.md | 36 ++++++++
|
||||||
|
- backend/alembic/versions/0009_gezogen_von.py | 42 +++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 38 ++++++++
|
||||||
|
- backend/app/models/objekt.py | 6 ++
|
||||||
|
- backend/app/schemas/objekt.py | 6 ++
|
||||||
|
- backend/app/services/objekte.py | 6 +-
|
||||||
|
- backend/tests/test_objekte.py | 131 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -225,3 +225,20 @@ Keine Commits in dieser Session.
|
|||||||
- backend/tests/test_geraet_instanz.py | 99 +++++++++++++++++++++++++
|
- backend/tests/test_geraet_instanz.py | 99 +++++++++++++++++++++++++
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-04 20:49 – 20:54 (5m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- backend/DEVLOG.md | 36 ++++++++
|
||||||
|
- backend/alembic/versions/0009_gezogen_von.py | 42 +++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 38 ++++++++
|
||||||
|
- backend/app/models/objekt.py | 6 ++
|
||||||
|
- backend/app/schemas/objekt.py | 6 ++
|
||||||
|
- backend/app/services/objekte.py | 6 +-
|
||||||
|
- backend/tests/test_objekte.py | 131 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from app.api.v1.endpoints import (
|
|||||||
dashboard,
|
dashboard,
|
||||||
eskalation,
|
eskalation,
|
||||||
fehlbestaende,
|
fehlbestaende,
|
||||||
|
geraet_instanz,
|
||||||
health,
|
health,
|
||||||
historie,
|
historie,
|
||||||
kontrollen,
|
kontrollen,
|
||||||
@@ -27,4 +28,5 @@ api_router.include_router(kontrollen.router, tags=["kontrollen"])
|
|||||||
api_router.include_router(fehlbestaende.router, tags=["fehlbestaende"])
|
api_router.include_router(fehlbestaende.router, tags=["fehlbestaende"])
|
||||||
api_router.include_router(eskalation.router, tags=["eskalation"])
|
api_router.include_router(eskalation.router, tags=["eskalation"])
|
||||||
api_router.include_router(historie.router, tags=["historie"])
|
api_router.include_router(historie.router, tags=["historie"])
|
||||||
|
api_router.include_router(geraet_instanz.router, tags=["geraet_instanz"])
|
||||||
api_router.include_router(dashboard.router, tags=["dashboard"])
|
api_router.include_router(dashboard.router, tags=["dashboard"])
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.deps import require_roles
|
||||||
|
from app.db.session import get_db
|
||||||
|
from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
|
||||||
|
from app.models.geraet_instanz import GeraetInstanz
|
||||||
|
from app.models.objektposition import Objektposition
|
||||||
|
from app.schemas.geraet_instanz import GeraetInstanzCreate, GeraetInstanzRead, GeraetInstanzStatusUpdate
|
||||||
|
from app.services.geraet_instanz import aendere_status, erstelle, liste_fuer_position, loesche
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
_materialverantwortliche = require_roles(
|
||||||
|
RolleTyp.administration, RolleTyp.materialverantwortlicher, RolleTyp.leitungsverantwortlicher
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _hauptserver_id(db: AsyncSession) -> int:
|
||||||
|
result = await db.execute(select(Systemknoten.id).where(Systemknoten.typ == KnotenTyp.haupt))
|
||||||
|
return result.scalar_one()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/objektpositionen/{position_id}/geraete", response_model=list[GeraetInstanzRead])
|
||||||
|
async def liste_geraete(
|
||||||
|
position_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche)
|
||||||
|
) -> list[GeraetInstanz]:
|
||||||
|
return await liste_fuer_position(db, position_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/objektpositionen/{position_id}/geraete",
|
||||||
|
response_model=GeraetInstanzRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
async def erstelle_geraet(
|
||||||
|
position_id: uuid.UUID,
|
||||||
|
payload: GeraetInstanzCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
_=Depends(_materialverantwortliche),
|
||||||
|
) -> GeraetInstanz:
|
||||||
|
"""Karte 14: löst objektposition.seriennummer als Einzelfeld ab - beliebig
|
||||||
|
viele Geräte-Exemplare pro Position."""
|
||||||
|
position = await db.get(Objektposition, position_id)
|
||||||
|
if position is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objektposition nicht gefunden")
|
||||||
|
try:
|
||||||
|
return await erstelle(db, objektposition_id=position_id, seriennummer=payload.seriennummer)
|
||||||
|
except IntegrityError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Seriennummer an dieser Position bereits vergeben"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/geraete/{geraet_id}", response_model=GeraetInstanzRead)
|
||||||
|
async def aendere_geraet_status(
|
||||||
|
geraet_id: uuid.UUID,
|
||||||
|
payload: GeraetInstanzStatusUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user=Depends(_materialverantwortliche),
|
||||||
|
) -> GeraetInstanz:
|
||||||
|
instanz = await db.get(GeraetInstanz, geraet_id)
|
||||||
|
if instanz is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
|
||||||
|
|
||||||
|
zustaendiger_server_id = await _hauptserver_id(db)
|
||||||
|
aktualisiert, _fehlbestand_id = await aendere_status(
|
||||||
|
db,
|
||||||
|
instanz=instanz,
|
||||||
|
status=payload.status,
|
||||||
|
pruefdatum=payload.pruefdatum,
|
||||||
|
bemerkung=payload.bemerkung,
|
||||||
|
erzeuge_fehlbestand=payload.erzeuge_fehlbestand,
|
||||||
|
benutzer_id=current_user.id,
|
||||||
|
zustaendiger_server_id=zustaendiger_server_id,
|
||||||
|
)
|
||||||
|
return aktualisiert
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/geraete/{geraet_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def loesche_geraet(
|
||||||
|
geraet_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche)
|
||||||
|
) -> None:
|
||||||
|
instanz = await db.get(GeraetInstanz, geraet_id)
|
||||||
|
if instanz is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden")
|
||||||
|
await loesche(db, instanz)
|
||||||
@@ -106,6 +106,7 @@ async def hole_objektposition_per_code(
|
|||||||
ablaufdatum=position.ablaufdatum,
|
ablaufdatum=position.ablaufdatum,
|
||||||
chargennummer=position.chargennummer,
|
chargennummer=position.chargennummer,
|
||||||
code=position.code,
|
code=position.code,
|
||||||
|
pruefintervall_monate=position.pruefintervall_monate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -179,6 +180,7 @@ async def liste_objektpositionen(
|
|||||||
ablaufdatum=p.ablaufdatum,
|
ablaufdatum=p.ablaufdatum,
|
||||||
chargennummer=p.chargennummer,
|
chargennummer=p.chargennummer,
|
||||||
code=p.code,
|
code=p.code,
|
||||||
|
pruefintervall_monate=p.pruefintervall_monate,
|
||||||
)
|
)
|
||||||
for p in positionen
|
for p in positionen
|
||||||
]
|
]
|
||||||
@@ -307,6 +309,7 @@ async def aendere_objektposition(
|
|||||||
ablaufdatum=position.ablaufdatum,
|
ablaufdatum=position.ablaufdatum,
|
||||||
chargennummer=position.chargennummer,
|
chargennummer=position.chargennummer,
|
||||||
code=position.code,
|
code=position.code,
|
||||||
|
pruefintervall_monate=position.pruefintervall_monate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -351,4 +354,5 @@ async def fuege_zusatzposition_hinzu(
|
|||||||
ablaufdatum=position.ablaufdatum,
|
ablaufdatum=position.ablaufdatum,
|
||||||
chargennummer=position.chargennummer,
|
chargennummer=position.chargennummer,
|
||||||
code=position.code,
|
code=position.code,
|
||||||
|
pruefintervall_monate=position.pruefintervall_monate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import uuid
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
from app.models.geraet_instanz import GeraetStatus
|
||||||
|
|
||||||
|
|
||||||
|
class GeraetInstanzCreate(BaseModel):
|
||||||
|
seriennummer: str
|
||||||
|
|
||||||
|
|
||||||
|
class GeraetInstanzRead(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: uuid.UUID
|
||||||
|
objektposition_id: uuid.UUID
|
||||||
|
seriennummer: str
|
||||||
|
pruefdatum: date | None
|
||||||
|
naechste_pruefung: date | None
|
||||||
|
status: GeraetStatus
|
||||||
|
bemerkung: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class GeraetInstanzStatusUpdate(BaseModel):
|
||||||
|
status: GeraetStatus
|
||||||
|
pruefdatum: date | None = None
|
||||||
|
bemerkung: str | None = None
|
||||||
|
# Karte 14: "pro Gerät entscheidbar" - defekt/in_reparatur kann optional
|
||||||
|
# einen Fehlbestand erzeugen (nutzt bestehende Fehlbestand-Infrastruktur),
|
||||||
|
# muss aber nicht (z.B. Ersatzgerät bereits vorhanden).
|
||||||
|
erzeuge_fehlbestand: bool = False
|
||||||
@@ -52,6 +52,7 @@ class ObjektpositionRead(BaseModel):
|
|||||||
ablaufdatum: date | None
|
ablaufdatum: date | None
|
||||||
chargennummer: str | None
|
chargennummer: str | None
|
||||||
code: str | None
|
code: str | None
|
||||||
|
pruefintervall_monate: int | None
|
||||||
|
|
||||||
|
|
||||||
class ObjektpositionUpdate(BaseModel):
|
class ObjektpositionUpdate(BaseModel):
|
||||||
@@ -62,6 +63,7 @@ class ObjektpositionUpdate(BaseModel):
|
|||||||
ablaufdatum: date | None = None
|
ablaufdatum: date | None = None
|
||||||
chargennummer: str | None = None
|
chargennummer: str | None = None
|
||||||
code: str | None = None
|
code: str | None = None
|
||||||
|
pruefintervall_monate: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class ObjektpositionCreate(BaseModel):
|
class ObjektpositionCreate(BaseModel):
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import calendar
|
||||||
|
import uuid
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||||
|
from app.models.geraet_instanz import GeraetInstanz, GeraetStatus
|
||||||
|
from app.models.objekt import Objekt
|
||||||
|
from app.models.objektposition import Objektposition
|
||||||
|
from app.services import benachrichtigung as benachrichtigung_service
|
||||||
|
from app.services import historie as historie_service
|
||||||
|
|
||||||
|
|
||||||
|
def _plus_monate(stichtag: date, monate: int) -> date:
|
||||||
|
"""Kein python-dateutil als Dependency für eine einzelne Monatsaddition -
|
||||||
|
Tag wird auf das Monatsende gekappt, falls der Zielmonat kürzer ist
|
||||||
|
(z.B. 31. Januar + 1 Monat -> 28./29. Februar)."""
|
||||||
|
monats_index = stichtag.month - 1 + monate
|
||||||
|
jahr = stichtag.year + monats_index // 12
|
||||||
|
monat = monats_index % 12 + 1
|
||||||
|
letzter_tag = calendar.monthrange(jahr, monat)[1]
|
||||||
|
return date(jahr, monat, min(stichtag.day, letzter_tag))
|
||||||
|
|
||||||
|
|
||||||
|
async def liste_fuer_position(db: AsyncSession, objektposition_id) -> list[GeraetInstanz]:
|
||||||
|
result = await db.execute(
|
||||||
|
select(GeraetInstanz).where(GeraetInstanz.objektposition_id == objektposition_id)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
async def erstelle(db: AsyncSession, *, objektposition_id, seriennummer: str) -> GeraetInstanz:
|
||||||
|
instanz = GeraetInstanz(objektposition_id=objektposition_id, seriennummer=seriennummer)
|
||||||
|
db.add(instanz)
|
||||||
|
await db.flush()
|
||||||
|
return instanz
|
||||||
|
|
||||||
|
|
||||||
|
async def loesche(db: AsyncSession, instanz: GeraetInstanz) -> None:
|
||||||
|
await db.delete(instanz)
|
||||||
|
|
||||||
|
|
||||||
|
async def aendere_status(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
instanz: GeraetInstanz,
|
||||||
|
status: GeraetStatus,
|
||||||
|
pruefdatum: date | None,
|
||||||
|
bemerkung: str | None,
|
||||||
|
erzeuge_fehlbestand: bool,
|
||||||
|
benutzer_id: int,
|
||||||
|
zustaendiger_server_id: int,
|
||||||
|
) -> tuple[GeraetInstanz, uuid.UUID | None]:
|
||||||
|
"""Karte 14: Status ändern (einsatzbereit/defekt/in_reparatur), optional
|
||||||
|
Prüfung erfassen (naechste_pruefung wird neu berechnet, sofern die
|
||||||
|
Objektposition ein pruefintervall_monate hat), optional einen Fehlbestand
|
||||||
|
erzeugen (pro Gerät entscheidbar, nutzt bestehende Fehlbestand-
|
||||||
|
Infrastruktur statt eigenem Parallel-Konzept)."""
|
||||||
|
alter_status = instanz.status
|
||||||
|
instanz.status = status
|
||||||
|
instanz.bemerkung = bemerkung
|
||||||
|
|
||||||
|
if pruefdatum is not None:
|
||||||
|
instanz.pruefdatum = pruefdatum
|
||||||
|
objektposition = await db.get(Objektposition, instanz.objektposition_id)
|
||||||
|
if objektposition is not None and objektposition.pruefintervall_monate:
|
||||||
|
instanz.naechste_pruefung = _plus_monate(pruefdatum, objektposition.pruefintervall_monate)
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
await historie_service.log(
|
||||||
|
db,
|
||||||
|
zustaendiger_server_id=zustaendiger_server_id,
|
||||||
|
benutzer_id=benutzer_id,
|
||||||
|
ereignistyp="geraet_status_geaendert",
|
||||||
|
entitaet_typ="geraet_instanz",
|
||||||
|
entitaet_id=instanz.id,
|
||||||
|
alter_wert={"status": alter_status.value},
|
||||||
|
neuer_wert={"status": status.value, "seriennummer": instanz.seriennummer},
|
||||||
|
begruendung=bemerkung,
|
||||||
|
)
|
||||||
|
|
||||||
|
fehlbestand_id = None
|
||||||
|
if erzeuge_fehlbestand and status != GeraetStatus.einsatzbereit:
|
||||||
|
fehlbestand_id = await _erzeuge_fehlbestand(
|
||||||
|
db,
|
||||||
|
instanz=instanz,
|
||||||
|
benutzer_id=benutzer_id,
|
||||||
|
zustaendiger_server_id=zustaendiger_server_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return instanz, fehlbestand_id
|
||||||
|
|
||||||
|
|
||||||
|
async def _erzeuge_fehlbestand(
|
||||||
|
db: AsyncSession, *, instanz: GeraetInstanz, benutzer_id: int, zustaendiger_server_id: int
|
||||||
|
):
|
||||||
|
objektposition = await db.get(Objektposition, instanz.objektposition_id)
|
||||||
|
objekt = await db.get(Objekt, objektposition.objekt_id)
|
||||||
|
|
||||||
|
fehlbestand = Fehlbestand(
|
||||||
|
erzeugt_von_server_id=zustaendiger_server_id,
|
||||||
|
objekt_id=objektposition.objekt_id,
|
||||||
|
material_id=objektposition.material_id,
|
||||||
|
standort_id=objekt.standort_id,
|
||||||
|
sollmenge=Decimal(1),
|
||||||
|
istmenge=Decimal(0),
|
||||||
|
fehlmenge=Decimal(1),
|
||||||
|
entstanden_am=datetime.now(timezone.utc),
|
||||||
|
festgestellt_von=benutzer_id,
|
||||||
|
ursache=f"Gerät {instanz.seriennummer} ({instanz.status.value})",
|
||||||
|
status=FehlbestandStatus.offen,
|
||||||
|
)
|
||||||
|
db.add(fehlbestand)
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
await historie_service.log(
|
||||||
|
db,
|
||||||
|
zustaendiger_server_id=zustaendiger_server_id,
|
||||||
|
benutzer_id=benutzer_id,
|
||||||
|
ereignistyp="fehlbestand_entstanden",
|
||||||
|
entitaet_typ="fehlbestand",
|
||||||
|
entitaet_id=fehlbestand.id,
|
||||||
|
neuer_wert={"ursache": fehlbestand.ursache, "geraet_instanz_id": str(instanz.id)},
|
||||||
|
)
|
||||||
|
await benachrichtigung_service.benachrichtige_neuer_fehlbestand(db, fehlbestand=fehlbestand)
|
||||||
|
return fehlbestand.id
|
||||||
@@ -97,3 +97,93 @@ async def test_pruefintervall_monate_default_und_setzen(db_session, objekt_mit_p
|
|||||||
await db_session.flush()
|
await db_session.flush()
|
||||||
await db_session.refresh(position)
|
await db_session.refresh(position)
|
||||||
assert position.pruefintervall_monate == 24
|
assert position.pruefintervall_monate == 24
|
||||||
|
|
||||||
|
|
||||||
|
from tests.conftest import auth_header, login # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_endpunkte_crud_und_pruefdatum(client, db_session, objekt_mit_position, materialverantwortlicher_user):
|
||||||
|
objekt, material = objekt_mit_position
|
||||||
|
token = await login(client, "materialverantwortlicher1")
|
||||||
|
|
||||||
|
positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token))
|
||||||
|
position_id = positionen.json()[0]["id"]
|
||||||
|
|
||||||
|
await client.patch(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/positionen/{position_id}",
|
||||||
|
json={"pruefintervall_monate": 12},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
|
||||||
|
erstellt = await client.post(
|
||||||
|
f"/api/v1/objektpositionen/{position_id}/geraete",
|
||||||
|
json={"seriennummer": "SN-100"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert erstellt.status_code == 201
|
||||||
|
geraet_id = erstellt.json()["id"]
|
||||||
|
assert erstellt.json()["status"] == "einsatzbereit"
|
||||||
|
|
||||||
|
liste = await client.get(f"/api/v1/objektpositionen/{position_id}/geraete", headers=auth_header(token))
|
||||||
|
assert len(liste.json()) == 1
|
||||||
|
|
||||||
|
doppelt = await client.post(
|
||||||
|
f"/api/v1/objektpositionen/{position_id}/geraete",
|
||||||
|
json={"seriennummer": "SN-100"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert doppelt.status_code == 409
|
||||||
|
|
||||||
|
geprueft = await client.patch(
|
||||||
|
f"/api/v1/geraete/{geraet_id}",
|
||||||
|
json={"status": "einsatzbereit", "pruefdatum": "2026-01-15"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert geprueft.status_code == 200
|
||||||
|
assert geprueft.json()["naechste_pruefung"] == "2027-01-15"
|
||||||
|
|
||||||
|
geloescht = await client.delete(f"/api/v1/geraete/{geraet_id}", headers=auth_header(token))
|
||||||
|
assert geloescht.status_code == 204
|
||||||
|
leer = await client.get(f"/api/v1/objektpositionen/{position_id}/geraete", headers=auth_header(token))
|
||||||
|
assert leer.json() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_defekt_kann_fehlbestand_erzeugen(client, objekt_mit_position, materialverantwortlicher_user):
|
||||||
|
objekt, material = objekt_mit_position
|
||||||
|
token = await login(client, "materialverantwortlicher1")
|
||||||
|
|
||||||
|
positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token))
|
||||||
|
position_id = positionen.json()[0]["id"]
|
||||||
|
|
||||||
|
erstellt = await client.post(
|
||||||
|
f"/api/v1/objektpositionen/{position_id}/geraete",
|
||||||
|
json={"seriennummer": "SN-200"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
geraet_id = erstellt.json()["id"]
|
||||||
|
|
||||||
|
ohne_fehlbestand = await client.patch(
|
||||||
|
f"/api/v1/geraete/{geraet_id}",
|
||||||
|
json={"status": "defekt", "bemerkung": "Display kaputt"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert ohne_fehlbestand.status_code == 200
|
||||||
|
|
||||||
|
vorher = await client.get(
|
||||||
|
"/api/v1/fehlbestaende", params={"objekt_id": objekt.id}, headers=auth_header(token)
|
||||||
|
)
|
||||||
|
anzahl_vorher = len(vorher.json())
|
||||||
|
|
||||||
|
mit_fehlbestand = await client.patch(
|
||||||
|
f"/api/v1/geraete/{geraet_id}",
|
||||||
|
json={"status": "defekt", "bemerkung": "Display kaputt", "erzeuge_fehlbestand": True},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert mit_fehlbestand.status_code == 200
|
||||||
|
|
||||||
|
nachher = await client.get(
|
||||||
|
"/api/v1/fehlbestaende", params={"objekt_id": objekt.id}, headers=auth_header(token)
|
||||||
|
)
|
||||||
|
assert len(nachher.json()) == anzahl_vorher + 1
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const TEST_OBJEKT = {
|
|||||||
vorlage_id: 1,
|
vorlage_id: 1,
|
||||||
standort_id: 1,
|
standort_id: 1,
|
||||||
status: "aktiv",
|
status: "aktiv",
|
||||||
|
gezogen_von_objekt_id: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TEST_MATERIAL = {
|
export const TEST_MATERIAL = {
|
||||||
@@ -41,6 +42,8 @@ export const TEST_POSITION = {
|
|||||||
seriennummer: null,
|
seriennummer: null,
|
||||||
ablaufdatum: null,
|
ablaufdatum: null,
|
||||||
chargennummer: null,
|
chargennummer: null,
|
||||||
|
code: null,
|
||||||
|
pruefintervall_monate: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TEST_VORLAGE = {
|
export const TEST_VORLAGE = {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface Objekt {
|
|||||||
vorlage_id: number | null;
|
vorlage_id: number | null;
|
||||||
standort_id: number;
|
standort_id: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
gezogen_von_objekt_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Objektposition {
|
export interface Objektposition {
|
||||||
@@ -19,6 +20,18 @@ export interface Objektposition {
|
|||||||
seriennummer: string | null;
|
seriennummer: string | null;
|
||||||
ablaufdatum: string | null;
|
ablaufdatum: string | null;
|
||||||
chargennummer: string | null;
|
chargennummer: string | null;
|
||||||
|
code: string | null;
|
||||||
|
pruefintervall_monate: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeraetInstanz {
|
||||||
|
id: string;
|
||||||
|
objektposition_id: string;
|
||||||
|
seriennummer: string;
|
||||||
|
pruefdatum: string | null;
|
||||||
|
naechste_pruefung: string | null;
|
||||||
|
status: "einsatzbereit" | "defekt" | "in_reparatur";
|
||||||
|
bemerkung: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Kontrolle {
|
export interface Kontrolle {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { GeraetInstanz } from "../../api/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
objektpositionId: string;
|
||||||
|
pruefintervallMonate: number | null;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<GeraetInstanz["status"], string> = {
|
||||||
|
einsatzbereit: "OK",
|
||||||
|
defekt: "Defekt",
|
||||||
|
in_reparatur: "In Reparatur",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Karte 14: löst das Einzelfeld objektposition.seriennummer ab - beliebig
|
||||||
|
* viele Geräte-Exemplare pro Position (z.B. 2 Pulsoxymeter im selben
|
||||||
|
* Rucksack), je mit eigenem Prüfdatum/Status. */
|
||||||
|
export function GeraeteInstanzenListe({ objektpositionId, pruefintervallMonate, onFehler }: Props) {
|
||||||
|
const [instanzen, setInstanzen] = useState<GeraetInstanz[]>([]);
|
||||||
|
const [neueSeriennummer, setNeueSeriennummer] = useState("");
|
||||||
|
const [bearbeiteId, setBearbeiteId] = useState<string | null>(null);
|
||||||
|
const [statusAuswahl, setStatusAuswahl] = useState<GeraetInstanz["status"]>("einsatzbereit");
|
||||||
|
const [pruefdatum, setPruefdatum] = useState("");
|
||||||
|
const [bemerkung, setBemerkung] = useState("");
|
||||||
|
const [erzeugeFehlbestand, setErzeugeFehlbestand] = useState(false);
|
||||||
|
|
||||||
|
async function laden() {
|
||||||
|
try {
|
||||||
|
const daten = await apiRequest<GeraetInstanz[]>(`/objektpositionen/${objektpositionId}/geraete`);
|
||||||
|
setInstanzen(daten);
|
||||||
|
} catch {
|
||||||
|
onFehler("Geräte-Liste konnte nicht geladen werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
laden();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [objektpositionId]);
|
||||||
|
|
||||||
|
async function hinzufuegen() {
|
||||||
|
if (!neueSeriennummer) return;
|
||||||
|
try {
|
||||||
|
await apiRequest(`/objektpositionen/${objektpositionId}/geraete`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { seriennummer: neueSeriennummer },
|
||||||
|
});
|
||||||
|
setNeueSeriennummer("");
|
||||||
|
await laden();
|
||||||
|
} catch {
|
||||||
|
onFehler("Gerät konnte nicht hinzugefügt werden (Seriennummer evtl. schon vergeben).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearbeiten(instanz: GeraetInstanz) {
|
||||||
|
setBearbeiteId(bearbeiteId === instanz.id ? null : instanz.id);
|
||||||
|
setStatusAuswahl(instanz.status);
|
||||||
|
setPruefdatum("");
|
||||||
|
setBemerkung(instanz.bemerkung ?? "");
|
||||||
|
setErzeugeFehlbestand(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function speichernStatus(instanz: GeraetInstanz) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/geraete/${instanz.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: {
|
||||||
|
status: statusAuswahl,
|
||||||
|
pruefdatum: pruefdatum || null,
|
||||||
|
bemerkung: bemerkung || null,
|
||||||
|
erzeuge_fehlbestand: statusAuswahl !== "einsatzbereit" && erzeugeFehlbestand,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setBearbeiteId(null);
|
||||||
|
await laden();
|
||||||
|
} catch {
|
||||||
|
onFehler("Status konnte nicht geändert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function entfernen(id: string) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/geraete/${id}`, { method: "DELETE" });
|
||||||
|
await laden();
|
||||||
|
} catch {
|
||||||
|
onFehler("Gerät konnte nicht entfernt werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: "0.5rem" }}>
|
||||||
|
{pruefintervallMonate === null && (
|
||||||
|
<p className="text-muted" style={{ fontSize: "0.85rem" }}>
|
||||||
|
Kein Prüfintervall gesetzt – Prüfdatum wird gespeichert, nächste Fälligkeit aber nicht berechnet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ul className="card-list">
|
||||||
|
{instanzen.map((instanz) => (
|
||||||
|
<li key={instanz.id} style={{ padding: "0.35rem 0" }}>
|
||||||
|
<div className="row-between">
|
||||||
|
<span>
|
||||||
|
{instanz.status === "einsatzbereit" ? "✓" : "✗"} {instanz.seriennummer}{" "}
|
||||||
|
<span className="badge badge-neutral">{STATUS_LABEL[instanz.status]}</span>{" "}
|
||||||
|
{instanz.naechste_pruefung && (
|
||||||
|
<span className="text-muted">nächste Prüfung: {instanz.naechste_pruefung}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="row">
|
||||||
|
<button className="btn btn-secondary" onClick={() => bearbeiten(instanz)}>
|
||||||
|
{bearbeiteId === instanz.id ? "Schließen" : "Bearbeiten"}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger" onClick={() => entfernen(instanz.id)}>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{bearbeiteId === instanz.id && (
|
||||||
|
<div className="stack" style={{ marginTop: "0.4rem", paddingLeft: "1rem" }}>
|
||||||
|
<div className="row" style={{ flexWrap: "wrap" }}>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={statusAuswahl}
|
||||||
|
onChange={(e) => setStatusAuswahl(e.target.value as GeraetInstanz["status"])}
|
||||||
|
>
|
||||||
|
<option value="einsatzbereit">Einsatzbereit</option>
|
||||||
|
<option value="defekt">Defekt</option>
|
||||||
|
<option value="in_reparatur">In Reparatur</option>
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="input"
|
||||||
|
value={pruefdatum}
|
||||||
|
onChange={(e) => setPruefdatum(e.target.value)}
|
||||||
|
placeholder="Prüfdatum"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={bemerkung}
|
||||||
|
onChange={(e) => setBemerkung(e.target.value)}
|
||||||
|
placeholder="Bemerkung"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{statusAuswahl !== "einsatzbereit" && (
|
||||||
|
<label className="row" style={{ gap: "0.4rem" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={erzeugeFehlbestand}
|
||||||
|
onChange={(e) => setErzeugeFehlbestand(e.target.checked)}
|
||||||
|
/>
|
||||||
|
Fehlbestand erzeugen
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<button className="btn btn-primary" onClick={() => speichernStatus(instanz)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="row" style={{ marginTop: "0.5rem" }}>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="Neue Seriennummer"
|
||||||
|
value={neueSeriennummer}
|
||||||
|
onChange={(e) => setNeueSeriennummer(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-secondary" onClick={hinzufuegen} disabled={!neueSeriennummer}>
|
||||||
|
Gerät hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { apiRequest } from "../../api/client";
|
import { apiRequest } from "../../api/client";
|
||||||
import type { Material, Objektposition } from "../../api/types";
|
import type { Material, Objektposition } from "../../api/types";
|
||||||
|
import { GeraeteInstanzenListe } from "./GeraeteInstanzenListe";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
objektId: number;
|
objektId: number;
|
||||||
@@ -12,14 +13,14 @@ interface Props {
|
|||||||
interface ZeilenFormular {
|
interface ZeilenFormular {
|
||||||
ablaufdatum: string;
|
ablaufdatum: string;
|
||||||
chargennummer: string;
|
chargennummer: string;
|
||||||
seriennummer: string;
|
pruefintervallMonate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function leeresFormular(p: Objektposition): ZeilenFormular {
|
function leeresFormular(p: Objektposition): ZeilenFormular {
|
||||||
return {
|
return {
|
||||||
ablaufdatum: p.ablaufdatum ?? "",
|
ablaufdatum: p.ablaufdatum ?? "",
|
||||||
chargennummer: p.chargennummer ?? "",
|
chargennummer: p.chargennummer ?? "",
|
||||||
seriennummer: p.seriennummer ?? "",
|
pruefintervallMonate: p.pruefintervall_monate?.toString() ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
|||||||
body: {
|
body: {
|
||||||
ablaufdatum: formular.ablaufdatum || null,
|
ablaufdatum: formular.ablaufdatum || null,
|
||||||
chargennummer: formular.chargennummer || null,
|
chargennummer: formular.chargennummer || null,
|
||||||
seriennummer: formular.seriennummer || null,
|
pruefintervall_monate: formular.pruefintervallMonate ? Number(formular.pruefintervallMonate) : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p)));
|
setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p)));
|
||||||
@@ -113,18 +114,30 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props
|
|||||||
)}
|
)}
|
||||||
{typ === "geraet_sn" && (
|
{typ === "geraet_sn" && (
|
||||||
<input
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
className="input"
|
className="input"
|
||||||
placeholder="Seriennummer"
|
placeholder="Prüfintervall (Monate)"
|
||||||
value={formular.seriennummer}
|
value={formular.pruefintervallMonate}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormulare((f) => ({ ...f, [p.id]: { ...formular, seriennummer: e.target.value } }))
|
setFormulare((f) => ({
|
||||||
|
...f,
|
||||||
|
[p.id]: { ...formular, pruefintervallMonate: e.target.value },
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button className="btn btn-primary" onClick={() => speichern(p)} disabled={speichertId === p.id}>
|
<button className="btn btn-primary" onClick={() => speichern(p)} disabled={speichertId === p.id}>
|
||||||
Speichern
|
{typ === "geraet_sn" ? "Prüfintervall speichern" : "Speichern"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{typ === "geraet_sn" && (
|
||||||
|
<GeraeteInstanzenListe
|
||||||
|
objektpositionId={p.id}
|
||||||
|
pruefintervallMonate={p.pruefintervall_monate}
|
||||||
|
onFehler={onFehler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
const [neuerCode, setNeuerCode] = useState("");
|
const [neuerCode, setNeuerCode] = useState("");
|
||||||
const [neuerName, setNeuerName] = useState("");
|
const [neuerName, setNeuerName] = useState("");
|
||||||
const [neuerStandortId, setNeuerStandortId] = useState<number | "">("");
|
const [neuerStandortId, setNeuerStandortId] = useState<number | "">("");
|
||||||
|
const [gezogenVonId, setGezogenVonId] = useState<number | "">("");
|
||||||
|
const [kopplungBearbeitenId, setKopplungBearbeitenId] = useState<number | null>(null);
|
||||||
|
const [kopplungAuswahl, setKopplungAuswahl] = useState<number | "">("");
|
||||||
|
|
||||||
async function codeVorschlagen() {
|
async function codeVorschlagen() {
|
||||||
if (!praefix) {
|
if (!praefix) {
|
||||||
@@ -73,11 +76,13 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
objekttyp_id: vorlage?.objekttyp_id,
|
objekttyp_id: vorlage?.objekttyp_id,
|
||||||
vorlage_id: vorlageId,
|
vorlage_id: vorlageId,
|
||||||
standort_id: standortId,
|
standort_id: standortId,
|
||||||
|
gezogen_von_objekt_id: gezogenVonId || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setPraefix("");
|
setPraefix("");
|
||||||
setCode("");
|
setCode("");
|
||||||
setName("");
|
setName("");
|
||||||
|
setGezogenVonId("");
|
||||||
await onGeaendert();
|
await onGeaendert();
|
||||||
} catch {
|
} catch {
|
||||||
onFehler("Objekt konnte nicht angelegt werden (Code evtl. schon vergeben).");
|
onFehler("Objekt konnte nicht angelegt werden (Code evtl. schon vergeben).");
|
||||||
@@ -86,6 +91,30 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function objektName(id: number | null): string {
|
||||||
|
if (id === null) return "";
|
||||||
|
const o = objekte.find((x) => x.id === id);
|
||||||
|
return o ? `${o.name} (${o.code})` : `Objekt ${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function kopplungBearbeiten(o: Objekt) {
|
||||||
|
setKopplungBearbeitenId(kopplungBearbeitenId === o.id ? null : o.id);
|
||||||
|
setKopplungAuswahl(o.gezogen_von_objekt_id ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kopplungSpeichern(objektId: number) {
|
||||||
|
try {
|
||||||
|
await apiRequest(`/objekte/${objektId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { gezogen_von_objekt_id: kopplungAuswahl || null },
|
||||||
|
});
|
||||||
|
setKopplungBearbeitenId(null);
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Kopplung konnte nicht geändert werden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function duplizierenStarten(o: Objekt) {
|
function duplizierenStarten(o: Objekt) {
|
||||||
setDuplizierId(duplizierId === o.id ? null : o.id);
|
setDuplizierId(duplizierId === o.id ? null : o.id);
|
||||||
setNeuerPraefix("");
|
setNeuerPraefix("");
|
||||||
@@ -167,6 +196,21 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} />
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Gezogen von (optional, Anhänger-Kopplung)</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={gezogenVonId}
|
||||||
|
onChange={(e) => setGezogenVonId(Number(e.target.value) || "")}
|
||||||
|
>
|
||||||
|
<option value="">Kein Zugfahrzeug</option>
|
||||||
|
{objekte.map((o) => (
|
||||||
|
<option key={o.id} value={o.id}>
|
||||||
|
{o.name} ({o.code})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button className="btn btn-primary" onClick={anlegen} disabled={!code || !name || wirdAngelegt}>
|
<button className="btn btn-primary" onClick={anlegen} disabled={!code || !name || wirdAngelegt}>
|
||||||
Anlegen
|
Anlegen
|
||||||
</button>
|
</button>
|
||||||
@@ -180,6 +224,9 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
<div className="row-between" style={{ padding: "0.4rem 0" }}>
|
<div className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
<span>
|
<span>
|
||||||
{o.name} <span className="text-muted">({o.code})</span>
|
{o.name} <span className="text-muted">({o.code})</span>
|
||||||
|
{o.gezogen_von_objekt_id !== null && (
|
||||||
|
<span className="text-muted"> – gezogen von {objektName(o.gezogen_von_objekt_id)}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<button
|
<button
|
||||||
@@ -188,11 +235,35 @@ export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGea
|
|||||||
>
|
>
|
||||||
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
||||||
</button>
|
</button>
|
||||||
|
<button className="btn btn-secondary" onClick={() => kopplungBearbeiten(o)}>
|
||||||
|
{kopplungBearbeitenId === o.id ? "Schließen" : "Kopplung ändern"}
|
||||||
|
</button>
|
||||||
<button className="btn btn-secondary" onClick={() => duplizierenStarten(o)}>
|
<button className="btn btn-secondary" onClick={() => duplizierenStarten(o)}>
|
||||||
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
{duplizierId === o.id ? "Schließen" : "Duplizieren"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{kopplungBearbeitenId === o.id && (
|
||||||
|
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={kopplungAuswahl}
|
||||||
|
onChange={(e) => setKopplungAuswahl(Number(e.target.value) || "")}
|
||||||
|
>
|
||||||
|
<option value="">Kein Zugfahrzeug</option>
|
||||||
|
{objekte
|
||||||
|
.filter((x) => x.id !== o.id)
|
||||||
|
.map((x) => (
|
||||||
|
<option key={x.id} value={x.id}>
|
||||||
|
{x.name} ({x.code})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button className="btn btn-primary" onClick={() => kopplungSpeichern(o.id)}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{ausgeklapptId === o.id && (
|
{ausgeklapptId === o.id && (
|
||||||
<div style={{ marginTop: "0.5rem" }}>
|
<div style={{ marginTop: "0.5rem" }}>
|
||||||
<ObjektPositionenPanel objektId={o.id} materialien={materialien} onFehler={onFehler} />
|
<ObjektPositionenPanel objektId={o.id} materialien={materialien} onFehler={onFehler} />
|
||||||
|
|||||||
Reference in New Issue
Block a user