240 lines
9.4 KiB
TypeScript
240 lines
9.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import type { ReactElement } from 'react';
|
|
import { Layout } from '@/components/layout/Layout';
|
|
import {
|
|
PrimaryButton,
|
|
DataTable,
|
|
Modal,
|
|
FormField,
|
|
FormTextArea,
|
|
FormSelect,
|
|
StatusBadge,
|
|
Pagination,
|
|
type Column,
|
|
} from '@/components/shared';
|
|
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<typeof overrideSchema>;
|
|
|
|
const NotificationTemplates = (): ReactElement => {
|
|
const [templates, setTemplates] = useState<any[]>([]);
|
|
const [modules, setModules] = useState<any[]>([]);
|
|
const [selectedModule, setSelectedModule] = useState<string>('all');
|
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
|
|
// Pagination
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [limit, setLimit] = useState(10);
|
|
const [totalItems, setTotalItems] = useState(0);
|
|
const [totalPages, setTotalPages] = useState(0);
|
|
|
|
// Override Modal
|
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
|
const [selectedTemplate, setSelectedTemplate] = useState<any>(null);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
reset,
|
|
formState: { errors }
|
|
} = useForm<OverrideFormValues>({
|
|
resolver: zodResolver(overrideSchema),
|
|
defaultValues: {
|
|
title_template: '',
|
|
message_template: '',
|
|
email_subject_template: '',
|
|
email_body_template: '',
|
|
is_active: true
|
|
}
|
|
});
|
|
|
|
const fetchModules = async () => {
|
|
try {
|
|
const res = await moduleService.getMyModules();
|
|
if (res.success) {
|
|
setModules(res.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch modules:', err);
|
|
}
|
|
};
|
|
|
|
const fetchTemplates = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const res = await notificationService.getTemplates({
|
|
limit,
|
|
offset: (currentPage - 1) * limit,
|
|
module_id: selectedModule === 'all' ? undefined : selectedModule
|
|
});
|
|
if (res.success) {
|
|
setTemplates(res.data);
|
|
setTotalItems(res.pagination?.total || res.data.length);
|
|
setTotalPages(res.pagination?.pages || 1);
|
|
}
|
|
} catch (err: any) {
|
|
showToast.error('Failed to load templates');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { fetchModules(); }, []);
|
|
useEffect(() => { fetchTemplates(); }, [currentPage, limit, selectedModule]);
|
|
|
|
const onOverride = async (data: OverrideFormValues) => {
|
|
try {
|
|
await notificationService.overrideTemplate(selectedTemplate.code, data);
|
|
showToast.success('Template override saved');
|
|
setModalOpen(false);
|
|
reset();
|
|
fetchTemplates();
|
|
} catch (err: any) {
|
|
const errorMessage = err.response?.data?.error?.message || err.message || 'Action failed';
|
|
showToast.error(errorMessage);
|
|
}
|
|
};
|
|
|
|
const handleReset = async (code: string) => {
|
|
if (!confirm('Are you sure you want to reset this template to the global default? Your custom changes will be lost.')) return;
|
|
try {
|
|
await notificationService.resetTemplate(code);
|
|
showToast.success('Template reset to default');
|
|
fetchTemplates();
|
|
} catch (err: any) {
|
|
showToast.error(err.message || 'Reset failed');
|
|
}
|
|
};
|
|
|
|
const columns: Column<any>[] = [
|
|
{ key: 'category', label: 'Category', render: (t) => <span className="text-[10px] font-bold uppercase text-gray-500 bg-gray-100 px-2 py-1 rounded">{t.category_name}</span> },
|
|
{ key: 'code', label: 'Event', render: (t) => <span className="text-sm font-semibold">{t.code}</span> },
|
|
{ key: 'source', label: 'Source', render: (t) => (
|
|
<StatusBadge variant={t.tenant_id ? 'success' : 'process'}>
|
|
{t.tenant_id ? 'Custom Override' : 'System Default'}
|
|
</StatusBadge>
|
|
)},
|
|
{ key: 'preview', label: 'Title Preview', render: (t) => <span className="text-xs truncate max-w-xs block text-gray-500">{t.title_template}</span> },
|
|
{
|
|
key: 'actions',
|
|
label: 'Actions',
|
|
align: 'right',
|
|
render: (t) => (
|
|
<div className="flex justify-end gap-2">
|
|
<button onClick={() => {
|
|
setSelectedTemplate(t);
|
|
reset({
|
|
title_template: t.title_template,
|
|
message_template: t.message_template,
|
|
email_subject_template: t.email_subject_template || '',
|
|
email_body_template: t.email_body_template || '',
|
|
is_active: t.is_active
|
|
});
|
|
setModalOpen(true);
|
|
}} className="p-2 text-blue-600 hover:bg-blue-50 rounded-md transition-colors flex items-center gap-1">
|
|
<Edit className="w-4 h-4" /> <span className="text-xs font-semibold uppercase">Edit</span>
|
|
</button>
|
|
{t.tenant_id && (
|
|
<button onClick={() => handleReset(t.code)} className="p-2 text-orange-600 hover:bg-orange-50 rounded-md transition-colors flex items-center gap-1">
|
|
<RotateCcw className="w-4 h-4" /> <span className="text-xs font-semibold uppercase">Reset</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<Layout
|
|
currentPage="Notification Templates"
|
|
pageHeader={{
|
|
title: 'Custom Notifications',
|
|
description: 'Customize the content and delivery of platform notifications for your organization.',
|
|
}}
|
|
>
|
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[500px]">
|
|
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/30 gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<Building className="w-4 h-4 text-blue-500" />
|
|
<h2 className="text-sm font-semibold text-gray-700">Notification Templates</h2>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 min-w-[300px]">
|
|
<div className="flex items-center gap-2 text-xs font-medium text-gray-400 mr-1">
|
|
<Filter className="w-3.5 h-3.5" /> Filter by Module
|
|
</div>
|
|
<div className="w-full">
|
|
<FormSelect
|
|
label=""
|
|
value={selectedModule}
|
|
onValueChange={(val) => { setSelectedModule(val); setCurrentPage(1); }}
|
|
options={[
|
|
{ value: 'all', label: 'All Modules' },
|
|
...modules.map(m => ({ value: m.id, label: m.name }))
|
|
]}
|
|
placeholder="Select Module"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<DataTable columns={columns} data={templates} isLoading={isLoading} keyExtractor={(t) => t.code} />
|
|
</div>
|
|
|
|
<Pagination
|
|
currentPage={currentPage}
|
|
totalPages={totalPages}
|
|
totalItems={totalItems}
|
|
limit={limit}
|
|
onPageChange={setCurrentPage}
|
|
onLimitChange={(l) => { setLimit(l); setCurrentPage(1); }}
|
|
/>
|
|
</div>
|
|
|
|
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={`Customize: ${selectedTemplate?.code}`} maxWidth="2xl">
|
|
<div className="p-6 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider">In-App Notification</h3>
|
|
<FormField label="Title Template" required error={errors.title_template?.message} {...register('title_template')} />
|
|
<FormTextArea label="Message Template" required error={errors.message_template?.message} rows={3} {...register('message_template')} />
|
|
</div>
|
|
<div className="space-y-4">
|
|
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider">Email Notification</h3>
|
|
<FormField label="Email Subject" error={errors.email_subject_template?.message} placeholder="Inherits from title if blank" {...register('email_subject_template')} />
|
|
<FormTextArea label="Email Body (HTML)" error={errors.email_body_template?.message} rows={6} {...register('email_body_template')} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-between items-center pt-6 border-t">
|
|
<div className="text-xs text-gray-400 max-w-sm">
|
|
💡 You can use placeholders like <code>{`{{user_name}}`}</code>, <code>{`{{entity_name}}`}</code>, and <code>{`{{action}}`}</code>.
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<button onClick={() => setModalOpen(false)} className="px-5 py-2 text-sm text-gray-600 hover:text-gray-900 transition-colors">Cancel</button>
|
|
<PrimaryButton onClick={handleSubmit(onOverride)} className="px-8">Save Override</PrimaryButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</Layout>
|
|
);
|
|
};
|
|
|
|
export default NotificationTemplates;
|