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
+21
View File
@@ -424,15 +424,36 @@ CREATE TABLE IF NOT EXISTS crm_acquisitions (
opportunity_id INT UNSIGNED NULL,
converted_at DATETIME NULL,
created_by INT UNSIGNED NULL,
deleted_at DATETIME NULL, -- im Papierkorb (UTC)
deleted_by INT UNSIGNED NULL, -- wer; bewusst ohne Fremdschluessel, s.u.
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_crm_acq_found (found_at),
KEY idx_crm_acq_converted (converted_at),
KEY idx_crm_acq_deleted (deleted_at),
CONSTRAINT fk_crm_acq_opp FOREIGN KEY (opportunity_id) REFERENCES crm_opportunities (id) ON DELETE SET NULL,
CONSTRAINT fk_crm_acq_user FOREIGN KEY (created_by) REFERENCES crm_users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*
* Papierkorb der Akquise. Loeschen setzt `deleted_at`, die Zeile bleibt stehen -
* vorher war es ein echtes DELETE, und ein Fehlklick war damit endgueltig: der
* Eintrag tauchte in keiner Liste mehr auf, auch nicht im Archiv.
*
* Der Papierkorb hat seine eigene Uhr (`deleted_at`), nicht die des Funddatums:
* ein von Hand geloeschter Eintrag liegt 90 Tage dort, gleich wie alt der Fund
* ist. Der Aufraeumlauf ueber `found_at` laesst geloeschte Zeilen deshalb
* ausdruecklich liegen - sonst wuerde ein alter Fund im Papierkorb sofort
* verschwinden.
*
* `deleted_by` traegt bewusst keinen Fremdschluessel: Spalten lassen sich per
* ADD COLUMN IF NOT EXISTS idempotent nachziehen, ein Fremdschluessel nicht.
*/
ALTER TABLE crm_acquisitions ADD COLUMN IF NOT EXISTS deleted_at DATETIME NULL AFTER created_by;
ALTER TABLE crm_acquisitions ADD COLUMN IF NOT EXISTS deleted_by INT UNSIGNED NULL AFTER deleted_at;
ALTER TABLE crm_acquisitions ADD INDEX IF NOT EXISTS idx_crm_acq_deleted (deleted_at);
/*
* Auswahllisten der Akquise. "Von", "Kanal" und "Berater" sind Dropdowns, deren
* Inhalte in den Einstellungen gepflegt werden.