feat: globale Suche und Anbindung an das ICON CRM

Beides lag bisher nur als nicht eingecheckte Arbeit im Arbeitsverzeichnis,
waehrend auf Gitea nur die Doku dazu stand (docs/12-suche.md). Damit ist
der Code jetzt versioniert.

Globale Suche:
- server/src/routes/search.js: Volltext ueber Kontakte, Opportunities und
  Akquise, dazu die erweiterte Suche mit dem Modell des Scanners
- src/components/GlobalSearch.tsx im Kopf der Anwendung; ein Treffer
  springt in die Ansicht und oeffnet den Datensatz (navigation.tsx)

ICON CRM:
- server/src/routes/integrations.js liefert die Kontakte mit gesetztem
  Haekchen hinaus, server/src/routes/iconCrm.js holt die Kontakte von
  drueben herein - beide nur lesend, mit festem Token statt JWT
- server/src/contactBridge.js und src/lib/contactMatch.ts erkennen
  Dubletten, src/components/leads/IconMergeDialog.tsx laesst sie
  zusammenfuehren
- server/src/ai.js buendelt die OpenAI-Aufrufe

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-17 23:07:59 +02:00
co-authored by Claude Opus 5
parent ad7f806258
commit 49c03f5e1b
22 changed files with 2339 additions and 23 deletions
+21 -1
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { OpportunityGrid } from './OpportunityGrid';
import {
OpportunityList,
@@ -33,6 +33,9 @@ interface OpportunitiesProps {
users: AppUser[];
isLoading: boolean;
reload: () => void;
/** Von aussen gewuenschte Opportunity (globale Suche) - wird geoeffnet. */
focusOpportunityId?: number | null;
onFocusHandled?: () => void;
}
const VIEW_BUTTONS: { mode: OpportunityViewMode; label: string; icon: string }[] = [
@@ -99,6 +102,8 @@ export const Opportunities: React.FC<OpportunitiesProps> = ({
users,
isLoading,
reload,
focusOpportunityId,
onFocusHandled,
}) => {
const { addToast } = useToast();
// Kanban zuerst: die Pipeline ist die Arbeitsansicht, Kachel und Liste sind
@@ -117,6 +122,21 @@ export const Opportunities: React.FC<OpportunitiesProps> = ({
// Fuer welche Opportunity gerade Kopien fuer andere Berater angelegt werden.
const [toCopy, setToCopy] = useState<Opportunity | null>(null);
/*
* Ein Treffer der globalen Suche soll die Karte zeigen, nicht nur die Liste.
* Der Wunsch wird genau einmal erfuellt und danach zurueckgemeldet - sonst
* spraenge der Dialog beim naechsten Schliessen wieder auf.
*/
useEffect(() => {
if (!focusOpportunityId) return;
const wanted = opportunities.find((item) => item.id === focusOpportunityId);
if (wanted) {
setEditing(wanted);
setIsModalOpen(true);
}
onFocusHandled?.();
}, [focusOpportunityId, opportunities, onFocusHandled]);
const filtered = useMemo(() => {
const terms = searchTerm
.toLowerCase()