Files
ICON-CRM/docs/06-schnittstellen.md

139 lines
6.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 06 Schnittstellen
Zwei fremde Systeme hängen am CRM: die **ICON Website** liest die Projektbörse,
und das **perinco CRM** tauscht Kontakte in beide Richtungen aus.
## ICON Website
Seit dem 14.09.2026 liegt die Website auf demselben Server wie das CRM
(`/var/www/icon_website/dist`, Vhost `icon-experts.de` + `www`). Vorher lief sie
auf `212.227.135.41` – derselben Maschine wie perinco CRM, n8n, Docker und
Supabase.
```mermaid
flowchart LR
V(("Besucher")) --> W["Apache<br/>icon-experts.de"]
W -->|"/"| S["Website (dist)"]
W -->|"/api/public<br/>ProxyPass"| E["Express :5007"]
E --> DB[("icon_CRM")]
```
Der Vhost reicht **`/api/public` an das CRM-Backend durch**. Das ist kein
Schönheitsfehler, sondern der Grund, warum die Content-Security-Policy der Seite
weiter mit `connect-src 'self'` auskommt: der Aufruf ist gleicher Herkunft.
Zeigte die Seite direkt auf `icrm.icon-experts.de`, müsste dieser Host in der CSP
stehen und der Browser bräuchte einen CORS-Vorabruf.
### Der öffentliche Endpunkt
`GET /api/public/tenders` – ohne Anmeldung, nur Lesen, neueste zuerst. Dazu
`GET /api/public/tenders/:id` für einen direkten Link. Beide antworten mit
`Access-Control-Allow-Origin: *` und `Cache-Control: public, max-age=60`.
```json
[{ "id": 8, "title": "…", "status": "Aktiv", "notes": "<h2>…</h2>",
"created_at": "…", "updated_at": "…" }]
```
**Nicht** ausgeliefert werden Kunde, zugewiesene Experten, Quelle, Fristen und
Phase. Das ist der eigentliche Gewinn gegenüber dem vorherigen Weg: die Website
las mit dem Supabase-Anon-Key direkt in der Tabelle und durfte `select('*')` –
der Schlüssel lag dafür im Browser-Bundle.
> **Zwei Bedingungen, nicht eine:** `online = 1` **und** `status <> 'Archiviert'`.
> Im Altbestand steht eine archivierte Ausschreibung, deren Schalter noch an ist –
> über Supabase war sie unsichtbar, über diesen Endpunkt wäre sie mit der
> Kategorie „Archiviert" auf der Website aufgetaucht.
Auf Seiten der Website hängt das an `src/lib/iconCrm.js`; `src/pages/Experts.jsx`
ruft nur noch `fetchTenders()` auf. `@supabase/supabase-js` wird dort seither
nirgends mehr benutzt.
**Der Schalter `online` in der Liste der Ausschreibungen entscheidet also
unmittelbar, was auf der Website steht** – einen Klick entfernt, ohne Umweg.
## perinco CRM, in beide Richtungen
*Kontakte → Häkchen „perinco".* Die Kontakte des perinco CRM werden zusätzlich
angezeigt – **grün umrandet**, rein lesend, und **nichts davon wird
gespeichert**. Ein nächtlicher Abgleich in die eigene Datenbank wäre der falsche
Weg: dann stünde derselbe Mensch in zwei Systemen, und irgendwann wäre zu
klären, welcher Eintrag gilt.
Dasselbe gibt es spiegelbildlich drüben: im perinco CRM erscheinen die
ICON-Kontakte hinter dem Häkchen „ICON", **gold** umrandet.
```mermaid
flowchart LR
I["ICON CRM<br/>icrm.icon-experts.de"] -->|"GET /api/integrations/contacts<br/>Bearer-Token"| P["perinco CRM<br/>pcrm.perinco.de"]
P -->|"GET /api/integrations/contacts<br/>Bearer-Token"| I
```
| Richtung | Wer fragt | Wer antwortet | Token |
| --- | --- | --- | --- |
| ICON liest perinco | `server/src/routes/perinco.js` | drüben `server/src/routes/integrations.js` | hier `PERINCO_API_TOKEN` (+ `PERINCO_API_URL`), drüben `ICON_API_TOKEN` |
| perinco liest ICON | drüben `server/src/routes/iconCrm.js` | hier `server/src/routes/integrations.js` | hier `PERINCO_INBOUND_TOKEN`, drüben der gleiche Wert |
Mitgeschickt wird der Token als `Authorization: Bearer …`.
Beide Router arbeiten mit **festem Token statt JWT** – der Aufrufer ist ein
Backend, kein angemeldeter Mensch – und verglichen wird mit
`crypto.timingSafeEqual`. Beide kennen ausschließlich `GET`; einen Schreibweg
gibt es nicht, auch später nicht.
Fehlt einer der Werte, bleibt das Häkchen unsichtbar und beide Systeme laufen
normal weiter. Ein Knopf, der nur eine Fehlermeldung erzeugt, ist keiner.
### Wer weitergegeben wird
**Ein Häkchen am einzelnen Datensatz entscheidet.** Im perinco CRM trägt jeder
Kontakt das Feld *„Im ICON CRM anzeigen"* (`crm_leads.icon`); nur wer es gesetzt
hat, taucht hier auf. Beim Anlegen der Spalte wurden alle vorhandenen Kontakte
auf gesetzt gestellt – sie waren bis dahin sichtbar, und ein neues Feld darf
nichts verschwinden lassen. **Neue Kontakte starten ungesetzt.**
Kontakte im Vorbereich „Aus Mails" (`mail_pool = 1`) bleiben außen vor:
unvollständig und teils Dubletten.
### Feldabbildung
Abgebildet wird auf die **vorhandenen** Felder; neue wurden ausdrücklich keine
angelegt:
| perinco `crm_leads` | ICON |
| --- | --- |
| `first_name`, `last_name`, `position` | gleichnamig |
| `company` | wird als Unternehmensname angezeigt |
| `email`, `phone` bzw. `mobile` | `email`, `phone` |
| `linkedin`, `avatar`, `tags` | gleichnamig |
| `kind` (Recruiter/Partner/Endkunde) | `kind` – **unverändert**, nicht auf die ICON-Arten umgebogen |
| `source` | `lead_source` |
| `notes` | `bio` |
Bleiben drüben, weil ICON bei Kontakten kein Gegenstück hat: Anrede, Website,
Anschrift. Ebenso Zuständiger und Ersteller – das sind Benutzer *jenes* Systems,
deren Kennungen hier nichts bedeuten.
Die Kennung bekommt ein Präfix (`perinco-31`), damit sie nie mit einer
ICON-Kennung verwechselt wird. Sie ist eine Zeichenkette und gehört nicht in die
Verweise der erweiterten Suche.
### Wo sie auftauchen
* **Kontaktliste und Kacheln** – grün umrandet, mit Herkunftsmerkmal
* **Netzwerkansicht** – grün umrandete Kreise, über eine fein gestrichelte grüne
Kante an ihr Unternehmen gehängt. Die Verbindung läuft über den
**Firmennamen**, denn drüben steht dort nur Text und keine Kennung; groß/klein
wird ignoriert, damit „KfW" und „KFW" zusammenfinden. Passt kein Name, steht
der Kontakt für sich – das ist ehrlicher als eine geratene Zuordnung.
* **Erweiterte Suche** – sie zählen mit, werden aber klar als perinco-Kontakte
benannt.
Verbinden lassen sie sich nicht, und ihre Seitenleiste bietet keine Karte an –
gepflegt werden sie drüben.
> **Erfahrung aus dem Betrieb:** Ein zusätzlicher Filter in der Kontaktliste
> kann die Liste still leeren und sieht dann aus wie Datenverlust. Deshalb sind
> aktive Filter sichtbar ausgezeichnet, der Leerzustand benennt sie, und daneben
> steht *Filter zurücksetzen*.