feat(akquise): Papierkorb fuer geloeschte Eintraege

Loeschen in der Akquise war ein echtes DELETE: der Eintrag stand danach
in keiner Liste, auch nicht im Archiv, und war nicht zurueckholbar. Jetzt
setzt es `deleted_at`/`deleted_by`, die Zeile bleibt 90 Tage stehen.

Der Papierkorb ist die eine Ausnahme von der Regel, dass diese Tabelle
keinen Zustand fuehrt: Loeschen von Hand ist eine Entscheidung, kein
Ablauf. Was dort liegt, blenden alle anderen Abfragen ueber NOT_DELETED
aus - Eingang, Archiv, Menuezahlen und Dashboard. Der Aufraeumlauf ueber
`found_at` laesst geloeschte Zeilen ausdruecklich liegen, sonst waere ein
alter Fund im Papierkorb sofort weg; der Papierkorb raeumt ab
`deleted_at`.

Zwei Rueckwege, deshalb zwei Routen. `/reopen` (bisher `/restore`) holt
aus dem Archiv in den Eingang und setzt das Funddatum auf heute.
`/restore` holt aus dem Papierkorb zurueck und laesst das Funddatum in
Ruhe - der Eintrag soll dort auftauchen, wo er hingehoert, also im
Archiv, wenn er alt ist. Die Meldung nennt das Ziel.

Dazu `DELETE /:id/permanent` und `POST /trash/empty`. Im Papierkorb wird
nicht gepflegt: Bearbeiten und Uebernehmen antworten 409.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-03 10:17:21 +02:00
co-authored by Claude Opus 5
parent f319b8207b
commit 6e66bd39d7
17 changed files with 732 additions and 87 deletions
@@ -25,8 +25,8 @@ interface AcquisitionTableProps {
onEdit: (entry: Acquisition) => void;
onDelete: (entry: Acquisition) => void;
onConvert: (entry: Acquisition) => void;
/** Nur im Archiv gesetzt - im Eingang gibt es nichts wiederherzustellen. */
onRestore?: (entry: Acquisition) => void;
/** Nur im Archiv gesetzt - im Eingang gibt es nichts zurueckzuholen. */
onReopen?: (entry: Acquisition) => void;
}
const COLUMNS: { key: AcquisitionSortKey | null; label: string; className?: string }[] = [
@@ -118,7 +118,7 @@ export const AcquisitionTable: React.FC<AcquisitionTableProps> = ({
onEdit,
onDelete,
onConvert,
onRestore,
onReopen,
}) => (
<div className="overflow-hidden rounded-2xl border border-ink-200 bg-surface shadow-card">
<div className="scrollbar-slim overflow-x-auto">
@@ -230,12 +230,12 @@ export const AcquisitionTable: React.FC<AcquisitionTableProps> = ({
<td className="whitespace-nowrap px-4 py-3 text-right">
<div className="inline-flex gap-1">
{onRestore && (
{onReopen && (
<button
onClick={() => onRestore(entry)}
onClick={() => onReopen(entry)}
disabled={isBusy}
title="Zurück in die Akquise: der Eintrag bekommt das heutige Datum und steht wieder im Eingang."
aria-label={`${entry.title} wiederherstellen`}
aria-label={`${entry.title} zurück in die Akquise`}
className="rounded-lg p-2 text-ink-400 transition hover:bg-brand-50 hover:text-brand-700 disabled:opacity-50"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">