feat(opportunities): Uebernahme zurueck in die Akquise schieben

Im Dialog einer Opportunity aus der Akquise nimmt "Zurueck in die Akquise"
eine versehentliche Uebernahme zurueck: alle Opportunities aus dem Eintrag
wandern in den Papierkorb, der Eintrag steht mit seinem Funddatum wieder in
Eingang bzw. Archiv. Neue Spalte crm_opportunities.acquisition_id, damit bei
mehreren Beratern alle gefunden werden. Wiederherstellen haengt den Eintrag
wieder an die Opportunity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-17 13:12:03 +02:00
co-authored by Claude Opus 5
parent 0ad7b32dbf
commit 33dedaedfb
9 changed files with 270 additions and 19 deletions
+11
View File
@@ -66,6 +66,7 @@ erDiagram
int id PK
varchar title
varchar consultant "fuer welchen Berater"
int acquisition_id "Herkunft in der Akquise, ohne FK"
enum stage "Neu .. Beauftragt | Absage"
tinyint probability "0-100"
decimal daily_rate
@@ -199,6 +200,16 @@ Acht Eigenheiten, die man kennen muss:
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 Rückweg von der Opportunity** (`POST /api/opportunities/:id/acquisition`)
braucht die Gegenrichtung: `crm_acquisitions.opportunity_id` kennt nur die
*erste* Opportunity einer Übernahme, bei mehreren Beratern entstehen aber
mehrere. Deshalb trägt jede Opportunity `acquisition_id` (seit 17.09.2026,
ohne Fremdschlüssel - die Akquise löscht nach 90 Tagen). `schema.sql` trägt
den Verweis für ältere Übernahmen nach, kann das aber nur für die erste.
Der Rückweg legt alle lebenden Opportunities mit dieser `acquisition_id` in
den Papierkorb und leert beim Eintrag `converted_at` und `opportunity_id`;
`found_at` bleibt. Wird eine davon wiederhergestellt, setzt `/restore` den
Verweis wieder, sofern der Eintrag frei ist.
- **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
+28 -5
View File
@@ -266,12 +266,35 @@ zum Löschen wieder von vorn.
Bei einem **Rückfaller** (die Bewerbung war schon eine Opportunity) fragt das CRM
nach: mit dem Wiederherstellen fällt der Verweis auf die gelöschte Opportunity
weg. Der Eintrag ist danach wieder ein gewöhnlicher Fund - liegt die Opportunity
noch im Papierkorb und wird sie zurückgeholt, steht sie ab dann *neben* dem
Eintrag statt an seiner Stelle.
weg. Der Eintrag ist danach wieder ein gewöhnlicher Fund. Liegt die Opportunity
noch im Papierkorb und wird sie zurückgeholt, hängt der Eintrag wieder an ihr und
verschwindet aus der Akquise - dasselbe Projekt steht nie an zwei Stellen.
Ein Eintrag, dessen Opportunity noch lebt, lässt sich nicht zurückholen - er
steht ohnehin in keiner der beiden Listen.
Ein Eintrag, dessen Opportunity noch lebt, lässt sich hier nicht zurückholen - er
steht ohnehin in keiner der beiden Listen. Dafür gibt es den Weg von der
Opportunity aus (nächster Abschnitt).
### Übernahme zurücknehmen
Aus Versehen übernommen? Im Dialog der Opportunity steht unten links
**Zurück in die Akquise** - nur bei Opportunities, die aus der Akquise kommen und
deren Eintrag dort noch existiert. Nach einer Rückfrage passiert:
* Der Eintrag steht wieder in der Akquise, als wäre er nie übernommen worden. Das
**Funddatum bleibt**: jung im Eingang, älter als 14 Tage im Archiv. In der
Auswertung zählt er nicht mehr als *übernommen*.
* Die Opportunity wandert in den **Papierkorb** der Opportunities. Waren beim
Eintrag **mehrere Berater** eingetragen, sind bei der Übernahme mehrere
Opportunities entstanden - sie wandern **alle** mit.
* Ungespeicherte Änderungen im Dialog gehen verloren.
Zurücknehmen darf, wer alle betroffenen Opportunities löschen darf (Ersteller,
Zuständiger oder Administrator). Holt jemand eine davon aus dem Papierkorb
zurück, hängt der Eintrag wieder an ihr und verschwindet aus der Akquise.
> Bei Übernahmen **vor dem 17.09.2026** kennt nur die jeweils *erste*
> Opportunity ihren Eintrag. Bei mehreren Beratern bleiben die übrigen dann
> stehen und müssen von Hand gelöscht werden.
### Papierkorb
+1 -1
View File
@@ -99,7 +99,7 @@ Alle unter `router.use(requireAuth)`.
| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id`; Vorbereich: `GET /mail-pool/sources`, `POST /mail-pool/promote`, `POST /:id/promote` | jeder Angemeldete |
| `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 |
| `opportunities.js` | `GET /?trash=true`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `POST /:id/restore`, `POST /:id/acquisition`, `POST /trash/empty` | ändern, löschen und wiederherstellen nur Ersteller, Zuständiger oder Admin; Leeren jeder Angemeldete. `/:id/acquisition` nimmt eine Übernahme zurück: alle lebenden Opportunities mit derselben `acquisition_id` in den Papierkorb, beim Eintrag `converted_at`/`opportunity_id` leeren (Funddatum bleibt), in einer Transaktion. `/restore` hängt den Eintrag wieder an die Opportunity, wenn er frei ist |
| `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. Dazu `splitConsultants` / `joinConsultants` / `normalizeConsultants` für das Feld *Berater*, das mehrere Namen kommagetrennt hält (Gegenstück im Frontend: `src/types.ts`) |