feat(dokumente): DOC-002 feste Dokumenttypen statt Freitext
Neues Pflichtfeld dokumenttyp (Enum: Prüfprotokoll/Wartungsbericht/ Bedienungsanleitung/Rechnung/Zulassungsdokument/Sonstiges, Migration 0026) - macht Dokumente kategorisier- und filterbar statt nur per Freitext- Beschreibung auffindbar zu sein. Backend: Pflichtfeld beim Upload, optionaler Query-Filter bei GET /dokumente. Frontend: Auswahl-Dropdown beim Upload, Typ-Badge + Filter-Dropdown in der Liste (DokumentePanel). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -67,12 +67,14 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
export async function ladeDokumentHoch(felder: {
|
||||
entitaet_typ: string;
|
||||
entitaet_id: string;
|
||||
dokumenttyp: string;
|
||||
beschreibung?: string;
|
||||
datei: File;
|
||||
}): Promise<unknown> {
|
||||
const formData = new FormData();
|
||||
formData.append("entitaet_typ", felder.entitaet_typ);
|
||||
formData.append("entitaet_id", felder.entitaet_id);
|
||||
formData.append("dokumenttyp", felder.dokumenttyp);
|
||||
if (felder.beschreibung) formData.append("beschreibung", felder.beschreibung);
|
||||
formData.append("datei", felder.datei);
|
||||
|
||||
|
||||
@@ -125,6 +125,14 @@ export interface Mangel {
|
||||
erledigt_am: string | null;
|
||||
}
|
||||
|
||||
export type DokumentTyp =
|
||||
| "pruefprotokoll"
|
||||
| "wartungsbericht"
|
||||
| "bedienungsanleitung"
|
||||
| "rechnung"
|
||||
| "zulassungsdokument"
|
||||
| "sonstiges";
|
||||
|
||||
export interface Dokument {
|
||||
id: string;
|
||||
entitaet_typ: string;
|
||||
@@ -132,6 +140,7 @@ export interface Dokument {
|
||||
dateiname: string;
|
||||
mime_type: string;
|
||||
groesse_bytes: number;
|
||||
dokumenttyp: DokumentTyp;
|
||||
beschreibung: string | null;
|
||||
hochgeladen_von: number;
|
||||
hochgeladen_am: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { ApiError, apiRequest, ladeDokumentAnsehen, ladeDokumentHerunter, ladeDokumentHoch } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Dokument } from "../api/types";
|
||||
import type { Dokument, DokumentTyp } from "../api/types";
|
||||
|
||||
interface Props {
|
||||
entitaetTyp: string;
|
||||
@@ -10,6 +10,17 @@ interface Props {
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
const DOKUMENTTYP_LABEL: Record<DokumentTyp, string> = {
|
||||
pruefprotokoll: "Prüfprotokoll",
|
||||
wartungsbericht: "Wartungsbericht",
|
||||
bedienungsanleitung: "Bedienungsanleitung",
|
||||
rechnung: "Rechnung",
|
||||
zulassungsdokument: "Zulassungsdokument",
|
||||
sonstiges: "Sonstiges",
|
||||
};
|
||||
|
||||
const DOKUMENTTYPEN = Object.keys(DOKUMENTTYP_LABEL) as DokumentTyp[];
|
||||
|
||||
function formatGroesse(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
||||
@@ -26,13 +37,16 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
const [dokumente, setDokumente] = useState<Dokument[]>([]);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [beschreibung, setBeschreibung] = useState("");
|
||||
const [dokumenttyp, setDokumenttyp] = useState<DokumentTyp>("sonstiges");
|
||||
const [filterTyp, setFilterTyp] = useState<DokumentTyp | "">("");
|
||||
const [wirdHochgeladen, setWirdHochgeladen] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
try {
|
||||
const filter = filterTyp ? `&dokumenttyp=${filterTyp}` : "";
|
||||
const daten = await apiRequest<Dokument[]>(
|
||||
`/dokumente?entitaet_typ=${entitaetTyp}&entitaet_id=${encodeURIComponent(entitaetId)}`
|
||||
`/dokumente?entitaet_typ=${entitaetTyp}&entitaet_id=${encodeURIComponent(entitaetId)}${filter}`
|
||||
);
|
||||
setDokumente(daten);
|
||||
} catch {
|
||||
@@ -45,14 +59,14 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entitaetTyp, entitaetId]);
|
||||
}, [entitaetTyp, entitaetId, filterTyp]);
|
||||
|
||||
async function hochladen(e: { target: HTMLInputElement }) {
|
||||
const datei = e.target.files?.[0];
|
||||
if (!datei) return;
|
||||
setWirdHochgeladen(true);
|
||||
try {
|
||||
await ladeDokumentHoch({ entitaet_typ: entitaetTyp, entitaet_id: entitaetId, beschreibung, datei });
|
||||
await ladeDokumentHoch({ entitaet_typ: entitaetTyp, entitaet_id: entitaetId, dokumenttyp, beschreibung, datei });
|
||||
setBeschreibung("");
|
||||
e.target.value = "";
|
||||
await laden();
|
||||
@@ -96,6 +110,18 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
return (
|
||||
<div className="stack">
|
||||
<div className="row" style={{ flexWrap: "wrap" }}>
|
||||
<select
|
||||
className="input"
|
||||
value={dokumenttyp}
|
||||
onChange={(e) => setDokumenttyp(e.target.value as DokumentTyp)}
|
||||
style={{ maxWidth: "12rem" }}
|
||||
>
|
||||
{DOKUMENTTYPEN.map((typ) => (
|
||||
<option key={typ} value={typ}>
|
||||
{DOKUMENTTYP_LABEL[typ]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
value={beschreibung}
|
||||
@@ -105,6 +131,21 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
/>
|
||||
<input type="file" accept=".pdf,.jpg,.jpeg,.png,.webp" onChange={hochladen} disabled={wirdHochgeladen} />
|
||||
</div>
|
||||
{dokumente.length > 0 || filterTyp ? (
|
||||
<select
|
||||
className="input"
|
||||
value={filterTyp}
|
||||
onChange={(e) => setFilterTyp(e.target.value as DokumentTyp | "")}
|
||||
style={{ maxWidth: "14rem" }}
|
||||
>
|
||||
<option value="">Alle Typen</option>
|
||||
{DOKUMENTTYPEN.map((typ) => (
|
||||
<option key={typ} value={typ}>
|
||||
{DOKUMENTTYP_LABEL[typ]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
{laedt && <p className="text-muted">Lade…</p>}
|
||||
{!laedt && dokumente.length === 0 && <p className="text-muted">Keine Dokumente.</p>}
|
||||
{!laedt && dokumente.length > 0 && (
|
||||
@@ -112,6 +153,9 @@ export function DokumentePanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
{dokumente.map((d) => (
|
||||
<li key={d.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
<span>
|
||||
<span className="badge badge-neutral" style={{ marginRight: "0.4rem" }}>
|
||||
{DOKUMENTTYP_LABEL[d.dokumenttyp]}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => ansehen(d)}
|
||||
title="In neuem Tab ansehen"
|
||||
|
||||
Reference in New Issue
Block a user