diff --git a/docs/02-datenmodell.md b/docs/02-datenmodell.md
index b369c42..0f96942 100644
--- a/docs/02-datenmodell.md
+++ b/docs/02-datenmodell.md
@@ -168,12 +168,23 @@ Acht Eigenheiten, die man kennen muss:
Zustand der Opportunity abgeleitet (Papierkorb oder weg), nicht gespeichert:
wird sie zurückgeholt, verschwindet der Eintrag von selbst wieder. Die Regeln
dafür stehen in `server/src/acquisitionRules.js`.
- Aus derselben Logik folgt, was **Wiederherstellen** (`POST /:id/restore`) tut:
+ Aus derselben Logik folgt, was **Zurück in den Eingang** (`POST /:id/reopen`) tut:
Es setzt `found_at` auf heute und leert `converted_at` samt `opportunity_id` -
ohne das Zurücksetzen von `converted_at` bliebe ein Rückfaller im Archiv
stehen, gleich welches Datum er trägt. Ein Kennzeichen
„wiederhergestellt" gibt es nicht; es wäre der Zustand, den diese Tabelle
gerade nicht führt.
+- **Der Papierkorb ist die Ausnahme davon.** Löschen von Hand ist eine
+ Entscheidung und kein Ablauf, deshalb steht sie in `deleted_at` (plus
+ `deleted_by`, ohne Fremdschlüssel - siehe `schema.sql`). Vorher war das
+ Löschen ein echtes `DELETE` und damit endgültig; jetzt bleibt die Zeile
+ 90 Tage stehen. Was dort liegt, blenden **alle** anderen Abfragen aus
+ (`NOT_DELETED`): Eingang, Archiv, Menüzahlen und Dashboard.
+ Der Papierkorb rechnet ab `deleted_at`, nicht ab `found_at` - der
+ Aufräumlauf über das Funddatum lässt gelöschte Zeilen deshalb ausdrücklich
+ liegen, sonst wäre ein alter Fund im Papierkorb sofort weg.
+ `POST /:id/restore` räumt nur `deleted_at`/`deleted_by` weg und lässt das
+ Funddatum in Ruhe: der Eintrag soll dort wieder auftauchen, wo er hingehört.
### Postfach crm@perinco.de
diff --git a/docs/05-frontend.md b/docs/05-frontend.md
index cf0532b..49f8de6 100644
--- a/docs/05-frontend.md
+++ b/docs/05-frontend.md
@@ -13,7 +13,9 @@ flowchart TD
K --> AQ["Akquise"]
AQ --> AQ1["Eingang Funde der letzten 14 Tage"]
AQ --> AQ2["Archiv älter; nach 90 Tagen gelöscht"]
- AQ2 -. "Wiederherstellen (Funddatum = heute)" .-> AQ1
+ AQ --> AQ3["Papierkorb von Hand gelöscht; 90 Tage"]
+ AQ2 -. "Zurück in den Eingang (Funddatum = heute)" .-> AQ1
+ AQ3 -. "Wiederherstellen (Funddatum bleibt)" .-> AQ2
K --> O["Opportunities"]
K --> C["Interviews"]
K --> L["Kontakte"]
diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md
index bfd55c4..3c7cecc 100644
--- a/docs/07-anleitung-benutzer.md
+++ b/docs/07-anleitung-benutzer.md
@@ -37,6 +37,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
│ Akquise ▾ │ │
│ Eingang │ │
│ Archiv 3│ │
+│ Papierkorb2│ │
│ Opportuni…▾ │───────────────────────────────────────────┤
│ Alle │ │
│ Papierkorb2│ │
@@ -70,7 +71,7 @@ die Scanner-Einstellungen. Alles andere ist für beide Rollen gleich.
* Die Menüpunkte **Akquise**, **Opportunities**, **Kontakte**, **Mails** und
**Scanner** klappen beim Anklicken ihre Unterpunkte auf. Hinter einigen steht
eine Zahl: *Aus Mails* (Funde, die auf eine Entscheidung warten), *Archiv*
- (Akquise-Einträge älter als 14 Tage), beide *Papierkörbe* und *Unpassend*
+ (Akquise-Einträge älter als 14 Tage), alle drei *Papierkörbe* und *Unpassend*
(verworfene Scanner-Treffer). Bei 0 steht dort nichts - eine Null wäre nur
Rauschen.
* Jedes Fenster, das sich in den Vordergrund legt, schließt auf drei Wege: das
@@ -129,8 +130,9 @@ Volumen und Zuständigem - also alles, wo gerade der Kunde am Zug ist.
## Akquise
Der Eingang für gefundene Projekte - alles, was per Mail, Telefon oder über ein
-Portal hereinkommt, bevor daraus ein Geschäft wird. Zwei Unterpunkte: **Eingang**
-mit den Funden der letzten 14 Tage und **Archiv** mit allem Älteren.
+Portal hereinkommt, bevor daraus ein Geschäft wird. Drei Unterpunkte: **Eingang**
+mit den Funden der letzten 14 Tage, **Archiv** mit allem Älteren und der
+**Papierkorb** mit dem, was jemand von Hand gelöscht hat.
### Eintragen
@@ -183,7 +185,7 @@ wird geleert - für das nächste Projekt aus derselben Quelle.
| **Projekttitel** | mit Link anklickbar (neuer Tab); darunter klein Endkunde, Tagessatz bzw. Festpreis und Personentage |
| **Berater** | für wen |
| **Opportunity** | ein Klick macht daraus eine Opportunity in der Phase *Erstgespräch Berater* |
-| Pfeil / Stift / Papierkorb | zurück in die Akquise (nur im Archiv), bearbeiten (füllt das Formular oben) und löschen |
+| Pfeil / Stift / Papierkorb | zurück in die Akquise (nur im Archiv), bearbeiten (füllt das Formular oben) und löschen (legt den Eintrag in den **Papierkorb**) |
Die Spaltenköpfe sortieren, die Suche geht über Titel, Anbieter, Berater,
Endkunde, Beschreibung und Notizen.
@@ -193,8 +195,11 @@ Endkunde, Beschreibung und Notizen.
* Nach **14 Tagen** rutscht ein Eintrag ins **Archiv**. Dort lässt er sich weiter
bearbeiten und jederzeit übernehmen - der Stift klappt das Formular auch im
Archiv auf.
-* Nach **90 Tagen** wird er **gelöscht**. Wer ein Projekt behalten will,
- übernimmt es als Opportunity.
+* Nach **90 Tagen** wird er **gelöscht** - endgültig, und nicht über den
+ Papierkorb: das ist der Ablauf der Frist, keine Entscheidung. Gesichert wird
+ vorher nur die Zahl für das Dashboard (Tag, Berater, Anzahl), die
+ Projektdaten sind dann weg. Wer ein Projekt behalten will, übernimmt es als
+ Opportunity.
* Wird ein Eintrag **übernommen**, verschwindet er aus Eingang und Archiv - er
lebt als Opportunity weiter.
* Wird diese **Opportunity gelöscht**, taucht die Bewerbung wieder im **Archiv**
@@ -226,8 +231,37 @@ weg. Der Eintrag ist danach wieder ein gewöhnlicher Fund - liegt die Opportunit
noch im Papierkorb und wird sie zurückgeholt, steht sie ab dann *neben* dem
Eintrag statt an seiner Stelle.
-Ein Eintrag, dessen Opportunity noch lebt, lässt sich nicht wiederherstellen -
-er steht ohnehin in keiner der beiden Listen.
+Ein Eintrag, dessen Opportunity noch lebt, lässt sich nicht zurückholen - er
+steht ohnehin in keiner der beiden Listen.
+
+### Papierkorb
+
+Der **Papierkorb** unter *Akquise* fängt auf, was jemand von Hand löscht. Vorher
+war ein Fehlklick endgültig: der Eintrag stand danach in keiner Liste, auch nicht
+im Archiv.
+
+| Spalte | Bedeutung |
+| --- | --- |
+| **Projekttitel** | darunter klein Endkunde, Tagessatz bzw. Festpreis und Personentage |
+| **Anbieter**, **Berater** | wie in der Akquise |
+| **Gefunden** | das Funddatum - es entscheidet, wo der Eintrag nach dem Zurückholen landet |
+| **Gelöscht** | wann, und wie lange er noch liegt |
+| **Von** | wer gelöscht hat |
+| **Wiederherstellen** / Papierkorb-Symbol | zurückholen oder endgültig löschen |
+
+* Ein gelöschter Eintrag liegt **90 Tage** im Papierkorb, danach räumt das CRM
+ selbst auf. Die Frist zählt ab dem Löschen, nicht ab dem Funddatum - ein alter
+ Fund, den jemand heute wegwirft, verschwindet also nicht sofort.
+* **Wiederherstellen** ändert das Funddatum **nicht**. Der Eintrag geht dorthin,
+ wo er nach seinem Alter hingehört: in den *Eingang*, wenn er jünger als 14 Tage
+ ist, sonst ins *Archiv*. Wer ihn im Eingang haben will, holt ihn dort mit dem
+ Pfeil weiter (siehe oben).
+* **Endgültig löschen** und **Papierkorb leeren** sind unumkehrbar.
+* In der **Auswertung** zählt ein Eintrag im Papierkorb nicht mit - ein
+ Fehleintrag soll die Zahl der Bewerbungen nicht verfälschen. Wird er
+ zurückgeholt, zählt er wieder.
+* Im Papierkorb wird nicht gepflegt: Bearbeiten und die Übernahme in eine
+ Opportunity gehen erst nach dem Zurückholen.
### Aus dem Eintrag wird eine Opportunity
diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md
index a8c4035..0763e32 100644
--- a/docs/09-quellcode.md
+++ b/docs/09-quellcode.md
@@ -92,9 +92,9 @@ Alle unter `router.use(requireAuth)`.
| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete |
| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; ändern/löschen nur Ersteller oder Admin |
| `opportunities.js` | `GET /?trash=true`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `POST /:id/restore`, `POST /trash/empty` | ändern, löschen und wiederherstellen nur Ersteller, Zuständiger oder Admin; Leeren jeder Angemeldete |
-| `acquisitions.js` | `GET /?view=eingang\|archiv\|alle`, `POST /`, `PATCH /:id`, `DELETE /:id`, `POST /:id/opportunity`, `POST /:id/restore`; Auswahllisten: `GET/POST /options`, `PATCH/DELETE /options/:id` | Einträge ändern/löschen/wiederherstellen nur Ersteller oder Admin; Auswahllisten jeder Angemeldete. `/restore` setzt `found_at` auf heute und leert `converted_at`/`opportunity_id` - sonst hält die Übernahme den Eintrag im Archiv fest; bei lebender Opportunity antwortet es 409 |
-| `badges.js` | `GET /` | Zahlen für die Seitenleiste: Akquise-Archiv, beide Papierkörbe, verworfene Scanner-Treffer. Der Scanner-Zähler ist `null`, wenn dessen Datenbank fehlt |
-| `acquisitionRules.js` (kein Router) | Fristen und die SQL-Bedingungen `IS_OPEN`, `IN_ARCHIVE`, `NOT_DUE`, `OPPORTUNITY_GONE`. Geteilt von Liste, Zählern, Aufräumlauf und Menüzahl - zwei Kopien derselben Regel sind einmal auseinandergelaufen |
+| `acquisitions.js` | `GET /?view=eingang\|archiv\|papierkorb\|alle`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `DELETE /:id/permanent`, `POST /trash/empty`, `POST /:id/opportunity`, `POST /:id/reopen`, `POST /:id/restore`; Auswahllisten: `GET/POST /options`, `PATCH/DELETE /options/:id` | Einträge ändern, löschen und zurückholen nur Ersteller oder Admin; Auswahllisten und Leeren jeder Angemeldete. Zwei verschiedene Rückwege, nicht zu verwechseln: `/reopen` holt aus dem **Archiv** in den Eingang (setzt `found_at` auf heute und leert `converted_at`/`opportunity_id` - sonst hält die Übernahme den Eintrag im Archiv fest), `/restore` holt aus dem **Papierkorb** zurück (leert nur `deleted_at`/`deleted_by`, das Funddatum bleibt). Beide antworten 409, wenn der Eintrag im falschen Zustand ist |
+| `badges.js` | `GET /` | Zahlen für die Seitenleiste: Akquise-Archiv und dessen Papierkorb, Papierkorb der Opportunities und der Mails, verworfene Scanner-Treffer. Der Scanner-Zähler ist `null`, wenn dessen Datenbank fehlt |
+| `acquisitionRules.js` (kein Router) | Fristen und die SQL-Bedingungen `IS_OPEN`, `IN_ARCHIVE`, `NOT_DUE`, `OPPORTUNITY_GONE`, `NOT_DELETED`, `IN_TRASH`. Geteilt von Liste, Zählern, Aufräumlauf und Menüzahl - zwei Kopien derselben Regel sind einmal auseinandergelaufen |
| `dashboard.js` | `GET /?from=&to=&bucket=tag\|monat\|quartal\|jahr` | drei Reihen über der Zeit plus Bewerbungen je Berater. Liest Akquise **und** `crm_acquisition_history`; Termine und Phasenlisten stehen bewusst nicht drin - die lädt die App ohnehin |
| `mail.js` | `GET /status`, `/messages`, `/messages/:id`, `/candidates`; `POST /messages/:id/links`, `/messages/:id/restore`, `/candidates/:id/adopt`, `/candidates/:id/reject`, `/trash/empty`; `DELETE /links/:id`, `/messages/:id`; `requireAdmin`: `GET/POST /settings`, `POST /test`, `POST /sync` | lesen, zuordnen und löschen jeder; einrichten Admin |
@@ -274,7 +274,8 @@ Warum genau diese Konstanten festgelegt sind, steht in
| `components/dashboard/dashboardUi.tsx` | `useCountUp()` (Zahl läuft hoch, respektiert `prefers-reduced-motion`), `StatCard`, `SeriesChart`, `BarList`, `Panel` - von Hand statt Diagrammbibliothek |
| `components/acquisition/Acquisitions.tsx` | Akquise und Archiv (`view`); Suche, Sortierung, Übernahme, Löschen, Wiederherstellen. Das Formular steht im Archiv nur beim Bearbeiten - ohne diesen Zweig blieb der Stift dort ohne Wirkung |
| `components/acquisition/AcquisitionForm.tsx` | Eingabe über der Liste; behält Datum, Anbieter, Kanal und Berater für den nächsten Eintrag |
-| `components/acquisition/AcquisitionTable.tsx` | die Liste; `safeLink()` lässt nur http(s) zum Link werden. Der Pfeil zum Wiederherstellen erscheint nur, wenn `onRestore` gesetzt ist - im Eingang gibt es nichts wiederherzustellen |
+| `components/acquisition/AcquisitionTable.tsx` | die Liste; `safeLink()` lässt nur http(s) zum Link werden. Der Pfeil zurück in den Eingang erscheint nur, wenn `onReopen` gesetzt ist - im Eingang gibt es nichts zurückzuholen |
+| `components/acquisition/AcquisitionTrash.tsx` | Papierkorb der Akquise: zurückholen, endgültig löschen, leeren. Die Meldung nach dem Zurückholen nennt das Ziel (Eingang oder Archiv) - das Funddatum bleibt, also landet ein alter Fund im Archiv |
| `components/acquisition/acquisitionUi.tsx` | `fieldClass` (abgesetzte Felder) und `OptionSelect` - zeigt einen Wert, der nicht mehr in der Liste steht, als solchen; `alwaysOffer` hält den eigenen Namen für *Von* immer wählbar. *Anbieter* nutzt es nicht: Freitext mit `