feat(dokument): Dokumente-Modul (Roadmap Phase 5)
Polymorphe Datei-Anhänge (PDF/JPEG/PNG/WebP, Whitelist statt Blacklist -
owasp-Grundsatz) an beliebige Ressource, gleiches entitaet_typ/entitaet_id-
Muster wie Mangel/Historie. Lokale Ablage (settings.upload_dir, kein Cloud-
Zwang, Self-Hosting-Anforderung), server-generierter Dateiname verhindert
Path-Traversal/Namenskollisionen.
POST /dokumente (multipart), GET /dokumente (Filter Pflicht: entitaet_typ +
entitaet_id), GET /dokumente/{id}/download, DELETE /dokumente/{id}. Hochladen:
alle Mitarbeiter+, Löschen: Materialverantwortliche+Leitung+Admin.
Frontend: wiederverwendbares DokumentePanel (Upload/Liste/Download/Löschen),
eingebunden in ObjektSection (je Objekt) und MangelListePage (Fotos zu
Mängeln) - weitere Ressourcen (Geräteinstanz, Fahrzeugdetails, Benutzer)
können denselben Baustein später einfach wiederverwenden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { DokumentePanel } from "../components/DokumentePanel";
|
||||
import type { Mangel, MangelPrioritaet, MangelStatus, Objekt } from "../api/types";
|
||||
|
||||
const STATUS_LABEL: Record<MangelStatus, string> = {
|
||||
@@ -37,6 +38,7 @@ export function MangelListePage() {
|
||||
const [beschreibung, setBeschreibung] = useState("");
|
||||
const [prioritaet, setPrioritaet] = useState<MangelPrioritaet>("normal");
|
||||
const [wirdGemeldet, setWirdGemeldet] = useState(false);
|
||||
const [fotosOffenId, setFotosOffenId] = useState<string | null>(null);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
@@ -160,6 +162,18 @@ export function MangelListePage() {
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ marginTop: "0.5rem" }}
|
||||
onClick={() => setFotosOffenId(fotosOffenId === m.id ? null : m.id)}
|
||||
>
|
||||
{fotosOffenId === m.id ? "Dokumente schließen" : "Dokumente/Fotos"}
|
||||
</button>
|
||||
{fotosOffenId === m.id && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<DokumentePanel entitaetTyp="mangel" entitaetId={m.id} onFehler={setFehler} />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Beladungsvorlage, Fahrzeugdetails, Material, Objekt, Objekttyp, Standort } from "../../api/types";
|
||||
import { DokumentePanel } from "../../components/DokumentePanel";
|
||||
import { ObjektPositionenPanel } from "./ObjektPositionenPanel";
|
||||
|
||||
const STATUS_OPTIONEN = [
|
||||
@@ -44,6 +45,7 @@ export function ObjektSection({
|
||||
const [fahrzeugId, setFahrzeugId] = useState<number | "">("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
|
||||
const [dokumenteOffenId, setDokumenteOffenId] = useState<number | null>(null);
|
||||
const [duplizierId, setDuplizierId] = useState<number | null>(null);
|
||||
const [neuerPraefix, setNeuerPraefix] = useState("");
|
||||
const [neuerCode, setNeuerCode] = useState("");
|
||||
@@ -397,6 +399,12 @@ export function ObjektSection({
|
||||
>
|
||||
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setDokumenteOffenId(dokumenteOffenId === o.id ? null : o.id)}
|
||||
>
|
||||
{dokumenteOffenId === o.id ? "Dokumente schließen" : "Dokumente"}
|
||||
</button>
|
||||
{!nurPflegen && (
|
||||
<button className="btn btn-secondary" onClick={() => fahrzeugBearbeiten(o)}>
|
||||
{fahrzeugBearbeitenId === o.id ? "Schließen" : "Fahrzeug zuordnen"}
|
||||
@@ -524,6 +532,11 @@ export function ObjektSection({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{dokumenteOffenId === o.id && (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<DokumentePanel entitaetTyp="objekt" entitaetId={String(o.id)} onFehler={onFehler} />
|
||||
</div>
|
||||
)}
|
||||
{duplizierId === o.id && (
|
||||
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user