feat(core): add SettingsSystem page
This commit is contained in:
@@ -0,0 +1,277 @@
|
|||||||
|
import React, { useEffect } 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<typeof settingsSchema>;
|
||||||
|
|
||||||
|
export function SettingsSystemPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
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<SettingsFormValues>({
|
||||||
|
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 (
|
||||||
|
<div className="space-y-4" data-testid="settings-system-page">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-96" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6" data-testid="settings-system-page">
|
||||||
|
<h1 className="text-2xl font-bold text-secondary-900">{t('systemSettings.title')}</h1>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} noValidate className="space-y-4">
|
||||||
|
<Card title={t('systemSettings.companyInfo')}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Input
|
||||||
|
{...register('company_name')}
|
||||||
|
label={t('systemSettings.companyName')}
|
||||||
|
required
|
||||||
|
error={errors.company_name?.message}
|
||||||
|
data-testid="settings-company-name"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('company_legal_form')}
|
||||||
|
label={t('systemSettings.legalForm')}
|
||||||
|
error={errors.company_legal_form?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t('systemSettings.companyAddress')}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Input
|
||||||
|
{...register('company_street')}
|
||||||
|
label={t('address.street')}
|
||||||
|
required
|
||||||
|
error={errors.company_street?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('company_zip')}
|
||||||
|
label={t('address.zip')}
|
||||||
|
required
|
||||||
|
error={errors.company_zip?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('company_city')}
|
||||||
|
label={t('address.city')}
|
||||||
|
required
|
||||||
|
error={errors.company_city?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('company_country')}
|
||||||
|
label={t('address.country')}
|
||||||
|
required
|
||||||
|
error={errors.company_country?.message}
|
||||||
|
maxLength={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t('systemSettings.taxBankInfo')}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Input
|
||||||
|
{...register('tax_number')}
|
||||||
|
label={t('systemSettings.taxNumber')}
|
||||||
|
error={errors.tax_number?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('vat_id')}
|
||||||
|
label={t('systemSettings.vatId')}
|
||||||
|
error={errors.vat_id?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('iban')}
|
||||||
|
label={t('systemSettings.iban')}
|
||||||
|
error={errors.iban?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('bic')}
|
||||||
|
label={t('systemSettings.bic')}
|
||||||
|
error={errors.bic?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('bank_name')}
|
||||||
|
label={t('systemSettings.bankName')}
|
||||||
|
error={errors.bank_name?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('ceo')}
|
||||||
|
label={t('systemSettings.ceo')}
|
||||||
|
error={errors.ceo?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('trade_register')}
|
||||||
|
label={t('systemSettings.tradeRegister')}
|
||||||
|
error={errors.trade_register?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={t('systemSettings.defaults')}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-secondary-700">
|
||||||
|
{t('systemSettings.defaultCurrency')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
{...register('default_currency_id')}
|
||||||
|
className="w-full px-3 py-2 text-sm rounded-md border border-secondary-300 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{currencies.map((c: any) => (
|
||||||
|
<option key={c.id} value={c.id}>{c.code} — {c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-secondary-700">
|
||||||
|
{t('systemSettings.defaultTax')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
{...register('default_tax_id')}
|
||||||
|
className="w-full px-3 py-2 text-sm rounded-md border border-secondary-300 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
{taxes.map((t: any) => (
|
||||||
|
<option key={t.id} value={t.id}>{t.name} ({t.rate}%)</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
{...register('invoice_prefix')}
|
||||||
|
label={t('systemSettings.invoicePrefix')}
|
||||||
|
error={errors.invoice_prefix?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('quote_prefix')}
|
||||||
|
label={t('systemSettings.quotePrefix')}
|
||||||
|
error={errors.quote_prefix?.message}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('payment_terms_days', { valueAsNumber: true })}
|
||||||
|
label={t('systemSettings.paymentTermsDays')}
|
||||||
|
type="number"
|
||||||
|
error={errors.payment_terms_days?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button type="submit" isLoading={isSubmitting} data-testid="settings-system-save">
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user