Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha

This commit is contained in:
Stephanos A
2026-07-08 18:24:19 +03:00
6 changed files with 447 additions and 53 deletions

View File

@@ -10,8 +10,12 @@ import ActionButton from '@/components/ui/ActionButton';
import { notificationsApi } from '@/lib/api';
import { formatDateTime } from '@/lib/utils';
const CHANNEL_OPTIONS = ['EMAIL', 'SMS', 'PUSH', 'IN_APP'];
export default function NotificationsPage() {
const [showModal, setShowModal] = useState(false);
const [editing, setEditing] = useState<any | null>(null);
const [templateError, setTemplateError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'templates' | 'send' | 'history'>('templates');
const [sendForm, setSendForm] = useState({ recipientType: 'ALL', channel: 'EMAIL', subject: '', message: '' });
const [sendError, setSendError] = useState<string | null>(null);
@@ -34,10 +38,54 @@ export default function NotificationsPage() {
mutationFn: notificationsApi.createTemplate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
setShowModal(false);
closeTemplateModal();
},
onError: (e: any) => setTemplateError(e?.response?.data?.message || e?.message || 'Failed to create template'),
});
const updateTemplateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: any }) => notificationsApi.updateTemplate(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
closeTemplateModal();
},
onError: (e: any) => setTemplateError(e?.response?.data?.message || e?.message || 'Failed to update template'),
});
const openCreateModal = () => {
setEditing(null);
setTemplateError(null);
setShowModal(true);
};
const openEditModal = (template: any) => {
setEditing(template);
setTemplateError(null);
setShowModal(true);
};
const closeTemplateModal = () => {
setShowModal(false);
setEditing(null);
setTemplateError(null);
};
const handleTemplateSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTemplateError(null);
const fd = new FormData(e.currentTarget);
const channel = fd.get('channel') as string;
const subject = (fd.get('subject') as string) || undefined;
const bodyTemplate = fd.get('bodyTemplate') as string;
const active = fd.get('active') === 'on';
if (editing) {
await updateTemplateMutation.mutateAsync({ id: editing.id, data: { channel, subject, bodyTemplate, active } });
} else {
await createTemplateMutation.mutateAsync({ code: fd.get('code') as string, channel, subject, bodyTemplate, active });
}
};
const sendMutation = useMutation({
mutationFn: notificationsApi.send,
onSuccess: () => {
@@ -59,23 +107,36 @@ export default function NotificationsPage() {
const historyArray = Array.isArray(historyData) ? historyData : (historyData as any)?.items || [];
const templateColumns = [
{ key: 'name', label: 'Template Name', render: (t: any) => <span className="font-medium">{t.name}</span> },
{ key: 'channel', label: 'Channel', render: (t: any) => <Badge>{t.channel || t.type}</Badge> },
{ key: 'code', label: 'Code / Event Key', render: (t: any) => <span className="font-mono text-sm font-medium">{t.code}</span> },
{ key: 'channel', label: 'Channel', render: (t: any) => <Badge>{t.channel}</Badge> },
{
key: 'subject',
label: 'Subject / Body',
render: (t: any) => <span className="text-sm text-muted-foreground truncate max-w-xs block">{t.subject || t.body || t.content || '—'}</span>,
render: (t: any) => (
<span className="text-sm text-muted-foreground truncate max-w-xs block">
{t.subject ? `${t.subject}` : ''}{(t.bodyTemplate || '').replace(/\n/g, ' ') || '—'}
</span>
),
},
{
key: 'active',
label: 'Status',
render: (t: any) => (
<Badge variant="status" status={t.isActive !== false ? 'CONFIRMED' : 'CANCELLED'}>
{t.isActive !== false ? 'Active' : 'Inactive'}
<Badge variant="status" status={t.active !== false ? 'CONFIRMED' : 'CANCELLED'}>
{t.active !== false ? 'Active' : 'Inactive'}
</Badge>
),
},
{ key: 'createdAt', label: 'Created', render: (t: any) => <span className="text-sm text-muted-foreground">{formatDateTime(t.createdAt)}</span> },
{
key: 'actions',
label: '',
render: (t: any) => (
<button onClick={() => openEditModal(t)} className="text-sm font-medium text-primary hover:underline">
Edit
</button>
),
},
];
const historyColumns = [
@@ -106,7 +167,7 @@ export default function NotificationsPage() {
<p className="text-muted-foreground">Manage notification templates and send messages</p>
</div>
{activeTab === 'templates' && (
<ActionButton icon={Plus} onClick={() => setShowModal(true)}>New Template</ActionButton>
<ActionButton icon={Plus} onClick={openCreateModal}>New Template</ActionButton>
)}
</div>
@@ -184,43 +245,68 @@ export default function NotificationsPage() {
</div>
)}
<Modal isOpen={showModal} onClose={() => setShowModal(false)} title="Create Notification Template">
<form
onSubmit={async (e) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
await createTemplateMutation.mutateAsync({
name: fd.get('name') as string,
channel: fd.get('channel') as string,
subject: fd.get('subject') as string,
body: fd.get('body') as string,
});
}}
className="space-y-4"
>
<Modal
isOpen={showModal}
onClose={closeTemplateModal}
title={editing ? `Edit Template — ${editing.code}` : 'Create Notification Template'}
>
<form onSubmit={handleTemplateSubmit} className="space-y-4">
<div>
<label className="label">Template Name *</label>
<input type="text" name="name" className="input" placeholder="e.g., Booking Confirmation" required />
<label className="label">Code / Event Key {editing ? '' : '*'}</label>
<input
type="text"
name="code"
className="input font-mono"
placeholder="e.g., booking.created"
defaultValue={editing?.code ?? ''}
required={!editing}
disabled={!!editing}
/>
{editing && (
<p className="mt-1 text-xs text-muted-foreground">Code is the event key and cannot be changed.</p>
)}
</div>
<div>
<label className="label">Channel</label>
<select name="channel" className="input">
<option value="EMAIL">Email</option>
<option value="SMS">SMS</option>
<option value="PUSH">Push Notification</option>
<label className="label">Channel *</label>
<select name="channel" className="input" defaultValue={editing?.channel ?? 'EMAIL'} required>
{CHANNEL_OPTIONS.map((c) => (
<option key={c} value={c}>{c}</option>
))}
<option value="SMS,EMAIL">SMS,EMAIL</option>
<option value="SMS,EMAIL,IN_APP">SMS,EMAIL,IN_APP</option>
</select>
<p className="mt-1 text-xs text-muted-foreground">
One channel, or a comma-separated list (EMAIL, SMS, PUSH, IN_APP).
</p>
</div>
<div>
<label className="label">Subject</label>
<input type="text" name="subject" className="input" placeholder="Enter subject" />
<input type="text" name="subject" className="input" placeholder="Enter subject" defaultValue={editing?.subject ?? ''} />
</div>
<div>
<label className="label">Body *</label>
<textarea name="body" className="input" rows={4} placeholder="Enter template body" required />
<textarea
name="bodyTemplate"
className="input font-mono text-sm"
rows={8}
placeholder="Enter template body"
defaultValue={editing?.bodyTemplate ?? ''}
required
/>
<p className="mt-1 text-xs text-muted-foreground">
Use <code>{'{{variable}}'}</code> placeholders (e.g. <code>{'{{passengerName}}'}</code>, <code>{'{{bookingRef}}'}</code>) they are filled in when the notification is sent.
</p>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" name="active" id="template-active" defaultChecked={editing ? editing.active !== false : true} />
<label htmlFor="template-active" className="text-sm">Active</label>
</div>
{templateError && <p className="text-sm text-red-600 dark:text-red-400">{templateError}</p>}
<div className="flex justify-end gap-2">
<ActionButton type="button" variant="secondary" onClick={() => setShowModal(false)}>Cancel</ActionButton>
<ActionButton type="submit" loading={createTemplateMutation.isPending}>Create Template</ActionButton>
<ActionButton type="button" variant="secondary" onClick={closeTemplateModal}>Cancel</ActionButton>
<ActionButton type="submit" loading={createTemplateMutation.isPending || updateTemplateMutation.isPending}>
{editing ? 'Save Changes' : 'Create Template'}
</ActionButton>
</div>
</form>
</Modal>