mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
feat(crm): add CRM Settings page with module visibility toggles
- New CrmSettingsContext with localStorage persistence (later swappable to API) - CrmSettingsPage: toggle switches per CRM module (Kontakte, Unternehmen, Vorgänge, Pipelines), only accessible for PLATFORM_ADMIN/TENANT_ADMIN - CrmModuleGuard: route protection for disabled modules (redirect to dashboard) - Sidebar: NavLinks conditionally rendered based on module settings - "Einstellungen" NavLink in CRM section (admin-only, gear icon) - CRM section hidden when all modules disabled and user is not admin Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
028364cd7d
commit
411a6bbbcb
5 changed files with 693 additions and 121 deletions
146
packages/frontend/src/crm/settings/CrmSettingsContext.tsx
Normal file
146
packages/frontend/src/crm/settings/CrmSettingsContext.tsx
Normal file
|
|
@ -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<CrmModuleKey, CrmModuleConfig>;
|
||||
}
|
||||
|
||||
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<CrmSettings>;
|
||||
// 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<CrmSettingsContextValue | null>(null);
|
||||
|
||||
export function CrmSettingsProvider({ children }: { children: ReactNode }) {
|
||||
const [settings, setSettings] = useState<CrmSettings>(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 (
|
||||
<CrmSettingsCtx.Provider
|
||||
value={{ settings, updateSettings, toggleModule, isModuleEnabled }}
|
||||
>
|
||||
{children}
|
||||
</CrmSettingsCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 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 <Navigate to="/" replace />;
|
||||
return <>{children}</>;
|
||||
}
|
||||
151
packages/frontend/src/crm/settings/CrmSettingsPage.module.css
Normal file
151
packages/frontend/src/crm/settings/CrmSettingsPage.module.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
225
packages/frontend/src/crm/settings/CrmSettingsPage.tsx
Normal file
225
packages/frontend/src/crm/settings/CrmSettingsPage.tsx
Normal file
|
|
@ -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: (
|
||||
<svg {...iconProps}>
|
||||
<circle cx="8" cy="5" r="3" />
|
||||
<path d="M2 14c0-2.5 2.5-4.5 6-4.5s6 2 6 4.5" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'companies',
|
||||
name: 'Unternehmen',
|
||||
description: 'Unternehmensverwaltung mit Verknüpfung zu Kontakten & Vorgängen',
|
||||
icon: (
|
||||
<svg {...iconProps}>
|
||||
<rect x="2" y="6" width="12" height="9" rx="1" />
|
||||
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
||||
<path d="M5 9h2v2H5zM9 9h2v2H9z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'deals',
|
||||
name: 'Vorgänge',
|
||||
description: 'Vorgänge & Sales-Pipeline-Tracking',
|
||||
icon: (
|
||||
<svg {...iconProps}>
|
||||
<path d="M2 2h3l2 9h6l2-6H6" />
|
||||
<circle cx="7" cy="14" r="1" />
|
||||
<circle cx="13" cy="14" r="1" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'pipelines',
|
||||
name: 'Pipelines',
|
||||
description: 'Pipeline-Konfiguration & Stufen-Management',
|
||||
icon: (
|
||||
<svg {...iconProps}>
|
||||
<rect x="1" y="2" width="14" height="3" rx="0.5" />
|
||||
<rect x="1" y="7" width="10" height="3" rx="0.5" />
|
||||
<rect x="1" y="12" width="6" height="3" rx="0.5" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================
|
||||
// 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 <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
const anyDisabled = Object.values(settings.modules).some(
|
||||
(m) => !m.enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Zurück */}
|
||||
<Link to="/" className={styles.backLink}>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<path d="M9 2L4 7l5 5" />
|
||||
</svg>
|
||||
Zurück zum Dashboard
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 600, marginBottom: '1.5rem' }}>
|
||||
CRM Einstellungen
|
||||
</h1>
|
||||
|
||||
{/* Module-Card */}
|
||||
<div className={styles.card}>
|
||||
<h2 className={styles.cardTitle}>Module</h2>
|
||||
<p className={styles.cardDesc}>
|
||||
Aktiviere oder deaktiviere einzelne CRM-Module. Deaktivierte Module
|
||||
werden aus dem Menü ausgeblendet.
|
||||
</p>
|
||||
|
||||
<div className={styles.moduleList}>
|
||||
{MODULES.map((mod) => {
|
||||
const enabled = settings.modules[mod.key]?.enabled ?? true;
|
||||
return (
|
||||
<div key={mod.key} className={styles.moduleItem}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
background: enabled
|
||||
? 'var(--color-primary-bg, #eff6ff)'
|
||||
: 'var(--color-bg)',
|
||||
color: enabled
|
||||
? 'var(--color-primary)'
|
||||
: 'var(--color-text-muted)',
|
||||
transition: 'background 0.2s, color 0.2s',
|
||||
}}
|
||||
>
|
||||
{mod.icon}
|
||||
</span>
|
||||
<div className={styles.moduleInfo}>
|
||||
<span
|
||||
className={styles.moduleName}
|
||||
style={{
|
||||
opacity: enabled ? 1 : 0.5,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
>
|
||||
{mod.name}
|
||||
</span>
|
||||
<span className={styles.moduleDesc}>{mod.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className={styles.toggle}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(e) =>
|
||||
toggleModule(mod.key, e.target.checked)
|
||||
}
|
||||
/>
|
||||
<span className={styles.toggleTrack} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{anyDisabled && (
|
||||
<div className={styles.warning}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
>
|
||||
<path d="M8 1L1 14h14L8 1z" />
|
||||
<path d="M8 6v3M8 11.5v.5" />
|
||||
</svg>
|
||||
<span>
|
||||
Deaktivierte Module werden aus dem Menü ausgeblendet.
|
||||
Bestehende Daten bleiben erhalten und sind nach Reaktivierung
|
||||
wieder verfügbar.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Platzhalter für zukünftige Einstellungen */}
|
||||
<div className={styles.card} style={{ opacity: 0.6 }}>
|
||||
<h2 className={styles.cardTitle}>Weitere Einstellungen</h2>
|
||||
<p className={styles.cardDesc}>
|
||||
Zusätzliche Konfigurationsmöglichkeiten werden in zukünftigen
|
||||
Versionen verfügbar sein.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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={
|
||||
<PrivateRoute>
|
||||
<AppLayout />
|
||||
<CrmSettingsProvider>
|
||||
<AppLayout />
|
||||
</CrmSettingsProvider>
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<DashboardPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
{/* CRM-Bereich */}
|
||||
<Route path="crm/contacts" element={<ContactsPage />} />
|
||||
<Route path="crm/contacts/:id" element={<ContactDetailPage />} />
|
||||
<Route path="crm/companies" element={<CompaniesPage />} />
|
||||
<Route path="crm/companies/:id" element={<CompanyDetailPage />} />
|
||||
<Route path="crm/deals" element={<DealsPage />} />
|
||||
<Route path="crm/deals/:id" element={<DealDetailPage />} />
|
||||
<Route path="crm/pipelines" element={<PipelinesPage />} />
|
||||
<Route path="crm/contacts" element={<CrmModuleGuard module="contacts"><ContactsPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/contacts/:id" element={<CrmModuleGuard module="contacts"><ContactDetailPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/companies" element={<CrmModuleGuard module="companies"><CompaniesPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/companies/:id" element={<CrmModuleGuard module="companies"><CompanyDetailPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/deals" element={<CrmModuleGuard module="deals"><DealsPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/deals/:id" element={<CrmModuleGuard module="deals"><DealDetailPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/pipelines" element={<CrmModuleGuard module="pipelines"><PipelinesPage /></CrmModuleGuard>} />
|
||||
<Route path="crm/settings" element={<CrmSettingsPage />} />
|
||||
{/* Admin-Bereich mit eigenem Layout (Top-Tabs) */}
|
||||
<Route path="admin" element={<AdminLayout />}>
|
||||
<Route index element={<Navigate to="users" replace />} />
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
</NavLink>
|
||||
|
||||
{/* CRM-Bereich (aufklappbar) */}
|
||||
{!collapsed ? (
|
||||
<button
|
||||
className={styles.navSectionToggle}
|
||||
onClick={() => setCrmOpen((p) => !p)}
|
||||
>
|
||||
<span>CRM</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className={`${styles.chevron} ${crmOpen ? styles.chevronOpen : ''}`}
|
||||
>
|
||||
<path d="M3 4.5l3 3 3-3" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<div className={styles.navDivider} />
|
||||
)}
|
||||
{(crmOpen || collapsed) && (
|
||||
{/* CRM-Bereich (aufklappbar) — nur sichtbar wenn Module aktiv oder Admin */}
|
||||
{(anyCrmModuleEnabled || isAdmin) && (
|
||||
<>
|
||||
<NavLink
|
||||
to="/crm/contacts"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Kontakte"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{!collapsed ? (
|
||||
<button
|
||||
className={styles.navSectionToggle}
|
||||
onClick={() => setCrmOpen((p) => !p)}
|
||||
>
|
||||
<circle cx="8" cy="5" r="3" />
|
||||
<path d="M2 14c0-2.761 2.686-5 6-5s6 2.239 6 5" />
|
||||
</svg>
|
||||
{!collapsed && 'Kontakte'}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/crm/companies"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Unternehmen"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="2" y="6" width="12" height="9" rx="1" />
|
||||
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
||||
<path d="M5 9h2v2H5zM9 9h2v2H9z" />
|
||||
</svg>
|
||||
{!collapsed && 'Unternehmen'}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/crm/deals"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Vorgänge"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="1" y="5" width="14" height="9" rx="1" />
|
||||
<path d="M5 5V3a2 2 0 012-2h2a2 2 0 012 2v2" />
|
||||
<path d="M1 9h14" />
|
||||
</svg>
|
||||
{!collapsed && 'Vorgänge'}
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/crm/pipelines"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Pipelines"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="1" y="2" width="4" height="12" rx="0.5" />
|
||||
<rect x="6" y="2" width="4" height="8" rx="0.5" />
|
||||
<rect x="11" y="2" width="4" height="10" rx="0.5" />
|
||||
</svg>
|
||||
{!collapsed && 'Pipelines'}
|
||||
</NavLink>
|
||||
<span>CRM</span>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
className={`${styles.chevron} ${crmOpen ? styles.chevronOpen : ''}`}
|
||||
>
|
||||
<path d="M3 4.5l3 3 3-3" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<div className={styles.navDivider} />
|
||||
)}
|
||||
{(crmOpen || collapsed) && (
|
||||
<>
|
||||
{isModuleEnabled('contacts') && (
|
||||
<NavLink
|
||||
to="/crm/contacts"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Kontakte"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="8" cy="5" r="3" />
|
||||
<path d="M2 14c0-2.761 2.686-5 6-5s6 2.239 6 5" />
|
||||
</svg>
|
||||
{!collapsed && 'Kontakte'}
|
||||
</NavLink>
|
||||
)}
|
||||
{isModuleEnabled('companies') && (
|
||||
<NavLink
|
||||
to="/crm/companies"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Unternehmen"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="2" y="6" width="12" height="9" rx="1" />
|
||||
<path d="M5 6V3a1 1 0 011-1h4a1 1 0 011 1v3" />
|
||||
<path d="M5 9h2v2H5zM9 9h2v2H9z" />
|
||||
</svg>
|
||||
{!collapsed && 'Unternehmen'}
|
||||
</NavLink>
|
||||
)}
|
||||
{isModuleEnabled('deals') && (
|
||||
<NavLink
|
||||
to="/crm/deals"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Vorgänge"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="1" y="5" width="14" height="9" rx="1" />
|
||||
<path d="M5 5V3a2 2 0 012-2h2a2 2 0 012 2v2" />
|
||||
<path d="M1 9h14" />
|
||||
</svg>
|
||||
{!collapsed && 'Vorgänge'}
|
||||
</NavLink>
|
||||
)}
|
||||
{isModuleEnabled('pipelines') && (
|
||||
<NavLink
|
||||
to="/crm/pipelines"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="Pipelines"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="1" y="2" width="4" height="12" rx="0.5" />
|
||||
<rect x="6" y="2" width="4" height="8" rx="0.5" />
|
||||
<rect x="11" y="2" width="4" height="10" rx="0.5" />
|
||||
</svg>
|
||||
{!collapsed && 'Pipelines'}
|
||||
</NavLink>
|
||||
)}
|
||||
{/* CRM Einstellungen (nur Admins) */}
|
||||
{isAdmin && (
|
||||
<NavLink
|
||||
to="/crm/settings"
|
||||
className={({ isActive }) =>
|
||||
`${styles.navLink} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
title="CRM Einstellungen"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="8" cy="8" r="2" />
|
||||
<path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41" />
|
||||
</svg>
|
||||
{!collapsed && 'Einstellungen'}
|
||||
</NavLink>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue