feat(frontend): add tabbed layout to CRM Settings page

- Restructure CRM Settings into 3 tabs: Module | Lexoffice Sync | Weitere Einstellungen
- Extract LexwareSyncContent as reusable component from LexwareSyncPage
- Embed Import/Export directly in Settings "Lexoffice Sync" tab
- Move Industries, AccountTypes, RelationshipTypes configs to "Weitere Einstellungen" tab
- Keep standalone /crm/lexware-sync route as fallback
- Add tab bar styles matching existing design pattern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-11 09:07:16 +01:00
parent 55329188f6
commit 4e5c26cadd
3 changed files with 1107 additions and 189 deletions

View file

@ -776,54 +776,17 @@ function ExportTab() {
}
// ============================================================
// LexwareSyncPage — Main
// LexwareSyncContent — Reusable Import/Export tabs
// ============================================================
type TabKey = 'import' | 'export';
type SyncTabKey = 'import' | 'export';
export function LexwareSyncPage() {
const { user } = useAuth();
const { isModuleEnabled } = useCrmSettings();
const [activeTab, setActiveTab] = useState<TabKey>('import');
// Zugriffskontrolle
if (
user?.role !== 'PLATFORM_ADMIN' &&
user?.role !== 'TENANT_ADMIN'
) {
return <Navigate to="/" replace />;
}
if (!isModuleEnabled('lexware')) {
return <Navigate to="/crm/settings" replace />;
}
export function LexwareSyncContent() {
const [activeTab, setActiveTab] = useState<SyncTabKey>('import');
return (
<div>
{/* Zurück */}
<Link to="/crm/settings" 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 zu CRM Einstellungen
</Link>
{/* Header */}
<div className={styles.header}>
<h1 className={styles.title}>
<span className={styles.lxBadge}>LX</span>
Lexware Office Synchronisation
</h1>
</div>
{/* Tab Bar */}
<>
{/* Sub-Tab Bar */}
<div className={styles.tabBar}>
<button
className={`${styles.tab} ${activeTab === 'import' ? styles.tabActive : ''}`}
@ -870,6 +833,56 @@ export function LexwareSyncPage() {
{activeTab === 'import' && <ImportTab />}
{activeTab === 'export' && <ExportTab />}
</div>
</>
);
}
// ============================================================
// LexwareSyncPage — Standalone page (fallback route)
// ============================================================
export function LexwareSyncPage() {
const { user } = useAuth();
const { isModuleEnabled } = useCrmSettings();
// Zugriffskontrolle
if (
user?.role !== 'PLATFORM_ADMIN' &&
user?.role !== 'TENANT_ADMIN'
) {
return <Navigate to="/" replace />;
}
if (!isModuleEnabled('lexware')) {
return <Navigate to="/crm/settings" replace />;
}
return (
<div>
{/* Zurück */}
<Link to="/crm/settings" 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 zu CRM Einstellungen
</Link>
{/* Header */}
<div className={styles.header}>
<h1 className={styles.title}>
<span className={styles.lxBadge}>LX</span>
Lexware Office Synchronisation
</h1>
</div>
<LexwareSyncContent />
</div>
);
}

View file

@ -149,3 +149,282 @@
border-color: #854d0e;
color: #fde68a;
}
/* ============================================================ */
/* Config Lists (Industries, AccountTypes, RelationshipTypes) */
/* ============================================================ */
.configHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.configHeader h2 {
margin: 0;
}
.addBtn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-primary);
background: var(--color-primary-bg, #eff6ff);
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.addBtn:hover {
background: var(--color-primary);
color: white;
}
.configTable {
width: 100%;
border-collapse: collapse;
font-size: 0.8125rem;
}
.configTable th {
text-align: left;
padding: 0.5rem 0.75rem;
font-weight: 500;
color: var(--color-text-muted);
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.configTable td {
padding: 0.625rem 0.75rem;
border-bottom: 1px solid var(--color-border);
vertical-align: middle;
}
.configTable tr:last-child td {
border-bottom: none;
}
.colorDot {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1);
vertical-align: middle;
}
.actionsCell {
display: flex;
align-items: center;
gap: 0.25rem;
justify-content: flex-end;
}
.iconBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.iconBtn:hover {
background: var(--color-bg);
color: var(--color-text);
}
.iconBtnDanger:hover {
background: #fef2f2;
color: #dc2626;
}
:global([data-theme='dark']) .iconBtnDanger:hover {
background: #450a0a;
color: #fca5a5;
}
/* Inline edit row */
.inlineForm {
display: flex;
align-items: center;
gap: 0.5rem;
}
.inlineInput {
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-bg);
color: var(--color-text);
outline: none;
transition: border-color 0.15s;
}
.inlineInput:focus {
border-color: var(--color-primary);
}
.inlineInput::placeholder {
color: var(--color-text-muted);
}
.colorInput {
width: 32px;
height: 28px;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
background: none;
}
.colorInput::-webkit-color-swatch-wrapper {
padding: 2px;
}
.colorInput::-webkit-color-swatch {
border: none;
border-radius: 3px;
}
.saveBtn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
font-weight: 500;
color: white;
background: var(--color-primary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: opacity 0.15s;
}
.saveBtn:hover {
opacity: 0.9;
}
.saveBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cancelBtn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text-muted);
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.15s;
}
.cancelBtn:hover {
background: var(--color-bg);
}
.emptyRow {
color: var(--color-text-muted);
font-style: italic;
text-align: center;
}
.sortBtns {
display: flex;
flex-direction: column;
gap: 0;
}
.sortBtn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 14px;
padding: 0;
border: none;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.5rem;
line-height: 1;
}
.sortBtn:hover {
color: var(--color-primary);
}
.sortBtn:disabled {
opacity: 0.25;
cursor: not-allowed;
}
/* ============================================================ */
/* Settings Tab Bar */
/* ============================================================ */
.settingsTabBar {
display: flex;
gap: 0;
border-bottom: 2px solid var(--color-border);
margin-bottom: 1.5rem;
}
.settingsTab {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-muted);
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
white-space: nowrap;
}
.settingsTab:hover {
color: var(--color-text);
}
.settingsTabActive {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
/* Disabled hint (e.g. Lexware module not active) */
.disabledHint {
display: flex;
align-items: flex-start;
gap: 0.75rem;
color: var(--color-text-muted);
}
.disabledHint svg {
flex-shrink: 0;
margin-top: 0.125rem;
}

View file

@ -1,9 +1,30 @@
import { useState, useCallback } from 'react';
import { Link, Navigate } from 'react-router-dom';
import { useAuth } from '../../auth/AuthContext';
import {
useCrmSettings,
type CrmModuleKey,
} from './CrmSettingsContext';
import {
useIndustries,
useCreateIndustry,
useUpdateIndustry,
useDeleteIndustry,
useAccountTypes,
useCreateAccountType,
useUpdateAccountType,
useDeleteAccountType,
useRelationshipTypes,
useCreateRelationshipType,
useUpdateRelationshipType,
useDeleteRelationshipType,
} from '../hooks';
import type {
Industry,
AccountType,
RelationshipType,
} from '../types';
import { LexwareSyncContent } from '../lexware/LexwareSyncPage';
import styles from './CrmSettingsPage.module.css';
// ============================================================
@ -90,13 +111,578 @@ const MODULES: ModuleDef[] = [
},
];
// ============================================================
// Kleine SVG-Icons
// ============================================================
const PlusIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M7 2v10M2 7h10" />
</svg>
);
const PencilIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 2l2 2-8 8H2v-2l8-8z" />
</svg>
);
const TrashIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M2 4h10M5 4V2.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5V4M11 4v7.5a1 1 0 01-1 1H4a1 1 0 01-1-1V4" />
</svg>
);
const ArrowUpIcon = () => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M5 8V2M2.5 4.5L5 2l2.5 2.5" />
</svg>
);
const ArrowDownIcon = () => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M5 2v6M2.5 5.5L5 8l2.5-2.5" />
</svg>
);
// ============================================================
// IndustriesConfig
// ============================================================
function IndustriesConfig() {
const { data } = useIndustries();
const createMut = useCreateIndustry();
const updateMut = useUpdateIndustry();
const deleteMut = useDeleteIndustry();
const industries: Industry[] = data?.data ?? [];
const [editId, setEditId] = useState<string | null>(null);
const [addMode, setAddMode] = useState(false);
const [name, setName] = useState('');
const [color, setColor] = useState('#6B7280');
const startAdd = useCallback(() => {
setEditId(null);
setName('');
setColor('#6B7280');
setAddMode(true);
}, []);
const startEdit = useCallback((item: Industry) => {
setAddMode(false);
setEditId(item.id);
setName(item.name);
setColor(item.color);
}, []);
const cancel = useCallback(() => {
setEditId(null);
setAddMode(false);
setName('');
setColor('#6B7280');
}, []);
const handleSave = useCallback(() => {
if (!name.trim()) return;
if (addMode) {
createMut.mutate(
{ name: name.trim(), color },
{ onSuccess: cancel },
);
} else if (editId) {
updateMut.mutate(
{ id: editId, data: { name: name.trim(), color } },
{ onSuccess: cancel },
);
}
}, [addMode, editId, name, color, createMut, updateMut, cancel]);
const handleDelete = useCallback(
(id: string) => {
if (window.confirm('Branche wirklich löschen?')) {
deleteMut.mutate(id);
}
},
[deleteMut],
);
const handleSort = useCallback(
(item: Industry, direction: 'up' | 'down') => {
const newOrder =
direction === 'up'
? Math.max(0, item.sortOrder - 1)
: item.sortOrder + 1;
updateMut.mutate({ id: item.id, data: { sortOrder: newOrder } });
},
[updateMut],
);
const isSaving = createMut.isPending || updateMut.isPending;
return (
<div className={styles.card}>
<div className={styles.configHeader}>
<div>
<h2 className={styles.cardTitle}>Branchen</h2>
<p className={styles.cardDesc} style={{ marginBottom: 0 }}>
Branchen-Kategorien für Unternehmen. Farben werden als Badge angezeigt.
</p>
</div>
<button className={styles.addBtn} onClick={startAdd} disabled={addMode}>
<PlusIcon /> Hinzufügen
</button>
</div>
<table className={styles.configTable}>
<thead>
<tr>
<th style={{ width: 40 }}>#</th>
<th>Name</th>
<th style={{ width: 60 }}>Farbe</th>
<th style={{ width: 100, textAlign: 'right' }}>Aktionen</th>
</tr>
</thead>
<tbody>
{addMode && (
<tr>
<td></td>
<td>
<div className={styles.inlineForm}>
<input
className={styles.inlineInput}
style={{ flex: 1 }}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Branchenname"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') cancel();
}}
/>
</div>
</td>
<td>
<input
type="color"
className={styles.colorInput}
value={color}
onChange={(e) => setColor(e.target.value)}
/>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
Speichern
</button>
<button className={styles.cancelBtn} onClick={cancel}>
Abbrechen
</button>
</div>
</td>
</tr>
)}
{industries.length === 0 && !addMode && (
<tr>
<td colSpan={4} className={styles.emptyRow}>
Noch keine Branchen definiert
</td>
</tr>
)}
{industries.map((item, idx) =>
editId === item.id ? (
<tr key={item.id}>
<td>{idx + 1}</td>
<td>
<div className={styles.inlineForm}>
<input
className={styles.inlineInput}
style={{ flex: 1 }}
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') cancel();
}}
/>
</div>
</td>
<td>
<input
type="color"
className={styles.colorInput}
value={color}
onChange={(e) => setColor(e.target.value)}
/>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
Speichern
</button>
<button className={styles.cancelBtn} onClick={cancel}>
Abbrechen
</button>
</div>
</td>
</tr>
) : (
<tr key={item.id}>
<td>
<div className={styles.sortBtns}>
<button
className={styles.sortBtn}
onClick={() => handleSort(item, 'up')}
disabled={idx === 0}
title="Nach oben"
>
<ArrowUpIcon />
</button>
<button
className={styles.sortBtn}
onClick={() => handleSort(item, 'down')}
disabled={idx === industries.length - 1}
title="Nach unten"
>
<ArrowDownIcon />
</button>
</div>
</td>
<td>{item.name}</td>
<td>
<span
className={styles.colorDot}
style={{ backgroundColor: item.color }}
title={item.color}
/>
</td>
<td>
<div className={styles.actionsCell}>
<button
className={styles.iconBtn}
onClick={() => startEdit(item)}
title="Bearbeiten"
>
<PencilIcon />
</button>
<button
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
onClick={() => handleDelete(item.id)}
title="Löschen"
>
<TrashIcon />
</button>
</div>
</td>
</tr>
),
)}
</tbody>
</table>
</div>
);
}
// ============================================================
// Generic ConfigList (for AccountTypes & RelationshipTypes)
// ============================================================
interface ConfigListItem {
id: string;
name: string;
sortOrder: number;
_count?: Record<string, number>;
}
interface ConfigListProps<T extends ConfigListItem> {
title: string;
description: string;
items: T[];
deleteConfirmText: string;
emptyText: string;
onCreate: (data: { name: string; sortOrder?: number }) => void;
onUpdate: (id: string, data: { name?: string; sortOrder?: number }) => void;
onDelete: (id: string) => void;
isCreating: boolean;
isUpdating: boolean;
}
function ConfigList<T extends ConfigListItem>({
title,
description,
items,
deleteConfirmText,
emptyText,
onCreate,
onUpdate,
onDelete,
isCreating,
isUpdating,
}: ConfigListProps<T>) {
const [editId, setEditId] = useState<string | null>(null);
const [addMode, setAddMode] = useState(false);
const [name, setName] = useState('');
const startAdd = useCallback(() => {
setEditId(null);
setName('');
setAddMode(true);
}, []);
const startEdit = useCallback((item: T) => {
setAddMode(false);
setEditId(item.id);
setName(item.name);
}, []);
const cancel = useCallback(() => {
setEditId(null);
setAddMode(false);
setName('');
}, []);
const handleSave = useCallback(() => {
if (!name.trim()) return;
if (addMode) {
onCreate({ name: name.trim() });
cancel();
} else if (editId) {
onUpdate(editId, { name: name.trim() });
cancel();
}
}, [addMode, editId, name, onCreate, onUpdate, cancel]);
const handleDelete = useCallback(
(id: string) => {
if (window.confirm(deleteConfirmText)) {
onDelete(id);
}
},
[onDelete, deleteConfirmText],
);
const handleSort = useCallback(
(item: T, direction: 'up' | 'down') => {
const newOrder =
direction === 'up'
? Math.max(0, item.sortOrder - 1)
: item.sortOrder + 1;
onUpdate(item.id, { sortOrder: newOrder });
},
[onUpdate],
);
const isSaving = isCreating || isUpdating;
return (
<div className={styles.card}>
<div className={styles.configHeader}>
<div>
<h2 className={styles.cardTitle}>{title}</h2>
<p className={styles.cardDesc} style={{ marginBottom: 0 }}>
{description}
</p>
</div>
<button className={styles.addBtn} onClick={startAdd} disabled={addMode}>
<PlusIcon /> Hinzufügen
</button>
</div>
<table className={styles.configTable}>
<thead>
<tr>
<th style={{ width: 40 }}>#</th>
<th>Name</th>
<th style={{ width: 100, textAlign: 'right' }}>Aktionen</th>
</tr>
</thead>
<tbody>
{addMode && (
<tr>
<td></td>
<td>
<div className={styles.inlineForm}>
<input
className={styles.inlineInput}
style={{ flex: 1 }}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name eingeben"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') cancel();
}}
/>
</div>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
Speichern
</button>
<button className={styles.cancelBtn} onClick={cancel}>
Abbrechen
</button>
</div>
</td>
</tr>
)}
{items.length === 0 && !addMode && (
<tr>
<td colSpan={3} className={styles.emptyRow}>
{emptyText}
</td>
</tr>
)}
{items.map((item, idx) =>
editId === item.id ? (
<tr key={item.id}>
<td>{idx + 1}</td>
<td>
<div className={styles.inlineForm}>
<input
className={styles.inlineInput}
style={{ flex: 1 }}
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave();
if (e.key === 'Escape') cancel();
}}
/>
</div>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
Speichern
</button>
<button className={styles.cancelBtn} onClick={cancel}>
Abbrechen
</button>
</div>
</td>
</tr>
) : (
<tr key={item.id}>
<td>
<div className={styles.sortBtns}>
<button
className={styles.sortBtn}
onClick={() => handleSort(item, 'up')}
disabled={idx === 0}
title="Nach oben"
>
<ArrowUpIcon />
</button>
<button
className={styles.sortBtn}
onClick={() => handleSort(item, 'down')}
disabled={idx === items.length - 1}
title="Nach unten"
>
<ArrowDownIcon />
</button>
</div>
</td>
<td>{item.name}</td>
<td>
<div className={styles.actionsCell}>
<button
className={styles.iconBtn}
onClick={() => startEdit(item)}
title="Bearbeiten"
>
<PencilIcon />
</button>
<button
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
onClick={() => handleDelete(item.id)}
title="Löschen"
>
<TrashIcon />
</button>
</div>
</td>
</tr>
),
)}
</tbody>
</table>
</div>
);
}
// ============================================================
// AccountTypesConfig
// ============================================================
function AccountTypesConfig() {
const { data } = useAccountTypes();
const createMut = useCreateAccountType();
const updateMut = useUpdateAccountType();
const deleteMut = useDeleteAccountType();
const items: AccountType[] = data?.data ?? [];
return (
<ConfigList
title="Kontotypen"
description="Kontotypen zur Klassifizierung von Unternehmen (z.B. Interessent, Endkunde, Partner)."
items={items}
deleteConfirmText="Kontotyp wirklich löschen?"
emptyText="Noch keine Kontotypen definiert"
onCreate={(d) => createMut.mutate(d)}
onUpdate={(id, d) => updateMut.mutate({ id, data: d })}
onDelete={(id) => deleteMut.mutate(id)}
isCreating={createMut.isPending}
isUpdating={updateMut.isPending}
/>
);
}
// ============================================================
// RelationshipTypesConfig
// ============================================================
function RelationshipTypesConfig() {
const { data } = useRelationshipTypes();
const createMut = useCreateRelationshipType();
const updateMut = useUpdateRelationshipType();
const deleteMut = useDeleteRelationshipType();
const items: RelationshipType[] = data?.data ?? [];
return (
<ConfigList
title="Beziehungstypen"
description="Beziehungstypen für Unternehmensverknüpfungen (z.B. Muttergesellschaft, Abrechnungspartner)."
items={items}
deleteConfirmText="Beziehungstyp wirklich löschen?"
emptyText="Noch keine Beziehungstypen definiert"
onCreate={(d) => createMut.mutate(d)}
onUpdate={(id, d) => updateMut.mutate({ id, data: d })}
onDelete={(id) => deleteMut.mutate(id)}
isCreating={createMut.isPending}
isUpdating={updateMut.isPending}
/>
);
}
// ============================================================
// Page Component
// ============================================================
type SettingsTab = 'module' | 'lexware' | 'settings';
export function CrmSettingsPage() {
const { user } = useAuth();
const { settings, toggleModule } = useCrmSettings();
const { settings, toggleModule, isModuleEnabled } = useCrmSettings();
const [activeTab, setActiveTab] = useState<SettingsTab>('module');
// Zugriffskontrolle: nur Admins
if (
@ -132,169 +718,209 @@ export function CrmSettingsPage() {
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>
{/* Top-Level Tab Bar */}
<div className={styles.settingsTabBar}>
<button
className={`${styles.settingsTab} ${activeTab === 'module' ? styles.settingsTabActive : ''}`}
onClick={() => setActiveTab('module')}
>
<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="2" width="5" height="5" rx="1" />
<rect x="9" y="2" width="5" height="5" rx="1" />
<rect x="2" y="9" width="5" height="5" rx="1" />
<rect x="9" y="9" width="5" height="5" rx="1" />
</svg>
Module
</button>
<button
className={`${styles.settingsTab} ${activeTab === 'lexware' ? styles.settingsTabActive : ''}`}
onClick={() => setActiveTab('lexware')}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 8a7 7 0 0112.9-3.8M15 8a7 7 0 01-12.9 3.8" />
<path d="M14 1v3.2h-3.2M2 15v-3.2h3.2" />
</svg>
Lexoffice Sync
</button>
<button
className={`${styles.settingsTab} ${activeTab === 'settings' ? styles.settingsTabActive : ''}`}
onClick={() => setActiveTab('settings')}
>
<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>
Weitere Einstellungen
</button>
</div>
<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
{/* ===== Tab Content ===== */}
{/* Tab: Module */}
{activeTab === 'module' && (
<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',
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',
gap: '0.75rem',
}}
>
{mod.icon}
</span>
<div className={styles.moduleInfo}>
<span
className={styles.moduleName}
style={{
opacity: enabled ? 1 : 0.5,
transition: 'opacity 0.2s',
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.name}
{mod.icon}
</span>
<span className={styles.moduleDesc}>{mod.description}</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>
<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>
{/* Lexware Synchronisation — nur wenn Modul aktiv */}
{settings.modules.lexware?.enabled && (
<div className={styles.card}>
<h2 className={styles.cardTitle}>
<span
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 24,
height: 24,
borderRadius: 4,
background: 'linear-gradient(135deg, #2563eb, #7c3aed)',
color: 'white',
fontSize: '0.5625rem',
fontWeight: 700,
letterSpacing: '0.5px',
marginRight: '0.5rem',
verticalAlign: 'middle',
}}
>
LX
</span>
Lexware Office Synchronisation
</h2>
<p className={styles.cardDesc}>
Kontakte aus Lexware Office importieren oder CRM-Daten nach Lexware
exportieren.
</p>
<Link
to="/crm/lexware-sync"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1rem',
background: 'var(--color-primary)',
color: 'white',
borderRadius: 'var(--radius-sm)',
textDecoration: 'none',
fontSize: '0.875rem',
fontWeight: 500,
transition: 'opacity 0.15s',
}}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 8a7 7 0 0112.9-3.8M15 8a7 7 0 01-12.9 3.8" />
<path d="M14 1v3.2h-3.2M2 15v-3.2h3.2" />
</svg>
Import / Export öffnen
</Link>
{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>
{/* Tab: Lexoffice Sync */}
{activeTab === 'lexware' && (
<>
{isModuleEnabled('lexware') ? (
<LexwareSyncContent />
) : (
<div className={styles.card}>
<div className={styles.disabledHint}>
<svg
width="20"
height="20"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="8" cy="8" r="7" />
<path d="M8 5v3M8 10.5v.5" />
</svg>
<div>
<strong>Lexware Office Modul deaktiviert</strong>
<p style={{ margin: '0.25rem 0 0', fontSize: '0.8125rem' }}>
Aktiviere das Lexware Office Modul im Tab &quot;Module&quot;,
um die Import/Export-Funktionen nutzen zu können.
</p>
</div>
</div>
</div>
)}
</>
)}
{/* Tab: Weitere Einstellungen */}
{activeTab === 'settings' && (
<>
<IndustriesConfig />
<AccountTypesConfig />
<RelationshipTypesConfig />
</>
)}
</div>
);
}