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 = () => (
perinco

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} />
setIsSidebarOpen((open) => !open)} />
{currentView === 'Akquise' && loadData(true)} />} {currentView === 'Archiv' && ( loadData(true)} /> )} {currentView === 'Opportunity-Papierkorb' && ( loadData(true)} /> )} {currentView === 'Opportunities' && ( loadData(true)} /> )} {currentView === 'Interviews' && ( loadData(true)} /> )} {currentView === 'Kontakte' && ( loadData(true)} focusLeadId={focusLeadId} onFocusHandled={() => setFocusLeadId(null)} /> )} {currentView === 'Aus Mails' && ( loadData(true)} onOpenLead={(leadId) => goTo('Kontakte', { leadId })} /> )} {currentView === 'Mails' && ( loadData(true)} /> )} {currentView === 'Papierkorb' && ( loadData(true)} mode="papierkorb" /> )} {currentView === 'Scanner' && } {currentView === 'Unpassend' && } {currentView === 'Suchagenten' && } {currentView === 'Börsen-Konten' && } {currentView === 'Einstellungen' && }
); }; export default App;