feat(ui): UI-005 begründete Einsatzbereitschaft + Dokumente in der Akte
Backend: objekt_readiness() in dashboard.py liefert Status+Gründe für
ein einzelnes Objekt, Fallunterscheidung aus einsatzbereitschaft()
extrahiert (_kategorie_und_gruende, keine Duplizierung). Neue Felder
an /akte/objekt/{id}. Frontend: ReadinessBadge + Gründeliste ersetzt
den rohen objekt.status-Badge (Objekt-Lebenszyklus-Status bleibt
separat sichtbar). Dokumente waren in der Akte bisher nur Text ohne
Interaktion - jetzt DokumentePanel eingebunden plus neuer "Ansehen"-
Klick (PDF/Bild im neuen Tab statt Zwangs-Download).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -106,6 +106,24 @@ export async function ladeDokumentHerunter(dokumentId: string, dateiname: string
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// UI-005 (Epic 20): Dokumente waren bisher nur per Zwangs-Download einsehbar,
|
||||
// nicht direkt im Browser anschaubar (PDF/Bild in neuem Tab statt Datei
|
||||
// speichern). Auch hier: Auth-Header nötig, daher Blob statt einfachem
|
||||
// <a href> auf den Download-Endpunkt. Object-URL wird bewusst NICHT sofort
|
||||
// revoked (Tab braucht sie noch); der Browser räumt sie beim Tab-Schließen auf.
|
||||
export async function ladeDokumentAnsehen(dokumentId: string): Promise<void> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
||||
|
||||
const response = await fetch(`${BASE_URL}/dokumente/${dokumentId}/download`, { headers });
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, null);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank", "noopener");
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<string> {
|
||||
const body = new URLSearchParams({ username, password });
|
||||
const response = await fetch(`${BASE_URL}/auth/login`, {
|
||||
|
||||
@@ -262,6 +262,8 @@ export interface ZustaendigerBenutzer {
|
||||
|
||||
export interface Akte {
|
||||
objekt: Objekt;
|
||||
einsatzbereitschaft_status: "ready" | "limited" | "not-ready" | "unknown";
|
||||
einsatzbereitschaft_gruende: string[];
|
||||
objekttyp_name: string;
|
||||
standort_name: string;
|
||||
fahrzeugdetails: Fahrzeugdetails | null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||
import { apiRequest, ladeDokumentAnsehen, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Dokument } from "../api/types";
|
||||
|
||||
@@ -63,6 +63,14 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ansehen(d: Dokument) {
|
||||
try {
|
||||
await ladeDokumentAnsehen(d.id);
|
||||
} catch {
|
||||
onFehler("Datei konnte nicht geöffnet werden.");
|
||||
}
|
||||
}
|
||||
|
||||
async function herunterladen(d: Dokument) {
|
||||
try {
|
||||
await ladeDokumentHerunter(d.id, d.dateiname);
|
||||
@@ -100,7 +108,8 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
<li key={d.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
<span>
|
||||
<button
|
||||
onClick={() => herunterladen(d)}
|
||||
onClick={() => ansehen(d)}
|
||||
title="In neuem Tab ansehen"
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
@@ -115,11 +124,16 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
{d.beschreibung && <span className="text-muted"> – {d.beschreibung}</span>}
|
||||
<span className="text-muted"> ({formatGroesse(d.groesse_bytes)})</span>
|
||||
</span>
|
||||
{darfLoeschen && (
|
||||
<button className="btn btn-secondary" onClick={() => loeschen(d)}>
|
||||
Löschen
|
||||
<span className="row" style={{ gap: "0.4rem" }}>
|
||||
<button className="btn btn-secondary" onClick={() => herunterladen(d)}>
|
||||
Herunterladen
|
||||
</button>
|
||||
)}
|
||||
{darfLoeschen && (
|
||||
<button className="btn btn-secondary" onClick={() => loeschen(d)}>
|
||||
Löschen
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Spiegelt die Grund-Codes aus app/services/dashboard.py (einsatzbereitschaft/
|
||||
* objekt_readiness) - gemeinsam genutzt von DashboardPage und AktePage. */
|
||||
export const GRUND_TEXT: Record<string, string> = {
|
||||
fehlbestand_offen: "Offener Fehlbestand",
|
||||
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
|
||||
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
||||
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
||||
mangel_kritisch_offen: "Kritischer Mangel offen",
|
||||
in_wartung: "In Wartung",
|
||||
hu_uvv_ueberfaellig: "HU/UVV überfällig",
|
||||
noch_nie_kontrolliert: "Noch nie kontrolliert",
|
||||
mindermenge_genehmigt: "Mindermenge genehmigt",
|
||||
};
|
||||
@@ -3,6 +3,9 @@ import { Link, useParams } from "react-router-dom";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import type { Akte } from "../api/types";
|
||||
import { DokumentePanel } from "../components/DokumentePanel";
|
||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||
|
||||
const MANGEL_PRIORITAET_LABEL: Record<string, string> = {
|
||||
niedrig: "Niedrig",
|
||||
@@ -39,11 +42,20 @@ export function AktePage() {
|
||||
<div>
|
||||
<h1 style={{ marginBottom: "0.15rem" }}>{objekt.name}</h1>
|
||||
<div className="text-muted">
|
||||
{objekt.code} · {akte.objekttyp_name}
|
||||
{objekt.code} · {akte.objekttyp_name} · Objekt-Status: {objekt.status}
|
||||
</div>
|
||||
</div>
|
||||
<span className="badge badge-neutral">{objekt.status}</span>
|
||||
<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 className="card" style={{ marginTop: "1rem" }}>
|
||||
<h3 style={{ marginTop: 0 }}>Stammdaten</h3>
|
||||
@@ -118,16 +130,7 @@ export function AktePage() {
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
<h3 style={{ marginTop: 0 }}>Dokumente</h3>
|
||||
{akte.dokumente.length === 0 && <p className="text-muted">Keine Dokumente hinterlegt.</p>}
|
||||
{akte.dokumente.length > 0 && (
|
||||
<ul>
|
||||
{akte.dokumente.map((d) => (
|
||||
<li key={d.id}>
|
||||
{d.dateiname} <span className="text-muted">({Math.round(d.groesse_bytes / 1024)} KB)</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<DokumentePanel entitaetTyp="objekt" entitaetId={String(objekt.id)} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: "1rem" }}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { InspectionStatus } from "../components/status/InspectionStatus";
|
||||
import { InventoryStatus } from "../components/status/InventoryStatus";
|
||||
import type { Readiness } from "../components/status/ReadinessBadge";
|
||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||
|
||||
interface Kennzahlen {
|
||||
anzahl_offener_fehlbestaende: number;
|
||||
@@ -69,18 +70,6 @@ function readinessVonGruenden(gruende: string[]): Readiness {
|
||||
return "not-ready";
|
||||
}
|
||||
|
||||
const GRUND_TEXT: Record<string, string> = {
|
||||
fehlbestand_offen: "Offener Fehlbestand",
|
||||
ablaufdatum_abgelaufen: "Ablaufdatum überschritten",
|
||||
pruefung_ueberfaellig: "Geräteprüfung überfällig",
|
||||
kontrolle_laeuft: "Kontrolle läuft (gesperrt)",
|
||||
mangel_kritisch_offen: "Kritischer Mangel offen",
|
||||
in_wartung: "In Wartung",
|
||||
hu_uvv_ueberfaellig: "HU/UVV überfällig",
|
||||
noch_nie_kontrolliert: "Noch nie kontrolliert",
|
||||
mindermenge_genehmigt: "Mindermenge genehmigt",
|
||||
};
|
||||
|
||||
// Leitungs-Dashboard (Karte-Nachtrag): zwei neue Kacheln (Einsatzbereitschaft,
|
||||
// Prüftermine) plus die bestehende Ablaufdaten-Kachel, alle drei eigenständig -
|
||||
// widersprechen sich nicht, da überfällige Prüfung/Ablauf beide zusätzlich als
|
||||
|
||||
Reference in New Issue
Block a user