502 lines
31 KiB
TypeScript
502 lines
31 KiB
TypeScript
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>
|
|
);
|
|
};
|