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
236 lines
8.8 KiB
TypeScript
236 lines
8.8 KiB
TypeScript
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>
|
||
);
|
||
}
|