feat(wartung): MAINT-007 Tankbuch/Kraftstoffverbrauch je Fahrzeug
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
1 parent
96499d655f
commit
04a255429d
14 files changed
+500
-1
No files matched your search
@@ -167,7 +167,14 @@ ebenfalls P3/offen.
|
||||
- **Audit:** Änderung geloggt
|
||||
- **Akzeptanzkriterien:** zwei Einträge ergeben korrekten l/100km-Wert.
|
||||
- **Tests:** Verbrauchsberechnungstest.
|
||||
- **DoD:** offen, **P3 — kein MVP-Bestandteil**, analog MAINT-005-Priorisierung.
|
||||
- **DoD:** ✅ umgesetzt (2026-09-10). `tankbuch_eintrag`-Tabelle (Migration 0036),
|
||||
Verbrauch (Full-Tank-Methode, l/100km) serverseitig berechnet zwischen
|
||||
aufeinanderfolgenden Einträgen sortiert nach `kilometerstand`, nicht als
|
||||
gespeicherte Spalte. `GET/POST /objekte/{objekt_id}/tankbuch`,
|
||||
`DELETE /tankbuch/{eintrag_id}` (nur Materialverantwortliche+). Frontend:
|
||||
`TankbuchPanel` in der Akte, nur bei Fahrzeugen sichtbar
|
||||
(`fahrzeugdetails !== null`). `backend/app/services/tankbuch.py`,
|
||||
`backend/tests/test_tankbuch.py`, `frontend/src/components/TankbuchPanel.tsx`.
|
||||
**Referenz (Open-Source-Vergleich 2026-09-09):** FleetMS (`jmnda-dev/fleetms`,
|
||||
Elixir/Phoenix, AGPL-3.0 — Projekt selbst laut eigenem README unreif/Prototyp,
|
||||
nur als Ideengeber, kein Vorbild für Codequalität) führt ein einfaches Fuel-Log als
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""MAINT-007: Tankbuch/Kraftstoffverbrauch je Fahrzeug.
|
||||
|
||||
Revision ID: 0036_tankbuch
|
||||
Revises: 0035_akte_notiz
|
||||
Create Date: 2026-09-10
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0036_tankbuch"
|
||||
down_revision: Union[str, None] = "0035_akte_notiz"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE tankbuch_eintrag (
|
||||
id UUID PRIMARY KEY,
|
||||
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
|
||||
datum DATE NOT NULL,
|
||||
menge_liter NUMERIC NOT NULL,
|
||||
kosten NUMERIC,
|
||||
kilometerstand INTEGER NOT NULL,
|
||||
erfasst_von_benutzer_id INTEGER NOT NULL REFERENCES benutzer(id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_tankbuch_eintrag_objekt ON tankbuch_eintrag (objekt_id, kilometerstand)")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE tankbuch_eintrag")
|
||||
@@ -23,6 +23,7 @@ from app.api.v1.endpoints import (
|
||||
personal,
|
||||
stammdaten,
|
||||
suche,
|
||||
tankbuch,
|
||||
vorlagen,
|
||||
wartung,
|
||||
zustaendigkeit,
|
||||
@@ -60,3 +61,4 @@ api_router.include_router(lager.router, tags=["lager"])
|
||||
api_router.include_router(ausgabe.router, tags=["ausgabe"])
|
||||
api_router.include_router(wartung.router, tags=["wartung"])
|
||||
api_router.include_router(suche.router, tags=["suche"])
|
||||
api_router.include_router(tankbuch.router, tags=["tankbuch"])
|
||||
@@ -0,0 +1,78 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_current_user, require_roles
|
||||
from app.db.session import get_db
|
||||
from app.models.auth import Benutzer, KnotenTyp, RolleTyp, Systemknoten
|
||||
from app.models.tankbuch import TankbuchEintrag
|
||||
from app.schemas.tankbuch import TankbuchEintragCreate, TankbuchEintragRead
|
||||
from app.services.tankbuch import erstelle, liste_fuer_objekt, loesche
|
||||
from sqlalchemy import select
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_mitarbeiter_plus = require_roles(
|
||||
RolleTyp.mitarbeiter,
|
||||
RolleTyp.materialverantwortlicher,
|
||||
RolleTyp.leitungsverantwortlicher,
|
||||
RolleTyp.administration,
|
||||
)
|
||||
_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("/objekte/{objekt_id}/tankbuch", response_model=list[TankbuchEintragRead])
|
||||
async def liste_tankbuch(
|
||||
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
|
||||
) -> list[TankbuchEintragRead]:
|
||||
treffer = await liste_fuer_objekt(db, objekt_id)
|
||||
return [
|
||||
TankbuchEintragRead.model_validate(t["eintrag"]).model_copy(
|
||||
update={"verbrauch_l_pro_100km": t["verbrauch_l_pro_100km"]}
|
||||
)
|
||||
for t in treffer
|
||||
]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/objekte/{objekt_id}/tankbuch",
|
||||
response_model=TankbuchEintragRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def erstelle_tankbuch_eintrag(
|
||||
objekt_id: int,
|
||||
payload: TankbuchEintragCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||||
) -> TankbuchEintrag:
|
||||
if payload.objekt_id != objekt_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="objekt_id im Pfad und Body weichen ab")
|
||||
zustaendiger_server_id = await _hauptserver_id(db)
|
||||
return await erstelle(
|
||||
db,
|
||||
objekt_id=objekt_id,
|
||||
datum=payload.datum,
|
||||
menge_liter=payload.menge_liter,
|
||||
kosten=payload.kosten,
|
||||
kilometerstand=payload.kilometerstand,
|
||||
erfasst_von_benutzer_id=current_user.id,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/tankbuch/{eintrag_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def loesche_tankbuch_eintrag(
|
||||
eintrag_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche)
|
||||
) -> None:
|
||||
eintrag = await db.get(TankbuchEintrag, eintrag_id)
|
||||
if eintrag is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tankbuch-Eintrag nicht gefunden")
|
||||
await loesche(db, eintrag)
|
||||
@@ -24,6 +24,7 @@ from app.models.personal import (
|
||||
Qualifikationstyp,
|
||||
)
|
||||
from app.models.stammdaten import Bereich, Kategorie, Material, MaterialTyp, Objekttyp, Standort
|
||||
from app.models.tankbuch import TankbuchEintrag
|
||||
from app.models.vorlage import Beladungsvorlage, VorlageStatus, Vorlagenposition
|
||||
from app.models.wartung import (
|
||||
IntervallTyp,
|
||||
@@ -86,6 +87,7 @@ __all__ = [
|
||||
"MaterialTyp",
|
||||
"Objekttyp",
|
||||
"Standort",
|
||||
"TankbuchEintrag",
|
||||
"Beladungsvorlage",
|
||||
"VorlageStatus",
|
||||
"Vorlagenposition",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Integer, Numeric
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class TankbuchEintrag(Base):
|
||||
"""MAINT-007: Betankungs-Log je Fahrzeug (Objekt) - Verbrauchsberechnung
|
||||
zwischen aufeinanderfolgenden Einträgen (sortiert nach kilometerstand) läuft
|
||||
in app/services/tankbuch.py, nicht als gespeicherte Spalte."""
|
||||
|
||||
__tablename__ = "tankbuch_eintrag"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
objekt_id: Mapped[int] = mapped_column(ForeignKey("objekt.id"), nullable=False)
|
||||
datum: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
menge_liter: Mapped[Decimal] = mapped_column(Numeric, nullable=False)
|
||||
kosten: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||
kilometerstand: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
erfasst_von_benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
|
||||
@@ -0,0 +1,25 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class TankbuchEintragCreate(BaseModel):
|
||||
objekt_id: int
|
||||
datum: date
|
||||
menge_liter: Decimal
|
||||
kosten: Decimal | None = None
|
||||
kilometerstand: int
|
||||
|
||||
|
||||
class TankbuchEintragRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
objekt_id: int
|
||||
datum: date
|
||||
menge_liter: Decimal
|
||||
kosten: Decimal | None
|
||||
kilometerstand: int
|
||||
erfasst_von_benutzer_id: int
|
||||
verbrauch_l_pro_100km: Decimal | None = None
|
||||
@@ -0,0 +1,74 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.tankbuch import TankbuchEintrag
|
||||
from app.services import historie as historie_service
|
||||
|
||||
|
||||
async def erstelle(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
objekt_id: int,
|
||||
datum,
|
||||
menge_liter: Decimal,
|
||||
kosten: Decimal | None,
|
||||
kilometerstand: int,
|
||||
erfasst_von_benutzer_id: int,
|
||||
zustaendiger_server_id: int,
|
||||
) -> TankbuchEintrag:
|
||||
eintrag = TankbuchEintrag(
|
||||
objekt_id=objekt_id,
|
||||
datum=datum,
|
||||
menge_liter=menge_liter,
|
||||
kosten=kosten,
|
||||
kilometerstand=kilometerstand,
|
||||
erfasst_von_benutzer_id=erfasst_von_benutzer_id,
|
||||
)
|
||||
db.add(eintrag)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=erfasst_von_benutzer_id,
|
||||
ereignistyp="tankbuch_erfasst",
|
||||
entitaet_typ="objekt",
|
||||
entitaet_id=objekt_id,
|
||||
neuer_wert={"menge_liter": str(menge_liter), "kilometerstand": kilometerstand},
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(eintrag)
|
||||
return eintrag
|
||||
|
||||
|
||||
async def loesche(db: AsyncSession, eintrag: TankbuchEintrag) -> None:
|
||||
await db.delete(eintrag)
|
||||
await db.commit()
|
||||
|
||||
|
||||
def _mit_verbrauch(eintraege: list[TankbuchEintrag]) -> list[dict]:
|
||||
"""MAINT-007: Full-Tank-Methode - Verbrauch eines Eintrags = dessen getankte
|
||||
Menge bezogen auf die seit dem VORHERIGEN Eintrag gefahrene Strecke (setzt
|
||||
voraus, dass zwischen zwei Einträgen jeweils vollgetankt wurde - Standard-
|
||||
Näherung für ein einfaches Tankbuch, kein Anspruch auf Tankfüllstands-Tracking)."""
|
||||
ergebnis = []
|
||||
vorheriger: TankbuchEintrag | None = None
|
||||
for eintrag in eintraege:
|
||||
verbrauch = None
|
||||
if vorheriger is not None:
|
||||
km_delta = eintrag.kilometerstand - vorheriger.kilometerstand
|
||||
if km_delta > 0:
|
||||
verbrauch = (eintrag.menge_liter / Decimal(km_delta)) * Decimal(100)
|
||||
ergebnis.append({"eintrag": eintrag, "verbrauch_l_pro_100km": verbrauch})
|
||||
vorheriger = eintrag
|
||||
return ergebnis
|
||||
|
||||
|
||||
async def liste_fuer_objekt(db: AsyncSession, objekt_id: int) -> list[dict]:
|
||||
result = await db.execute(
|
||||
select(TankbuchEintrag)
|
||||
.where(TankbuchEintrag.objekt_id == objekt_id)
|
||||
.order_by(TankbuchEintrag.kilometerstand)
|
||||
)
|
||||
return _mit_verbrauch(list(result.scalars().all()))
|
||||
@@ -0,0 +1,76 @@
|
||||
import pytest
|
||||
|
||||
from tests.conftest import login
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tankbuch_verbrauchsberechnung(client, mitarbeiter_user, standort_factory, objekt_factory):
|
||||
standort = await standort_factory("Wache-Tank")
|
||||
fahrzeug = await objekt_factory(name="RTW 1", code="RTW-TANK-1", standort=standort)
|
||||
token = await login(client, "mitarbeiter1")
|
||||
header = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
erster = await client.post(
|
||||
f"/api/v1/objekte/{fahrzeug.id}/tankbuch",
|
||||
json={
|
||||
"objekt_id": fahrzeug.id,
|
||||
"datum": "2026-09-01",
|
||||
"menge_liter": "40",
|
||||
"kosten": "70.00",
|
||||
"kilometerstand": 10000,
|
||||
},
|
||||
headers=header,
|
||||
)
|
||||
assert erster.status_code == 201
|
||||
assert erster.json()["verbrauch_l_pro_100km"] is None
|
||||
|
||||
zweiter = await client.post(
|
||||
f"/api/v1/objekte/{fahrzeug.id}/tankbuch",
|
||||
json={
|
||||
"objekt_id": fahrzeug.id,
|
||||
"datum": "2026-09-05",
|
||||
"menge_liter": "20",
|
||||
"kosten": "35.00",
|
||||
"kilometerstand": 10200,
|
||||
},
|
||||
headers=header,
|
||||
)
|
||||
assert zweiter.status_code == 201
|
||||
|
||||
liste = await client.get(f"/api/v1/objekte/{fahrzeug.id}/tankbuch", headers=header)
|
||||
assert liste.status_code == 200
|
||||
eintraege = liste.json()
|
||||
assert len(eintraege) == 2
|
||||
assert eintraege[0]["verbrauch_l_pro_100km"] is None
|
||||
assert float(eintraege[1]["verbrauch_l_pro_100km"]) == pytest.approx(10.0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tankbuch_loeschen_nur_materialverantwortliche(
|
||||
client, mitarbeiter_user, admin_user, standort_factory, objekt_factory
|
||||
):
|
||||
standort = await standort_factory("Wache-Tank-2")
|
||||
fahrzeug = await objekt_factory(name="RTW 2", code="RTW-TANK-2", standort=standort)
|
||||
token_mitarbeiter = await login(client, "mitarbeiter1")
|
||||
header_mitarbeiter = {"Authorization": f"Bearer {token_mitarbeiter}"}
|
||||
|
||||
erstellt = await client.post(
|
||||
f"/api/v1/objekte/{fahrzeug.id}/tankbuch",
|
||||
json={
|
||||
"objekt_id": fahrzeug.id,
|
||||
"datum": "2026-09-01",
|
||||
"menge_liter": "40",
|
||||
"kosten": None,
|
||||
"kilometerstand": 5000,
|
||||
},
|
||||
headers=header_mitarbeiter,
|
||||
)
|
||||
eintrag_id = erstellt.json()["id"]
|
||||
|
||||
verboten = await client.delete(f"/api/v1/tankbuch/{eintrag_id}", headers=header_mitarbeiter)
|
||||
assert verboten.status_code == 403
|
||||
|
||||
token_admin = await login(client, "admin1")
|
||||
header_admin = {"Authorization": f"Bearer {token_admin}"}
|
||||
erlaubt = await client.delete(f"/api/v1/tankbuch/{eintrag_id}", headers=header_admin)
|
||||
assert erlaubt.status_code == 204
|
||||
@@ -166,6 +166,17 @@ export interface AkteNotiz {
|
||||
erstellt_am: string;
|
||||
}
|
||||
|
||||
export interface TankbuchEintrag {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
datum: string;
|
||||
menge_liter: string;
|
||||
kosten: string | null;
|
||||
kilometerstand: number;
|
||||
erfasst_von_benutzer_id: number;
|
||||
verbrauch_l_pro_100km: string | null;
|
||||
}
|
||||
|
||||
export interface Lagerbewegung {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ApiError, apiRequest } from "../api/client";
|
||||
import type { TankbuchEintrag } from "../api/types";
|
||||
|
||||
interface Props {
|
||||
objektId: number;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
// MAINT-007: Betankungs-Log je Fahrzeug, Verbrauch (l/100km) kommt bereits
|
||||
// serverseitig berechnet aus app/services/tankbuch.py (Full-Tank-Methode).
|
||||
export function TankbuchPanel({ objektId, onFehler }: Props) {
|
||||
const [eintraege, setEintraege] = useState<TankbuchEintrag[]>([]);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [datum, setDatum] = useState("");
|
||||
const [mengeLiter, setMengeLiter] = useState("");
|
||||
const [kosten, setKosten] = useState("");
|
||||
const [kilometerstand, setKilometerstand] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
try {
|
||||
const daten = await apiRequest<TankbuchEintrag[]>(`/objekte/${objektId}/tankbuch`);
|
||||
setEintraege(daten);
|
||||
} catch {
|
||||
onFehler("Tankbuch konnte nicht geladen werden.");
|
||||
} finally {
|
||||
setLaedt(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [objektId]);
|
||||
|
||||
async function anlegen() {
|
||||
if (!datum || !mengeLiter || !kilometerstand) return;
|
||||
setWirdAngelegt(true);
|
||||
try {
|
||||
await apiRequest(`/objekte/${objektId}/tankbuch`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
objekt_id: objektId,
|
||||
datum,
|
||||
menge_liter: mengeLiter,
|
||||
kosten: kosten || null,
|
||||
kilometerstand: Number(kilometerstand),
|
||||
},
|
||||
});
|
||||
setDatum("");
|
||||
setMengeLiter("");
|
||||
setKosten("");
|
||||
setKilometerstand("");
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Tankbuch-Eintrag konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setWirdAngelegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen(eintrag: TankbuchEintrag) {
|
||||
try {
|
||||
await apiRequest(`/tankbuch/${eintrag.id}`, { method: "DELETE" });
|
||||
await laden();
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError && fehler.status === 403) {
|
||||
onFehler("Löschen nur für Materialverantwortliche/Administrator.");
|
||||
} else {
|
||||
onFehler("Eintrag konnte nicht gelöscht werden.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.4rem" }}>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
value={datum}
|
||||
onChange={(e) => setDatum(e.target.value)}
|
||||
style={{ minWidth: "9rem" }}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={mengeLiter}
|
||||
onChange={(e) => setMengeLiter(e.target.value)}
|
||||
placeholder="Liter"
|
||||
style={{ width: "6rem" }}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={kosten}
|
||||
onChange={(e) => setKosten(e.target.value)}
|
||||
placeholder="Kosten €"
|
||||
style={{ width: "7rem" }}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
value={kilometerstand}
|
||||
onChange={(e) => setKilometerstand(e.target.value)}
|
||||
placeholder="Kilometerstand"
|
||||
style={{ width: "8rem" }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={anlegen}
|
||||
disabled={!datum || !mengeLiter || !kilometerstand || wirdAngelegt}
|
||||
>
|
||||
Erfassen
|
||||
</button>
|
||||
</div>
|
||||
{laedt && <p className="text-muted">Lade…</p>}
|
||||
{!laedt && eintraege.length === 0 && <p className="text-muted">Noch keine Betankungen erfasst.</p>}
|
||||
{!laedt && eintraege.length > 0 && (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Liter</th>
|
||||
<th>Kosten</th>
|
||||
<th>km-Stand</th>
|
||||
<th>Verbrauch</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[...eintraege].reverse().map((e) => (
|
||||
<tr key={e.id}>
|
||||
<td>{new Date(e.datum).toLocaleDateString("de-DE")}</td>
|
||||
<td>{e.menge_liter} l</td>
|
||||
<td>{e.kosten ? `${e.kosten} €` : "–"}</td>
|
||||
<td>{e.kilometerstand} km</td>
|
||||
<td>{e.verbrauch_l_pro_100km ? `${Number(e.verbrauch_l_pro_100km).toFixed(1)} l/100km` : "–"}</td>
|
||||
<td>
|
||||
<button className="btn btn-secondary" onClick={() => loeschen(e)}>
|
||||
Löschen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import type { Akte } from "../api/types";
|
||||
import { DokumentePanel } from "../components/DokumentePanel";
|
||||
import { NotizenPanel } from "../components/NotizenPanel";
|
||||
import { TankbuchPanel } from "../components/TankbuchPanel";
|
||||
import { WartungSection } from "../components/WartungSection";
|
||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||
@@ -289,6 +290,13 @@ export function AktePage() {
|
||||
<NotizenPanel entitaetTyp="objekt" entitaetId={String(objekt.id)} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
{istFahrzeug && (
|
||||
<div className="akte-tankbuch card">
|
||||
<h3 style={{ marginTop: 0 }}>Tankbuch</h3>
|
||||
<TankbuchPanel objektId={objekt.id} onFehler={setFehler} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="akte-historie card">
|
||||
<h3 style={{ marginTop: 0 }}>Historie</h3>
|
||||
{akte.historie.length === 0 && <p className="text-muted">Noch keine Änderungen protokolliert.</p>}
|
||||
|
||||
@@ -26,4 +26,5 @@
|
||||
.akte-maengel { grid-area: maengel; }
|
||||
.akte-dokumente { grid-area: dokumente; }
|
||||
.akte-notizen { grid-area: notizen; }
|
||||
.akte-tankbuch { grid-area: tankbuch; }
|
||||
.akte-historie { grid-area: historie; }
|
||||
@@ -20,6 +20,7 @@
|
||||
"maengel verantwortlich"
|
||||
"dokumente dokumente"
|
||||
"notizen notizen"
|
||||
"tankbuch tankbuch"
|
||||
"historie historie";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user