diff --git a/packages/frontend/src/crm/settings/CrmSettingsContext.tsx b/packages/frontend/src/crm/settings/CrmSettingsContext.tsx new file mode 100644 index 0000000..abb43c2 --- /dev/null +++ b/packages/frontend/src/crm/settings/CrmSettingsContext.tsx @@ -0,0 +1,146 @@ +import { + createContext, + useContext, + useState, + useCallback, + type ReactNode, +} from 'react'; +import { Navigate } from 'react-router-dom'; + +// ============================================================ +// Types +// ============================================================ + +export type CrmModuleKey = 'contacts' | 'companies' | 'deals' | 'pipelines'; + +export interface CrmModuleConfig { + enabled: boolean; + label?: string; // Für spätere Umbenennung +} + +export interface CrmSettings { + modules: Record; +} + +interface CrmSettingsContextValue { + settings: CrmSettings; + updateSettings: (next: CrmSettings) => void; + toggleModule: (key: CrmModuleKey, enabled: boolean) => void; + isModuleEnabled: (key: CrmModuleKey) => boolean; +} + +// ============================================================ +// Defaults & localStorage +// ============================================================ + +const STORAGE_KEY = 'crm-settings'; + +const DEFAULT_SETTINGS: CrmSettings = { + modules: { + contacts: { enabled: true }, + companies: { enabled: true }, + deals: { enabled: true }, + pipelines: { enabled: true }, + }, +}; + +function loadSettings(): CrmSettings { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return DEFAULT_SETTINGS; + const parsed = JSON.parse(raw) as Partial; + // Merge mit Defaults (falls neue Module hinzukommen) + return { + modules: { + ...DEFAULT_SETTINGS.modules, + ...(parsed.modules ?? {}), + }, + }; + } catch { + return DEFAULT_SETTINGS; + } +} + +function saveSettings(settings: CrmSettings): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } catch { + // localStorage voll oder nicht verfügbar — ignorieren + } +} + +// ============================================================ +// Context + Provider +// ============================================================ + +const CrmSettingsCtx = createContext(null); + +export function CrmSettingsProvider({ children }: { children: ReactNode }) { + const [settings, setSettings] = useState(loadSettings); + + const updateSettings = useCallback((next: CrmSettings) => { + setSettings(next); + saveSettings(next); + }, []); + + const toggleModule = useCallback( + (key: CrmModuleKey, enabled: boolean) => { + setSettings((prev) => { + const next: CrmSettings = { + ...prev, + modules: { + ...prev.modules, + [key]: { ...prev.modules[key], enabled }, + }, + }; + saveSettings(next); + return next; + }); + }, + [], + ); + + const isModuleEnabled = useCallback( + (key: CrmModuleKey) => settings.modules[key]?.enabled ?? true, + [settings], + ); + + return ( + + {children} + + ); +} + +// ============================================================ +// Hook +// ============================================================ + +export function useCrmSettings(): CrmSettingsContextValue { + const ctx = useContext(CrmSettingsCtx); + if (!ctx) { + throw new Error('useCrmSettings must be used within CrmSettingsProvider'); + } + return ctx; +} + +// ============================================================ +// Route Guard +// ============================================================ + +/** + * Wraps a CRM page — redirects to "/" if the module is disabled. + */ +export function CrmModuleGuard({ + module: mod, + children, +}: { + module: CrmModuleKey; + children: ReactNode; +}) { + const { isModuleEnabled } = useCrmSettings(); + if (!isModuleEnabled(mod)) return ; + return <>{children}; +} diff --git a/packages/frontend/src/crm/settings/CrmSettingsPage.module.css b/packages/frontend/src/crm/settings/CrmSettingsPage.module.css new file mode 100644 index 0000000..3478308 --- /dev/null +++ b/packages/frontend/src/crm/settings/CrmSettingsPage.module.css @@ -0,0 +1,151 @@ +/* CRM Settings Page */ + +.backLink { + display: inline-flex; + align-items: center; + gap: 0.375rem; + color: var(--color-text-muted); + font-size: 0.875rem; + text-decoration: none; + margin-bottom: 1.5rem; +} + +.backLink:hover { + color: var(--color-text); +} + +.card { + background: var(--color-bg-card); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.cardTitle { + font-size: 1rem; + font-weight: 600; + margin: 0 0 0.25rem; +} + +.cardDesc { + font-size: 0.8125rem; + color: var(--color-text-muted); + margin: 0 0 1.25rem; +} + +/* Module list */ +.moduleList { + display: flex; + flex-direction: column; + gap: 0; +} + +.moduleItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 0; + border-bottom: 1px solid var(--color-border); +} + +.moduleItem:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.moduleItem:first-child { + padding-top: 0; +} + +.moduleInfo { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.moduleName { + font-size: 0.9375rem; + font-weight: 500; + color: var(--color-text); +} + +.moduleDesc { + font-size: 0.8125rem; + color: var(--color-text-muted); +} + +/* Toggle switch */ +.toggle { + position: relative; + width: 44px; + height: 24px; + flex-shrink: 0; +} + +.toggle input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.toggleTrack { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--color-border); + border-radius: 12px; + cursor: pointer; + transition: background 0.2s; +} + +.toggle input:checked + .toggleTrack { + background: var(--color-primary); +} + +.toggleTrack::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: transform 0.2s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); +} + +.toggle input:checked + .toggleTrack::after { + transform: translateX(20px); +} + +.toggle input:focus-visible + .toggleTrack { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Warning */ +.warning { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.75rem; + background: #fefce8; + border: 1px solid #fde68a; + border-radius: var(--radius-sm); + font-size: 0.8125rem; + color: #92400e; + margin-top: 1.25rem; +} + +/* Dark mode override for warning */ +:global([data-theme='dark']) .warning { + background: #422006; + border-color: #854d0e; + color: #fde68a; +} diff --git a/packages/frontend/src/crm/settings/CrmSettingsPage.tsx b/packages/frontend/src/crm/settings/CrmSettingsPage.tsx new file mode 100644 index 0000000..ea63050 --- /dev/null +++ b/packages/frontend/src/crm/settings/CrmSettingsPage.tsx @@ -0,0 +1,225 @@ +import { Link, Navigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthContext'; +import { + useCrmSettings, + type CrmModuleKey, +} from './CrmSettingsContext'; +import styles from './CrmSettingsPage.module.css'; + +// ============================================================ +// Module definitions (UI-Beschreibung je Modul) +// ============================================================ + +interface ModuleDef { + key: CrmModuleKey; + name: string; + description: string; + icon: React.ReactNode; +} + +const iconProps = { + width: 18, + height: 18, + viewBox: '0 0 16 16', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 1.5, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, +}; + +const MODULES: ModuleDef[] = [ + { + key: 'contacts', + name: 'Kontakte', + description: 'Kontaktverwaltung (Personen & Organisationen)', + icon: ( + + + + + ), + }, + { + key: 'companies', + name: 'Unternehmen', + description: 'Unternehmensverwaltung mit Verknüpfung zu Kontakten & Vorgängen', + icon: ( + + + + + + ), + }, + { + key: 'deals', + name: 'Vorgänge', + description: 'Vorgänge & Sales-Pipeline-Tracking', + icon: ( + + + + + + ), + }, + { + key: 'pipelines', + name: 'Pipelines', + description: 'Pipeline-Konfiguration & Stufen-Management', + icon: ( + + + + + + ), + }, +]; + +// ============================================================ +// Page Component +// ============================================================ + +export function CrmSettingsPage() { + const { user } = useAuth(); + const { settings, toggleModule } = useCrmSettings(); + + // Zugriffskontrolle: nur Admins + if ( + user?.role !== 'PLATFORM_ADMIN' && + user?.role !== 'TENANT_ADMIN' + ) { + return ; + } + + const anyDisabled = Object.values(settings.modules).some( + (m) => !m.enabled, + ); + + return ( +
+ {/* Zurück */} + + + + + Zurück zum Dashboard + + + {/* Header */} +

