From 8b3873d67680bcb2423ca7e4a3aae36d31a477e2 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 24 Jul 2026 22:35:35 +0200 Subject: [PATCH] feat: restructure settings menu + add automation/agents to topbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/components/layout/TopBar.tsx | 18 +- frontend/src/pages/Settings.tsx | 14 +- frontend/src/pages/SettingsFirmendaten.tsx | 277 ++++++++++++++++ frontend/src/pages/SettingsStammdaten.tsx | 313 ++++++++++++++++++ frontend/src/pages/SettingsSystem.tsx | 299 ++--------------- frontend/src/pages/SettingsUserManagement.tsx | 48 +++ frontend/src/routes/index.tsx | 4 + 7 files changed, 698 insertions(+), 275 deletions(-) create mode 100644 frontend/src/pages/SettingsFirmendaten.tsx create mode 100644 frontend/src/pages/SettingsStammdaten.tsx create mode 100644 frontend/src/pages/SettingsUserManagement.tsx diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 40460c7..dc72b47 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -8,7 +8,7 @@ import { useLogout } from '@/api/hooks'; import { Avatar } from '@/components/ui/Avatar'; import { SearchDropdown } from '@/components/shared/SearchDropdown'; import { SuggestionBadge } from '@/components/ai/SuggestionBadge'; -import { Building, ChevronDown, Menu } from 'lucide-react'; +import { Building, ChevronDown, Menu, Zap, Bot } from 'lucide-react'; export function TopBar() { const { t } = useTranslation(); @@ -116,6 +116,22 @@ export function TopBar() { > {t('topbar.settings')} + + + + + + ); +} diff --git a/frontend/src/pages/SettingsStammdaten.tsx b/frontend/src/pages/SettingsStammdaten.tsx new file mode 100644 index 0000000..b038c9f --- /dev/null +++ b/frontend/src/pages/SettingsStammdaten.tsx @@ -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([]); + const [editing, setEditing] = useState
(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
[] = [ + { 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) => ( +
+ + +
+ ), + }, + ]; + + return ( +
+ + + {t('common.add', 'Hinzufügen')} + + }> + row.id} emptyMessage="Noch keine Adressen angelegt" /> + + + {showForm && editing && ( + +
+ setEditing({ ...editing, label: e.target.value })} + /> + )} + setEditing({ ...editing, street: e.target.value })} + /> + setEditing({ ...editing, zip: e.target.value })} + /> + setEditing({ ...editing, city: e.target.value })} + /> + setEditing({ ...editing, country: e.target.value })} + /> +
+
+ + +
+
+ )} + + ); +} + +// --- 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([]); + const [editing, setEditing] = useState(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[] = [ + { 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) => ( +
+ + +
+ ), + }, + ]; + + return ( +
+ + + {t('common.add', 'Hinzufügen')} + + }> +
row.id} emptyMessage="Noch keine Konten angelegt" /> + + + {showForm && editing && ( + +
+ setEditing({ ...editing, bankName: e.target.value })} + /> + setEditing({ ...editing, iban: e.target.value })} + /> + setEditing({ ...editing, bic: e.target.value })} + /> + setEditing({ ...editing, accountHolder: e.target.value })} + /> + setEditing({ ...editing, defaultTax: e.target.value })} + /> +
+
+ + +
+
+ )} + + ); +} + +// --- Sonstige Daten Tab --- +function SonstigeDatenTab() { + return ( +
+ + + +
+ ); +} + +// --- 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 ( +
+

Stammdaten

