feat(akquise): Archiv bearbeiten und wiederherstellen
Im Archiv setzte der Stift nur `editing` - das Formular stand dort nicht, also passierte sichtbar nichts. Es wird jetzt auch im Archiv gezeigt, aber nur beim Bearbeiten: eingetragen wird weiterhin im Eingang. Dazu ein Weg zurueck. `POST /api/acquisitions/:id/restore` setzt `found_at` auf heute und leert `converted_at` samt `opportunity_id` - ohne Letzteres haelt die Uebernahme einen Rueckfaeller im Archiv fest, gleich welches Datum er traegt. Eine Spalte "wiederhergestellt" gibt es bewusst nicht; die Tabelle fuehrt keinen Zustand. Der Pfeil erscheint nur im Archiv. Ein nie uebernommener Eintrag wird ohne Rueckfrage zurueckgeholt, ein Rueckfaeller mit - dabei faellt der Verweis auf die geloeschte Opportunity weg. Fuer diese Rueckfrage kennt `ConfirmDialog` jetzt `tone="neutral"`: ein roter Knopf an einer harmlosen Stelle nimmt der Farbe ihre Bedeutung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ea886e4d1b
commit
f319b8207b
@@ -168,6 +168,12 @@ 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:
|
||||
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.
|
||||
|
||||
### Postfach crm@perinco.de
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
K --> O["Opportunities"]
|
||||
K --> C["Interviews"]
|
||||
K --> L["Kontakte"]
|
||||
|
||||
@@ -183,7 +183,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* |
|
||||
| Stift / Papierkorb | 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 |
|
||||
|
||||
Die Spaltenköpfe sortieren, die Suche geht über Titel, Anbieter, Berater,
|
||||
Endkunde, Beschreibung und Notizen.
|
||||
@@ -191,7 +191,8 @@ Endkunde, Beschreibung und Notizen.
|
||||
### Was von selbst passiert
|
||||
|
||||
* Nach **14 Tagen** rutscht ein Eintrag ins **Archiv**. Dort lässt er sich weiter
|
||||
bearbeiten und jederzeit übernehmen.
|
||||
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.
|
||||
* Wird ein Eintrag **übernommen**, verschwindet er aus Eingang und Archiv - er
|
||||
@@ -207,6 +208,27 @@ Endkunde, Beschreibung und Notizen.
|
||||
Verschoben wird dabei nichts: Eingang und Archiv sind dieselbe Liste, nur nach
|
||||
dem Funddatum geteilt. Es gibt also keinen Zustand, der klemmen kann.
|
||||
|
||||
### Zurück in die Akquise
|
||||
|
||||
Der **Pfeil** in der Archivliste holt einen Eintrag zurück in den Eingang - für
|
||||
ein Projekt, das doch noch aktuell ist, oder für eine Bewerbung, die nach dem
|
||||
Löschen ihrer Opportunity von vorn beginnt. Der Eintrag bekommt dabei das
|
||||
**heutige Datum**; damit zählen die 14 Tage bis zum Archiv und die 90 Tage bis
|
||||
zum Löschen wieder von vorn.
|
||||
|
||||
> **Zur Statistik:** Das neue Funddatum verschiebt die Bewerbung auch im
|
||||
> Dashboard in den heutigen Zeitraum - sie ist dort keine Bewerbung von damals
|
||||
> mehr.
|
||||
|
||||
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.
|
||||
|
||||
Ein Eintrag, dessen Opportunity noch lebt, lässt sich nicht wiederherstellen -
|
||||
er steht ohnehin in keiner der beiden Listen.
|
||||
|
||||
### Aus dem Eintrag wird eine Opportunity
|
||||
|
||||
Der Knopf **Opportunity** legt sie sofort an: Phase *Erstgespräch Berater*, 10 % Wahrscheinlichkeit,
|
||||
|
||||
@@ -219,8 +219,9 @@ Browser-Download.
|
||||
* **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`,
|
||||
`labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`,
|
||||
`primaryButtonClass` aus `components/scanner/scannerUi.tsx`.
|
||||
* **Meldungen als Toast**, Löschabfragen über `ConfirmDialog` - nie `alert()`
|
||||
oder `confirm()`.
|
||||
* **Meldungen als Toast**, Rückfragen über `ConfirmDialog` - nie `alert()`
|
||||
oder `confirm()`. Rot (`tone="danger"`, Standard) nur, wenn etwas verloren
|
||||
geht; sonst `tone="neutral"`, damit die Farbe ihre Bedeutung behält.
|
||||
* **Rechte immer doppelt**: Oberfläche blendet aus, Backend antwortet mit 403.
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -92,7 +92,7 @@ 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`; Auswahllisten: `GET/POST /options`, `PATCH/DELETE /options/:id` | Einträge ändern/löschen nur Ersteller oder Admin; Auswahllisten 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 |
|
||||
| `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 |
|
||||
@@ -272,9 +272,9 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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/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 |
|
||||
@@ -285,7 +285,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `components/scanner/scannerUi.tsx` | `cardClass`, Buttonklassen, `MatchBadge`, `MatchStatusBadge`, `PortalStatusPill`, `formatDateTime`, `projectTimestamp` |
|
||||
| `components/mail/mailUi.tsx` | `Badge`, `ConfidenceBar`, Herkunfts- und Statusbeschriftungen, `formatAddresses`, `avatarColor` |
|
||||
| `components/mail/MailBody.tsx` | Mailinhalt im `<iframe sandbox="">`, externe Bilder per CSP gesperrt |
|
||||
| `components/ui/Modal.tsx` | `Modal` und `ConfirmDialog`; `useDismiss()` schließt per Escape und per Klick in den Hintergrund. Escape trifft nur den obersten Dialog (Stapel `openModals`), der Hintergrundklick muss dort beginnen **und** enden |
|
||||
| `components/ui/Modal.tsx` | `Modal` und `ConfirmDialog`; `useDismiss()` schließt per Escape und per Klick in den Hintergrund. Escape trifft nur den obersten Dialog (Stapel `openModals`), der Hintergrundklick muss dort beginnen **und** enden. `ConfirmDialog` kennt zwei Tonlagen: `tone="danger"` (Standard, rot - es geht etwas verloren) und `tone="neutral"` (grün, bloße Rückfrage, z. B. beim Wiederherstellen) |
|
||||
| `components/Toast.tsx` | `useToast().addToast(text, kind)` mit `success`, `error` oder `info` |
|
||||
| `components/ui/DictationField.tsx` | Mikrofon-Knopf, Hinweisbox, Textfeld mit Diktat |
|
||||
| `lib/imageCompress.ts` | `compressImage()` auf 256 px, WebP → JPEG → PNG |
|
||||
|
||||
Reference in New Issue
Block a user