Added PFM Console UI project
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Shield,
|
||||
History,
|
||||
CheckCircle,
|
||||
Bell,
|
||||
Send,
|
||||
Radio,
|
||||
Filter,
|
||||
UserCheck,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
getAuditLogs,
|
||||
} from '../services/api';
|
||||
import { Modal } from '../components/ui/Modal';
|
||||
import { useToast } from '../components/ui/Toast';
|
||||
import { useAuth } from '../auth/AuthProvider';
|
||||
import {
|
||||
EmailStatuses,
|
||||
NotificationStatus,
|
||||
NotificationPlatform,
|
||||
} from '../types';
|
||||
import {
|
||||
formatDate,
|
||||
getEmailStatusLabel,
|
||||
getEmailStatusBadge,
|
||||
getNotificationStatusLabel,
|
||||
getNotificationStatusBadge,
|
||||
getNotificationPlatformLabel,
|
||||
getUserEmail,
|
||||
EMAIL_STATUSES_OPTIONS,
|
||||
NOTIFICATION_STATUS_OPTIONS,
|
||||
NOTIFICATION_PLATFORM_OPTIONS,
|
||||
} from '../lib/formatters';
|
||||
|
||||
interface NotificationItem {
|
||||
id: number;
|
||||
recipient: string;
|
||||
platform: NotificationPlatform;
|
||||
status: NotificationStatus;
|
||||
subject: string;
|
||||
sentAt: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { success, error: toastError } = useToast();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'notifications' | 'audit' | 'profile'>('notifications');
|
||||
const [isSendNotificationModalOpen, setIsSendNotificationModalOpen] = useState(false);
|
||||
|
||||
// Email Gateway & Notification Engine states
|
||||
const [emailGatewayStatus, setEmailGatewayStatus] = useState<EmailStatuses>(EmailStatuses.Connected);
|
||||
const [selectedPlatformFilter, setSelectedPlatformFilter] = useState<number | 'all'>('all');
|
||||
const [selectedStatusFilter, setSelectedStatusFilter] = useState<number | 'all'>('all');
|
||||
|
||||
// Dispatch Form State
|
||||
const [dispatchPlatform, setDispatchPlatform] = useState<NotificationPlatform>(NotificationPlatform.Email);
|
||||
const [dispatchStatus, setDispatchStatus] = useState<NotificationStatus>(NotificationStatus.Sent);
|
||||
const [dispatchRecipient, setDispatchRecipient] = useState('');
|
||||
const [dispatchSubject, setDispatchSubject] = useState('');
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
|
||||
// Queries
|
||||
const { data: auditLogs = [], isLoading: loadingAudit } = useQuery({
|
||||
queryKey: ['auditLogs'],
|
||||
queryFn: () => getAuditLogs(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white tracking-tight">
|
||||
System Settings & Communications
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Communication dispatch gateways, statutory audit trail, and active session claims.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{activeTab === 'notifications' && (
|
||||
<button
|
||||
onClick={() => setIsSendNotificationModalOpen(true)}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
Dispatch Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b border-slate-800/80 pb-3 overflow-x-auto">
|
||||
<button
|
||||
onClick={() => setActiveTab('notifications')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap ${activeTab === 'notifications'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
Notifications & Email Gateways ({notifications.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('audit')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap ${activeTab === 'audit'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
Statutory Audit Trail ({auditLogs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('profile')}
|
||||
className={`px-4 py-2 text-xs font-medium rounded-xl transition flex items-center gap-2 whitespace-nowrap ${activeTab === 'profile'
|
||||
? 'bg-blue-600 text-white shadow-lg shadow-blue-600/30'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
|
||||
}`}
|
||||
>
|
||||
<UserCheck className="w-4 h-4" />
|
||||
Session & Token Claims
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab 1: Notifications & Email Gateways */}
|
||||
{activeTab === 'notifications' && (
|
||||
<div className="space-y-6">
|
||||
{/* Status KPI Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-400">Email Gateway Status</span>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-semibold border ${getEmailStatusBadge(emailGatewayStatus).className}`}>
|
||||
{getEmailStatusBadge(emailGatewayStatus).label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<label className="block text-[11px] font-medium text-slate-400 mb-1.5">Update Gateway Status:</label>
|
||||
<select
|
||||
value={emailGatewayStatus}
|
||||
onChange={(e) => {
|
||||
const newStatus = Number(e.target.value) as EmailStatuses;
|
||||
setEmailGatewayStatus(newStatus);
|
||||
success('Gateway State Updated', `Email service transitioned to ${getEmailStatusLabel(newStatus)}`);
|
||||
}}
|
||||
className="w-full text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium transition [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{EMAIL_STATUSES_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400">Queued / Active Pipeline</span>
|
||||
<div className="text-2xl font-bold font-mono text-white">
|
||||
{notifications.filter((n) => n.status === NotificationStatus.Pending || n.status === NotificationStatus.Queued || n.status === NotificationStatus.Processing).length}
|
||||
</div>
|
||||
<span className="text-[11px] text-slate-400">Automated retry scheduler active</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400">Delivery Success Ratio</span>
|
||||
<div className="text-2xl font-bold font-mono text-emerald-400">97.4%</div>
|
||||
<span className="text-[11px] text-slate-400">Cross-platform SLA target: ≥95.0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-slate-300">
|
||||
<Filter className="w-4 h-4 text-slate-400" />
|
||||
Filter Notification Logs:
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs text-slate-400 font-medium">Platform:</label>
|
||||
<select
|
||||
value={selectedPlatformFilter}
|
||||
onChange={(e) => setSelectedPlatformFilter(e.target.value === 'all' ? 'all' : Number(e.target.value))}
|
||||
className="text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium transition [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value="all">All Channels ({NOTIFICATION_PLATFORM_OPTIONS.length})</option>
|
||||
{NOTIFICATION_PLATFORM_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs text-slate-400 font-medium">Status:</label>
|
||||
<select
|
||||
value={selectedStatusFilter}
|
||||
onChange={(e) => setSelectedStatusFilter(e.target.value === 'all' ? 'all' : Number(e.target.value))}
|
||||
className="text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium transition [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
<option value="all">All Statuses ({NOTIFICATION_STATUS_OPTIONS.length})</option>
|
||||
{NOTIFICATION_STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications Table */}
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||
<th className="py-3.5 px-5">Platform Channel</th>
|
||||
<th className="py-3.5 px-4">Recipient Destination</th>
|
||||
<th className="py-3.5 px-4">Subject / Notification Message</th>
|
||||
<th className="py-3.5 px-4">Dispatch Timestamp</th>
|
||||
<th className="py-3.5 px-4">Status</th>
|
||||
<th className="py-3.5 px-5 text-right">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{notifications
|
||||
.filter((n) => selectedPlatformFilter === 'all' || n.platform === selectedPlatformFilter)
|
||||
.filter((n) => selectedStatusFilter === 'all' || n.status === selectedStatusFilter)
|
||||
.map((item) => {
|
||||
const statusBadge = getNotificationStatusBadge(item.status);
|
||||
return (
|
||||
<tr key={item.id} className="hover:bg-slate-800/40 transition-colors">
|
||||
<td className="py-3.5 px-5">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[11px] font-semibold bg-slate-800/80 text-slate-200 border border-slate-700/80">
|
||||
<Radio className="w-3 h-3 text-blue-400" />
|
||||
{getNotificationPlatformLabel(item.platform)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3.5 px-4 font-mono font-semibold text-white">
|
||||
{item.recipient}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 max-w-xs truncate text-slate-300">
|
||||
<div className="font-semibold text-white truncate">{item.subject}</div>
|
||||
{item.errorMessage && (
|
||||
<div className="text-[10px] text-rose-400 font-mono mt-0.5">{item.errorMessage}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-slate-400 font-mono text-[11px]">
|
||||
{formatDate(item.sentAt)}
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<select
|
||||
value={item.status}
|
||||
onChange={(e) => {
|
||||
const newStatus = Number(e.target.value) as NotificationStatus;
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) => (n.id === item.id ? { ...n, status: newStatus } : n))
|
||||
);
|
||||
success('Status Updated', `Notification #${item.id} status updated to ${getNotificationStatusLabel(newStatus)}`);
|
||||
}}
|
||||
className={`text-[10px] font-semibold border rounded-full px-2 py-0.5 bg-slate-900 cursor-pointer ${statusBadge.className} [&>option]:bg-slate-900 [&>option]:text-white`}
|
||||
>
|
||||
{NOTIFICATION_STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</td>
|
||||
<td className="py-3.5 px-5 text-right">
|
||||
<button
|
||||
onClick={() => {
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) =>
|
||||
n.id === item.id
|
||||
? { ...n, status: NotificationStatus.Delivered, errorMessage: undefined, sentAt: new Date().toISOString() }
|
||||
: n
|
||||
)
|
||||
);
|
||||
success('Re-dispatched', `Notification #${item.id} re-sent successfully via ${getNotificationPlatformLabel(item.platform)}`);
|
||||
}}
|
||||
className="px-2.5 py-1 text-[11px] font-semibold text-slate-300 hover:text-white bg-slate-800/80 hover:bg-slate-700 rounded-lg border border-slate-700/60 transition"
|
||||
>
|
||||
Retry / Resend
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Statutory Audit Trail */}
|
||||
{activeTab === 'audit' && (
|
||||
<div className="bg-[#0f1523]/80 backdrop-blur-md rounded-2xl border border-slate-800/80 shadow-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 uppercase tracking-wider font-semibold">
|
||||
<th className="py-3.5 px-5">Timestamp</th>
|
||||
<th className="py-3.5 px-4">Action Type</th>
|
||||
<th className="py-3.5 px-4">Target Entity</th>
|
||||
<th className="py-3.5 px-4">Actor Email</th>
|
||||
<th className="py-3.5 px-5 text-right">Origin Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/50">
|
||||
{loadingAudit ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-slate-500">
|
||||
Loading audit log entries...
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
auditLogs.map((log) => (
|
||||
<tr key={log.id} className="hover:bg-slate-800/40 transition-colors">
|
||||
<td className="py-3 px-5 text-slate-400 font-mono">{formatDate(log.createdAt)}</td>
|
||||
<td className="py-3 px-4 font-bold text-white">{log.action}</td>
|
||||
<td className="py-3 px-4 text-blue-400 font-medium font-mono">
|
||||
{log.entityType} #{log.entityId}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-mono font-medium text-slate-200">
|
||||
{getUserEmail(log.userEmail)}
|
||||
</td>
|
||||
<td className="py-3 px-5 text-right text-slate-400 font-mono text-[11px]">
|
||||
{log.details || 'Web Session'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 3: Session & Token Claims */}
|
||||
{activeTab === 'profile' && (
|
||||
<div className="max-w-2xl bg-[#0f1523]/80 backdrop-blur-md p-6 rounded-2xl border border-slate-800/80 shadow-xl space-y-5">
|
||||
<div className="flex items-center gap-3 pb-4 border-b border-slate-800/80">
|
||||
<div className="h-12 w-12 rounded-full bg-gradient-to-r from-blue-600 to-indigo-600 flex items-center justify-center font-bold text-white text-base shadow-lg shadow-blue-500/25">
|
||||
{user?.name?.slice(0, 2).toUpperCase() || 'PF'}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white">{user?.name}</h3>
|
||||
<p className="text-xs text-slate-400 font-mono">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 text-xs">
|
||||
<h4 className="text-xs font-bold text-slate-300 uppercase tracking-wider">
|
||||
Token Claims Accessible by UI
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
|
||||
<div className="p-3.5 bg-slate-900/60 rounded-xl border border-slate-800/80">
|
||||
<span className="text-slate-400 block text-[11px] font-medium">User Identifier (sub)</span>
|
||||
<span className="font-mono text-white font-semibold">{user?.id}</span>
|
||||
</div>
|
||||
<div className="p-3.5 bg-slate-900/60 rounded-xl border border-slate-800/80">
|
||||
<span className="text-slate-400 block text-[11px] font-medium">Email Address (email)</span>
|
||||
<span className="font-mono text-white font-semibold">{user?.email}</span>
|
||||
</div>
|
||||
<div className="p-3.5 bg-slate-900/60 rounded-xl border border-slate-800/80">
|
||||
<span className="text-slate-400 block text-[11px] font-medium">Full Name (name)</span>
|
||||
<span className="text-white font-semibold">{user?.name}</span>
|
||||
</div>
|
||||
<div className="p-3.5 bg-slate-900/60 rounded-xl border border-slate-800/80">
|
||||
<span className="text-slate-400 block text-[11px] font-medium">Assigned Role (role)</span>
|
||||
<span className="text-blue-400 font-semibold">{user?.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal: Dispatch Multi-Channel Notification */}
|
||||
<Modal
|
||||
isOpen={isSendNotificationModalOpen}
|
||||
onClose={() => setIsSendNotificationModalOpen(false)}
|
||||
title="Dispatch Outbound Notification"
|
||||
subtitle="Route message through configured multi-channel notification gateway"
|
||||
>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!dispatchRecipient || !dispatchSubject) {
|
||||
toastError('Validation Error', 'Recipient and notification message are required');
|
||||
return;
|
||||
}
|
||||
const newItem: NotificationItem = {
|
||||
id: Date.now(),
|
||||
recipient: dispatchRecipient,
|
||||
platform: dispatchPlatform,
|
||||
status: dispatchStatus,
|
||||
subject: dispatchSubject,
|
||||
sentAt: new Date().toISOString(),
|
||||
};
|
||||
setNotifications((prev) => [newItem, ...prev]);
|
||||
success('Notification Queued', `Message queued for delivery via ${getNotificationPlatformLabel(dispatchPlatform)} (Status: ${getNotificationStatusLabel(dispatchStatus)})`);
|
||||
setIsSendNotificationModalOpen(false);
|
||||
setDispatchRecipient('');
|
||||
setDispatchSubject('');
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Notification Platform Channel *
|
||||
</label>
|
||||
<select
|
||||
value={dispatchPlatform}
|
||||
onChange={(e) => setDispatchPlatform(Number(e.target.value) as NotificationPlatform)}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium transition [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{NOTIFICATION_PLATFORM_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Initial Status Pipeline *
|
||||
</label>
|
||||
<select
|
||||
value={dispatchStatus}
|
||||
onChange={(e) => setDispatchStatus(Number(e.target.value) as NotificationStatus)}
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-medium transition [&>option]:bg-slate-900 [&>option]:text-white"
|
||||
>
|
||||
{NOTIFICATION_STATUS_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Recipient Destination Address / Handle *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={dispatchRecipient}
|
||||
onChange={(e) => setDispatchRecipient(e.target.value)}
|
||||
placeholder="e.g. user@funder.org.za or +27 82 123 4567 or https://webhook..."
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 font-mono transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
|
||||
Message Subject / Body *
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={dispatchSubject}
|
||||
onChange={(e) => setDispatchSubject(e.target.value)}
|
||||
placeholder="Enter notification content or payload summary..."
|
||||
className="w-full px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-3 border-t border-slate-800/80 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSendNotificationModalOpen(false)}
|
||||
className="px-4 py-2 text-xs font-medium text-slate-300 bg-slate-800/80 hover:bg-slate-700 rounded-xl transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-lg shadow-blue-500/20 transition"
|
||||
>
|
||||
Dispatch Notification
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user