+ +
+ +
+ +
+ {activeTab === 'firmendaten' && } + {activeTab === 'adressen' && } + {activeTab === 'konten' && } + {activeTab === 'sonstige' && } +
+
+ ); +} diff --git a/frontend/src/pages/SettingsSystem.tsx b/frontend/src/pages/SettingsSystem.tsx index ab61d77..1abd66e 100644 --- a/frontend/src/pages/SettingsSystem.tsx +++ b/frontend/src/pages/SettingsSystem.tsx @@ -1,277 +1,48 @@ -import React, { useEffect } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { - useSystemSettings, - useUpdateSystemSettings, - useCurrencies, - useTaxes, -} from '@/api/hooks'; -import { Input } from '@/components/ui/Input'; -import { Button } from '@/components/ui/Button'; -import { Card } from '@/components/ui/Card'; -import { Skeleton } from '@/components/ui/Skeleton'; -import { useToast } from '@/components/ui/Toast'; - -const settingsSchema = z.object({ - company_name: z.string().min(1, 'Name ist erforderlich').max(200), - company_legal_form: z.string().max(50).optional().or(z.literal('')), - company_street: z.string().min(1, 'Straße ist erforderlich').max(255), - company_city: z.string().min(1, 'Stadt ist erforderlich').max(100), - company_zip: z.string().min(1, 'PLZ ist erforderlich').max(20), - company_country: z.string().min(2, 'Land ist erforderlich').max(2), - tax_number: z.string().max(50).optional().or(z.literal('')), - vat_id: z.string().max(50).optional().or(z.literal('')), - iban: z.string().max(34).optional().or(z.literal('')), - bic: z.string().max(11).optional().or(z.literal('')), - bank_name: z.string().max(100).optional().or(z.literal('')), - ceo: z.string().max(100).optional().or(z.literal('')), - trade_register: z.string().max(100).optional().or(z.literal('')), - default_currency_id: z.string().optional().or(z.literal('')), - default_tax_id: z.string().optional().or(z.literal('')), - invoice_prefix: z.string().max(20).optional().or(z.literal('RE-')), - quote_prefix: z.string().max(20).optional().or(z.literal('AN-')), - payment_terms_days: z.number().int().min(0).max(365).default(14), -}); - -type SettingsFormValues = z.infer; +import { SettingsMenuOrderPage } from './SettingsMenuOrder'; +import { SettingsThemePage } from './SettingsTheme'; +import { SettingsPluginsPage } from './SettingsPlugins'; export function SettingsSystemPage() { const { t } = useTranslation(); - const toast = useToast(); + const [activeTab, setActiveTab] = useState('menu'); - const { data: settings, isLoading } = useSystemSettings(); - const { data: currenciesData } = useCurrencies(); - const { data: taxesData } = useTaxes(); - const updateMutation = useUpdateSystemSettings(); - - const currencies = currenciesData?.items ?? []; - const taxes = taxesData?.items ?? []; - - const { - register, - handleSubmit, - reset, - formState: { errors, isDirty, isSubmitting }, - } = useForm({ - resolver: zodResolver(settingsSchema), - defaultValues: { - company_name: '', - company_legal_form: '', - company_street: '', - company_city: '', - company_zip: '', - company_country: 'DE', - tax_number: '', - vat_id: '', - iban: '', - bic: '', - bank_name: '', - ceo: '', - trade_register: '', - default_currency_id: '', - default_tax_id: '', - invoice_prefix: 'RE-', - quote_prefix: 'AN-', - payment_terms_days: 14, - }, - }); - - useEffect(() => { - if (settings && Object.keys(settings).length > 0) { - reset({ - company_name: settings.company_name || '', - company_legal_form: settings.company_legal_form || '', - company_street: settings.company_street || '', - company_city: settings.company_city || '', - company_zip: settings.company_zip || '', - company_country: settings.company_country || 'DE', - tax_number: settings.tax_number || '', - vat_id: settings.vat_id || '', - iban: settings.iban || '', - bic: settings.bic || '', - bank_name: settings.bank_name || '', - ceo: settings.ceo || '', - trade_register: settings.trade_register || '', - default_currency_id: settings.default_currency_id || '', - default_tax_id: settings.default_tax_id || '', - invoice_prefix: settings.invoice_prefix || 'RE-', - quote_prefix: settings.quote_prefix || 'AN-', - payment_terms_days: settings.payment_terms_days || 14, - }); - } - }, [settings, reset]); - - const onSubmit = async (values: SettingsFormValues) => { - try { - const payload = { ...values }; - if (!payload.default_currency_id) payload.default_currency_id = null as any; - if (!payload.default_tax_id) payload.default_tax_id = null as any; - await updateMutation.mutateAsync(payload); - toast.success(t('systemSettings.saved')); - } catch (err: any) { - toast.error(err.message || t('systemSettings.saveFailed')); - } - }; - - if (isLoading) { - return ( -
- - -
- ); - } + const tabs = [ + { key: 'menu', label: t('settings.menuOrder', 'Menü') }, + { key: 'theme', label: t('settings.theme', 'Theme') }, + { key: 'plugins', label: t('settings.plugins', 'Plugins') }, + ]; return (
-

{t('systemSettings.title')}

+

System

-
- -
- - -
-
+
+ +
- -
- - - - -
-
- - -
- - - - - - - -
-
- - -
-
- - -
-
- - -
- - - -
-
- -
- -
- +
+ {activeTab === 'menu' && } + {activeTab === 'theme' && } + {activeTab === 'plugins' && } +
); } diff --git a/frontend/src/pages/SettingsUserManagement.tsx b/frontend/src/pages/SettingsUserManagement.tsx new file mode 100644 index 0000000..04421f6 --- /dev/null +++ b/frontend/src/pages/SettingsUserManagement.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SettingsUsersPage } from './SettingsUsers'; +import { SettingsRolesPage } from './SettingsRoles'; +import { SettingsGroupsPage } from './SettingsGroups'; + +export function SettingsUserManagementPage() { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState('users'); + + const tabs = [ + { key: 'users', label: t('settings.users', 'Benutzer') }, + { key: 'roles', label: t('settings.roles', 'Rollen') }, + { key: 'groups', label: t('settings.groups', 'Gruppen') }, + ]; + + return ( +
+

Nutzerverwaltung

+ +
+ +
+ +
+ {activeTab === 'users' && } + {activeTab === 'roles' && } + {activeTab === 'groups' && } +
+
+ ); +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 5c63a14..7a5838c 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -37,6 +37,8 @@ const ProactiveAISettings = React.lazy(() => import('@/pages/ProactiveAISettings const SettingsThemePage = React.lazy(() => import('@/pages/SettingsTheme').then(m => ({ default: m.SettingsThemePage }))); const SettingsMcpPage = React.lazy(() => import('@/pages/SettingsMcp').then(m => ({ default: m.SettingsMcpPage }))); const SettingsMenuOrderPage = React.lazy(() => import('@/pages/SettingsMenuOrder').then(m => ({ default: m.SettingsMenuOrderPage }))); +const SettingsStammdatenPage = React.lazy(() => import('@/pages/SettingsStammdaten').then(m => ({ default: m.SettingsStammdatenPage }))); +const SettingsUserManagementPage = React.lazy(() => import('@/pages/SettingsUserManagement').then(m => ({ default: m.SettingsUserManagementPage }))); const AutomationDashboardPage = React.lazy(() => import('@/pages/AutomationDashboard').then(m => ({ default: m.AutomationDashboardPage }))); const AgentDashboardPage = React.lazy(() => import('@/pages/AgentDashboard').then(m => ({ default: m.AgentDashboardPage }))); const AutomationSettingsPage = React.lazy(() => import('@/pages/AutomationSettings').then(m => ({ default: m.AutomationSettingsPage }))); @@ -99,6 +101,8 @@ const router = createBrowserRouter([ path: '/settings', element: withSuspense(), children: [ + { path: 'stammdaten', element: withSuspense() }, + { path: 'user-management', element: withSuspense() }, { path: 'roles', element: withSuspense() }, { path: 'users', element: withSuspense() }, { path: 'groups', element: withSuspense() },