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>
205 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
};
|