mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 03:26: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() {
|
export function LexwareSyncContent() {
|
||||||
const { user } = useAuth();
|
const [activeTab, setActiveTab] = useState<SyncTabKey>('import');
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{/* Zurück */}
|
{/* Sub-Tab Bar */}
|
||||||
<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 */}
|
|
||||||
<div className={styles.tabBar}>
|
<div className={styles.tabBar}>
|
||||||
<button
|
<button
|
||||||
className={`${styles.tab} ${activeTab === 'import' ? styles.tabActive : ''}`}
|
className={`${styles.tab} ${activeTab === 'import' ? styles.tabActive : ''}`}
|
||||||
|
|
@ -870,6 +833,56 @@ export function LexwareSyncPage() {
|
||||||
{activeTab === 'import' && <ImportTab />}
|
{activeTab === 'import' && <ImportTab />}
|
||||||
{activeTab === 'export' && <ExportTab />}
|
{activeTab === 'export' && <ExportTab />}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,3 +149,282 @@
|
||||||
border-color: #854d0e;
|
border-color: #854d0e;
|
||||||
color: #fde68a;
|
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 { Link, Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../auth/AuthContext';
|
import { useAuth } from '../../auth/AuthContext';
|
||||||
import {
|
import {
|
||||||
useCrmSettings,
|
useCrmSettings,
|
||||||
type CrmModuleKey,
|
type CrmModuleKey,
|
||||||
} from './CrmSettingsContext';
|
} 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';
|
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
|
// Page Component
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
type SettingsTab = 'module' | 'lexware' | 'settings';
|
||||||
|
|
||||||
export function CrmSettingsPage() {
|
export function CrmSettingsPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { settings, toggleModule } = useCrmSettings();
|
const { settings, toggleModule, isModuleEnabled } = useCrmSettings();
|
||||||
|
const [activeTab, setActiveTab] = useState<SettingsTab>('module');
|
||||||
|
|
||||||
// Zugriffskontrolle: nur Admins
|
// Zugriffskontrolle: nur Admins
|
||||||
if (
|
if (
|
||||||
|
|
@ -132,169 +718,209 @@ export function CrmSettingsPage() {
|
||||||
CRM Einstellungen
|
CRM Einstellungen
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Module-Card */}
|
{/* Top-Level Tab Bar */}
|
||||||
<div className={styles.card}>
|
<div className={styles.settingsTabBar}>
|
||||||
<h2 className={styles.cardTitle}>Module</h2>
|
<button
|
||||||
<p className={styles.cardDesc}>
|
className={`${styles.settingsTab} ${activeTab === 'module' ? styles.settingsTabActive : ''}`}
|
||||||
Aktiviere oder deaktiviere einzelne CRM-Module. Deaktivierte Module
|
onClick={() => setActiveTab('module')}
|
||||||
werden aus dem Menü ausgeblendet.
|
>
|
||||||
</p>
|
<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}>
|
{/* ===== Tab Content ===== */}
|
||||||
{MODULES.map((mod) => {
|
|
||||||
const enabled = settings.modules[mod.key]?.enabled ?? true;
|
{/* Tab: Module */}
|
||||||
return (
|
{activeTab === 'module' && (
|
||||||
<div key={mod.key} className={styles.moduleItem}>
|
<div className={styles.card}>
|
||||||
<div
|
<h2 className={styles.cardTitle}>Module</h2>
|
||||||
style={{
|
<p className={styles.cardDesc}>
|
||||||
display: 'flex',
|
Aktiviere oder deaktiviere einzelne CRM-Module. Deaktivierte Module
|
||||||
alignItems: 'center',
|
werden aus dem Menü ausgeblendet.
|
||||||
gap: '0.75rem',
|
</p>
|
||||||
}}
|
|
||||||
>
|
<div className={styles.moduleList}>
|
||||||
<span
|
{MODULES.map((mod) => {
|
||||||
|
const enabled = settings.modules[mod.key]?.enabled ?? true;
|
||||||
|
return (
|
||||||
|
<div key={mod.key} className={styles.moduleItem}>
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
gap: '0.75rem',
|
||||||
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
|
<span
|
||||||
className={styles.moduleName}
|
|
||||||
style={{
|
style={{
|
||||||
opacity: enabled ? 1 : 0.5,
|
display: 'flex',
|
||||||
transition: 'opacity 0.2s',
|
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>
|
||||||
<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>
|
</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>
|
||||||
<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>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lexware Synchronisation — nur wenn Modul aktiv */}
|
{anyDisabled && (
|
||||||
{settings.modules.lexware?.enabled && (
|
<div className={styles.warning}>
|
||||||
<div className={styles.card}>
|
<svg
|
||||||
<h2 className={styles.cardTitle}>
|
width="16"
|
||||||
<span
|
height="16"
|
||||||
style={{
|
viewBox="0 0 16 16"
|
||||||
display: 'inline-flex',
|
fill="none"
|
||||||
alignItems: 'center',
|
stroke="currentColor"
|
||||||
justifyContent: 'center',
|
strokeWidth="1.5"
|
||||||
width: 24,
|
strokeLinecap="round"
|
||||||
height: 24,
|
strokeLinejoin="round"
|
||||||
borderRadius: 4,
|
style={{ flexShrink: 0, marginTop: 1 }}
|
||||||
background: 'linear-gradient(135deg, #2563eb, #7c3aed)',
|
>
|
||||||
color: 'white',
|
<path d="M8 1L1 14h14L8 1z" />
|
||||||
fontSize: '0.5625rem',
|
<path d="M8 6v3M8 11.5v.5" />
|
||||||
fontWeight: 700,
|
</svg>
|
||||||
letterSpacing: '0.5px',
|
<span>
|
||||||
marginRight: '0.5rem',
|
Deaktivierte Module werden aus dem Menü ausgeblendet.
|
||||||
verticalAlign: 'middle',
|
Bestehende Daten bleiben erhalten und sind nach Reaktivierung
|
||||||
}}
|
wieder verfügbar.
|
||||||
>
|
</span>
|
||||||
LX
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Platzhalter für zukünftige Einstellungen */}
|
{/* Tab: Lexoffice Sync */}
|
||||||
<div className={styles.card} style={{ opacity: 0.6 }}>
|
{activeTab === 'lexware' && (
|
||||||
<h2 className={styles.cardTitle}>Weitere Einstellungen</h2>
|
<>
|
||||||
<p className={styles.cardDesc}>
|
{isModuleEnabled('lexware') ? (
|
||||||
Zusätzliche Konfigurationsmöglichkeiten werden in zukünftigen
|
<LexwareSyncContent />
|
||||||
Versionen verfügbar sein.
|
) : (
|
||||||
</p>
|
<div className={styles.card}>
|
||||||
</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue