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:
Agent Zero
2026-07-24 00:28:11 +02:00
parent 2931a850c0
commit c334d02989
7 changed files with 306 additions and 109 deletions
+26 -19
View File
@@ -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 {
useGroups,
useCreateGroup,
@@ -81,9 +84,19 @@ export function SettingsGroupsPage() {
const updateGroupMutation = useUpdateGroup();
const deleteGroupMutation = useDeleteGroup();
// ── Zod Schema for create form ──
const groupCreateSchema = z.object({
name: z.string().min(1, 'required'),
description: z.string().optional().default(''),
});
type GroupCreateFormData = z.infer<typeof groupCreateSchema>;
const { register: registerGroup, handleSubmit: handleSubmitGroup, reset: resetGroup, formState: { errors: groupErrors } } = useForm<GroupCreateFormData>({
resolver: zodResolver(groupCreateSchema),
defaultValues: { name: '', description: '' },
});
const [createOpen, setCreateOpen] = useState(false);
const [newGroupName, setNewGroupName] = useState('');
const [newGroupDescription, setNewGroupDescription] = useState('');
const [newGroupPermissions, setNewGroupPermissions] = useState<Record<string, any>>({});
const [newGroupDenied, setNewGroupDenied] = useState<string[]>([]);
const [editingGroup, setEditingGroup] = useState<EditingGroup | null>(null);
@@ -117,22 +130,17 @@ export function SettingsGroupsPage() {
return groups;
}, [allPermissions]);
const handleCreateGroup = async () => {
if (!newGroupName.trim()) {
toast.error(t('validation.required', 'Pflichtfeld'));
return;
}
const handleCreateGroup = async (data: GroupCreateFormData) => {
try {
await createGroupMutation.mutateAsync({
name: newGroupName.trim(),
description: newGroupDescription.trim() || null,
name: data.name.trim(),
description: data.description.trim() || null,
permissions: newGroupPermissions,
denied_permissions: newGroupDenied,
field_permissions: {},
});
toast.success(t('settings.groupCreated', 'Gruppe erstellt'));
setNewGroupName('');
setNewGroupDescription('');
resetGroup({ name: '', description: '' });
setNewGroupPermissions({});
setNewGroupDenied([]);
setCreateOpen(false);
@@ -243,7 +251,7 @@ export function SettingsGroupsPage() {
<h1 className="text-2xl font-bold text-secondary-900">
{t('settings.groups', 'Gruppen')}
</h1>
<Button onClick={() => setCreateOpen(true)} data-testid="create-group-btn">
<Button onClick={() => { resetGroup({ name: '', description: '' }); setCreateOpen(true); }} data-testid="create-group-btn">
{t('settings.createGroup', 'Gruppe erstellen')}
</Button>
</div>
@@ -317,18 +325,17 @@ export function SettingsGroupsPage() {
title={t('settings.createGroup', 'Gruppe erstellen')}
size="lg"
>
<div className="space-y-4" data-testid="create-group-form">
<form onSubmit={handleSubmitGroup(handleCreateGroup)} className="space-y-4" data-testid="create-group-form">
<Input
label={t('settings.groupName', 'Gruppenname')}
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
{...registerGroup('name')}
error={groupErrors.name?.message === 'required' ? t('validation.required') : undefined}
placeholder={t('settings.groupName', 'Gruppenname')}
data-testid="new-group-name"
/>
<Input
label={t('settings.groupDescription', 'Beschreibung')}
value={newGroupDescription}
onChange={(e) => setNewGroupDescription(e.target.value)}
{...registerGroup('description')}
placeholder={t('settings.groupDescription', 'Beschreibung')}
data-testid="new-group-description"
/>
@@ -384,14 +391,14 @@ export function SettingsGroupsPage() {
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setCreateOpen(false)}>{t('common.cancel')}</Button>
<Button
onClick={handleCreateGroup}
type="submit"
isLoading={createGroupMutation.isPending}
data-testid="save-group-btn"
>
{t('common.save')}
</Button>
</div>
</div>
</form>
</Modal>
{/* Edit Group Modal */}