import React, { useCallback, useEffect, useState } from 'react';
import { Sidebar } from './components/Sidebar';
import { Header } from './components/Header';
import { LoginScreen } from './components/LoginScreen';
import { Leads } from './components/leads/Leads';
import { MailPoolLeads } from './components/leads/MailPoolLeads';
import { Acquisitions } from './components/acquisition/Acquisitions';
import { Opportunities } from './components/opportunities/Opportunities';
import { OpportunityTrash } from './components/opportunities/OpportunityTrash';
import { Calendar } from './components/calendar/Calendar';
import { parseSqlDateTime } from './components/calendar/calendarUtils';
import { Settings } from './components/settings/Settings';
import { Mails } from './components/mail/Mails';
import { ScannerDashboard } from './components/scanner/ScannerDashboard';
import { ScannerRejected } from './components/scanner/ScannerRejected';
import { ScannerAgents } from './components/scanner/ScannerAgents';
import { ScannerPortals } from './components/scanner/ScannerPortals';
import { useToast } from './components/Toast';
import { NavigationProvider } from './lib/navigation';
import { useAuth } from './lib/auth';
import { connectionsApi, eventsApi, leadsApi, opportunitiesApi, usersApi } from './lib/api';
import {
OPEN_STAGES,
formatEuro,
opportunityVolume,
viewTitle,
type AppUser,
type CalendarEvent,
type Lead,
type LeadConnection,
type Opportunity,
type SettingsTab,
type ViewState,
} from './types';
const Splash: React.FC = () => (
Lade perinco CRM...
);
/** Erster Reiter im Menue - hier startet die App. */
const DEFAULT_VIEW: ViewState = 'Opportunities';
const App: React.FC = () => {
const { user, isLoading } = useAuth();
const { addToast } = useToast();
const [currentView, setCurrentView] = useState(DEFAULT_VIEW);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
/**
* Gewuenschter Reiter der Einstellungen. Der Diktat-Hinweis springt so direkt
* nach Whisper, das keinen eigenen Menuepunkt mehr hat.
*/
const [settingsTab, setSettingsTab] = useState(undefined);
/**
* Kontakt, der in der Kontaktansicht sofort geoeffnet werden soll. Wird beim
* Sprung aus der Mailansicht gesetzt und von dort wieder zurueckgesetzt.
*/
const [focusLeadId, setFocusLeadId] = useState(null);
/**
* Alle Kontakte inklusive Vorbereich - getrennt wird direkt darunter. Der
* Server liefert absichtlich beide Bestaende in einem Aufruf: die Ansicht
* "Aus Mails" braucht ohnehin beide, um Dubletten zu erkennen.
*/
const [allLeads, setAllLeads] = useState([]);
const [connections, setConnections] = useState([]);
const [users, setUsers] = useState([]);
const [events, setEvents] = useState([]);
const [opportunities, setOpportunities] = useState([]);
const [isDataLoading, setIsDataLoading] = useState(false);
/**
* Ansichtswechsel fuer Menue und tief liegende Komponenten. `tab` waehlt
* gleich den passenden Reiter der Einstellungen; ohne Angabe entscheidet die
* Einstellungsseite selbst (Rolle admin: Nutzerverwaltung, sonst Mein Konto).
*/
const goTo = useCallback((view: ViewState, options?: { tab?: SettingsTab; leadId?: number }) => {
setCurrentView(view);
setSettingsTab(options?.tab);
setFocusLeadId(options?.leadId ?? null);
}, []);
const loadData = useCallback(
async (silent = false) => {
if (!user) return;
if (!silent) setIsDataLoading(true);
try {
// Die Benutzerliste braucht jeder: Netzwerkdiagramm, Zustaendigkeit,
// @-Mentions, Teilnehmer und Farben der Termine. /users/directory ist
// deshalb - anders als /users - fuer alle Rollen freigegeben.
const [leadRows, connectionRows, eventRows, userRows, opportunityRows] = await Promise.all([
leadsApi.list(),
connectionsApi.list(),
eventsApi.list(),
usersApi.directory(),
opportunitiesApi.list(),
]);
setAllLeads(leadRows);
setConnections(connectionRows);
setEvents(eventRows);
setUsers(userRows);
setOpportunities(opportunityRows);
} catch (err) {
addToast(err instanceof Error ? err.message : 'Daten konnten nicht geladen werden.', 'error');
} finally {
setIsDataLoading(false);
}
},
[user, addToast]
);
useEffect(() => {
if (user) loadData();
}, [user, loadData]);
if (isLoading) return ;
if (!user) return ;
/*
* Kontakte und Vorbereich trennen. Ueberall im CRM - Opportunities, Interviews,
* Netzwerk, Mailzuordnung - gilt nur der gepflegte Bestand; die Funde aus dem
* Postfach sind Rohmaterial und sollen in keiner Auswahlliste auftauchen.
*/
const leads = allLeads.filter((lead) => !lead.mail_pool);
const mailPoolLeads = allLeads.filter((lead) => lead.mail_pool);
const upcomingCount = events.filter(
(event) => parseSqlDateTime(event.ends_at).getTime() >= Date.now()
).length;
const openOpportunities = opportunities.filter((item) => OPEN_STAGES.includes(item.stage));
const openVolume = openOpportunities.reduce(
(sum, item) => sum + (opportunityVolume(item) ?? 0),
0
);
// Untertitel der Kopfzeile. Die Scanner-Reiter bringen ihre Kennzahlen selbst
// mit, deshalb steht hier nur, worum es auf der Seite geht.
const STATIC_SUBTITLES: Partial> = {
Akquise: 'Gefundene Projekte - eintragen, prüfen, zur Opportunity machen',
'Opportunity-Papierkorb': 'Gelöschte Opportunities - nach 90 Tagen endgültig entfernt',
Archiv: 'Akquise-Einträge älter als 14 Tage - nach 90 Tagen werden sie gelöscht',
Mails: 'Postfach crm@perinco.de - Mails zuordnen und Kontakte übernehmen',
Papierkorb: 'Im CRM gelöschte Mails - im Postfach crm@perinco.de bleiben sie erhalten',
Scanner: 'Projekt Scanner - Suchagenten, Portale und Projektübereinstimmungen',
Unpassend: 'Projekt Scanner - als „passt nicht" markierte Treffer',
Suchagenten: 'Projekt Scanner - Beraterprofile mit CV und Suchbegriffen',
'Börsen-Konten': 'Projekt Scanner - Verbindungen zu den Jobbörsen',
Einstellungen: 'Konto, Diktat, Benutzer, Postfach und Scanner',
};
const subtitle =
currentView === 'Opportunities'
? `${opportunities.length} ${
opportunities.length === 1 ? 'Opportunity' : 'Opportunities'
} · ${openOpportunities.length} offen · ${formatEuro(openVolume)} Pipeline`
: currentView === 'Kontakte'
? `${leads.length} ${leads.length === 1 ? 'Kontakt' : 'Kontakte'} · ${connections.length} ${
connections.length === 1 ? 'Verbindung' : 'Verbindungen'
}`
: currentView === 'Aus Mails'
? `${mailPoolLeads.length} ${
mailPoolLeads.length === 1 ? 'Fund' : 'Funde'
} aus dem Postfach · noch nicht in den Kontakten`
: currentView === 'Interviews'
? `Gemeinsame Termine im Team · ${events.length} ${
events.length === 1 ? 'Termin' : 'Termine'
} · ${upcomingCount} anstehend`
: STATIC_SUBTITLES[currentView];
return (
setIsSidebarOpen((open) => !open)}
mailPoolCount={mailPoolLeads.length}
/>
);
};
export default App;