Phase 6.3: SettingsForms on RHF + Zod
- SettingsCurrencies: RHF+Zod (code required max 3, name required, symbol required max 5) - SettingsTaxes: RHF+Zod (name required, rate numeric 0-100, country max 2) - SettingsSequences: RHF+Zod (name required, padding numeric 1-10) - SettingsUsers: RHF+Zod (name required, email valid, password min 8) - SettingsRoles: RHF+Zod for create form (name required) - SettingsGroups: RHF+Zod for create form (name required, description optional) - Error display under each field - Preserved all existing functionality: CRUD, permissions, members - Added 2 validation tests for SettingsCurrencies
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useUsers, useCreateUser, useUpdateUser, useDeleteUser, useRoles } from '@/api/hooks';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
@@ -21,6 +24,16 @@ const LEGACY_ROLES = [
|
||||
{ value: 'viewer', label: 'Viewer' },
|
||||
];
|
||||
|
||||
// ── Zod Schema ──
|
||||
const inviteSchema = z.object({
|
||||
name: z.string().min(1, 'required'),
|
||||
email: z.string().min(1, 'required').email('invalidEmail'),
|
||||
password: z.string().min(8, 'passwordTooShort'),
|
||||
role_id: z.string().optional().default(''),
|
||||
});
|
||||
|
||||
type InviteFormData = z.infer<typeof inviteSchema>;
|
||||
|
||||
export function SettingsUsersPage() {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -31,13 +44,19 @@ export function SettingsUsersPage() {
|
||||
const deleteUserMutation = useDeleteUser();
|
||||
|
||||
const [inviteOpen, setInviteOpen] = useState(false);
|
||||
const [inviteEmail, setInviteEmail] = useState('');
|
||||
const [inviteName, setInviteName] = useState('');
|
||||
const [invitePassword, setInvitePassword] = useState('');
|
||||
const [inviteRoleId, setInviteRoleId] = useState('');
|
||||
const [confirmDeactivate, setConfirmDeactivate] = useState<any>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState<any>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<InviteFormData>({
|
||||
resolver: zodResolver(inviteSchema),
|
||||
defaultValues: { name: '', email: '', password: '', role_id: '' },
|
||||
});
|
||||
|
||||
const users = data?.items ?? [];
|
||||
const customRoles = rolesData?.items ?? [];
|
||||
|
||||
@@ -69,34 +88,23 @@ export function SettingsUsersPage() {
|
||||
return 'viewer';
|
||||
};
|
||||
|
||||
const handleInvite = async () => {
|
||||
if (!inviteEmail.trim() || !inviteName.trim() || !invitePassword.trim()) {
|
||||
toast.error(t('validation.required'));
|
||||
return;
|
||||
}
|
||||
if (invitePassword.length < 8) {
|
||||
toast.error(t('auth.passwordTooShort'));
|
||||
return;
|
||||
}
|
||||
const onSubmit = async (data: InviteFormData) => {
|
||||
try {
|
||||
const payload: any = {
|
||||
email: inviteEmail.trim(),
|
||||
name: inviteName.trim(),
|
||||
password: invitePassword,
|
||||
email: data.email.trim(),
|
||||
name: data.name.trim(),
|
||||
password: data.password,
|
||||
is_active: true,
|
||||
};
|
||||
if (inviteRoleId.startsWith('role_id:')) {
|
||||
payload.role_id = inviteRoleId.substring('role_id:'.length);
|
||||
if (data.role_id.startsWith('role_id:')) {
|
||||
payload.role_id = data.role_id.substring('role_id:'.length);
|
||||
payload.role = 'viewer';
|
||||
} else {
|
||||
payload.role = inviteRoleId || 'viewer';
|
||||
payload.role = data.role_id || 'viewer';
|
||||
}
|
||||
await createUserMutation.mutateAsync(payload);
|
||||
toast.success(t('settings.userInvited'));
|
||||
setInviteEmail('');
|
||||
setInviteName('');
|
||||
setInvitePassword('');
|
||||
setInviteRoleId('');
|
||||
reset({ name: '', email: '', password: '', role_id: '' });
|
||||
setInviteOpen(false);
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || t('common.error'));
|
||||
@@ -147,6 +155,14 @@ export function SettingsUsersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const errorMsg = (key: string | undefined) => {
|
||||
if (!key) return undefined;
|
||||
if (key === 'required') return t('validation.required');
|
||||
if (key === 'invalidEmail') return t('validation.email');
|
||||
if (key === 'passwordTooShort') return t('auth.passwordTooShort');
|
||||
return key;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-users-page">
|
||||
@@ -164,7 +180,7 @@ export function SettingsUsersPage() {
|
||||
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-users-page">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-secondary-900">{t('settings.users')}</h1>
|
||||
<Button onClick={() => setInviteOpen(true)} data-testid="invite-user-btn">
|
||||
<Button onClick={() => { reset({ name: '', email: '', password: '', role_id: '' }); setInviteOpen(true); }} data-testid="invite-user-btn">
|
||||
{t('settings.inviteUser')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -228,48 +244,47 @@ export function SettingsUsersPage() {
|
||||
)}
|
||||
|
||||
<Modal open={inviteOpen} onClose={() => setInviteOpen(false)} title={t('settings.inviteUser')}>
|
||||
<div className="space-y-4" data-testid="invite-user-form">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" data-testid="invite-user-form">
|
||||
<Input
|
||||
label={t('settings.name')}
|
||||
type="text"
|
||||
value={inviteName}
|
||||
onChange={(e) => setInviteName(e.target.value)}
|
||||
{...register('name')}
|
||||
error={errorMsg(errors.name?.message)}
|
||||
placeholder={t('settings.name')}
|
||||
data-testid="invite-name"
|
||||
/>
|
||||
<Input
|
||||
label={t('settings.inviteEmail')}
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
{...register('email')}
|
||||
error={errorMsg(errors.email?.message)}
|
||||
placeholder="neu.mitarbeiter@firma.de"
|
||||
data-testid="invite-email"
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
type="password"
|
||||
value={invitePassword}
|
||||
onChange={(e) => setInvitePassword(e.target.value)}
|
||||
{...register('password')}
|
||||
error={errorMsg(errors.password?.message)}
|
||||
placeholder="********"
|
||||
data-testid="invite-password"
|
||||
/>
|
||||
<Select
|
||||
label={t('settings.inviteRole')}
|
||||
options={roleOptions}
|
||||
value={inviteRoleId}
|
||||
onChange={(e) => setInviteRoleId(e.target.value)}
|
||||
{...register('role_id')}
|
||||
/>
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="secondary" onClick={() => setInviteOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
onClick={handleInvite}
|
||||
isLoading={createUserMutation.isPending}
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
data-testid="send-invite-btn"
|
||||
>
|
||||
{t('settings.inviteUser')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
Reference in New Issue
Block a user