Neue Spalten stage_changed_at/stage_changed_by, gesetzt nur bei echtem
Phasenwechsel per PATCH. Der Dialog zeigt unter "Angelegt von" die Zeile
"Verschoben von ... am ... um ... in ...".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Uebernahme aus der Akquise schrieb den Link bisher nur in die Notizen.
Jetzt hat die Opportunity eine Spalte `link`, gefuellt bei Uebernahme und
Kopie; im Dialog steht sie rechts neben der Phase mit Knopf "Oeffnen".
schema.sql traegt den Link fuer aeltere Uebernahmen nach. Die Pruefung liegt
in server/src/link.js und gilt fuer Akquise und Opportunities gleich.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Bei der Uebernahme aus der Akquise verschwand die wichtigste Angabe nach
dem Titel: fuer wen das Projekt laeuft. Die Opportunity hatte kein Feld
dafuer, also schrieb die Uebernahme den Namen in den Herkunftsvermerk der
Notizen - in Pipeline, Liste und Kacheln war er damit nicht zu sehen, und
suchen liess sich nur im Notiztext.
crm_opportunities.consultant traegt ihn jetzt selbst. Genau ein Name je
Opportunity: passt ein Fund zu drei Beratern, entstehen bei der
Uebernahme drei Opportunities. Sichtbar in Pipeline-Karte, Kachel,
Papierkorb und als sortierbare Spalte direkt hinter dem Titel; die Suche
fasst ihn mit ein, und im Dialog kommt die Auswahl aus derselben Liste
wie in der Akquise (Einstellungen -> Akquise), damit nicht "Nico" und
"Nico N." nebeneinander entstehen.
Bestehende Opportunities holt migrateOpportunityConsultant() beim Start
aus ihren Notizen nach - nur dort, wo die Spalte leer ist. Damit ist der
Lauf wiederholbar und ueberschreibt nichts, was seither von Hand gesetzt
wurde; ein Merker in crm_settings ist dafuer nicht noetig. Der Text in
den Notizen bleibt stehen: er ist der Herkunftsnachweis samt Anbieter,
Kanal und Link.
Der Namenszusatz im Titel entfaellt wieder. Er war gestern dazugekommen,
damit sich mehrere Opportunities aus einem Fund unterscheiden liessen -
mit einer eigenen Spalte ist er nur noch Rauschen.
Nebenbei: der Block, der won_at nachzieht, stand zweimal in schema.sql.
Idempotent und damit harmlos, aber eine Kopie - jetzt einmal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Erster Menuepunkt und Startansicht; das Logo oben links fuehrt dorthin zurueck.
Vier Kennzahlen (Bewerbungen, daraus Opportunities samt Quote, gewonnene
Projekte, aktuell beim Kunden), ein Saeulendiagramm mit denselben drei Reihen
ueber der Zeit, die Bewerbungen je Berater, die naechsten Termine und alle
Opportunities in Kundenvorstellung bzw. Interview Kunde. Zeitraum ueber vier
Vorgaben oder frei, Einteilung nach Tag, Monat, Quartal oder Jahr.
Bewerbung heisst dabei: ein Eintrag in der Akquise - jeder Eintrag bedeutet, dass
ein Profil raus ist (so vom Nutzer festgelegt).
Zwei Dinge dafuer am Datenmodell:
- crm_opportunities.won_at haelt den Zeitpunkt des Wechsels nach "Beauftragt".
Die Phase allein sagt nur, wie es jetzt steht, nicht wann es passiert ist.
Beim Wechsel heraus wird das Datum geleert, sonst zaehlt das Dashboard einen
Gewinn, den es nicht gibt.
- Die Akquise loescht nach 90 Tagen - damit waere jede Jahresauswertung falsch.
Der Aufraeumlauf sichert deshalb vorher Tag, Berater und Anzahl in
crm_acquisition_history, in derselben Transaktion wie das Loeschen. Gesichert
wird nur, was verschwindet; die Auswertung addiert beide Quellen und kann sich
damit nicht ueberschneiden. Projektdaten sind nach dem Loeschen weiterhin weg.
Termine und die Phasenlisten kommen nicht aus einem eigenen Endpunkt: Termine und
Opportunities laedt die App ohnehin komplett, das Dashboard filtert sie selbst.
Diagramme von Hand (SVG-freie Balken plus Tailwind-Uebergaenge) statt einer
Bibliothek - drei Darstellungen rechtfertigen keine weitere Abhaengigkeit, und so
kommen die Farben aus denselben Tokens wie der Rest. Die Zahlen laufen hoch, die
Saeulen wachsen; wer Bewegung im System abgeschaltet hat, bekommt sofort den
Endwert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drei Dinge:
- Die Phasen sind jetzt die des Vermittlungsprozesses: Neu, Beworben,
Erstgespraech Berater, Kundenvorstellung, Interview Kunde, Zusage, Beauftragt,
Absage. Offen ist alles bis einschliesslich Zusage - dort ist der Einsatz
bestaetigt, die Beauftragung steht aber noch aus. migrateOpportunityStages()
stellt bestehende Datenbanken um und setzt alle vorhandenen Opportunities auf
"Neu": in welcher der neuen Phasen ein Projekt steckt, sagt der alte Trichter
nicht (so mit dem Nutzer entschieden).
Der Datenbankwert heisst 'Erstgespraech Berater' ohne Umlaut - Projektkonvention
fuer Datenbankwerte. Angezeigt wird er ueber STAGE_LABELS; dazu gibt es
STAGE_HINTS mit der Erlaeuterung je Phase als Titelhinweis an Auswahl, Badge
und Spaltenkopf.
- Kanban ist die erste Ansicht der Opportunities. Die Pipeline ist die
Arbeitsansicht, Kachel und Liste sind zum Nachschauen.
- Hinter "Archiv", beiden "Papierkorb"-Punkten und "Unpassend" steht die Anzahl,
wie schon bei "Aus Mails". Die Zahlen kommen aus einem eigenen Endpunkt
(/api/badges), weil sie alle ins Menue gehoeren und zusammen kommen sollen;
Inhalte laedt weiter jede Ansicht selbst. Der Scanner liegt in einer eigenen
Datenbank und kann fehlen - sein Zaehler ist dann null und die Leiste zeigt
dort einfach keine Zahl. Der Aufruf haengt bewusst hinter den Daten und nicht
im Promise.all: faellt er aus, bleibt das Menue ohne Zahlen statt die Ansicht
mit einem Fehler.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Das Loeschsymbol gab es in der Kachelansicht schon, es war aber bis zum
Ueberfahren durchsichtig - ein Knopf, den man nur findet, wenn man ihn kennt.
Jetzt ist er sichtbar, in der Liste ebenso, und die Pipeline-Karte hat ihn
ueberhaupt zum ersten Mal.
Geloescht wird nicht mehr endgueltig: DELETE setzt deleted_at, die Zeile bleibt.
An einer Opportunity haengen Ansprechpartner, Verlauf und Mailzuordnungen, und
ein Fehlklick soll das nicht kosten. Der Papierkorb ist ein Unterpunkt unter
Opportunities und zeigt, wann und von wem geloescht wurde und wie lange der
Eintrag noch liegt. Zurueckholen darf, wer auch loeschen darf; das Leeren jeder
Angemeldete - was dort liegt, hat schon jemand mit den noetigen Rechten
weggeworfen. Beides fragt vorher nach.
Nach 90 Tagen raeumt das Backend selbst auf, beim Abruf der Liste und hoechstens
einmal pro Stunde je Prozess - dasselbe Muster wie in der Akquise. Ein eigener
Zeitplan waere eine weitere Stelle, die ausfallen kann, und die Abfragen filtern
Faelliges ohnehin heraus.
Im Papierkorb liegende Opportunities sind aus allen Listen, Summen und
Zuordnungsdialogen heraus und lassen sich nicht bearbeiten (409) - erst
zurueckholen, dann aendern.
Nebenbei: der Header bekommt seinen Titel jetzt ueber viewTitle(). Der
Ansichtsname musste eindeutig sein ("Opportunity-Papierkorb", weil die Mails
schon einen Papierkorb haben), angezeigt wird beides als "Papierkorb".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drei Dinge, die im Alltag gefehlt haben:
- Projektdaten kennen jetzt einen Projektpreis. Ist er gesetzt, gilt er als
Volumen statt Tagessatz x Laufzeit - Festpreisprojekte mussten bisher
ausgerechnet und in den Tagessatz gefaelscht werden.
- Dialoge schliessen auch per Klick in den Hintergrund, nicht nur ueber das X.
Escape trifft dabei nur den obersten Dialog; eine Textmarkierung, die im
Dialog beginnt und daneben endet, schliesst nichts.
- Fehlt ein Ansprechpartner noch im CRM, oeffnet "Neuer Kontakt..." in der
Opportunity denselben Dialog wie unter Kontakte. Der frische Kontakt haengt
danach sofort als Ansprechpartner dran, ohne die Opportunity zu verlassen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Menüs, Dialoge, Schaltflächen, Platzhalter, Hinweise und die
Fehlermeldungen der API standen in Ersatzschreibweise (oe/ae/ue). Sie
erscheinen als Toast oder Beschriftung direkt in der Oberfläche und
gehören damit richtig geschrieben; sechs sichtbare Texte mit ss statt ß
sind gleich mit erledigt ("Schließen", "Straße", "zu groß").
Unangetastet bleibt bewusst:
- Kommentare im Quelltext - die sind durchgängig ASCII, und der Diff wäre
dreimal so groß ohne Nutzen für den Benutzer
- Protokollzeilen (console.*) - die landen im journal, nicht in der App
- Datenbankwerte und interne Kennungen: 'uebernommen', 'empfaenger',
'vorschlaege', 'laeuft-bereits' stecken in Enums, SQL und
Zustandsvergleichen. Ein Umlaut darin hätte still falsche Ergebnisse
geliefert
- server/src/mail/extract.js - die Muster decken absichtlich beide
Schreibweisen ab: /gr(ü|ue)(ß|ss)e/
- scanner/utils/crypto.js - der Ersatzschlüssel darf sich nicht ändern,
sonst sind gespeicherte Portal-Passwörter nicht mehr lesbar
Geprüft mit einem Rundgang durch die Ansichten und Dialoge: sichtbarer
Text plus title/aria-label/placeholder enthalten keine
Ersatzschreibweisen mehr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der eigenstaendige Projekt Scanner zieht komplett ins CRM ein und laeuft
parallel zur bestehenden App weiter - jede geteilte Ressource ist verdoppelt
statt geteilt, damit die alte Installation unberuehrt bleibt:
- eigene Datenbank perinco_scanner, legt sich beim Start selbst an
- eigener SCANNER_ENCRYPTION_KEY, eigenes Upload-Verzeichnis
- eigener Scheduler mit Zeitversatz, damit nicht beide Instanzen gleichzeitig
mit denselben Portal-Cookies scrapen (Rate-Limits, CAPTCHA, doppelter RAM)
- Sperre gegen ueberlappende Laeufe: Scheduler und "Jetzt scannen" teilen sie,
ein zweiter Aufruf bekommt 409 statt doppelter OpenAI-Kosten
Backend unter /api/scanner hinter der CRM-Anmeldung; die HTTP-Basic-Auth des
Originals entfaellt. CV-PDFs kommen als Stream mit Token statt aus einem
statischen Verzeichnis. Bestandsdaten holt "npm run scanner:import" aus der
alten Datenbank, ohne sie zu veraendern.
Oberflaeche in Tailwind und CI-Farben neu gebaut: Menuepunkt Scanner mit
aufklappbaren Unterpunkten, Einstellungen als eigener Reiter.
Whisper hat keinen eigenen Menuepunkt mehr, sondern ist ein Reiter in den
Einstellungen. Damit diese Geraeteeinstellung jeder Rolle offensteht, sind die
Einstellungen jetzt fuer alle sichtbar - welche Reiter erscheinen, entscheidet
die Rolle.
Ausserdem: kein festes Admin-Passwort mehr im Quelltext, playwright exakt auf
1.60.0 gepinnt (nutzt den vorhandenen Chromium-Build des Servers), technische
Dokumentation mit Funktionszeichnungen unter docs/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>