import React, { useEffect, useState } from 'react'; import { ACQUISITION_VIEWS, CONTACT_VIEWS, MAIL_VIEWS, OPPORTUNITY_VIEWS, SCANNER_VIEWS, type ViewState, } from '../types'; import { UserAvatar } from './ui/UserAvatar'; import { useAuth } from '../lib/auth'; interface SidebarProps { currentView: ViewState; onViewChange: (view: ViewState) => void; isOpen: boolean; onToggle: () => void; /** * Zahlen an den Unterpunkten. Was auf eine Entscheidung wartet oder sich * ansammelt, soll man sehen, ohne die Ansicht zu oeffnen. 0 zeigt nichts an - * eine Null waere nur Rauschen. */ counts?: Partial>; } interface MenuItem { name: ViewState; icon: string; /** Abweichende Beschriftung; ohne Angabe wird der Ansichtsname gezeigt. */ label?: string; } /** * Aufklappbarer Menuepunkt: der Kopf wechselt auf `item.name` und zeigt die * Unterpunkte. `views` sind alle Ansichten der Gruppe - liegt die aktuelle * darunter, bleibt die Gruppe offen und hervorgehoben. */ interface MenuGroup { item: MenuItem; children: MenuItem[]; views: ViewState[]; } const isGroup = (entry: MenuItem | MenuGroup): entry is MenuGroup => 'children' in entry; const CONTACTS_ICON = 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z'; const MAIL_ICON = 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'; /** * Kontakte. Der Menuepunkt klappt den gepflegten Bestand und den Vorbereich * "Aus Mails" darunter auf - dort sammelt der Postfachabgleich Personen, die * noch niemand gesichtet hat. */ const CONTACTS_GROUP: MenuGroup = { item: { name: 'Kontakte', icon: CONTACTS_ICON }, views: CONTACT_VIEWS, children: [ { name: 'Kontakte', label: 'Alle Kontakte', icon: CONTACTS_ICON }, { name: 'Aus Mails', icon: MAIL_ICON }, ], }; const ACQUISITION_ICON = 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01'; const ARCHIVE_ICON = 'M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4'; /** * Akquise. Der Eingang zeigt die Funde der letzten 14 Tage, darunter klappt das * Archiv mit allem Aelteren auf. Beides ist dieselbe Liste - geteilt wird nach * dem Funddatum, nicht durch Verschieben. */ const ACQUISITION_GROUP: MenuGroup = { item: { name: 'Akquise', icon: ACQUISITION_ICON }, views: ACQUISITION_VIEWS, children: [ { name: 'Akquise', label: 'Eingang', icon: ACQUISITION_ICON }, { name: 'Archiv', icon: ARCHIVE_ICON }, ], }; const TRASH_ICON = 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16'; const OPPORTUNITY_ICON = 'M9 17V7m4 10V11m4 6v-3m4 7H5a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2z'; /** * Opportunities mit ihrem Papierkorb. Geloeschte Opportunities liegen dort 90 * Tage, damit ein Fehlklick nicht Verlauf, Ansprechpartner und Mailbezuege * kostet. */ const OPPORTUNITIES_GROUP: MenuGroup = { item: { name: 'Opportunities', icon: OPPORTUNITY_ICON }, views: OPPORTUNITY_VIEWS, children: [ { name: 'Opportunities', label: 'Alle', icon: OPPORTUNITY_ICON }, { name: 'Opportunity-Papierkorb', label: 'Papierkorb', icon: TRASH_ICON }, ], }; /** * Mails. Darunter klappt der Papierkorb auf: Mails, die im CRM geloescht wurden. * Im Postfach bleiben sie stehen - der Abgleich holt sie aber nicht wieder. */ const MAILS_GROUP: MenuGroup = { item: { name: 'Mails', icon: MAIL_ICON }, views: MAIL_VIEWS, children: [ { name: 'Mails', label: 'Postfach', icon: MAIL_ICON }, { name: 'Papierkorb', icon: TRASH_ICON }, ], }; /** * Projekt Scanner. Wie bei den Kontakten klappen die Reiter darunter auf - * "Scanner" selbst ist dabei das Dashboard. Die Einstellungen des Scanners * stehen nicht hier, sondern als Reiter in den CRM-Einstellungen. */ const SCANNER_GROUP: MenuGroup = { item: { name: 'Scanner', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' }, views: SCANNER_VIEWS, children: [ { name: 'Scanner', label: 'Dashboard', icon: 'M9 17V7m4 10V11m4 6v-3m4 7H5a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2z', }, { name: 'Unpassend', icon: 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636', }, { name: 'Suchagenten', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z', }, { name: 'Börsen-Konten', icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9', }, ], }; // Kategorien in Anzeigereihenfolge, einzelne Punkte und Gruppen gemischt. // Einstellungen bleibt separat am unteren Rand. const CATEGORY_ENTRIES: (MenuItem | MenuGroup)[] = [ ACQUISITION_GROUP, OPPORTUNITIES_GROUP, { // Der Terminkalender heisst im CRM "Interviews": genutzt wird er fuer // Gespraechstermine, nicht als allgemeiner Kalender. name: 'Interviews', icon: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z', }, CONTACTS_GROUP, MAILS_GROUP, SCANNER_GROUP, ]; /* * Whisper hat keinen eigenen Menuepunkt mehr - es ist ein Reiter in den * Einstellungen. Deshalb sind die Einstellungen jetzt fuer jede Rolle sichtbar: * jeder pflegt sein Konto und richtet sein eigenes Diktat ein. Welche Reiter * erscheinen, entscheidet die Rolle innerhalb der Einstellungen. */ const SETTINGS_ITEM: MenuItem = { name: 'Einstellungen', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z', }; function useIsMobile() { const [isMobile, setIsMobile] = useState(() => typeof window === 'undefined' ? false : window.innerWidth < 1024 ); useEffect(() => { const onResize = () => setIsMobile(window.innerWidth < 1024); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); return isMobile; } export const Sidebar: React.FC = ({ currentView, onViewChange, isOpen, onToggle, counts = {}, }) => { const { user, logout } = useAuth(); const isMobile = useIsMobile(); const showLabels = isOpen || isMobile; /* * Offene Gruppen, nach dem Namen des Kopfeintrags. Wer per Klick in einer * Gruppenansicht landet, soll die Unterpunkte auch sehen - deshalb oeffnet der * Effekt die Gruppe, zu der die aktuelle Ansicht gehoert. */ const [openGroups, setOpenGroups] = useState(() => CATEGORY_ENTRIES.filter(isGroup) .filter((group) => group.views.includes(currentView)) .map((group) => group.item.name) ); useEffect(() => { const active = CATEGORY_ENTRIES.filter(isGroup).find((group) => group.views.includes(currentView)); if (!active) return; setOpenGroups((open) => (open.includes(active.item.name) ? open : [...open, active.item.name])); }, [currentView]); const renderItem = ( item: MenuItem, options: { indented?: boolean; badge?: number | null } = {} ) => { const isActive = currentView === item.name; return ( ); }; /** * Aufklappbare Gruppe: der Klick oeffnet die Unterpunkte und wechselt gleich * auf die Hauptansicht der Gruppe. Bei eingeklappter Leiste gibt es nur das * Symbol, die Unterpunkte erscheinen erst mit den Beschriftungen. */ const renderGroup = (group: MenuGroup) => { const { item, children, views } = group; const isActive = currentView === item.name; const isInGroup = views.includes(currentView); const isGroupOpen = openGroups.includes(item.name); return (
{showLabels && isGroupOpen && (
{children.map((child) => renderItem(child, { indented: true, badge: counts[child.name] ?? undefined }) )}
)}
); }; return ( <> {isOpen && isMobile && (
)} ); };