feat(assets): ASSET-012 Ressourcen-Buchungskalender (Verfügbarkeitsprüfung)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
1 parent
1ad693073e
commit
3d596ab456
14 files changed
+438
-6
No files matched your search
@@ -469,12 +469,15 @@ Details zu ASSET-001 … ASSET-010. ASSET-010 ergänzt am 2026-09-09 aus Open-So
|
||||
- **Tests:** Überschneidungstest (exakte Deckung, Teilüberschneidung,
|
||||
Randfall exakt aneinandergrenzend — grenzt NICHT als Konflikt, siehe
|
||||
Standard-Intervall-Semantik).
|
||||
- **DoD:** offen.
|
||||
- **Referenz (HiOrg-Server-Vergleich 2026-09-10):** HiOrg-Server bietet
|
||||
Ressourcen-/Terminverwaltung für Material und Fahrzeuge — bewusst nur der
|
||||
Verfügbarkeitsprüfungs-Teil übernommen, keine volle
|
||||
Termin-/Veranstaltungsverwaltung (das wäre Scope-Creep über MABEAs
|
||||
Materialmanagement-Kernauftrag hinaus).
|
||||
- **DoD:** ✅ umgesetzt (2026-09-10). `ressourcenbuchung` (Migration 0049,
|
||||
DB-CHECK `von < bis`), Überschneidungsprüfung in
|
||||
`app/services/ressourcenbuchung.py` (Standard-Intervall-Overlap,
|
||||
aneinandergrenzende Zeiträume sind KEIN Konflikt). `GET
|
||||
/objekte/{id}/ressourcenbuchungen`, `POST /ressourcenbuchungen` (409 bei
|
||||
Konflikt), `DELETE /ressourcenbuchungen/{id}`. Frontend: einfache Liste
|
||||
(kein Kalender-Widget, wie in der Kachel als MVP-Ausreichend markiert) in
|
||||
der Akte JEDES Objekts (nicht nur Fahrzeuge — Material-Sets/Geräte können
|
||||
genauso gebucht werden). `backend/tests/test_ressourcenbuchung.py`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""ASSET-012: Ressourcen-Buchungskalender (reine Verfügbarkeitsprüfung).
|
||||
|
||||
Revision ID: 0049_ressourcenbuchung
|
||||
Revises: 0048_verleih
|
||||
Create Date: 2026-09-10
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0049_ressourcenbuchung"
|
||||
down_revision: Union[str, None] = "0048_verleih"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
CREATE TABLE ressourcenbuchung (
|
||||
id UUID PRIMARY KEY,
|
||||
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
|
||||
von TIMESTAMPTZ NOT NULL,
|
||||
bis TIMESTAMPTZ NOT NULL,
|
||||
bezeichnung TEXT NOT NULL,
|
||||
gebucht_von_benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
|
||||
erstellt_am TIMESTAMPTZ NOT NULL,
|
||||
CHECK (von < bis)
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute("CREATE INDEX ix_ressourcenbuchung_objekt ON ressourcenbuchung (objekt_id, von, bis)")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP TABLE ressourcenbuchung")
|
||||
@@ -27,6 +27,7 @@ from app.api.v1.endpoints import (
|
||||
pruefung,
|
||||
readiness_regel,
|
||||
readiness_snapshot,
|
||||
ressourcenbuchung,
|
||||
stammdaten,
|
||||
suche,
|
||||
tankbuch,
|
||||
@@ -73,6 +74,7 @@ api_router.include_router(wartung.router, tags=["wartung"])
|
||||
api_router.include_router(suche.router, tags=["suche"])
|
||||
api_router.include_router(tankbuch.router, tags=["tankbuch"])
|
||||
api_router.include_router(verleih.router, tags=["verleih"])
|
||||
api_router.include_router(ressourcenbuchung.router, tags=["ressourcenbuchung"])
|
||||
api_router.include_router(readiness_snapshot.router, tags=["readiness_snapshot"])
|
||||
api_router.include_router(readiness_regel.router, tags=["readiness_regel"])
|
||||
api_router.include_router(einsatz.router, tags=["einsatz"])
|
||||
@@ -0,0 +1,76 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
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 Benutzer, RolleTyp
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.ressourcenbuchung import Ressourcenbuchung
|
||||
from app.schemas.ressourcenbuchung import RessourcenbuchungCreate, RessourcenbuchungRead
|
||||
from app.services.ressourcenbuchung import (
|
||||
BuchungskonfliktError,
|
||||
UngueltigerZeitraumError,
|
||||
erstelle_buchung,
|
||||
liste_buchungen_fuer_objekt,
|
||||
loesche_buchung,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_mitarbeiter_plus = require_roles(
|
||||
RolleTyp.mitarbeiter,
|
||||
RolleTyp.materialverantwortlicher,
|
||||
RolleTyp.leitungsverantwortlicher,
|
||||
RolleTyp.administration,
|
||||
)
|
||||
_materialverantwortliche = require_roles(RolleTyp.administration, RolleTyp.materialverantwortlicher)
|
||||
|
||||
|
||||
@router.get("/objekte/{objekt_id}/ressourcenbuchungen", response_model=list[RessourcenbuchungRead])
|
||||
async def hole_buchungen(
|
||||
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(_mitarbeiter_plus)
|
||||
) -> list[Ressourcenbuchung]:
|
||||
return await liste_buchungen_fuer_objekt(db, objekt_id)
|
||||
|
||||
|
||||
@router.post("/ressourcenbuchungen", response_model=RessourcenbuchungRead, status_code=status.HTTP_201_CREATED)
|
||||
async def erstelle_ressourcenbuchung(
|
||||
payload: RessourcenbuchungCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: Benutzer = Depends(_mitarbeiter_plus),
|
||||
) -> RessourcenbuchungRead:
|
||||
if await db.get(Objekt, payload.objekt_id) is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
|
||||
try:
|
||||
return await erstelle_buchung(
|
||||
db,
|
||||
objekt_id=payload.objekt_id,
|
||||
von=payload.von,
|
||||
bis=payload.bis,
|
||||
bezeichnung=payload.bezeichnung,
|
||||
benutzer_id=current_user.id,
|
||||
)
|
||||
except UngueltigerZeitraumError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="von muss vor bis liegen"
|
||||
) from exc
|
||||
except BuchungskonfliktError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail={
|
||||
"message": "Zeitraum überschneidet sich mit bestehender Buchung",
|
||||
"konflikte": [str(k.id) for k in exc.konflikte],
|
||||
},
|
||||
) from exc
|
||||
|
||||
|
||||
@router.delete("/ressourcenbuchungen/{buchung_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def entferne_ressourcenbuchung(
|
||||
buchung_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(_materialverantwortliche)
|
||||
) -> None:
|
||||
buchung = await db.get(Ressourcenbuchung, buchung_id)
|
||||
if buchung is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Buchung nicht gefunden")
|
||||
await loesche_buchung(db, buchung)
|
||||
@@ -21,6 +21,7 @@ from app.models.objektposition import Objektposition, ObjektpositionStatus
|
||||
from app.models.permission import Berechtigung, BenutzerRolleZuordnung, Rolle, RolleBerechtigung
|
||||
from app.models.pruefung import ObjekttypPruefart, Pruefart, Pruefung, PruefungErgebnis
|
||||
from app.models.readiness_regel import ReadinessRegel
|
||||
from app.models.ressourcenbuchung import Ressourcenbuchung
|
||||
from app.models.readiness_snapshot import ReadinessSnapshot, ReadinessZustand
|
||||
from app.models.personal import (
|
||||
BenutzerQualifikation,
|
||||
@@ -97,6 +98,7 @@ __all__ = [
|
||||
"Pruefung",
|
||||
"PruefungErgebnis",
|
||||
"ReadinessRegel",
|
||||
"Ressourcenbuchung",
|
||||
"ReadinessSnapshot",
|
||||
"ReadinessZustand",
|
||||
"BenutzerQualifikation",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Ressourcenbuchung(Base):
|
||||
"""ASSET-012: reine Verfügbarkeitsprüfung für Material/Fahrzeuge - KEINE
|
||||
Personal-Dienstplanung (bewusste Abgrenzung, siehe Kachel-Dokumentation).
|
||||
"ist Objekt X am Zeitraum Y schon verplant" statt Personaleinsatz."""
|
||||
|
||||
__tablename__ = "ressourcenbuchung"
|
||||
|
||||
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)
|
||||
von: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
bis: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
bezeichnung: Mapped[str] = mapped_column(String, nullable=False)
|
||||
gebucht_von_benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
|
||||
erstellt_am: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
|
||||
@@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class RessourcenbuchungCreate(BaseModel):
|
||||
objekt_id: int
|
||||
von: datetime
|
||||
bis: datetime
|
||||
bezeichnung: str
|
||||
|
||||
|
||||
class RessourcenbuchungRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: uuid.UUID
|
||||
objekt_id: int
|
||||
von: datetime
|
||||
bis: datetime
|
||||
bezeichnung: str
|
||||
gebucht_von_benutzer_id: int
|
||||
erstellt_am: datetime
|
||||
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.ressourcenbuchung import Ressourcenbuchung
|
||||
|
||||
|
||||
class UngueltigerZeitraumError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BuchungskonfliktError(Exception):
|
||||
def __init__(self, konflikte: list[Ressourcenbuchung]):
|
||||
self.konflikte = konflikte
|
||||
super().__init__(f"{len(konflikte)} überschneidende Buchung(en)")
|
||||
|
||||
|
||||
async def _ueberschneidende_buchungen(
|
||||
db: AsyncSession, *, objekt_id: int, von: datetime, bis: datetime
|
||||
) -> list[Ressourcenbuchung]:
|
||||
# Standard-Intervall-Overlap: von < bestehende.bis AND bis > bestehende.von
|
||||
# - exakt aneinandergrenzende Zeiträume (bis == andere.von) gelten NICHT
|
||||
# als Konflikt.
|
||||
result = await db.execute(
|
||||
select(Ressourcenbuchung).where(
|
||||
Ressourcenbuchung.objekt_id == objekt_id,
|
||||
Ressourcenbuchung.von < bis,
|
||||
Ressourcenbuchung.bis > von,
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def erstelle_buchung(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
objekt_id: int,
|
||||
von: datetime,
|
||||
bis: datetime,
|
||||
bezeichnung: str,
|
||||
benutzer_id: int,
|
||||
) -> Ressourcenbuchung:
|
||||
if von >= bis:
|
||||
raise UngueltigerZeitraumError()
|
||||
|
||||
konflikte = await _ueberschneidende_buchungen(db, objekt_id=objekt_id, von=von, bis=bis)
|
||||
if konflikte:
|
||||
raise BuchungskonfliktError(konflikte)
|
||||
|
||||
buchung = Ressourcenbuchung(
|
||||
objekt_id=objekt_id,
|
||||
von=von,
|
||||
bis=bis,
|
||||
bezeichnung=bezeichnung,
|
||||
gebucht_von_benutzer_id=benutzer_id,
|
||||
erstellt_am=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(buchung)
|
||||
await db.commit()
|
||||
await db.refresh(buchung)
|
||||
return buchung
|
||||
|
||||
|
||||
async def liste_buchungen_fuer_objekt(db: AsyncSession, objekt_id: int) -> list[Ressourcenbuchung]:
|
||||
result = await db.execute(
|
||||
select(Ressourcenbuchung).where(Ressourcenbuchung.objekt_id == objekt_id).order_by(Ressourcenbuchung.von)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def loesche_buchung(db: AsyncSession, buchung: Ressourcenbuchung) -> None:
|
||||
await db.delete(buchung)
|
||||
await db.commit()
|
||||
@@ -0,0 +1,70 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import auth_header, login
|
||||
|
||||
|
||||
def _zeit(stunden_ab_jetzt: int) -> str:
|
||||
return (datetime.now(timezone.utc) + timedelta(hours=stunden_ab_jetzt)).isoformat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_buchung_ohne_konflikt(client, db_session, mitarbeiter_user, standort_factory, objekt_factory):
|
||||
standort = await standort_factory("Wache-Buchung")
|
||||
objekt = await objekt_factory(name="RTW Buchung", code="BUCH-1", standort=standort)
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
erste = await client.post(
|
||||
"/api/v1/ressourcenbuchungen",
|
||||
json={"objekt_id": objekt.id, "von": _zeit(0), "bis": _zeit(2), "bezeichnung": "Lehrgang A"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert erste.status_code == 201
|
||||
|
||||
zweite = await client.post(
|
||||
"/api/v1/ressourcenbuchungen",
|
||||
json={"objekt_id": objekt.id, "von": _zeit(2), "bis": _zeit(4), "bezeichnung": "Lehrgang B"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
# exakt aneinandergrenzend - kein Konflikt
|
||||
assert zweite.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_buchung_mit_konflikt_abgelehnt(
|
||||
client, db_session, mitarbeiter_user, standort_factory, objekt_factory
|
||||
):
|
||||
standort = await standort_factory("Wache-Buchung2")
|
||||
objekt = await objekt_factory(name="RTW Buchung2", code="BUCH-2", standort=standort)
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
await client.post(
|
||||
"/api/v1/ressourcenbuchungen",
|
||||
json={"objekt_id": objekt.id, "von": _zeit(0), "bis": _zeit(4), "bezeichnung": "Lehrgang A"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
konflikt = await client.post(
|
||||
"/api/v1/ressourcenbuchungen",
|
||||
json={"objekt_id": objekt.id, "von": _zeit(2), "bis": _zeit(6), "bezeichnung": "Lehrgang B"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert konflikt.status_code == 409
|
||||
|
||||
liste = await client.get(f"/api/v1/objekte/{objekt.id}/ressourcenbuchungen", headers=auth_header(token))
|
||||
assert len(liste.json()) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_buchung_ungueltiger_zeitraum(client, mitarbeiter_user, standort_factory, objekt_factory):
|
||||
standort = await standort_factory("Wache-Buchung3")
|
||||
objekt = await objekt_factory(name="RTW Buchung3", code="BUCH-3", standort=standort)
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/ressourcenbuchungen",
|
||||
json={"objekt_id": objekt.id, "von": _zeit(4), "bis": _zeit(2), "bezeichnung": "Ungültig"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -479,6 +479,16 @@ export interface CustomRolle {
|
||||
berechtigung_ids: number[];
|
||||
}
|
||||
|
||||
export interface Ressourcenbuchung {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
von: string;
|
||||
bis: string;
|
||||
bezeichnung: string;
|
||||
gebucht_von_benutzer_id: number;
|
||||
erstellt_am: string;
|
||||
}
|
||||
|
||||
export interface ExterneStelle {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ApiError, apiRequest } from "../api/client";
|
||||
import type { Ressourcenbuchung } from "../api/types";
|
||||
|
||||
interface Props {
|
||||
objektId: number;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
// ASSET-012: reine Verfügbarkeitsliste (kein Kalender-Widget im MVP) -
|
||||
// verhindert Doppelbuchung, keine Personal-Dienstplanung.
|
||||
export function RessourcenbuchungPanel({ objektId, onFehler }: Props) {
|
||||
const [buchungen, setBuchungen] = useState<Ressourcenbuchung[]>([]);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [bezeichnung, setBezeichnung] = useState("");
|
||||
const [von, setVon] = useState("");
|
||||
const [bis, setBis] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
try {
|
||||
setBuchungen(await apiRequest<Ressourcenbuchung[]>(`/objekte/${objektId}/ressourcenbuchungen`));
|
||||
} catch {
|
||||
onFehler("Buchungen konnten nicht geladen werden.");
|
||||
} finally {
|
||||
setLaedt(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [objektId]);
|
||||
|
||||
async function anlegen() {
|
||||
if (!bezeichnung || !von || !bis) return;
|
||||
setWirdAngelegt(true);
|
||||
try {
|
||||
await apiRequest("/ressourcenbuchungen", {
|
||||
method: "POST",
|
||||
body: { objekt_id: objektId, von: new Date(von).toISOString(), bis: new Date(bis).toISOString(), bezeichnung },
|
||||
});
|
||||
setBezeichnung("");
|
||||
setVon("");
|
||||
setBis("");
|
||||
await laden();
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError && fehler.status === 409) {
|
||||
onFehler("Zeitraum überschneidet sich mit einer bestehenden Buchung.");
|
||||
} else {
|
||||
onFehler("Buchung konnte nicht angelegt werden.");
|
||||
}
|
||||
} finally {
|
||||
setWirdAngelegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function entfernen(id: string) {
|
||||
try {
|
||||
await apiRequest(`/ressourcenbuchungen/${id}`, { method: "DELETE" });
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Buchung konnte nicht entfernt werden.");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<div className="row" style={{ flexWrap: "wrap" }}>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Bezeichnung (z.B. Lehrgang X)"
|
||||
value={bezeichnung}
|
||||
onChange={(e) => setBezeichnung(e.target.value)}
|
||||
/>
|
||||
<input type="datetime-local" className="input" value={von} onChange={(e) => setVon(e.target.value)} />
|
||||
<input type="datetime-local" className="input" value={bis} onChange={(e) => setBis(e.target.value)} />
|
||||
<button className="btn btn-primary" onClick={anlegen} disabled={!bezeichnung || !von || !bis || wirdAngelegt}>
|
||||
Buchen
|
||||
</button>
|
||||
</div>
|
||||
{laedt && <p className="text-muted">Lade…</p>}
|
||||
{!laedt && buchungen.length === 0 && <p className="text-muted">Keine Buchungen.</p>}
|
||||
{!laedt && buchungen.length > 0 && (
|
||||
<ul className="card-list">
|
||||
{buchungen.map((b) => (
|
||||
<li key={b.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
<span>
|
||||
{b.bezeichnung} —{" "}
|
||||
{new Date(b.von).toLocaleString("de-DE")} bis {new Date(b.bis).toLocaleString("de-DE")}
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => entfernen(b.id)}>
|
||||
Entfernen
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { EinsatzHistoriePanel } from "../components/EinsatzHistoriePanel";
|
||||
import { HistorieIntegritaetBadge } from "../components/HistorieIntegritaetBadge";
|
||||
import { NotizenPanel } from "../components/NotizenPanel";
|
||||
import { ReadinessVerlaufPanel } from "../components/ReadinessVerlaufPanel";
|
||||
import { RessourcenbuchungPanel } from "../components/RessourcenbuchungPanel";
|
||||
import { TableWrapper } from "../components/ui/table-wrapper";
|
||||
import { TankbuchPanel } from "../components/TankbuchPanel";
|
||||
import { WartungSection } from "../components/WartungSection";
|
||||
@@ -313,6 +314,11 @@ export function AktePage() {
|
||||
<ReadinessVerlaufPanel objektId={objekt.id} />
|
||||
</div>
|
||||
|
||||
<div className="akte-ressourcenbuchung card">
|
||||
<h3 style={{ marginTop: 0 }}>Buchungen</h3>
|
||||
<RessourcenbuchungPanel objektId={objekt.id} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
<div className="akte-historie card">
|
||||
<div className="row-between" style={{ alignItems: "baseline" }}>
|
||||
<h3 style={{ marginTop: 0 }}>Historie</h3>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"dokumente"
|
||||
"notizen"
|
||||
"readiness"
|
||||
"ressourcenbuchung"
|
||||
"historie";
|
||||
}
|
||||
|
||||
@@ -30,4 +31,5 @@
|
||||
.akte-tankbuch { grid-area: tankbuch; }
|
||||
.akte-einsatzhistorie { grid-area: einsatzhistorie; }
|
||||
.akte-readiness-verlauf { grid-area: readiness; }
|
||||
.akte-ressourcenbuchung { grid-area: ressourcenbuchung; }
|
||||
.akte-historie { grid-area: historie; }
|
||||
@@ -23,6 +23,7 @@
|
||||
"tankbuch tankbuch"
|
||||
"einsatzhistorie einsatzhistorie"
|
||||
"readiness readiness"
|
||||
"ressourcenbuchung ressourcenbuchung"
|
||||
"historie historie";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user