+ CRM Einstellungen +

+ + {/* Module-Card */} +
+

Module

+

+ Aktiviere oder deaktiviere einzelne CRM-Module. Deaktivierte Module + werden aus dem Menü ausgeblendet. +

+ +
+ {MODULES.map((mod) => { + const enabled = settings.modules[mod.key]?.enabled ?? true; + return ( +
+
+ + {mod.icon} + +
+ + {mod.name} + + {mod.description} +
+
+ +
+ ); + })} +
+ + {anyDisabled && ( +
+ + + + + + Deaktivierte Module werden aus dem Menü ausgeblendet. + Bestehende Daten bleiben erhalten und sind nach Reaktivierung + wieder verfügbar. + +
+ )} +
+ + {/* Platzhalter für zukünftige Einstellungen */} +
+

Weitere Einstellungen

+

+ Zusätzliche Konfigurationsmöglichkeiten werden in zukünftigen + Versionen verfügbar sein. +

+
+
+ ); +} diff --git a/packages/frontend/src/shell/App.tsx b/packages/frontend/src/shell/App.tsx index 0b353a3..ac76299 100644 --- a/packages/frontend/src/shell/App.tsx +++ b/packages/frontend/src/shell/App.tsx @@ -18,6 +18,8 @@ import { DealDetailPage } from '../crm/deals/DealDetailPage'; import { PipelinesPage } from '../crm/pipelines/PipelinesPage'; import { CompaniesPage } from '../crm/companies/CompaniesPage'; import { CompanyDetailPage } from '../crm/companies/CompanyDetailPage'; +import { CrmSettingsProvider, CrmModuleGuard } from '../crm/settings/CrmSettingsContext'; +import { CrmSettingsPage } from '../crm/settings/CrmSettingsPage'; function PrivateRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuth(); @@ -49,20 +51,23 @@ export function App() { path="/" element={ - + + + } > } /> } /> {/* CRM-Bereich */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> {/* Admin-Bereich mit eigenem Layout (Top-Tabs) */} }> } /> diff --git a/packages/frontend/src/shell/AppLayout.tsx b/packages/frontend/src/shell/AppLayout.tsx index 118a060..d092b39 100644 --- a/packages/frontend/src/shell/AppLayout.tsx +++ b/packages/frontend/src/shell/AppLayout.tsx @@ -5,6 +5,7 @@ import { useAuth } from '../auth/AuthContext'; import { UserAvatar } from '../components/UserAvatar'; import api from '../api/client'; import { useTheme } from '../theme/ThemeContext'; +import { useCrmSettings } from '../crm/settings/CrmSettingsContext'; import styles from './AppLayout.module.css'; interface ExternalLink { @@ -112,6 +113,13 @@ export function AppLayout() { const { user, logout } = useAuth(); const navigate = useNavigate(); const { mode, setMode } = useTheme(); + const { isModuleEnabled } = useCrmSettings(); + const isAdmin = user?.role === 'PLATFORM_ADMIN' || user?.role === 'TENANT_ADMIN'; + const anyCrmModuleEnabled = + isModuleEnabled('contacts') || + isModuleEnabled('companies') || + isModuleEnabled('deals') || + isModuleEnabled('pipelines'); const [crmOpen, setCrmOpen] = useState(true); const [appsOpen, setAppsOpen] = useState(true); const [collapsed, setCollapsed] = useState(() => { @@ -234,121 +242,158 @@ export function AppLayout() { {!collapsed && 'Dashboard'} - {/* CRM-Bereich (aufklappbar) */} - {!collapsed ? ( - - ) : ( -
- )} - {(crmOpen || collapsed) && ( + {/* CRM-Bereich (aufklappbar) — nur sichtbar wenn Module aktiv oder Admin */} + {(anyCrmModuleEnabled || isAdmin) && ( <> - - `${styles.navLink} ${isActive ? styles.active : ''}` - } - title="Kontakte" - > - setCrmOpen((p) => !p)} > - - - - {!collapsed && 'Kontakte'} - - - `${styles.navLink} ${isActive ? styles.active : ''}` - } - title="Unternehmen" - > - - - - - - {!collapsed && 'Unternehmen'} - - - `${styles.navLink} ${isActive ? styles.active : ''}` - } - title="Vorgänge" - > - - - - - - {!collapsed && 'Vorgänge'} - - - `${styles.navLink} ${isActive ? styles.active : ''}` - } - title="Pipelines" - > - - - - - - {!collapsed && 'Pipelines'} - + CRM + + + + + ) : ( +
+ )} + {(crmOpen || collapsed) && ( + <> + {isModuleEnabled('contacts') && ( + + `${styles.navLink} ${isActive ? styles.active : ''}` + } + title="Kontakte" + > + + + + + {!collapsed && 'Kontakte'} + + )} + {isModuleEnabled('companies') && ( + + `${styles.navLink} ${isActive ? styles.active : ''}` + } + title="Unternehmen" + > + + + + + + {!collapsed && 'Unternehmen'} + + )} + {isModuleEnabled('deals') && ( + + `${styles.navLink} ${isActive ? styles.active : ''}` + } + title="Vorgänge" + > + + + + + + {!collapsed && 'Vorgänge'} + + )} + {isModuleEnabled('pipelines') && ( + + `${styles.navLink} ${isActive ? styles.active : ''}` + } + title="Pipelines" + > + + + + + + {!collapsed && 'Pipelines'} + + )} + {/* CRM Einstellungen (nur Admins) */} + {isAdmin && ( + + `${styles.navLink} ${isActive ? styles.active : ''}` + } + title="CRM Einstellungen" + > + + + + + {!collapsed && 'Einstellungen'} + + )} + + )} )}