feat(kontakte): Zustaendigkeit, LinkedIn-Foto, Netzwerkauswahl
Drei Verbesserungen an den Kontakten: * Beim Anlegen ist der angemeldete Benutzer voreingestellt zustaendig. Bisher stand dort der erste Benutzer der Liste - fast immer falsch. * Wird eine LinkedIn-Adresse hinterlegt, holt das CRM das oeffentliche Profilfoto und setzt es als Kontaktfoto: beim Verlassen des Feldes einmal je Adresse, solange kein Foto gesetzt ist, und auf Klick ueber den Knopf daneben. Der Abruf laeuft ueber das Backend (server/src/linkedinPhoto.js, POST /api/leads/linkedin-photo), weil ihn die Same-Origin-Policy im Browser verhindert. Gelesen wird die Open-Graph-Vorschau der Profilseite; Herkunft, Groesse und Antwortzeit des Bildes werden geprueft, Platzhalter-Silhouetten abgewiesen. Schlaegt der Abruf fehl, steht der Grund unter dem Feld. * In der Netzwerkansicht hebt ein Klick auf freie Flaeche die Auswahl auf - das Diagramm sieht danach aus wie beim Wechsel in die Ansicht. Ein Ziehen des Hintergrunds (ab 4 px) laesst die Auswahl stehen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3c9d83161e
commit
0a2cf52e8a
+34
-1
@@ -123,6 +123,36 @@ unbekannter Name kommt so nie in die Datenbank.
|
||||
Bewusst getrennt vom Setting-Modell des Scanners (andere Datenbank) und bewusst
|
||||
**ohne Geheimnisse**: Zugangsdaten stehen in `server/.env`.
|
||||
|
||||
### `server/src/linkedinPhoto.js`
|
||||
|
||||
Holt das Profilfoto zu einer LinkedIn-Adresse. Bedient wird damit
|
||||
`POST /api/leads/linkedin-photo`.
|
||||
|
||||
| Export | Zweck |
|
||||
| --- | --- |
|
||||
| `normalizeProfileUrl(eingabe)` | macht aus `linkedin.com/in/name` eine `https`-URL, wirft bei fremden Hosts |
|
||||
| `fetchLinkedInPhoto(eingabe)` | Profilseite lesen, `og:image` ziehen, Bild laden, als Data-URL zurückgeben |
|
||||
| `PhotoError` | Fehler mit `status`; die Route reicht Status und Text unverändert durch |
|
||||
|
||||
Der Weg in Stichpunkten und die Gründe dafür:
|
||||
|
||||
* **Über das Backend, nicht aus dem Browser.** Ein `fetch` auf linkedin.com aus
|
||||
der Seite heraus scheitert an der Same-Origin-Policy.
|
||||
* **Browser-Kennung im Kopf.** Ohne `User-Agent` antwortet LinkedIn oft gar
|
||||
nicht erst.
|
||||
* **Nur `og:image`/`twitter:image`**, ersatzweise eine `media.licdn.com`-Adresse
|
||||
aus dem Quelltext. Es wird nichts angemeldet und nichts Geschütztes gelesen.
|
||||
* **Herkunft wird geprüft.** Das Bild muss über `https` von `licdn.com` kommen
|
||||
und `/dms/image/` im Pfad haben - sonst könnte eine manipulierte Seite eine
|
||||
beliebige Adresse unterschieben.
|
||||
* **Platzhalter werden abgewiesen.** Profile ohne eigenes Foto liefern eine
|
||||
graue Silhouette (`ghost` im Pfad); die soll kein Kontaktfoto werden.
|
||||
* **Grenzen:** 12 s Zeitlimit je Anfrage, höchstens 5 MB Bild.
|
||||
|
||||
Gibt LinkedIn die Seite nicht heraus (Status 999 oder 403 bei zu vielen
|
||||
Anfragen von einer Adresse), kommt eine Meldung im Klartext zurück - das ist
|
||||
der Normalfall, mit dem die Oberfläche rechnet, kein Serverfehler.
|
||||
|
||||
### `server/src/mail/`
|
||||
|
||||
Postfachanbindung, fachlich in [11 Postfachanbindung](11-postfach.md).
|
||||
@@ -212,6 +242,7 @@ Zusagen, auf die sich der Rest verlässt:
|
||||
| `ApiError` | Fehler mit `status` |
|
||||
| `authApi`, `usersApi`, `leadsApi`, `opportunitiesApi`, `eventsApi`, `connectionsApi` | typisierte Endpunkte |
|
||||
| `leadsApi.mailPoolSources` / `.promote` / `.promoteMany` | Vorbereich „Aus Mails": Herkunft lesen, einzeln bzw. gesammelt übernehmen |
|
||||
| `leadsApi.linkedinPhoto(url)` | Profilfoto zu einer LinkedIn-Adresse als Data-URL; speichert nichts, das erledigt das Formular beim Sichern |
|
||||
|
||||
Zwei Feinheiten: Der `Content-Type` wird **nicht** gesetzt, wenn der Body
|
||||
`FormData` ist. Und ein Netzwerkfehler wird zu `ApiError(status 0)` mit
|
||||
@@ -278,6 +309,8 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `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>`. Dazu `ConsultantSelect` (mehrere Berater: Auswahlfeld hängt an, Plaketten nehmen weg) und `ConsultantList` für die Anzeige in der Liste |
|
||||
| `components/settings/AcquisitionSettings.tsx` | Reiter *Einstellungen → Akquise*: die vier Auswahllisten pflegen |
|
||||
| `components/leads/leadUi.tsx` | `inputClass`, `labelClass` (Beschriftung als eigene Zeile), `labelTextClass` + `labelRowClass` (Zeile mit Knopf rechts, z. B. Diktat - `min-h-7` haelt sie so hoch wie den Knopf, damit Felder in Nachbarspalten auf einer Linie beginnen), `KindBadge`, `Avatar`, Farben der Kontaktart |
|
||||
| `components/leads/LeadModal.tsx` | Kontaktformular. Beim Neuanlegen ist der angemeldete Benutzer *Zuständig* (`useAuth()`, Rückfallebene `users[0]`) - beim Bearbeiten bleibt die gespeicherte Zuständigkeit stehen. Neben dem LinkedIn-Feld holt *Foto* das Profilbild; beim Verlassen des Feldes geschieht das einmal je Adresse von selbst, aber nur solange noch kein Bild gesetzt ist (`triedProfiles`) - ein hochgeladenes Foto wird nie überschrieben. Fehler des automatischen Versuchs stehen als Hinweis unter dem Feld, nur der Klick auf *Foto* meldet sich als Toast |
|
||||
| `components/leads/LeadNetwork.tsx` | Netzwerkdiagramm (d3-force). Ein Klick auf leere Fläche hebt die Auswahl auf, das Diagramm ist danach wieder so hell wie beim Wechsel in die Ansicht; ein Ziehen des Hintergrunds (mehr als 4 px) verschiebt nur und lässt die Auswahl stehen. Knoten stoppen das `pointerdown`, ihre Klicks erreichen den Hintergrund also nicht |
|
||||
| `components/leads/mailPool.ts` | `findMailPoolMatch()`, `diffFields()`, `isConflict()`, `MAIL_POOL_FIELDS` - Dublettenabgleich des Vorbereichs |
|
||||
| `components/leads/MailPoolLeads.tsx` | Ansicht „Aus Mails": Goldmarkierung, Abweichungen, Übernehmen, Sammelübernahme |
|
||||
| `components/leads/MailPoolMergeDialog.tsx` | Feldauswahl beim Ergänzen eines vorhandenen Kontakts |
|
||||
@@ -288,7 +321,7 @@ Warum genau diese Konstanten festgelegt sind, steht in
|
||||
| `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 |
|
||||
| `lib/imageCompress.ts` | `compressImage(datei)` auf 256 px, WebP → JPEG → PNG; `compressImageSource(dataUrl)` geht denselben Weg für ein Bild, das nicht aus einer Datei kommt (LinkedIn-Foto) |
|
||||
|
||||
### `src/types.ts`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user