Files
ICON-CRM/src/App.tsx
T
schakal00783andClaude Opus 5.5 6a452f1510 Suspects: Vorstufe der Leads als Tabelle
Neuer Menüpunkt über Leads mit Unterpunkt Archiv.

- Tabelle Name, Funktion, Unternehmen, LinkedIn, Website, Notiz,
  Letzter Kontakt, Nächster Impuls; leere Zeile unten, speichert beim
  Verlassen. Links als Symbol mit URL im Tooltip.
- Nächster Impuls: Tage eingeben, Erinnerung über den ReminderWatcher
- Archivieren, Zurückholen, endgültig löschen (nur aus dem Archiv)
- Lead erstellen öffnet die Lead-Karte vorbelegt (LeadModal.initial),
  danach wandert der Suspect mit lead_id ins Archiv

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:11:36 +02:00

205 lines
7.5 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Company, Contact, DirectoryUser, View } from './types';
import { companiesApi, contactsApi, dashboardApi, usersApi } from './lib/api';
import { useAuth } from './lib/auth';
import { NavigationProvider, type OpenTarget, type SettingsTab } from './lib/navigation';
import { LoginScreen } from './components/LoginScreen';
import { Sidebar } from './components/Sidebar';
import { Header } from './components/Header';
import { Dashboard } from './components/dashboard/Dashboard';
import { Contacts } from './components/contacts/Contacts';
import { Companies } from './components/companies/Companies';
import { Leads } from './components/leads/Leads';
import { Suspects } from './components/suspects/Suspects';
import { ReminderWatcher } from './components/leads/ReminderWatcher';
import { Tenders } from './components/tenders/Tenders';
import { LiveBoard } from './components/tenders/LiveBoard';
import { Projects } from './components/projects/Projects';
import { History } from './components/history/History';
import { ScannerDashboard } from './components/scanner/ScannerDashboard';
import { ScannerAgents } from './components/scanner/ScannerAgents';
import { Settings } from './components/settings/Settings';
/*
* Rahmen der Anwendung: Anmeldung, Seitenleiste, Kopfzeile und der Wechsel
* zwischen den Ansichten.
*
* Zwei Dinge liegen bewusst hier oben:
* * Die Listen der Unternehmen und Benutzer werden einmal geladen und
* weitergereicht - fast jedes Formular braucht sie als Auswahl.
* * `open(type, id)` wechselt die Ansicht und reicht die Kennung dorthin
* durch. Damit kann jede @-Erwaehnung und jeder Suchtreffer die passende
* Karte oeffnen, ohne dass die Ebenen dazwischen davon wissen.
*/
export const App: React.FC = () => {
const { user, isLoading } = useAuth();
const [view, setView] = useState<View>('Dashboard');
const [collapsed, setCollapsed] = useState(false);
const [companies, setCompanies] = useState<Company[]>([]);
const [contacts, setContacts] = useState<Contact[]>([]);
const [users, setUsers] = useState<DirectoryUser[]>([]);
const [counts, setCounts] = useState<Record<string, number>>({});
const [pending, setPending] = useState<{ type: OpenTarget; id: number } | null>(null);
/** Reiter, mit dem die Einstellungen oeffnen sollen (z. B. aus dem Diktat). */
const [settingsTab, setSettingsTab] = useState<SettingsTab | null>(null);
const loadShared = useCallback(async () => {
if (!user) return;
try {
const [{ companies: rows }, { contacts: people }, { users: directory }, dashboard] =
await Promise.all([
companiesApi.list(),
contactsApi.list(),
usersApi.directory(),
dashboardApi.load(),
]);
setCompanies(rows);
setContacts(people);
setUsers(directory);
setCounts(dashboard.counts);
} catch {
/* Ein Fehler hier darf die Anwendung nicht blockieren - die einzelnen
Ansichten melden ihre eigenen Ladefehler. */
}
}, [user]);
useEffect(() => {
void loadShared();
}, [loadShared]);
const navigation = useMemo(
() => ({
setView,
open: (type: OpenTarget, id: number) => {
const target: Record<OpenTarget, View | null> = {
contact: 'Kontakte',
company: 'Unternehmen',
tender: 'Ausschreibungen',
project: 'Laufende Projekte',
lead: 'Leads',
user: 'Einstellungen',
};
const next = target[type];
if (!next) return;
setView(next);
setPending({ type, id });
},
goToSettings: (tab: SettingsTab) => {
setSettingsTab(tab);
setView('Einstellungen');
},
}),
[]
);
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-canvas">
<p className="text-sm text-ink-400">Wird geladen …</p>
</div>
);
}
if (!user) return <LoginScreen />;
/** Kennung nur an die Ansicht geben, die gerade sichtbar ist. */
const openIdFor = (type: OpenTarget) => (pending?.type === type ? pending.id : null);
const clearPending = () => setPending(null);
return (
<NavigationProvider value={navigation}>
<div className="flex h-screen overflow-hidden bg-canvas">
<Sidebar view={view} onSelect={setView} counts={counts} collapsed={collapsed} />
<div className="flex min-w-0 flex-1 flex-col">
<Header view={view} onToggleSidebar={() => setCollapsed((value) => !value)} />
<main className="flex-1 overflow-y-auto p-4 scrollbar-slim sm:p-6">
{view === 'Dashboard' && <Dashboard />}
{view === 'Kontakte' && (
<Contacts
companies={companies}
users={users}
onChanged={loadShared}
openId={openIdFor('contact')}
onOpenHandled={clearPending}
/>
)}
{view === 'Unternehmen' && (
<Companies
users={users}
onChanged={loadShared}
openId={openIdFor('company')}
onOpenHandled={clearPending}
/>
)}
{(view === 'Suspects' || view === 'Suspects · Archiv') && (
<Suspects
key={view}
archived={view === 'Suspects · Archiv'}
companies={companies}
contacts={contacts}
users={users}
onChanged={loadShared}
/>
)}
{(view === 'Leads' || view === 'Leads · Archiviert') && (
<Leads
key={view}
companies={companies}
users={users}
onChanged={loadShared}
openId={openIdFor('lead')}
onOpenHandled={clearPending}
archivedView={view === 'Leads · Archiviert'}
/>
)}
{(view === 'Ausschreibungen' || view === 'Ausschreibungen · Archiv') && (
<Tenders
key={view}
companies={companies}
contacts={contacts}
onChanged={loadShared}
openId={openIdFor('tender')}
onOpenHandled={clearPending}
archived={view === 'Ausschreibungen · Archiv'}
/>
)}
{view === 'Live!' && <LiveBoard onChanged={loadShared} />}
{view === 'Laufende Projekte' && (
<Projects
companies={companies}
onChanged={loadShared}
openId={openIdFor('project')}
onOpenHandled={clearPending}
/>
)}
{view === 'Scanner' && <ScannerDashboard />}
{view === 'Scanner · Suchagenten' && <ScannerAgents />}
{view === 'Historie' && <History users={users} />}
{view === 'Einstellungen' && (
<Settings initialTab={settingsTab} onTabConsumed={() => setSettingsTab(null)} />
)}
</main>
{/* Faellige und liegengebliebene Leads melden sich von selbst -
unabhaengig davon, welche Ansicht gerade offen ist. */}
<ReminderWatcher onChanged={loadShared} />
</div>
</div>
</NavigationProvider>
);
};