feat(objekt): letzte abgeschlossene Kontrolle je Objekt anzeigen
Bisher nirgends vermerkt/abrufbar, wann ein Objekt zuletzt komplett
kontrolliert wurde (nur roh in kontrolle.beendet_am ableitbar). Neues Feld
ObjektRead.letzte_kontrolle_am (jüngste Kontrolle mit status=abgeschlossen),
angezeigt in Objektliste und Admin-Objektsektion ("noch nie kontrolliert"
falls keine).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -3696,3 +3696,205 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/pages/admin/ObjektSection.tsx | 94 ++++++++++++++++++++++++++++++++++++----------------------------------
|
- frontend/src/pages/admin/ObjektSection.tsx | 94 ++++++++++++++++++++++++++++++++++++----------------------------------
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-05 10:32 – 10:33 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** backend
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 54fc296 feat(objekt): Zugfahrzeug-Flag am Objekttyp + Leitungs-Dashboard-Kacheln
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:33 – 10:33 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:33 – 10:35 (1m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:35 – 10:35 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:36 – 10:36 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:37 – 10:37 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 21 ++++
|
||||||
|
- backend/DEVLOG.md | 18 ++++
|
||||||
|
- backend/alembic/versions/0011_objekttyp_zugfahrzeug.py | 31 ++++++
|
||||||
|
- backend/app/api/v1/endpoints/dashboard.py | 33 ++++++-
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 25 +++--
|
||||||
|
- backend/app/models/stammdaten.py | 4 +
|
||||||
|
- backend/app/schemas/dashboard.py | 31 ++++++
|
||||||
|
- backend/app/schemas/stammdaten.py | 2 +
|
||||||
|
- backend/app/services/dashboard.py | 138 ++++++++++++++++++++++++++
|
||||||
|
- backend/tests/conftest.py | 4 +-
|
||||||
|
- backend/tests/test_dashboard.py | 130 +++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_objekte.py | 66 ++++++++++++-
|
||||||
|
- frontend/src/App.tsx | 18 ++++
|
||||||
|
- frontend/src/api/types.ts | 18 ++++
|
||||||
|
- frontend/src/components/AppShell.tsx | 10 ++
|
||||||
|
- frontend/src/pages/AdminPage.tsx | 1 +
|
||||||
|
- frontend/src/pages/DashboardPage.tsx | 161 +++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/FehlbestandListePage.tsx | 152 +++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 16 ++-
|
||||||
|
- frontend/src/pages/admin/ObjekttypSection.tsx | 32 +++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:38 – 10:39 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 69d6956 feat(login): Verantwortliche landen nach Login auf /dashboard
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 ++++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:40 – 10:40 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 ++++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -24,12 +24,27 @@ from app.schemas.objekt import (
|
|||||||
)
|
)
|
||||||
from app.services.objekte import (
|
from app.services.objekte import (
|
||||||
dupliziere_objekt,
|
dupliziere_objekt,
|
||||||
|
letzte_kontrollen_map,
|
||||||
naechster_freier_code,
|
naechster_freier_code,
|
||||||
objekt_aus_vorlage,
|
objekt_aus_vorlage,
|
||||||
sollmenge_effektiv,
|
sollmenge_effektiv,
|
||||||
sollmenge_map,
|
sollmenge_map,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _objekt_read(objekt: Objekt, letzte_kontrolle_am=None) -> ObjektRead:
|
||||||
|
return ObjektRead(
|
||||||
|
id=objekt.id,
|
||||||
|
code=objekt.code,
|
||||||
|
name=objekt.name,
|
||||||
|
objekttyp_id=objekt.objekttyp_id,
|
||||||
|
vorlage_id=objekt.vorlage_id,
|
||||||
|
standort_id=objekt.standort_id,
|
||||||
|
status=objekt.status,
|
||||||
|
fahrzeug_id=objekt.fahrzeug_id,
|
||||||
|
letzte_kontrolle_am=letzte_kontrolle_am,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
_admin_only = require_roles(RolleTyp.administration)
|
_admin_only = require_roles(RolleTyp.administration)
|
||||||
@@ -57,25 +72,28 @@ async def liste_objekte(
|
|||||||
standort_id: int | None = None,
|
standort_id: int | None = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_=Depends(get_current_user),
|
_=Depends(get_current_user),
|
||||||
) -> list[Objekt]:
|
) -> list[ObjektRead]:
|
||||||
stmt = select(Objekt)
|
stmt = select(Objekt)
|
||||||
if standort_id is not None:
|
if standort_id is not None:
|
||||||
stmt = stmt.where(Objekt.standort_id == standort_id)
|
stmt = stmt.where(Objekt.standort_id == standort_id)
|
||||||
result = await db.execute(stmt)
|
result = await db.execute(stmt)
|
||||||
return list(result.scalars().all())
|
objekte = list(result.scalars().all())
|
||||||
|
letzte_map = await letzte_kontrollen_map(db, [o.id for o in objekte])
|
||||||
|
return [_objekt_read(o, letzte_map.get(o.id)) for o in objekte]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/objekte/code/{code}", response_model=ObjektRead)
|
@router.get("/objekte/code/{code}", response_model=ObjektRead)
|
||||||
async def hole_objekt_per_code(
|
async def hole_objekt_per_code(
|
||||||
code: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
code: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||||
) -> Objekt:
|
) -> ObjektRead:
|
||||||
"""Lookup per QR/Barcode-Code (Karte 10). Muss VOR /objekte/{id} registriert
|
"""Lookup per QR/Barcode-Code (Karte 10). Muss VOR /objekte/{id} registriert
|
||||||
sein, sonst greift FastAPI/Starlette-Routing zuerst die generische ID-Route."""
|
sein, sonst greift FastAPI/Starlette-Routing zuerst die generische ID-Route."""
|
||||||
result = await db.execute(select(Objekt).where(Objekt.code == code))
|
result = await db.execute(select(Objekt).where(Objekt.code == code))
|
||||||
objekt = result.scalar_one_or_none()
|
objekt = result.scalar_one_or_none()
|
||||||
if objekt is None:
|
if objekt is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||||
return objekt
|
letzte_map = await letzte_kontrollen_map(db, [objekt.id])
|
||||||
|
return _objekt_read(objekt, letzte_map.get(objekt.id))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/objekte/naechster-code")
|
@router.get("/objekte/naechster-code")
|
||||||
@@ -129,11 +147,12 @@ async def hole_objektposition_per_code(
|
|||||||
@router.get("/objekte/{objekt_id}", response_model=ObjektRead)
|
@router.get("/objekte/{objekt_id}", response_model=ObjektRead)
|
||||||
async def hole_objekt(
|
async def hole_objekt(
|
||||||
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||||
) -> Objekt:
|
) -> ObjektRead:
|
||||||
objekt = await db.get(Objekt, objekt_id)
|
objekt = await db.get(Objekt, objekt_id)
|
||||||
if objekt is None:
|
if objekt is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||||
return objekt
|
letzte_map = await letzte_kontrollen_map(db, [objekt.id])
|
||||||
|
return _objekt_read(objekt, letzte_map.get(objekt.id))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/objekte/{objekt_id}/label.pdf")
|
@router.get("/objekte/{objekt_id}/label.pdf")
|
||||||
@@ -205,7 +224,7 @@ async def liste_objektpositionen(
|
|||||||
@router.post("/objekte", response_model=ObjektRead, status_code=status.HTTP_201_CREATED)
|
@router.post("/objekte", response_model=ObjektRead, status_code=status.HTTP_201_CREATED)
|
||||||
async def erstelle_objekt(
|
async def erstelle_objekt(
|
||||||
payload: ObjektCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
|
payload: ObjektCreate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
|
||||||
) -> Objekt:
|
) -> ObjektRead:
|
||||||
"""Prompt 09: neues Objekt aus einer Beladungsvorlage, startet "nicht kontrolliert"
|
"""Prompt 09: neues Objekt aus einer Beladungsvorlage, startet "nicht kontrolliert"
|
||||||
(Ist-Menge 0 in allen übernommenen Positionen)."""
|
(Ist-Menge 0 in allen übernommenen Positionen)."""
|
||||||
vorlage = await db.get(Beladungsvorlage, payload.vorlage_id)
|
vorlage = await db.get(Beladungsvorlage, payload.vorlage_id)
|
||||||
@@ -218,7 +237,7 @@ async def erstelle_objekt(
|
|||||||
zustaendiger_server_id = hauptserver.scalar_one()
|
zustaendiger_server_id = hauptserver.scalar_one()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await objekt_aus_vorlage(
|
objekt = await objekt_aus_vorlage(
|
||||||
db,
|
db,
|
||||||
code=payload.code,
|
code=payload.code,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
@@ -229,12 +248,13 @@ async def erstelle_objekt(
|
|||||||
)
|
)
|
||||||
except IntegrityError as exc:
|
except IntegrityError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Code bereits vergeben") from exc
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Code bereits vergeben") from exc
|
||||||
|
return _objekt_read(objekt)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/objekte/{objekt_id}", response_model=ObjektRead)
|
@router.patch("/objekte/{objekt_id}", response_model=ObjektRead)
|
||||||
async def aendere_objekt(
|
async def aendere_objekt(
|
||||||
objekt_id: int, payload: ObjektUpdate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
|
objekt_id: int, payload: ObjektUpdate, db: AsyncSession = Depends(get_db), _=Depends(_admin_only)
|
||||||
) -> Objekt:
|
) -> ObjektRead:
|
||||||
"""Fahrzeug-Zuordnung nachträglich setzen/lösen: jedes Objekt gehört zu
|
"""Fahrzeug-Zuordnung nachträglich setzen/lösen: jedes Objekt gehört zu
|
||||||
jedem Zeitpunkt höchstens einem Fahrzeug (1:n). Zielobjekt-Existenz und
|
jedem Zeitpunkt höchstens einem Fahrzeug (1:n). Zielobjekt-Existenz und
|
||||||
direkter Zyklus (A↔B) werden geprüft, mehrstufige Ketten sind fachlich
|
direkter Zyklus (A↔B) werden geprüft, mehrstufige Ketten sind fachlich
|
||||||
@@ -259,7 +279,8 @@ async def aendere_objekt(
|
|||||||
await db.flush()
|
await db.flush()
|
||||||
except IntegrityError as exc:
|
except IntegrityError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Ungültige Zuordnung") from exc
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Ungültige Zuordnung") from exc
|
||||||
return objekt
|
letzte_map = await letzte_kontrollen_map(db, [objekt.id])
|
||||||
|
return _objekt_read(objekt, letzte_map.get(objekt.id))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
@@ -270,7 +291,7 @@ async def dupliziere(
|
|||||||
payload: ObjektDuplizierenRequest,
|
payload: ObjektDuplizierenRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
_=Depends(_admin_only),
|
_=Depends(_admin_only),
|
||||||
) -> Objekt:
|
) -> ObjektRead:
|
||||||
"""Prompt 09: Struktur/Sollmengen kopiert, Ist-Menge/SN/Ablauf/Charge NICHT,
|
"""Prompt 09: Struktur/Sollmengen kopiert, Ist-Menge/SN/Ablauf/Charge NICHT,
|
||||||
neuer eindeutiger Code Pflicht."""
|
neuer eindeutiger Code Pflicht."""
|
||||||
quelle = await db.get(Objekt, objekt_id)
|
quelle = await db.get(Objekt, objekt_id)
|
||||||
@@ -278,7 +299,7 @@ async def dupliziere(
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Quell-Objekt nicht gefunden")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Quell-Objekt nicht gefunden")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await dupliziere_objekt(
|
neues_objekt = await dupliziere_objekt(
|
||||||
db,
|
db,
|
||||||
quelle=quelle,
|
quelle=quelle,
|
||||||
neuer_code=payload.neuer_code,
|
neuer_code=payload.neuer_code,
|
||||||
@@ -288,6 +309,7 @@ async def dupliziere(
|
|||||||
)
|
)
|
||||||
except IntegrityError as exc:
|
except IntegrityError as exc:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Code bereits vergeben") from exc
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Code bereits vergeben") from exc
|
||||||
|
return _objekt_read(neues_objekt)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/objekte/{objekt_id}/positionen/{position_id}", response_model=ObjektpositionRead)
|
@router.patch("/objekte/{objekt_id}/positionen/{position_id}", response_model=ObjektpositionRead)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
@@ -27,6 +27,7 @@ class ObjektRead(BaseModel):
|
|||||||
standort_id: int
|
standort_id: int
|
||||||
status: ObjektStatus
|
status: ObjektStatus
|
||||||
fahrzeug_id: int | None
|
fahrzeug_id: int | None
|
||||||
|
letzte_kontrolle_am: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class ObjektUpdate(BaseModel):
|
class ObjektUpdate(BaseModel):
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.kontrolle import Kontrolle, KontrollStatus
|
||||||
from app.models.objekt import Objekt
|
from app.models.objekt import Objekt
|
||||||
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
||||||
from app.models.vorlage import Beladungsvorlage, Vorlagenposition
|
from app.models.vorlage import Beladungsvorlage, Vorlagenposition
|
||||||
@@ -29,6 +31,22 @@ async def naechster_freier_code(db: AsyncSession, spalte, praefix: str) -> str:
|
|||||||
return f"{praefix}-{hoechste + 1:0{breite}d}"
|
return f"{praefix}-{hoechste + 1:0{breite}d}"
|
||||||
|
|
||||||
|
|
||||||
|
async def letzte_kontrollen_map(db: AsyncSession, objekt_ids: list[int]) -> dict[int, datetime]:
|
||||||
|
"""Nutzer-Frage: "wann war die letzte komplette Kontrolle vom Objekt" - bisher
|
||||||
|
nirgends vermerkt (kein Cache-Feld am Objekt, nur ableitbar aus kontrolle.
|
||||||
|
beendet_am). Liefert je Objekt den jüngsten Abschluss-Zeitpunkt einer
|
||||||
|
Kontrolle mit status=abgeschlossen; Objekte ohne abgeschlossene Kontrolle
|
||||||
|
fehlen im Ergebnis (Aufrufer behandelt das als "noch nie kontrolliert")."""
|
||||||
|
if not objekt_ids:
|
||||||
|
return {}
|
||||||
|
result = await db.execute(
|
||||||
|
select(Kontrolle.objekt_id, func.max(Kontrolle.beendet_am))
|
||||||
|
.where(Kontrolle.objekt_id.in_(objekt_ids), Kontrolle.status == KontrollStatus.abgeschlossen)
|
||||||
|
.group_by(Kontrolle.objekt_id)
|
||||||
|
)
|
||||||
|
return {objekt_id: beendet_am for objekt_id, beendet_am in result.all()}
|
||||||
|
|
||||||
|
|
||||||
async def sollmenge_map(db: AsyncSession, vorlage_id: int | None) -> dict[int, Decimal]:
|
async def sollmenge_map(db: AsyncSession, vorlage_id: int | None) -> dict[int, Decimal]:
|
||||||
"""material_id -> Sollmenge laut Vorlage (Prompt 10: Basis für die dynamische
|
"""material_id -> Sollmenge laut Vorlage (Prompt 10: Basis für die dynamische
|
||||||
Auflösung, wenn eine Objektposition keinen Override hat)."""
|
Auflösung, wenn eine Objektposition keinen Override hat)."""
|
||||||
|
|||||||
@@ -5,6 +5,38 @@ from app.schemas.vorlage import VorlagenpositionCreate
|
|||||||
from tests.conftest import auth_header, login
|
from tests.conftest import auth_header, login
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_letzte_kontrolle_am_wird_erst_nach_abschluss_gesetzt(
|
||||||
|
client, objekt_mit_position, mitarbeiter_user
|
||||||
|
):
|
||||||
|
"""Nutzer-Frage: wann war die letzte KOMPLETTE Kontrolle - vorher (None/
|
||||||
|
"noch nie"), nach starte_kontrolle allein noch nicht (erst status=abgeschlossen
|
||||||
|
zählt), erst nach /abschliessen gesetzt."""
|
||||||
|
objekt, material = objekt_mit_position
|
||||||
|
token = await login(client, "mitarbeiter1")
|
||||||
|
|
||||||
|
vor_kontrolle = await client.get(f"/api/v1/objekte/{objekt.id}", headers=auth_header(token))
|
||||||
|
assert vor_kontrolle.json()["letzte_kontrolle_am"] is None
|
||||||
|
|
||||||
|
start = await client.post(
|
||||||
|
f"/api/v1/objekte/{objekt.id}/kontrollen", json={"uebernehmen": False}, headers=auth_header(token)
|
||||||
|
)
|
||||||
|
kontrolle_id = start.json()["id"]
|
||||||
|
await client.put(
|
||||||
|
f"/api/v1/kontrollen/{kontrolle_id}/positionen/{material.id}",
|
||||||
|
json={"istmenge": "10"},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
|
||||||
|
waehrend = await client.get(f"/api/v1/objekte/{objekt.id}", headers=auth_header(token))
|
||||||
|
assert waehrend.json()["letzte_kontrolle_am"] is None # noch nicht abgeschlossen
|
||||||
|
|
||||||
|
await client.post(f"/api/v1/kontrollen/{kontrolle_id}/abschliessen", headers=auth_header(token))
|
||||||
|
|
||||||
|
nach_abschluss = await client.get(f"/api/v1/objekte/{objekt.id}", headers=auth_header(token))
|
||||||
|
assert nach_abschluss.json()["letzte_kontrolle_am"] is not None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_objekt_aus_vorlage_startet_unkontrolliert(
|
async def test_objekt_aus_vorlage_startet_unkontrolliert(
|
||||||
client, db_session, objekttyp_factory, material_factory, standort_factory, admin_user
|
client, db_session, objekttyp_factory, material_factory, standort_factory, admin_user
|
||||||
|
|||||||
@@ -191,3 +191,36 @@ Keine Commits in dieser Session.
|
|||||||
- frontend/src/pages/admin/ObjektSection.tsx | 29 ++++++++++++++++++++++++-----
|
- frontend/src/pages/admin/ObjektSection.tsx | 29 ++++++++++++++++++++++++-----
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-05 10:40 – 10:47 (6m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 ++++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:50 – 10:50 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** frontend
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 ++++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-05 10:51 – 10:51 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** frontend
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- frontend/src/pages/LoginPage.tsx | 11 ++++++++++-
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
Generated
+6264
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ export interface Objekt {
|
|||||||
standort_id: number;
|
standort_id: number;
|
||||||
status: string;
|
status: string;
|
||||||
fahrzeug_id: number | null;
|
fahrzeug_id: number | null;
|
||||||
|
letzte_kontrolle_am: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Objektposition {
|
export interface Objektposition {
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ export function ObjektListPage() {
|
|||||||
<span className="badge badge-neutral">{objekt.status}</span>
|
<span className="badge badge-neutral">{objekt.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-subtitle">{objekt.code}</div>
|
<div className="card-subtitle">{objekt.code}</div>
|
||||||
|
<div className="text-muted" style={{ fontSize: "0.85rem" }}>
|
||||||
|
{objekt.letzte_kontrolle_am
|
||||||
|
? `Letzte Kontrolle: ${new Date(objekt.letzte_kontrolle_am).toLocaleDateString("de-DE")}`
|
||||||
|
: "Noch nie kontrolliert"}
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -277,6 +277,13 @@ export function ObjektSection({
|
|||||||
{o.fahrzeug_id !== null && (
|
{o.fahrzeug_id !== null && (
|
||||||
<span className="text-muted"> – Fahrzeug: {objektName(o.fahrzeug_id)}</span>
|
<span className="text-muted"> – Fahrzeug: {objektName(o.fahrzeug_id)}</span>
|
||||||
)}
|
)}
|
||||||
|
<span className="text-muted">
|
||||||
|
{" "}
|
||||||
|
–{" "}
|
||||||
|
{o.letzte_kontrolle_am
|
||||||
|
? `zuletzt kontrolliert ${new Date(o.letzte_kontrolle_am).toLocaleDateString("de-DE")}`
|
||||||
|
: "noch nie kontrolliert"}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user