Qassure-frontend/src/pages/superadmin/NotificationTemplateMaster.tsx

327 lines
14 KiB
TypeScript

import { useState, useEffect } from 'react';
import type { ReactElement } from 'react';
import { Layout } from '@/components/layout/Layout';
import {
PrimaryButton,
DataTable,
Modal,
FormField,
FormSelect,
FormTextArea,
Pagination,
type Column,
} from '@/components/shared';
import { Plus, Search, Filter } from 'lucide-react';
import { notificationService } from '@/services/notification-service';
import { moduleService } from '@/services/module-service';
import { showToast } from '@/utils/toast';
const NotificationTemplateMaster = (): ReactElement => {
const [templates, setTemplates] = useState<any[]>([]);
const [modules, setModules] = useState<any[]>([]);
const [selectedModule, setSelectedModule] = useState<string>('all');
const [categories, setCategories] = useState<any[]>([]);
const [codes, setCodes] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
// Pagination & Search
const [currentPage, setCurrentPage] = useState(1);
const [limit, setLimit] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [search, setSearch] = useState('');
// Template Modal
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [form, setForm] = useState({
code: '',
name: '',
description: '',
category: '',
title_template: '',
message_template: '',
email_subject_template: '',
email_body_template: '',
default_priority: 'normal',
channels: ['in_app', 'email'],
is_active: true
});
const fetchData = async () => {
try {
setIsLoading(true);
const [tRes, cRes, mRes] = await Promise.all([
notificationService.getSuperAdminTemplates({
limit,
offset: (currentPage - 1) * limit,
search,
module_id: selectedModule === 'all' ? undefined : selectedModule
}),
notificationService.getCategories({ limit: 100 }), // Fetch more for dropdown
moduleService.getAll(1, 100)
]);
if (tRes.success) {
setTemplates(tRes.data);
setTotalItems(tRes.pagination?.total || tRes.data.length);
setTotalPages(tRes.pagination?.pages || 1);
}
if (cRes.success) {
setCategories(cRes.data);
}
if (mRes.success) {
setModules(mRes.data);
}
} catch (err: any) {
showToast.error(err.message || 'Failed to fetch data');
} finally {
setIsLoading(false);
}
};
useEffect(() => { fetchData(); }, [currentPage, limit, search, selectedModule]);
const fetchCodesForCategory = async (categoryCode: string) => {
if (!categoryCode) return;
try {
const res = await notificationService.getCodesByCategory(categoryCode, { limit: 100 });
if (res.success) setCodes(res.data);
} catch (e) {
console.error('Failed to load codes:', e);
}
};
const handleCategorySelect = async (categoryCode: string) => {
setForm(f => ({ ...f, category: categoryCode, code: '' }));
await fetchCodesForCategory(categoryCode);
};
const handleSave = async () => {
try {
// For now we use createTemplate (which can override based on code)
await notificationService.createTemplate(form);
showToast.success(editingId ? 'Template updated' : 'Template created');
setModalOpen(false);
fetchData();
} catch (err: any) {
showToast.error(err.message || 'Action failed');
}
};
const columns: Column<any>[] = [
{ key: 'category', label: 'Category', render: (t) => <span className="text-[10px] font-bold uppercase text-blue-600 bg-blue-50 px-2 py-1 rounded">{t.category_name}</span> },
{ key: 'code', label: 'Code', render: (t) => <code className="text-xs font-mono font-bold text-gray-700">{t.code}</code> },
{ key: 'name', label: 'Friendly Name', render: (t) => <span className="text-sm font-medium">{t.name}</span> },
{ key: 'title', label: 'Preview', render: (t) => <span className="text-xs truncate max-w-[200px] block text-gray-500">{t.title_template}</span> },
{ key: 'priority', label: 'Priority', render: (t) => <span className="capitalize text-[10px] bg-gray-100 px-1.5 py-0.5 rounded">{t.default_priority}</span> },
{
key: 'channels',
label: 'Channels',
render: (t) => (
<div className="flex gap-1">
{t.channels?.map((c: string) => (
<span key={c} className="text-[9px] bg-green-50 text-green-700 px-1 rounded border border-green-200 uppercase">{c}</span>
))}
</div>
)
},
{
key: 'actions',
label: 'Actions',
align: 'right',
render: (t) => (
<button onClick={() => {
setEditingId(t.id);
setForm({
code: t.code || '',
name: t.name || '',
description: t.description || '',
category: t.category || '',
title_template: t.title_template || '',
message_template: t.message_template || '',
email_subject_template: t.email_subject_template || '',
email_body_template: t.email_body_template || '',
default_priority: t.default_priority || 'normal',
channels: t.channels || ['in_app', 'email'],
is_active: t.is_active ?? true
});
setModalOpen(true);
}} className="text-xs text-blue-600 hover:underline font-semibold">
Edit
</button>
)
}
];
return (
<Layout
currentPage="Global Templates"
pageHeader={{
title: 'Global Notification Templates',
description: 'Define default notification templates for all tenants.',
}}
>
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden flex flex-col min-h-[600px]">
<div className="p-4 border-b flex flex-wrap justify-between items-center bg-gray-50/50 gap-4">
<div className="flex items-center gap-4 flex-1">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search templates..."
className="w-full pl-9 pr-4 py-2 border rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all"
value={search}
onChange={(e) => { setSearch(e.target.value); setCurrentPage(1); }}
/>
</div>
<div className="flex items-center gap-2 min-w-[200px]">
<div className="flex items-center gap-1.5 text-xs font-medium text-gray-400 whitespace-nowrap">
<Filter className="w-3.5 h-3.5" /> 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="Filter by Module"
/>
</div>
</div>
</div>
<PrimaryButton onClick={() => {
setEditingId(null);
setForm({
code: '', name: '', description: '', category: '',
title_template: '', message_template: '',
email_subject_template: '', email_body_template: '',
default_priority: 'normal', channels: ['in_app', 'email'],
is_active: true
});
setModalOpen(true);
}} className="flex gap-2">
<Plus className="w-4 h-4" /> New Template
</PrimaryButton>
</div>
<div className="flex-1">
<DataTable columns={columns} data={templates} isLoading={isLoading} keyExtractor={(t) => t.id} />
</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={editingId ? "Edit Notification Template" : "Create New Template"} maxWidth="2xl">
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6 max-h-[80vh] overflow-y-auto custom-scrollbar">
<div className="space-y-4">
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider">Identification</h3>
<FormSelect
label="Category"
value={form.category}
onValueChange={handleCategorySelect}
options={editingId
? [{ value: form.category, label: categories.find(c => c.code === form.category)?.name || form.category }]
: categories.map(c => ({ value: c.code, label: c.name }))
}
disabled={!!editingId}
placeholder="Select Category"
/>
<FormSelect
label="Event Code"
value={form.code}
onValueChange={(val) => {
const selectedCode = codes.find(c => c.code === val);
setForm({ ...form, code: val, name: selectedCode?.name || form.name });
}}
options={editingId
? [{ value: form.code, label: `${form.name} (${form.code})` }]
: codes.map(c => ({ value: c.code, label: `${c.name} (${c.code})` }))
}
disabled={!!editingId || !form.category}
placeholder="Select Event Code"
/>
<FormField label="Friendly Name" value={form.name} onChange={(e) => setForm({...form, name: e.target.value})} placeholder="e.g. Project Assigned" />
<FormTextArea label="Description" value={form.description} onChange={(e) => setForm({...form, description: e.target.value})} rows={2} />
</div>
<div className="space-y-4">
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider">Settings</h3>
<div className="grid grid-cols-2 gap-4">
<FormSelect
label="Priority"
value={form.default_priority}
onValueChange={(val) => setForm({...form, default_priority: val})}
options={[
{ value: 'low', label: 'Low' },
{ value: 'normal', label: 'Normal' },
{ value: 'high', label: 'High' },
{ value: 'urgent', label: 'Urgent' }
]}
/>
<div className="space-y-2">
<label className="text-xs font-medium text-gray-700">Channels</label>
<div className="flex gap-4 items-center h-10 px-1">
<label className="flex items-center gap-2 text-xs">
<input type="checkbox" checked={form.channels.includes('in_app')} onChange={(e) => {
const next = e.target.checked ? [...form.channels, 'in_app'] : form.channels.filter(c => c !== 'in_app');
setForm({...form, channels: next});
}} /> In-App
</label>
<label className="flex items-center gap-2 text-xs">
<input type="checkbox" checked={form.channels.includes('email')} onChange={(e) => {
const next = e.target.checked ? [...form.channels, 'email'] : form.channels.filter(c => c !== 'email');
setForm({...form, channels: next});
}} /> Email
</label>
</div>
</div>
</div>
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider pt-2 border-t">In-App Content</h3>
<FormField label="Title Template" value={form.title_template} onChange={(e) => setForm({...form, title_template: e.target.value})} placeholder="e.g. Task Assigned: {{task_name}}" />
<FormTextArea label="Message Template" value={form.message_template} onChange={(e) => setForm({...form, message_template: e.target.value})} placeholder="Use {{var}} for dynamic data" rows={3} />
</div>
<div className="md:col-span-2 space-y-4 pt-4 border-t">
<h3 className="text-[11px] font-bold uppercase text-gray-400 tracking-wider">Email Content</h3>
<FormField
label="Email Subject"
value={form.email_subject_template}
onChange={(e) => setForm({...form, email_subject_template: e.target.value})}
placeholder="Leave blank to use In-App title"
/>
<FormTextArea
label="Email Body (HTML supported)"
value={form.email_body_template}
onChange={(e) => setForm({...form, email_body_template: e.target.value})}
placeholder="Full HTML body template..."
rows={6}
/>
</div>
<div className="md:col-span-2 flex justify-end gap-3 pt-6 border-t sticky bottom-0 bg-white">
<button onClick={() => setModalOpen(false)} className="px-5 py-2 text-sm text-gray-600 transition-colors hover:text-gray-900">Cancel</button>
<PrimaryButton onClick={handleSave} className="px-10">
{editingId ? 'Update Template' : 'Create Template'}
</PrimaryButton>
</div>
</div>
</Modal>
</Layout>
);
};
export default NotificationTemplateMaster;