feat(akte): FILE-009 Freitext-Notizen an beliebiger Ressource
Polymorphes akte_notiz-Modell (gleiches entitaet_typ/entitaet_id-Muster wie Dokument/Historie/Mangel), CRUD-Endpunkte, NotizenPanel im Akte-Grid. Löschen nur durch Autor oder Administrator. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -157,6 +157,15 @@ export interface Dokument {
|
||||
ist_original: boolean;
|
||||
}
|
||||
|
||||
export interface AkteNotiz {
|
||||
id: string;
|
||||
entitaet_typ: string;
|
||||
entitaet_id: string;
|
||||
text: string;
|
||||
autor_id: number;
|
||||
erstellt_am: string;
|
||||
}
|
||||
|
||||
export interface Lagerbewegung {
|
||||
id: string;
|
||||
objekt_id: number;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ApiError, apiRequest } from "../api/client";
|
||||
import type { AkteNotiz } from "../api/types";
|
||||
|
||||
interface Props {
|
||||
entitaetTyp: string;
|
||||
entitaetId: string;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
function formatZeitpunkt(iso: string): string {
|
||||
return new Date(iso).toLocaleString("de-DE");
|
||||
}
|
||||
|
||||
// FILE-009: formlose Freitext-Notizen, gleiche entitaet_typ/entitaet_id-
|
||||
// Adressierung wie DokumentePanel.
|
||||
export function NotizenPanel({ entitaetTyp, entitaetId, onFehler }: Props) {
|
||||
const [notizen, setNotizen] = useState<AkteNotiz[]>([]);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [text, setText] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
|
||||
async function laden() {
|
||||
setLaedt(true);
|
||||
try {
|
||||
const daten = await apiRequest<AkteNotiz[]>(
|
||||
`/notizen?entitaet_typ=${entitaetTyp}&entitaet_id=${encodeURIComponent(entitaetId)}`
|
||||
);
|
||||
setNotizen(daten);
|
||||
} catch {
|
||||
onFehler("Notizen konnten nicht geladen werden.");
|
||||
} finally {
|
||||
setLaedt(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
laden();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entitaetTyp, entitaetId]);
|
||||
|
||||
async function anlegen() {
|
||||
if (!text.trim()) return;
|
||||
setWirdAngelegt(true);
|
||||
try {
|
||||
await apiRequest("/notizen", {
|
||||
method: "POST",
|
||||
body: { entitaet_typ: entitaetTyp, entitaet_id: entitaetId, text: text.trim() },
|
||||
});
|
||||
setText("");
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Notiz konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setWirdAngelegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen(notiz: AkteNotiz) {
|
||||
try {
|
||||
await apiRequest(`/notizen/${notiz.id}`, { method: "DELETE" });
|
||||
await laden();
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError && fehler.status === 403) {
|
||||
onFehler("Nur eigene Notizen oder als Administrator löschbar.");
|
||||
} else {
|
||||
onFehler("Notiz konnte nicht gelöscht werden.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack">
|
||||
<div className="row" style={{ flexWrap: "wrap", gap: "0.4rem" }}>
|
||||
<input
|
||||
className="input"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Notiz hinzufügen…"
|
||||
style={{ flex: 1, minWidth: "12rem" }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={anlegen} disabled={!text.trim() || wirdAngelegt}>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{laedt && <p className="text-muted">Lade…</p>}
|
||||
{!laedt && notizen.length === 0 && <p className="text-muted">Keine Notizen.</p>}
|
||||
{!laedt && notizen.length > 0 && (
|
||||
<ul className="card-list">
|
||||
{notizen.map((n) => (
|
||||
<li key={n.id} className="row-between" style={{ padding: "0.3rem 0" }}>
|
||||
<span>
|
||||
{n.text} <span className="text-muted">({formatZeitpunkt(n.erstellt_am)})</span>
|
||||
</span>
|
||||
<button className="btn btn-secondary" onClick={() => loeschen(n)}>
|
||||
Löschen
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { apiRequest, ladeObjektEtikett } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Akte } from "../api/types";
|
||||
import { DokumentePanel } from "../components/DokumentePanel";
|
||||
import { NotizenPanel } from "../components/NotizenPanel";
|
||||
import { WartungSection } from "../components/WartungSection";
|
||||
import { ReadinessBadge } from "../components/status/ReadinessBadge";
|
||||
import { GRUND_TEXT } from "../components/status/gruendeText";
|
||||
@@ -283,6 +284,11 @@ export function AktePage() {
|
||||
<DokumentePanel entitaetTyp="objekt" entitaetId={String(objekt.id)} onFehler={setFehler} />
|
||||
</div>
|
||||
|
||||
<div className="akte-notizen card">
|
||||
<h3 style={{ marginTop: 0 }}>Notizen</h3>
|
||||
<NotizenPanel 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>}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"verantwortlich"
|
||||
"maengel"
|
||||
"dokumente"
|
||||
"notizen"
|
||||
"historie";
|
||||
}
|
||||
|
||||
@@ -24,4 +25,5 @@
|
||||
.akte-verantwortlich { grid-area: verantwortlich; }
|
||||
.akte-maengel { grid-area: maengel; }
|
||||
.akte-dokumente { grid-area: dokumente; }
|
||||
.akte-notizen { grid-area: notizen; }
|
||||
.akte-historie { grid-area: historie; }
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"pruefungen beladung"
|
||||
"maengel verantwortlich"
|
||||
"dokumente dokumente"
|
||||
"notizen notizen"
|
||||
"historie historie";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user