From 2381260190ed79b973345f1ffb8463e438dfe39c Mon Sep 17 00:00:00 2001 From: Yashwin Date: Tue, 14 Apr 2026 10:28:09 +0530 Subject: [PATCH] refactor: implement react-hook-form with zod validation for notification templates and master forms, update service methods, and reorder table filter components. --- src/components/shared/SuppliersTable.tsx | 33 ++-- src/pages/superadmin/NotificationMaster.tsx | 103 +++++++--- .../superadmin/NotificationTemplateMaster.tsx | 184 +++++++++++------- src/pages/tenant/NotificationTemplates.tsx | 54 +++-- src/services/notification-service.ts | 5 + 5 files changed, 249 insertions(+), 130 deletions(-) diff --git a/src/components/shared/SuppliersTable.tsx b/src/components/shared/SuppliersTable.tsx index 1e107ae..259c1a2 100644 --- a/src/components/shared/SuppliersTable.tsx +++ b/src/components/shared/SuppliersTable.tsx @@ -270,22 +270,7 @@ export const SuppliersTable = ({ {showHeader && (
- { - setStatusFilter(val as string); - setCurrentPage(1); - }} - /> +
+ { + setStatusFilter(val as string); + setCurrentPage(1); + }} + />
; +type CodeFormValues = z.infer; const NotificationMaster = (): ReactElement => { const [categories, setCategories] = useState([]); @@ -32,7 +53,19 @@ const NotificationMaster = (): ReactElement => { // Category Modal const [categoryModalOpen, setCategoryModalOpen] = useState(false); const [editingCategory, setEditingCategory] = useState(null); - const [categoryForm, setCategoryForm] = useState({ name: '', code: '', description: '', module_id: '' }); + + // New: React Hook Form for Category + const { + register: registerCategory, + handleSubmit: handleCategorySubmit, + reset: resetCategory, + setValue: setCategoryValue, + formState: { errors: categoryErrors }, + watch: watchCategory, + } = useForm({ + resolver: zodResolver(categorySchema), + defaultValues: { name: '', code: '', description: '', module_id: '' } + }); // Code Modal const [codeModalOpen, setCodeModalOpen] = useState(false); @@ -46,7 +79,17 @@ const NotificationMaster = (): ReactElement => { const [codePages, setCodePages] = useState(0); const [editingCode, setEditingCode] = useState(null); - const [codeForm, setCodeForm] = useState({ code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' }); + + // New: React Hook Form for Code + const { + register: registerCode, + handleSubmit: handleCodeSubmit, + reset: resetCode, + formState: { errors: codeErrors }, + } = useForm({ + resolver: zodResolver(codeSchema), + defaultValues: { code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' } + }); // Delete Modal const [deleteModalOpen, setDeleteModalOpen] = useState(false); @@ -123,36 +166,45 @@ const NotificationMaster = (): ReactElement => { } }, [codePage]); - const handleSaveCategory = async () => { + const onSaveCategory = async (data: CategoryFormValues) => { try { + // Conditionally include module_id only if it's actually selected + const payload = { ...data }; + if (!payload.module_id) { + delete payload.module_id; + } + if (editingCategory) { - await notificationService.updateCategory(editingCategory.id, categoryForm); + await notificationService.updateCategory(editingCategory.id, payload); showToast.success('Category updated'); } else { - await notificationService.createCategory(categoryForm); + await notificationService.createCategory(payload); showToast.success('Category created'); } setCategoryModalOpen(false); + resetCategory(); fetchCategories(); } catch (err: any) { - showToast.error(err.message || 'Action failed'); + const errorMessage = err.response?.data?.error?.message || err.message || 'Action failed'; + showToast.error(errorMessage); } }; - const handleSaveCode = async () => { + const onSaveCode = async (data: CodeFormValues) => { try { if (editingCode) { - await notificationService.updateCode(editingCode.id, codeForm); + await notificationService.updateCode(editingCode.id, data); showToast.success('Code updated'); } else { - await notificationService.createCode(selectedCategory.id, codeForm); + await notificationService.createCode(selectedCategory.id, data); showToast.success('Code created'); } setEditingCode(null); - setCodeForm({ code: '', name: '', description: '', default_channels: ['in_app', 'email'], default_priority: 'normal' }); + resetCode(); fetchCodes(selectedCategory, codePage); } catch (err: any) { - showToast.error(err.message || 'Action failed'); + const errorMessage = err.response?.data?.error?.message || err.message || 'Action failed'; + showToast.error(errorMessage); } }; @@ -194,7 +246,7 @@ const NotificationMaster = (): ReactElement => { { setEditingCategory(c); - setCategoryForm({ name: c.name, code: c.code, description: c.description || '', module_id: c.module_id || '' }); + resetCategory({ name: c.name, code: c.code, description: c.description || '', module_id: c.module_id || '' }); setCategoryModalOpen(true); }} onDelete={() => { @@ -231,7 +283,7 @@ const NotificationMaster = (): ReactElement => {
{ setEditingCategory(null); - setCategoryForm({ name: '', code: '', description: '', module_id: '' }); + resetCategory({ name: '', code: '', description: '', module_id: '' }); setCategoryModalOpen(true); }} className="flex gap-2"> New Category @@ -256,23 +308,24 @@ const NotificationMaster = (): ReactElement => { setCategoryModalOpen(false)} title={editingCategory ? "Edit Category" : "New Category"} maxWidth="md">
- setCategoryForm({ ...categoryForm, name: e.target.value })} placeholder="e.g. Workflow" /> - setCategoryForm({ ...categoryForm, code: e.target.value })} placeholder="e.g. workflow" /> + +
- setCategoryForm({ ...categoryForm, description: e.target.value })} rows={2} /> + ({ value: m.id, label: m.name })) ]} - onValueChange={(val) => setCategoryForm({ ...categoryForm, module_id: val })} + onValueChange={(val) => setCategoryValue('module_id', val, { shouldValidate: true })} placeholder="Select a module" + error={categoryErrors.module_id?.message} />
- {editingCategory ? 'Update' : 'Create'} + {editingCategory ? 'Update' : 'Create'}
@@ -285,15 +338,15 @@ const NotificationMaster = (): ReactElement => { {editingCode ? 'Edit Event Code' : 'Add New Event Trigger'}
- setCodeForm({ ...codeForm, code: e.target.value })} disabled={!!editingCode} /> - setCodeForm({ ...codeForm, name: e.target.value })} /> + +
- setCodeForm({ ...codeForm, description: e.target.value })} rows={2} /> +
Auto-populates default channels (In-App, Email)
- {editingCode && } - {editingCode ? 'Update Code' : 'Add Code'} + {editingCode && } + {editingCode ? 'Update Code' : 'Add Code'}
@@ -321,7 +374,7 @@ const NotificationMaster = (): ReactElement => { - + {editingId ? 'Update Template' : 'Create Template'} diff --git a/src/pages/tenant/NotificationTemplates.tsx b/src/pages/tenant/NotificationTemplates.tsx index 21d878d..5ee98f2 100644 --- a/src/pages/tenant/NotificationTemplates.tsx +++ b/src/pages/tenant/NotificationTemplates.tsx @@ -16,6 +16,19 @@ import { Edit, RotateCcw, Building, Filter } from 'lucide-react'; import { notificationService } from '@/services/notification-service'; import { moduleService } from '@/services/module-service'; import { showToast } from '@/utils/toast'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const overrideSchema = z.object({ + title_template: z.string().min(1, 'Title template is required'), + message_template: z.string().min(1, 'Message template is required'), + email_subject_template: z.string().optional(), + email_body_template: z.string().optional(), + is_active: z.boolean() +}); + +type OverrideFormValues = z.infer; const NotificationTemplates = (): ReactElement => { const [templates, setTemplates] = useState([]); @@ -32,12 +45,21 @@ const NotificationTemplates = (): ReactElement => { // Override Modal const [modalOpen, setModalOpen] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState(null); - const [form, setForm] = useState({ - title_template: '', - message_template: '', - email_subject_template: '', - email_body_template: '', - is_active: true + + const { + register, + handleSubmit, + reset, + formState: { errors } + } = useForm({ + resolver: zodResolver(overrideSchema), + defaultValues: { + title_template: '', + message_template: '', + email_subject_template: '', + email_body_template: '', + is_active: true + } }); const fetchModules = async () => { @@ -74,14 +96,16 @@ const NotificationTemplates = (): ReactElement => { useEffect(() => { fetchModules(); }, []); useEffect(() => { fetchTemplates(); }, [currentPage, limit, selectedModule]); - const handleOverride = async () => { + const onOverride = async (data: OverrideFormValues) => { try { - await notificationService.overrideTemplate(selectedTemplate.code, form); + await notificationService.overrideTemplate(selectedTemplate.code, data); showToast.success('Template override saved'); setModalOpen(false); + reset(); fetchTemplates(); } catch (err: any) { - showToast.error(err.message || 'Action failed'); + const errorMessage = err.response?.data?.error?.message || err.message || 'Action failed'; + showToast.error(errorMessage); } }; @@ -113,7 +137,7 @@ const NotificationTemplates = (): ReactElement => {
- Save Override + Save Override
diff --git a/src/services/notification-service.ts b/src/services/notification-service.ts index 22a8b86..0d12b94 100644 --- a/src/services/notification-service.ts +++ b/src/services/notification-service.ts @@ -129,6 +129,11 @@ export const notificationService = { return response.data; }, + updateTemplate: async (id: string, data: any): Promise> => { + const response = await apiClient.put(`/notifications/templates/${id}`, data); + return response.data; + }, + overrideTemplate: async (code: string, data: any): Promise> => { const response = await apiClient.put(`/notifications/templates/${code}/override`, data); return response.data;