mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
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:
parent
55329188f6
commit
4e5c26cadd
3 changed files with 1107 additions and 189 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "Module",
|
||||
um die Import/Export-Funktionen nutzen zu können.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tab: Weitere Einstellungen */}
|
||||
{activeTab === 'settings' && (
|
||||
<>
|
||||
<IndustriesConfig />
|
||||
<AccountTypesConfig />
|
||||
<RelationshipTypesConfig />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue