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:
co-authored by
Claude Opus 5
parent
f319b8207b
commit
6e66bd39d7
+12
-1
@@ -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
|
||||
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@ flowchart TD
|
||||
K --> AQ["Akquise"]
|
||||
AQ --> AQ1["Eingang<br/>Funde der letzten 14 Tage"]
|
||||
AQ --> AQ2["Archiv<br/>älter; nach 90 Tagen gelöscht"]
|
||||
AQ2 -. "Wiederherstellen<br/>(Funddatum = heute)" .-> AQ1
|
||||
AQ --> AQ3["Papierkorb<br/>von Hand gelöscht; 90 Tage"]
|
||||
AQ2 -. "Zurück in den Eingang<br/>(Funddatum = heute)" .-> AQ1
|
||||
AQ3 -. "Wiederherstellen<br/>(Funddatum bleibt)" .-> AQ2
|
||||
K --> O["Opportunities"]
|
||||
K --> C["Interviews"]
|
||||
K --> L["Kontakte"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 `<datalist>` |
|
||||
| `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `KindBadge`, `Avatar`, Farben der Kontaktart |
|
||||
|
||||
@@ -26,6 +26,7 @@ Quelltext.
|
||||
| **Auswahllisten** (Akquise) | Inhalte der Dropdowns *Von*, *Kanal*, *Berater*. Tabelle `crm_acquisition_options`, gepflegt unter *Einstellungen → Akquise*. *Anbieter* ist Freitext mit Vorschlägen. |
|
||||
| **Art** (eines Kontakts) | Recruiter, Partner oder Endkunde. Spalte `crm_leads.kind`; hieß bis 18.08.2026 „Status" mit den Werten Neu .. Verloren. |
|
||||
| **Papierkorb** (Mails) | Im CRM gelöschte Mails. Umkehrbar, bis er geleert wird. Im Postfach crm@perinco.de bleiben die Mails in jedem Fall; der Abgleich holt gelöschte nicht wieder herein. |
|
||||
| **Papierkorb** (Akquise) | Von Hand gelöschte Akquise-Einträge, `crm_acquisitions.deleted_at`. Wiederherstellbar; endgültig weg nach 90 Tagen oder beim Leeren. Zählt in der Auswertung nicht mit. |
|
||||
| **Papierkorb** (Opportunities) | Gelöschte Opportunities, `crm_opportunities.deleted_at`. Wiederherstellbar; endgültig weg nach 90 Tagen oder beim Leeren. |
|
||||
| **Phase** / Stage | Fortschritt einer Opportunity im Vermittlungsprozess: Neu, Beworben, Erstgespräch Berater, Kundenvorstellung, Interview Kunde, Zusage, Beauftragt, Absage. Offen ist alles bis *Zusage*. In der Datenbank steht `Erstgespraech Berater` ohne Umlaut; angezeigt wird er (`STAGE_LABELS`). |
|
||||
| **Pipeline** | Zwei Bedeutungen, je nach Zusammenhang: die Kanban-Ansicht der Opportunities **oder** der Suchlauf des Scanners (`services/pipeline.js`). |
|
||||
|
||||
Reference in New Issue
Block a user