mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 03:06:39 +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 { PipelinesPage } from '../crm/pipelines/PipelinesPage';
|
||||||
import { CompaniesPage } from '../crm/companies/CompaniesPage';
|
import { CompaniesPage } from '../crm/companies/CompaniesPage';
|
||||||
import { CompanyDetailPage } from '../crm/companies/CompanyDetailPage';
|
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 }) {
|
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isLoading } = useAuth();
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
|
|
@ -49,20 +51,23 @@ export function App() {
|
||||||
path="/"
|
path="/"
|
||||||
element={
|
element={
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
|
<CrmSettingsProvider>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
|
</CrmSettingsProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route index element={<DashboardPage />} />
|
<Route index element={<DashboardPage />} />
|
||||||
<Route path="profile" element={<ProfilePage />} />
|
<Route path="profile" element={<ProfilePage />} />
|
||||||
{/* CRM-Bereich */}
|
{/* CRM-Bereich */}
|
||||||
<Route path="crm/contacts" element={<ContactsPage />} />
|
<Route path="crm/contacts" element={<CrmModuleGuard module="contacts"><ContactsPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/contacts/:id" element={<ContactDetailPage />} />
|
<Route path="crm/contacts/:id" element={<CrmModuleGuard module="contacts"><ContactDetailPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/companies" element={<CompaniesPage />} />
|
<Route path="crm/companies" element={<CrmModuleGuard module="companies"><CompaniesPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/companies/:id" element={<CompanyDetailPage />} />
|
<Route path="crm/companies/:id" element={<CrmModuleGuard module="companies"><CompanyDetailPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/deals" element={<DealsPage />} />
|
<Route path="crm/deals" element={<CrmModuleGuard module="deals"><DealsPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/deals/:id" element={<DealDetailPage />} />
|
<Route path="crm/deals/:id" element={<CrmModuleGuard module="deals"><DealDetailPage /></CrmModuleGuard>} />
|
||||||
<Route path="crm/pipelines" element={<PipelinesPage />} />
|
<Route path="crm/pipelines" element={<CrmModuleGuard module="pipelines"><PipelinesPage /></CrmModuleGuard>} />
|
||||||
|
<Route path="crm/settings" element={<CrmSettingsPage />} />
|
||||||
{/* Admin-Bereich mit eigenem Layout (Top-Tabs) */}
|
{/* Admin-Bereich mit eigenem Layout (Top-Tabs) */}
|
||||||
<Route path="admin" element={<AdminLayout />}>
|
<Route path="admin" element={<AdminLayout />}>
|
||||||
<Route index element={<Navigate to="users" replace />} />
|
<Route index element={<Navigate to="users" replace />} />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useAuth } from '../auth/AuthContext';
|
||||||
import { UserAvatar } from '../components/UserAvatar';
|
import { UserAvatar } from '../components/UserAvatar';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
import { useTheme } from '../theme/ThemeContext';
|
import { useTheme } from '../theme/ThemeContext';
|
||||||
|
import { useCrmSettings } from '../crm/settings/CrmSettingsContext';
|
||||||
import styles from './AppLayout.module.css';
|
import styles from './AppLayout.module.css';
|
||||||
|
|
||||||
interface ExternalLink {
|
interface ExternalLink {
|
||||||
|
|
@ -112,6 +113,13 @@ export function AppLayout() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { mode, setMode } = useTheme();
|
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 [crmOpen, setCrmOpen] = useState(true);
|
||||||
const [appsOpen, setAppsOpen] = useState(true);
|
const [appsOpen, setAppsOpen] = useState(true);
|
||||||
const [collapsed, setCollapsed] = useState(() => {
|
const [collapsed, setCollapsed] = useState(() => {
|
||||||
|
|
@ -234,7 +242,9 @@ export function AppLayout() {
|
||||||
{!collapsed && 'Dashboard'}
|
{!collapsed && 'Dashboard'}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{/* CRM-Bereich (aufklappbar) */}
|
{/* CRM-Bereich (aufklappbar) — nur sichtbar wenn Module aktiv oder Admin */}
|
||||||
|
{(anyCrmModuleEnabled || isAdmin) && (
|
||||||
|
<>
|
||||||
{!collapsed ? (
|
{!collapsed ? (
|
||||||
<button
|
<button
|
||||||
className={styles.navSectionToggle}
|
className={styles.navSectionToggle}
|
||||||
|
|
@ -258,6 +268,7 @@ export function AppLayout() {
|
||||||
)}
|
)}
|
||||||
{(crmOpen || collapsed) && (
|
{(crmOpen || collapsed) && (
|
||||||
<>
|
<>
|
||||||
|
{isModuleEnabled('contacts') && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/crm/contacts"
|
to="/crm/contacts"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
|
|
@ -280,6 +291,8 @@ export function AppLayout() {
|
||||||
</svg>
|
</svg>
|
||||||
{!collapsed && 'Kontakte'}
|
{!collapsed && 'Kontakte'}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
{isModuleEnabled('companies') && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/crm/companies"
|
to="/crm/companies"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
|
|
@ -303,6 +316,8 @@ export function AppLayout() {
|
||||||
</svg>
|
</svg>
|
||||||
{!collapsed && 'Unternehmen'}
|
{!collapsed && 'Unternehmen'}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
{isModuleEnabled('deals') && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/crm/deals"
|
to="/crm/deals"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
|
|
@ -326,6 +341,8 @@ export function AppLayout() {
|
||||||
</svg>
|
</svg>
|
||||||
{!collapsed && 'Vorgänge'}
|
{!collapsed && 'Vorgänge'}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
)}
|
||||||
|
{isModuleEnabled('pipelines') && (
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/crm/pipelines"
|
to="/crm/pipelines"
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
|
|
@ -349,6 +366,34 @@ export function AppLayout() {
|
||||||
</svg>
|
</svg>
|
||||||
{!collapsed && 'Pipelines'}
|
{!collapsed && 'Pipelines'}
|
||||||
</NavLink>
|
</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