Quellcode des ICON CRM (4)
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
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 { 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 { 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 === '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 === '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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user