feat: restructure settings menu + add automation/agents to topbar

TopBar:
- Add Automation and Agenten to user dropdown menu

Settings restructure:
- Stammdaten: Firmendaten, Adressen (CRUD), Konten (CRUD), Sonstige Daten
- Nutzerverwaltung: Benutzer, Rollen, Gruppen as tabs
- System: Menü, Theme, Plugins as tabs
- Reduce nav items from 15+ to 8
- Add end prop to all settings NavLinks
This commit is contained in:
Agent Zero
2026-07-24 22:35:35 +02:00
parent b4121082f7
commit 8b3873d676
7 changed files with 698 additions and 275 deletions
+313
View File
@@ -0,0 +1,313 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingsFirmendatenPage } from './SettingsFirmendaten';
import { SettingsCurrenciesPage } from './SettingsCurrencies';
import { SettingsTaxesPage } from './SettingsTaxes';
import { SettingsSequencesPage } from './SettingsSequences';
import { Card } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Select } from '@/components/ui/Select';
import { Table, TableColumn } from '@/components/ui/Table';
import { Plus, Trash2, Pencil } from 'lucide-react';
// --- Adressen Tab ---
interface Address {
id: string;
type: string;
street: string;
zip: string;
city: string;
country: string;
label: string;
}
const addressTypeOptions = [
{ value: 'standort', label: 'Standort' },
{ value: 'rechnung', label: 'Rechnungsadresse' },
{ value: 'lieferung', label: 'Lieferadresse' },
{ value: 'eigene', label: 'Eigene Bezeichnung' },
];
function AdressenTab() {
const { t } = useTranslation();
const [addresses, setAddresses] = useState<Address[]>([]);
const [editing, setEditing] = useState<Address | null>(null);
const [showForm, setShowForm] = useState(false);
const handleAdd = () => {
setEditing({ id: '', type: 'standort', street: '', zip: '', city: '', country: 'DE', label: '' });
setShowForm(true);
};
const handleEdit = (addr: Address) => {
setEditing({ ...addr });
setShowForm(true);
};
const handleDelete = (id: string) => {
setAddresses(addresses.filter(a => a.id !== id));
};
const handleSave = () => {
if (!editing) return;
if (editing.id) {
setAddresses(addresses.map(a => a.id === editing.id ? editing : a));
} else {
setAddresses([...addresses, { ...editing, id: Date.now().toString() }]);
}
setShowForm(false);
setEditing(null);
};
const columns: TableColumn<Address>[] = [
{ key: 'type', header: 'Art', render: (row) => addressTypeOptions.find(o => o.value === row.type)?.label || row.type },
{ key: 'street', header: t('address.street', 'Straße') },
{ key: 'zip', header: t('address.zip', 'PLZ') },
{ key: 'city', header: t('address.city', 'Stadt') },
{ key: 'country', header: t('address.country', 'Land') },
{ key: 'label', header: 'Bezeichnung', render: (row) => row.label || '—' },
{
key: 'actions', header: '', render: (row) => (
<div className="flex gap-2">
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label="Bearbeiten">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label="Löschen">
<Trash2 className="w-4 h-4" />
</button>
</div>
),
},
];
return (
<div className="space-y-4">
<Card title="Adressverwaltung" actions={
<Button size="sm" onClick={handleAdd}>
<Plus className="w-4 h-4 mr-1" />
{t('common.add', 'Hinzufügen')}
</Button>
}>
<Table columns={columns} data={addresses} rowKey={(row) => row.id} emptyMessage="Noch keine Adressen angelegt" />
</Card>
{showForm && editing && (
<Card title={editing.id ? 'Adresse bearbeiten' : 'Neue Adresse'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Select
label="Art"
options={addressTypeOptions}
value={editing.type}
onChange={(e) => setEditing({ ...editing, type: e.target.value })}
/>
{editing.type === 'eigene' && (
<Input
label="Eigene Bezeichnung"
value={editing.label}
onChange={(e) => setEditing({ ...editing, label: e.target.value })}
/>
)}
<Input
label={t('address.street', 'Straße')}
value={editing.street}
onChange={(e) => setEditing({ ...editing, street: e.target.value })}
/>
<Input
label={t('address.zip', 'PLZ')}
value={editing.zip}
onChange={(e) => setEditing({ ...editing, zip: e.target.value })}
/>
<Input
label={t('address.city', 'Stadt')}
value={editing.city}
onChange={(e) => setEditing({ ...editing, city: e.target.value })}
/>
<Input
label={t('address.country', 'Land')}
value={editing.country}
maxLength={2}
onChange={(e) => setEditing({ ...editing, country: e.target.value })}
/>
</div>
<div className="flex justify-end gap-3 mt-4">
<Button variant="secondary" onClick={() => { setShowForm(false); setEditing(null); }}>
{t('common.cancel', 'Abbrechen')}
</Button>
<Button onClick={handleSave}>{t('common.save', 'Speichern')}</Button>
</div>
</Card>
)}
</div>
);
}
// --- Konten Tab ---
interface BankAccount {
id: string;
bankName: string;
iban: string;
bic: string;
accountHolder: string;
defaultTax: string;
}
function KontenTab() {
const { t } = useTranslation();
const [accounts, setAccounts] = useState<BankAccount[]>([]);
const [editing, setEditing] = useState<BankAccount | null>(null);
const [showForm, setShowForm] = useState(false);
const handleAdd = () => {
setEditing({ id: '', bankName: '', iban: '', bic: '', accountHolder: '', defaultTax: '' });
setShowForm(true);
};
const handleEdit = (acc: BankAccount) => {
setEditing({ ...acc });
setShowForm(true);
};
const handleDelete = (id: string) => {
setAccounts(accounts.filter(a => a.id !== id));
};
const handleSave = () => {
if (!editing) return;
if (editing.id) {
setAccounts(accounts.map(a => a.id === editing.id ? editing : a));
} else {
setAccounts([...accounts, { ...editing, id: Date.now().toString() }]);
}
setShowForm(false);
setEditing(null);
};
const columns: TableColumn<BankAccount>[] = [
{ key: 'bankName', header: 'Bankname' },
{ key: 'iban', header: 'IBAN' },
{ key: 'bic', header: 'BIC' },
{ key: 'accountHolder', header: 'Kontoinhaber' },
{ key: 'defaultTax', header: 'Standard-Steuer', render: (row) => row.defaultTax || '—' },
{
key: 'actions', header: '', render: (row) => (
<div className="flex gap-2">
<button onClick={() => handleEdit(row)} className="p-1 rounded hover:bg-secondary-100" aria-label="Bearbeiten">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => handleDelete(row.id)} className="p-1 rounded hover:bg-danger-50 text-danger-600" aria-label="Löschen">
<Trash2 className="w-4 h-4" />
</button>
</div>
),
},
];
return (
<div className="space-y-4">
<Card title="Bankkontenverwaltung" actions={
<Button size="sm" onClick={handleAdd}>
<Plus className="w-4 h-4 mr-1" />
{t('common.add', 'Hinzufügen')}
</Button>
}>
<Table columns={columns} data={accounts} rowKey={(row) => row.id} emptyMessage="Noch keine Konten angelegt" />
</Card>
{showForm && editing && (
<Card title={editing.id ? 'Konto bearbeiten' : 'Neues Konto'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Bankname"
value={editing.bankName}
onChange={(e) => setEditing({ ...editing, bankName: e.target.value })}
/>
<Input
label="IBAN"
value={editing.iban}
onChange={(e) => setEditing({ ...editing, iban: e.target.value })}
/>
<Input
label="BIC"
value={editing.bic}
onChange={(e) => setEditing({ ...editing, bic: e.target.value })}
/>
<Input
label="Kontoinhaber"
value={editing.accountHolder}
onChange={(e) => setEditing({ ...editing, accountHolder: e.target.value })}
/>
<Input
label="Standard-Steuer"
value={editing.defaultTax}
onChange={(e) => setEditing({ ...editing, defaultTax: e.target.value })}
/>
</div>
<div className="flex justify-end gap-3 mt-4">
<Button variant="secondary" onClick={() => { setShowForm(false); setEditing(null); }}>
{t('common.cancel', 'Abbrechen')}
</Button>
<Button onClick={handleSave}>{t('common.save', 'Speichern')}</Button>
</div>
</Card>
)}
</div>
);
}
// --- Sonstige Daten Tab ---
function SonstigeDatenTab() {
return (
<div className="space-y-6">
<SettingsCurrenciesPage />
<SettingsTaxesPage />
<SettingsSequencesPage />
</div>
);
}
// --- Main Stammdaten Page ---
export function SettingsStammdatenPage() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('firmendaten');
const tabs = [
{ key: 'firmendaten', label: 'Firmendaten' },
{ key: 'adressen', label: 'Adressen' },
{ key: 'konten', label: 'Konten' },
{ key: 'sonstige', label: 'Sonstige Daten' },
];
return (
<div className="space-y-6" data-testid="settings-stammdaten-page">
<h1 className="text-2xl font-bold text-secondary-900">Stammdaten</h1>
<div className="border-b border-secondary-200">
<nav className="flex gap-4" role="tablist" aria-label="Stammdaten Tabs">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
role="tab"
aria-selected={activeTab === tab.key}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.key
? 'border-primary-500 text-primary-700'
: 'border-transparent text-secondary-600 hover:text-secondary-900 hover:border-secondary-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
<div role="tabpanel">
{activeTab === 'firmendaten' && <SettingsFirmendatenPage />}
{activeTab === 'adressen' && <AdressenTab />}
{activeTab === 'konten' && <KontenTab />}
{activeTab === 'sonstige' && <SonstigeDatenTab />}
</div>
</div>
);
}