Files
MABEA/frontend/src/pages/AktePage.tsx
T
patrickandClaude Sonnet 5 cc016f09d3
CI / backend-tests (push) Failing after 2m6s
CI / frontend-build (push) Successful in 18s
feat(akte): Ablaufdatum in der Beladung-Tabelle
Bevorstehende Ablaufdaten waren nur im Dashboard und als
Einsatzbereitschafts-Grund sichtbar, nicht direkt an der jeweiligen
Beladungsposition in der Akte. Neue Spalte "Ablauf", rot bei
überfälligem, gelb bei <30 Tagen verbleibender Zeit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-06 23:48:31 +02:00

236 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { apiRequest } from "../api/client";
import { useAuth } from "../auth/AuthContext";
import type { Akte } from "../api/types";
import { DokumentePanel } from "../components/DokumentePanel";
import { WartungSection } from "../components/WartungSection";
import { ReadinessBadge } from "../components/status/ReadinessBadge";
import { GRUND_TEXT } from "../components/status/gruendeText";
import { EREIGNIS_LABEL } from "../components/status/ereignisText";
function ablaufFarbe(ablaufdatum: string): string | undefined {
const tage = (new Date(ablaufdatum).getTime() - Date.now()) / (1000 * 60 * 60 * 24);
if (tage < 0) return "var(--color-danger)";
if (tage < 30) return "var(--color-warning)";
return undefined;
}
const MANGEL_PRIORITAET_LABEL: Record<string, string> = {
niedrig: "Niedrig",
normal: "Normal",
hoch: "Hoch",
};
export function AktePage() {
const { objektId } = useParams<{ objektId: string }>();
const { istVerantwortlich } = useAuth();
const [akte, setAkte] = useState<Akte | null>(null);
const [fehler, setFehler] = useState<string | null>(null);
const [laedt, setLaedt] = useState(true);
function laden() {
if (!objektId) return;
setLaedt(true);
apiRequest<Akte>(`/akte/objekt/${objektId}`)
.then(setAkte)
.catch(() => setFehler("Akte konnte nicht geladen werden."))
.finally(() => setLaedt(false));
}
useEffect(laden, [objektId]);
async function pruefungDurchgefuehrt(geraetId: string) {
try {
await apiRequest(`/geraete/${geraetId}`, {
method: "PATCH",
body: { status: "einsatzbereit", pruefdatum: new Date().toISOString().slice(0, 10) },
});
laden();
} catch {
setFehler("Prüfung konnte nicht erfasst werden.");
}
}
if (laedt) return <main className="page"><p className="text-muted">Lade</p></main>;
if (fehler) return <main className="page"><div className="alert alert-danger">{fehler}</div></main>;
if (!akte) return null;
const { objekt } = akte;
const istFahrzeug = akte.fahrzeugdetails !== null;
return (
<main className={`page-wide akte-grid${istFahrzeug ? " ist-fahrzeug" : ""}`}>
<div className="akte-kopf">
<div className="row-between" style={{ alignItems: "flex-start" }}>
<div>
<h1 style={{ marginBottom: "0.15rem" }}>{objekt.name}</h1>
<div className="text-muted">
{objekt.code} · {akte.objekttyp_name} · Objekt-Status: {objekt.status}
</div>
</div>
<ReadinessBadge status={akte.einsatzbereitschaft_status} />
</div>
{akte.einsatzbereitschaft_gruende.length > 0 && (
<ul className="card-list" style={{ marginTop: "0.5rem" }}>
{akte.einsatzbereitschaft_gruende.map((g) => (
<li key={g} className="text-muted" style={{ fontSize: "0.9rem" }}>
{GRUND_TEXT[g] ?? g}
</li>
))}
</ul>
)}
</div>
<div className="akte-aktionen row" style={{ flexWrap: "wrap" }}>
<Link to={`/objekte/${objekt.id}/kontrolle`} className="btn btn-primary">
Kontrolle starten
</Link>
<Link to={`/maengel?objekt_id=${objekt.id}`} className="btn btn-secondary">
Mangel melden
</Link>
</div>
<div className="akte-stamm card">
<h3 style={{ marginTop: 0 }}>Stammdaten</h3>
<div className="stack">
<div>Standort: {akte.standort_name}</div>
<div>
Letzte Kontrolle:{" "}
{akte.letzte_kontrolle_am
? new Date(akte.letzte_kontrolle_am).toLocaleString("de-DE")
: "noch nie kontrolliert"}
</div>
{akte.fahrzeugdetails && (
<>
{akte.fahrzeugdetails.kennzeichen && <div>Kennzeichen: {akte.fahrzeugdetails.kennzeichen}</div>}
{akte.fahrzeugdetails.funkrufname && <div>Funkrufname: {akte.fahrzeugdetails.funkrufname}</div>}
{akte.fahrzeugdetails.funkkenner && <div>Funkkenner: {akte.fahrzeugdetails.funkkenner}</div>}
{akte.fahrzeugdetails.kilometerstand !== null && (
<div>Kilometerstand: {akte.fahrzeugdetails.kilometerstand} km</div>
)}
</>
)}
</div>
</div>
<div className="akte-beladung card">
<h3 style={{ marginTop: 0 }}>Beladung</h3>
{akte.beladung.length === 0 && <p className="text-muted">Keine Positionen zugeordnet.</p>}
{akte.beladung.length > 0 && (
<div style={{ overflowX: "auto" }}>
<table className="table">
<thead>
<tr>
<th>Material</th>
<th>Fach</th>
<th>Soll</th>
<th>Ist</th>
<th>Ablauf</th>
</tr>
</thead>
<tbody>
{akte.beladung.map((b) => (
<tr key={b.objektposition_id}>
<td>{b.material_name}</td>
<td>{b.fach ?? ""}</td>
<td>
{b.sollmenge} {b.einheit}
</td>
<td className={Number(b.istmenge) < Number(b.sollmenge) ? "text-danger" : undefined}>
{b.istmenge} {b.einheit}
</td>
<td style={{ color: b.ablaufdatum ? ablaufFarbe(b.ablaufdatum) : undefined }}>
{b.ablaufdatum ? new Date(b.ablaufdatum).toLocaleDateString("de-DE") : ""}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="akte-verantwortlich card">
<h3 style={{ marginTop: 0 }}>Verantwortlichkeit</h3>
{akte.zustaendige_benutzer.length === 0 && akte.kontrollverantwortung.length === 0 && (
<p className="text-muted">Keine Zuordnung hinterlegt.</p>
)}
{akte.zustaendige_benutzer.length > 0 && (
<ul>
{akte.zustaendige_benutzer.map((z) => (
<li key={z.id}>
{z.name} <span className="text-muted">({z.quelle === "standort" ? "über Standort" : "direkt"})</span>
</li>
))}
</ul>
)}
</div>
<div className="akte-pruefungen card">
<h3 style={{ marginTop: 0 }}>Prüfungen (Geräte)</h3>
{akte.geraete.length === 0 && <p className="text-muted">Keine Geräte-Instanzen erfasst.</p>}
{akte.geraete.length > 0 && (
<ul>
{akte.geraete.map((g) => (
<li key={g.id} className="row-between" style={{ padding: "0.25rem 0" }}>
<span>
SN {g.seriennummer} {" "}
<span className={g.status === "einsatzbereit" ? "text-success" : "text-danger"}>{g.status}</span>
{g.naechste_pruefung && (
<span className="text-muted"> · nächste Prüfung {new Date(g.naechste_pruefung).toLocaleDateString("de-DE")}</span>
)}
</span>
{istVerantwortlich && (
<button className="btn btn-secondary" onClick={() => pruefungDurchgefuehrt(g.id)}>
Prüfung heute durchgeführt
</button>
)}
</li>
))}
</ul>
)}
</div>
<div className="akte-wartung">
<WartungSection objektId={objekt.id} onFehler={setFehler} />
</div>
<div className="akte-maengel card">
<h3 style={{ marginTop: 0 }}>Mängel</h3>
{akte.maengel.length === 0 && <p className="text-muted">Keine Mängel gemeldet.</p>}
{akte.maengel.length > 0 && (
<ul>
{akte.maengel.map((m) => (
<li key={m.id}>
{m.beschreibung} {MANGEL_PRIORITAET_LABEL[m.prioritaet]}, {m.status}
</li>
))}
</ul>
)}
</div>
<div className="akte-dokumente card">
<h3 style={{ marginTop: 0 }}>Dokumente</h3>
<DokumentePanel entitaetTyp="objekt" entitaetId={String(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>}
{akte.historie.length > 0 && (
<ul>
{akte.historie.map((h) => (
<li key={h.id}>
{new Date(h.zeitpunkt).toLocaleString("de-DE")} {EREIGNIS_LABEL[h.ereignistyp] ?? h.ereignistyp}
{h.benutzer_name && <span className="text-muted"> ({h.benutzer_name})</span>}
</li>
))}
</ul>
)}
</div>
</main>
);
}