feat(opportunities): Opportunity fuer andere Berater kopieren

Kopier-Symbol auf Kachel, Pipeline-Karte und Listenzeile sowie "Kopieren"
im Dialog. Ein Dialog fragt nach einem oder mehreren Beratern (aus der
Beraterliste der Akquise, ohne den eigenen) und der Phase der Kopie
(Standard: Neu). Neuer Endpoint POST /api/opportunities/:id/copy legt je
Berater eine Kopie mit Projektdaten, Ansprechpartnern, Zustaendigem und
acquisition_id an; Interaktion und naechster Schritt bleiben beim Original.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-17 13:31:59 +02:00
co-authored by Claude Opus 5
parent 33dedaedfb
commit 6724d0ab65
10 changed files with 392 additions and 3 deletions
+19
View File
@@ -367,6 +367,25 @@ Spalte steht ihr Volumen; eine Karte wird per Maus in die nächste Phase gezogen
![Pipeline-Ansicht mit den Phasen von Neu bis Absage](bilder/opportunities-pipeline.png)
### Für einen anderen Berater kopieren
Eine Opportunity läuft immer für **einen** Berater. Passt dasselbe Projekt auch
für Kollegen, legt das **Kopier-Symbol** (auf jeder Kachel, Karte und
Listenzeile, dazu *Kopieren* unten im Dialog) je Berater eine eigene Kopie an:
1. Kopier-Symbol anklicken.
2. Einen oder mehrere Berater ankreuzen - der Berater der Opportunity selbst
steht nicht zur Wahl. Die Liste ist dieselbe wie in der Akquise
(*Einstellungen → Akquise*).
3. Phase der Kopie wählen, vorgegeben ist **Neu**.
4. **Kopie anlegen**.
Mitkopiert werden Titel, Firma, Beschreibung, Tagessatz, Laufzeit, Projektpreis,
Zeitraum, Tags, Notizen, Ansprechpartner, Zuständiger und die Herkunft aus der
Akquise - *Zurück in die Akquise* nimmt die Kopien also mit. Nicht mitkopiert
werden letzte Interaktion und nächster Schritt: die gehören zum Verlauf des
Originals. Ungespeicherte Änderungen im Dialog kopiert das CRM nicht mit.
### Die Phasen
Sie folgen dem Vermittlungsprozess. Der Mauszeiger auf einer Phase zeigt dieselbe
+2 -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 /: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 |
| `opportunities.js` | `GET /?trash=true`, `POST /`, `PATCH /:id`, `DELETE /:id` (Papierkorb), `POST /:id/restore`, `POST /:id/acquisition`, `POST /:id/copy`, `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. `/:id/copy` (`{ consultants, stage }`) legt je Berater eine Kopie an - Projektdaten, Ansprechpartner, Zuständiger und `acquisition_id` kommen mit, Interaktion und nächster Schritt nicht; jeder Angemeldete |
| `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`) |
@@ -315,6 +315,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
| Datei | Inhalt |
| --- | --- |
| `components/opportunities/OpportunityCopyDialog.tsx` | Kopie für andere Berater: Auswahl aus der Beraterliste der Akquise (ohne den eigenen), Phase der Kopie |
| `components/opportunities/OpportunityTrash.tsx` | Papierkorb der Opportunities: wiederherstellen, leeren, Restlaufzeit je Eintrag |
| `components/opportunities/opportunityUi.tsx` | `STAGE_LABELS` (Anzeige, mit Umlaut), `STAGE_HINTS` (Erläuterung als Titelhinweis), `STAGE_STYLES`, `STAGE_COLORS`, `StageBadge` |
| `components/dashboard/Dashboard.tsx` | Startansicht: Zeitraumwahl, vier Kennzahlen, Verlauf, Berater, Termine, „Beim Kunden". `buildAxis()` erzeugt auch leere Abschnitte |