From 6870ca6b0f8b0114440e30925a470a75c50b34dc Mon Sep 17 00:00:00 2001 From: sazzadulalambd Date: Sun, 17 May 2026 23:26:10 +0600 Subject: [PATCH] feat: add real-time notification count to sidebar and implement dedicated admin notification management dashboard --- src/app/admin/notifications/page.tsx | 1403 ++++++++++++++++++++++++++ src/components/Sidebar.tsx | 36 +- 2 files changed, 1437 insertions(+), 2 deletions(-) create mode 100644 src/app/admin/notifications/page.tsx diff --git a/src/app/admin/notifications/page.tsx b/src/app/admin/notifications/page.tsx new file mode 100644 index 0000000..a7bd67c --- /dev/null +++ b/src/app/admin/notifications/page.tsx @@ -0,0 +1,1403 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Bell, Search, Trash2, CheckCheck, Filter, Shield, FileText, Users, Wallet, Wrench, + Battery, Zap, AlertTriangle, CheckCircle, Info, Eye, Sparkles, PlusCircle, Play, ChevronRight, X, + Mail, MessageSquare, Send, Calendar, Clock, ArrowRight, EyeOff, UserCheck +} from 'lucide-react'; +import Link from 'next/link'; +import toast from 'react-hot-toast'; + +interface Notification { + id: string; + category: 'kyc' | 'rental' | 'maintenance' | 'swap_station' | 'investor' | 'system'; + priority: 'low' | 'medium' | 'high' | 'critical'; + title: string; + message: string; + time: string; + read: boolean; + meta?: { + link?: string; + actionLabel?: string; + details?: Record; + }; +} + +interface Broadcast { + id: string; + title: string; + message: string; + channels: ('email' | 'sms' | 'push')[]; + targetType: 'all' | 'group' | 'specific'; + targetValue: string; // "All Users", "Biker", "Jamal Uddin" + sentAt: string; + status: 'sent' | 'scheduled'; + scheduleTime?: string; + recipientCount: number; +} + +const mockUsersList = [ + { id: 'USR-001', name: 'Rahim Ahmed', role: 'biker', phone: '+8801712345678', email: 'rahim@jaiben.com' }, + { id: 'USR-002', name: 'Karim Hasan', role: 'biker', phone: '+8801812345678', email: 'karim@jaiben.com' }, + { id: 'USR-003', name: 'Jamal Uddin', role: 'biker', phone: '+8801912345678', email: 'jamal@jaiben.com' }, + { id: 'USR-005', name: 'Farid Ahmed', role: 'biker', phone: '+8801612345678', email: 'farid@jaiben.com' }, + { id: 'USR-010', name: 'Tariqul Islam', role: 'investor', phone: '+8801512345678', email: 'tariqul@jaiben.com' }, + { id: 'USR-011', name: 'Sofia Chowdhury', role: 'investor', phone: '+8801312345678', email: 'sofia@jaiben.com' }, + { id: 'USR-020', name: 'Alim Swap Cabinet', role: 'swap-station', phone: '+8801412345678', email: 'alim@jaiben.com' }, + { id: 'USR-030', name: 'Dhaka City Merchant', role: 'merchant', phone: '+8801212345678', email: 'merchant@jaiben.com' } +]; + +const initialNotifications: Notification[] = [ + { + id: 'notif-001', + category: 'kyc', + priority: 'high', + title: 'New KYC Request Submitted', + message: 'Biker application USR-002 (Karim Hasan) uploaded NID and driving license for review.', + time: '2026-05-17T22:30:00Z', + read: false, + meta: { + link: '/admin/kyc', + actionLabel: 'Review Documents', + details: { + 'Applicant': 'Karim Hasan', + 'Type': 'Biker', + 'Submitted At': 'Today, 10:30 PM', + 'Documents': 'NID Front, NID Back, License' + } + } + }, + { + id: 'notif-002', + category: 'rental', + priority: 'critical', + title: 'Rental Overdue Alert - Bike Lock Pending', + message: 'Rental transaction RNT-002 (Karim Hasan) is 5 days overdue. Payment of ৳3,500 is pending.', + time: '2026-05-17T21:15:00Z', + read: false, + meta: { + link: '/admin/rentals', + actionLabel: 'Manage Overdue', + details: { + 'Rental ID': 'RNT-002', + 'Rider': 'Karim Hasan', + 'Vehicle': 'Yadea DT3 (BIKE-002)', + 'Dues Pending': '৳3,500', + 'Overdue Days': '5 Days', + 'Next Action': 'Automatic lock trigger in 12 hours' + } + } + }, + { + id: 'notif-003', + category: 'maintenance', + priority: 'medium', + title: 'High Temperature Battery Warning', + message: 'EV BIKE-005 reported battery cell temperature at 52°C. Investigation recommended.', + time: '2026-05-17T19:40:00Z', + read: false, + meta: { + link: '/admin/maintenance', + actionLabel: 'Schedule Repair', + details: { + 'EV ID': 'BIKE-005', + 'Battery SOC': '45%', + 'Reported Temp': '52°C (Warning)', + 'Location': 'Uttara Area', + 'Status': 'Operational' + } + } + }, + { + id: 'notif-004', + category: 'swap_station', + priority: 'high', + title: 'Low Battery Inventory Alert', + message: 'Swap Station "Gulshan Hub Cabinets" has only 1 fully charged battery remaining in cabinets.', + time: '2026-05-17T17:00:00Z', + read: false, + meta: { + link: '/admin/swap-stations', + actionLabel: 'Restock Station', + details: { + 'Station Name': 'Gulshan Hub Station', + 'Capacity': '8 Cabinets', + 'Available Charged': '1 Battery', + 'Empty Compartments': '5 Compartments', + 'Operator': 'JAIBEN Gulshan Staff' + } + } + }, + { + id: 'notif-005', + category: 'investor', + priority: 'low', + title: 'New Investment Plan Subscribed', + message: 'Investor USR-003 (Jamal Uddin) successfully subscribed to "5 Bike Plan" Tier. Deposited ৳900,000.', + time: '2026-05-17T14:20:00Z', + read: true, + meta: { + link: '/admin/investors', + actionLabel: 'View Investment', + details: { + 'Investor': 'Jamal Uddin', + 'Plan': '5 Bike FOCO Plan', + 'Base EV Price': '৳180,000 per EV', + 'Total Investment': '৳900,000', + 'Monthly ROI': '50% share' + } + } + }, +]; + +const initialBroadcasts: Broadcast[] = [ + { + id: 'broad-001', + title: 'System Maintenance Scheduled', + message: 'Dear riders, our swap cabinet network will undergo a scheduled database upgrade on Saturday from 2:00 AM to 4:00 AM.', + channels: ['email', 'push'], + targetType: 'all', + targetValue: 'All Users', + sentAt: '2026-05-16T10:00:00Z', + status: 'sent', + recipientCount: 450 + }, + { + id: 'broad-002', + title: 'New Rental Subscriptions Active', + message: 'Exciting news! We have added high capacity battery swap add-ons. Rent a battery for only ৳1,500/month additional.', + channels: ['sms'], + targetType: 'group', + targetValue: 'biker', + sentAt: '2026-05-15T12:00:00Z', + status: 'sent', + recipientCount: 320 + } +]; + +export default function AdminNotificationsPage() { + const [notifications, setNotifications] = useState([]); + const [broadcasts, setBroadcasts] = useState([]); + + // Tabs: 'inbox' (System Events) vs 'outbox' (Admin Messaging logs) + const [activeTab, setActiveTab] = useState<'inbox' | 'outbox'>('inbox'); + + // Filters + const [filter, setFilter] = useState('all'); + const [categoryFilter, setCategoryFilter] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + // Dialog/Modals State + const [selectedNotif, setSelectedNotif] = useState(null); + const [selectedBroadcast, setSelectedBroadcast] = useState(null); + const [showComposeModal, setShowComposeModal] = useState(false); + const [showSimulatorDrawer, setShowSimulatorDrawer] = useState(false); + + // Broadcast Form State + const [composeChannels, setComposeChannels] = useState<{ email: boolean; sms: boolean; push: boolean }>({ email: true, sms: false, push: true }); + const [composeTargetType, setComposeTargetType] = useState<'all' | 'group' | 'specific'>('all'); + const [composeTargetGroup, setComposeTargetGroup] = useState('biker'); + const [composeTargetUsers, setComposeTargetUsers] = useState(['USR-002']); // Pre-select Karim Hasan (BIKER) + const [userSearchQuery, setUserSearchQuery] = useState(''); + const [composeTitle, setComposeTitle] = useState(''); + const [composeMessage, setComposeMessage] = useState(''); + const [composeIsScheduled, setComposeIsScheduled] = useState(false); + const [composeScheduleDate, setComposeScheduleDate] = useState(''); + const [composeScheduleTime, setComposeScheduleTime] = useState(''); + const [isSending, setIsSending] = useState(false); + const [sendingStep, setSendingStep] = useState(''); + + useEffect(() => { + // Sync System Notifications + const savedNotifs = localStorage.getItem('jaiben_admin_notifications'); + if (savedNotifs) { + try { setNotifications(JSON.parse(savedNotifs)); } catch (e) { + setNotifications(initialNotifications); + saveNotifications(initialNotifications); + } + } else { + setNotifications(initialNotifications); + saveNotifications(initialNotifications); + } + + // Sync Outbox Broadcasts + const savedBroadcasts = localStorage.getItem('jaiben_admin_broadcasts'); + if (savedBroadcasts) { + try { setBroadcasts(JSON.parse(savedBroadcasts)); } catch (e) { + setBroadcasts(initialBroadcasts); + saveBroadcasts(initialBroadcasts); + } + } else { + setBroadcasts(initialBroadcasts); + saveBroadcasts(initialBroadcasts); + } + }, []); + + const saveNotifications = (newNotifs: Notification[]) => { + localStorage.setItem('jaiben_admin_notifications', JSON.stringify(newNotifs)); + window.dispatchEvent(new Event('jaiben_notifications_update')); + }; + + const saveBroadcasts = (newBroadcasts: Broadcast[]) => { + localStorage.setItem('jaiben_admin_broadcasts', JSON.stringify(newBroadcasts)); + }; + + const markAsRead = (id: string) => { + const updated = notifications.map(n => n.id === id ? { ...n, read: true } : n); + setNotifications(updated); + saveNotifications(updated); + }; + + const toggleReadStatus = (id: string) => { + const updated = notifications.map(n => n.id === id ? { ...n, read: !n.read } : n); + setNotifications(updated); + saveNotifications(updated); + toast.success(updated.find(n => n.id === id)?.read ? 'Marked as read' : 'Marked as unread'); + }; + + const deleteNotification = (id: string) => { + const updated = notifications.filter(n => n.id !== id); + setNotifications(updated); + saveNotifications(updated); + toast.success('Alert deleted'); + }; + + const markAllAsRead = () => { + const updated = notifications.map(n => ({ ...n, read: true })); + setNotifications(updated); + saveNotifications(updated); + toast.success('All alerts marked as read'); + }; + + const clearAllNotifications = () => { + if (confirm('Are you sure you want to delete all alert history?')) { + setNotifications([]); + saveNotifications([]); + toast.success('Inbox alerts cleared'); + } + }; + + const deleteBroadcast = (id: string) => { + if (confirm('Delete this broadcast log?')) { + const updated = broadcasts.filter(b => b.id !== id); + setBroadcasts(updated); + saveBroadcasts(updated); + toast.success('Broadcast log deleted'); + } + }; + + // Broadcast Message submission handler + const handleSendBroadcast = (e: React.FormEvent) => { + e.preventDefault(); + + if (!composeTitle.trim()) { + toast.error('Please enter a message title'); + return; + } + if (!composeMessage.trim()) { + toast.error('Please enter the message content'); + return; + } + + if (composeTargetType === 'specific' && composeTargetUsers.length === 0) { + toast.error('Please select at least one target recipient user'); + return; + } + + const selectedChannels = Object.entries(composeChannels) + .filter(([_, enabled]) => enabled) + .map(([channel]) => channel as 'email' | 'sms' | 'push'); + + if (selectedChannels.length === 0) { + toast.error('Please select at least one dispatch channel (SMS, Email, or Push)'); + return; + } + + setIsSending(true); + + // Multi-step send animation steps + const steps = ['Establishing secured connections...', 'Parsing user database targeted rules...', 'Delivering gateway payloads...', 'Verification completed!']; + let stepIdx = 0; + setSendingStep(steps[0]); + + const interval = setInterval(() => { + stepIdx++; + if (stepIdx < steps.length) { + setSendingStep(steps[stepIdx]); + } else { + clearInterval(interval); + finalizeBroadcast(); + } + }, 600); + + const finalizeBroadcast = () => { + // Calculate fake recipient count + let recipients = 1; + let targetDesc = ''; + if (composeTargetType === 'all') { + recipients = 512; + targetDesc = 'All Active Users'; + } else if (composeTargetType === 'group') { + recipients = composeTargetGroup === 'biker' ? 320 : composeTargetGroup === 'investor' ? 84 : 42; + targetDesc = composeTargetGroup.charAt(0).toUpperCase() + composeTargetGroup.slice(1) + ' Group'; + } else { + recipients = composeTargetUsers.length; + if (recipients === 0) { + recipients = 1; + targetDesc = 'Specific User'; + } else if (recipients === 1) { + const u = mockUsersList.find(usr => usr.id === composeTargetUsers[0]); + targetDesc = u ? `${u.name} (${u.role.toUpperCase()})` : 'Specific User'; + } else if (recipients === 2) { + const u1 = mockUsersList.find(usr => usr.id === composeTargetUsers[0]); + const u2 = mockUsersList.find(usr => usr.id === composeTargetUsers[1]); + targetDesc = `${u1?.name || 'User'}, ${u2?.name || 'User'}`; + } else { + const u1 = mockUsersList.find(usr => usr.id === composeTargetUsers[0]); + const u2 = mockUsersList.find(usr => usr.id === composeTargetUsers[1]); + targetDesc = `${u1?.name || 'User'}, ${u2?.name || 'User'} + ${recipients - 2} others`; + } + } + + const newBroadcast: Broadcast = { + id: `broad-${Date.now()}`, + title: composeTitle, + message: composeMessage, + channels: selectedChannels, + targetType: composeTargetType, + targetValue: targetDesc, + sentAt: new Date().toISOString(), + status: composeIsScheduled ? 'scheduled' : 'sent', + scheduleTime: composeIsScheduled ? `${composeScheduleDate} ${composeScheduleTime}` : undefined, + recipientCount: recipients + }; + + const updated = [newBroadcast, ...broadcasts]; + setBroadcasts(updated); + saveBroadcasts(updated); + + setIsSending(false); + setShowComposeModal(false); + + // Reset form fields + setComposeTitle(''); + setComposeMessage(''); + setComposeIsScheduled(false); + + if (composeIsScheduled) { + toast.success(`Message successfully scheduled for ${newBroadcast.scheduleTime}!`); + } else { + toast.success(`Successfully broadcasted to ${recipients} users!`); + } + }; + }; + + // Simulates a system event trigger in real-time + const triggerSimulation = (type: string) => { + let newNotif: Notification; + const nowStr = new Date().toISOString(); + + switch (type) { + case 'kyc': + newNotif = { + id: `sim-${Date.now()}`, + category: 'kyc', + priority: 'high', + title: 'Walk-in KYC Document Uploaded', + message: 'Front desk officer submitted remaining documents for biker Rahim Ahmed.', + time: nowStr, + read: false, + meta: { + link: '/admin/kyc', + actionLabel: 'Approve NID', + details: { + 'Applicant': 'Rahim Ahmed', + 'Hub Agent': 'Banani Front Desk', + 'Documents Uploaded': 'TIN Certificate, Bank Account Info', + 'Priority': 'High Action Needed' + } + } + }; + toast('New Biker KYC submitted!', { icon: '📝' }); + break; + case 'overdue': + newNotif = { + id: `sim-${Date.now()}`, + category: 'rental', + priority: 'critical', + title: 'CRITICAL: EV Rental Overdue Lock Triggered', + message: 'Rider USR-005 has failed to submit payment for 7 days. Automatic immobilization initiated.', + time: nowStr, + read: false, + meta: { + link: '/admin/rentals', + actionLabel: 'Immobilize Vehicle', + details: { + 'Rental ID': 'RNT-004', + 'Rider': 'Farid Ahmed', + 'Vehicle Plate': 'Dhaka Metro Ha-5678', + 'Overdue Duration': '7 Days', + 'Current Status': 'Locked' + } + } + }; + toast.error('CRITICAL: EV Rental Overdue Lock Triggered!', { duration: 5000 }); + break; + case 'battery': + newNotif = { + id: `sim-${Date.now()}`, + category: 'maintenance', + priority: 'high', + title: 'High Speed Charge Warning', + message: 'Swap station battery BAT-DH-004 reported 90°C terminal temperatures during fast charge.', + time: nowStr, + read: false, + meta: { + link: '/admin/maintenance', + actionLabel: 'Eject Battery', + details: { + 'Battery ID': 'BAT-DH-004', + 'Location': 'Banani Hub Cabinet #3', + 'Charge SOC': '88%', + 'Status': 'Fast Charging Ejected' + } + } + }; + toast.error('High Battery Charge Temp Warning!'); + break; + case 'investment': + newNotif = { + id: `sim-${Date.now()}`, + category: 'investor', + priority: 'medium', + title: 'Partial Payment Investment Received', + message: 'Investor Karim Hasan submitted 50% deposit for 1 Bike FOCO investment.', + time: nowStr, + read: false, + meta: { + link: '/admin/investors', + actionLabel: 'Review Ledger', + details: { + 'Investor Name': 'Karim Hasan', + 'Plan Subscribed': '1 Bike Plan', + 'Deposit Received': '৳100,000 (50% partial)', + 'Journal Status': 'Draft auto-posted' + } + } + }; + toast.success('New Investment Partial Payment Received!'); + break; + default: + return; + } + + const updated = [newNotif, ...notifications]; + setNotifications(updated); + saveNotifications(updated); + }; + + const getCategoryBadge = (category: string) => { + const config: Record = { + kyc: { label: 'KYC Request', bg: 'bg-blue-50 border-blue-200', text: 'text-blue-700', icon: Shield }, + rental: { label: 'Rentals', bg: 'bg-emerald-50 border-emerald-200', text: 'text-emerald-700', icon: FileText }, + maintenance: { label: 'Maintenance', bg: 'bg-amber-50 border-amber-200', text: 'text-amber-700', icon: Wrench }, + swap_station: { label: 'Swap Station', bg: 'bg-purple-50 border-purple-200', text: 'text-purple-700', icon: Zap }, + investor: { label: 'Investor', bg: 'bg-indigo-50 border-indigo-200', text: 'text-indigo-700', icon: Wallet }, + system: { label: 'System', bg: 'bg-slate-50 border-slate-200', text: 'text-slate-700', icon: Info }, + }; + const active = config[category] || config.system; + return ( + + {active.label} + + ); + }; + + const getPriorityBadge = (priority: string) => { + const config: Record = { + low: { label: 'Low', bg: 'bg-slate-100', text: 'text-slate-700' }, + medium: { label: 'Medium', bg: 'bg-amber-100', text: 'text-amber-700' }, + high: { label: 'High', bg: 'bg-orange-100', text: 'text-orange-700' }, + critical: { label: 'Critical', bg: 'bg-red-100 animate-pulse border-red-200 border', text: 'text-red-700 font-extrabold' }, + }; + const active = config[priority] || config.low; + return ( + + {active.label} + + ); + }; + + const filteredNotifs = notifications.filter(n => { + if (filter === 'unread' && n.read) return false; + if (categoryFilter !== 'all' && n.category !== categoryFilter) return false; + if (searchQuery) { + const q = searchQuery.toLowerCase(); + return n.title.toLowerCase().includes(q) || n.message.toLowerCase().includes(q); + } + return true; + }); + + const filteredBroadcasts = broadcasts.filter(b => { + if (searchQuery) { + const q = searchQuery.toLowerCase(); + return b.title.toLowerCase().includes(q) || b.message.toLowerCase().includes(q) || b.targetValue.toLowerCase().includes(q); + } + return true; + }); + + const unreadCount = notifications.filter(n => !n.read).length; + const criticalCount = notifications.filter(n => n.priority === 'critical' && !n.read).length; + + return ( +
+ {/* Header section with responsive layout and clear actions */} +
+
+
+
+ +
+
+

Notifications & Messaging

+

Disseminate email/SMS broadcasts and inspect real-time EV triggers

+
+
+
+ + {/* Global Controls Container */} +
+ + + + + {activeTab === 'inbox' && ( + <> + + + + )} +
+
+ + {/* Dynamic Tabs: Inbox vs Outbox */} +
+ + +
+ + {/* Stats Cards Section */} +
+
+
+

Pending Alerts

+

{unreadCount}

+
+
+ +
+
+
+
+

Critical Failures

+

{criticalCount}

+
+
+ +
+
+
+
+

Total Messages Outbox

+

+ {broadcasts.reduce((acc, curr) => acc + curr.recipientCount, 0)} +

+
+
+ +
+
+
+
+

Broadcast Events

+

{broadcasts.length}

+
+
+ +
+
+
+ + {/* Main Grid Layout */} +
+ + {/* Desktop Sidebar Filters: Kept for Desktop views */} +
+
+

Filter Inbox

+ + {activeTab === 'inbox' && ( +
+ {[ + { id: 'all', label: 'All Alerts' }, + { id: 'unread', label: 'Unread Only' }, + ].map(f => ( + + ))} +
+ )} + + {activeTab === 'inbox' && ( +
+

Inbox Categories

+
+ {[ + { id: 'all', label: 'All Categories' }, + { id: 'kyc', label: 'KYC Verification' }, + { id: 'rental', label: 'Rentals & Fines' }, + { id: 'maintenance', label: 'Vehicle Service' }, + { id: 'swap_station', label: 'Cabinet Network' }, + { id: 'investor', label: 'Investor Ledger' }, + ].map(cat => ( + + ))} +
+
+ )} + + {activeTab === 'outbox' && ( +
+ View all previous announcements, targeted SMS alerts, and newsletter dispatches sent by administrators. +
+ )} +
+
+ + {/* Dynamic Main Listing Section */} +
+ + {/* Responsive scrollable Category Pills on Mobile/Tablet */} + {activeTab === 'inbox' && ( +
+ {[ + { id: 'all', label: 'All Alert Categories' }, + { id: 'kyc', label: 'KYC verification' }, + { id: 'rental', label: 'Rentals' }, + { id: 'maintenance', label: 'Maintenance' }, + { id: 'swap_station', label: 'Swap Cabinets' }, + { id: 'investor', label: 'Investors' }, + ].map(cat => ( + + ))} +
+ )} + + {/* Search bar */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-3 border border-slate-200 rounded-xl text-sm bg-white text-slate-700 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" + /> +
+ + {/* List items Container */} +
+ + {/* ALERT INBOX TAB VIEW */} + {activeTab === 'inbox' && ( + filteredNotifs.length === 0 ? ( +
+ +

No alerts found

+

Try modifying your filter settings or search query.

+
+ ) : ( + filteredNotifs.map(notif => ( +
+
setSelectedNotif(notif)}> +
+ {getCategoryBadge(notif.category)} + {getPriorityBadge(notif.priority)} + + {new Date(notif.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })},{' '} + {new Date(notif.time).toLocaleDateString([], { month: 'short', day: 'numeric' })} + +
+

+ {!notif.read && } + {notif.title} +

+

{notif.message}

+
+ +
+ + +
+
+ )) + ) + )} + + {/* BROADCAST OUTBOX LOGS TAB VIEW */} + {activeTab === 'outbox' && ( + filteredBroadcasts.length === 0 ? ( +
+ +

No sent broadcasts

+

Announcements sent to riders/investors will appear here.

+
+ ) : ( + filteredBroadcasts.map(broad => ( +
+
setSelectedBroadcast(broad)}> +
+ + Broadcast Log + + + {/* Selected Channels Icons */} +
+ {broad.channels.includes('email') && } + {broad.channels.includes('sms') && } + {broad.channels.includes('push') && } +
+ + {/* Status tag */} + + {broad.status} + + + + {new Date(broad.sentAt).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} + +
+ +

{broad.title}

+

{broad.message}

+ +
+ Target: {broad.targetValue} + Delivered: {broad.recipientCount} accounts +
+
+ +
+ +
+
+ )) + ) + )} +
+
+
+ + {/* =========================================Compose MESSAGE BROADCAST MODAL ========================================= */} + {showComposeModal && ( +
+
+ + {/* Modal Header */} +
+
+ +

Compose Message & Broadcast

+
+ +
+ + {/* Modal Form */} +
+ + {/* Channel Selector Checkboxes */} +
+ +
+ {[ + { id: 'email', label: 'Email Dispatch', icon: Mail, color: 'border-blue-200 text-blue-600 bg-blue-50/20' }, + { id: 'sms', label: 'SMS Payload', icon: MessageSquare, color: 'border-emerald-200 text-emerald-600 bg-emerald-50/20' }, + { id: 'push', label: 'Push Alert', icon: Bell, color: 'border-amber-200 text-amber-600 bg-amber-50/20' }, + ].map(ch => { + const enabled = composeChannels[ch.id as 'email' | 'sms' | 'push']; + return ( + + ); + })} +
+
+ + {/* Recipient Target Rules */} +
+ +
+ {[ + { id: 'all', label: 'All Users' }, + { id: 'group', label: 'User Group' }, + { id: 'specific', label: 'Specific User' } + ].map(t => ( + + ))} +
+ + {/* Suboptions based on selected target */} + {composeTargetType === 'group' && ( +
+ + +
+ )} + + {composeTargetType === 'specific' && ( +
+
+ + {composeTargetUsers.length > 0 && ( + + )} +
+ + {/* Selected Users Badges / Pills */} + {composeTargetUsers.length > 0 && ( +
+ {composeTargetUsers.map(userId => { + const u = mockUsersList.find(usr => usr.id === userId); + if (!u) return null; + return ( + + {u.name} ({u.role.toUpperCase()}) + + + ); + })} +
+ )} + + {/* Search Field */} +
+ + setUserSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-1.5 border border-slate-200 rounded-lg text-xs bg-white text-slate-700 focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+ + {/* Quick helper controls */} +
+ +
+ + {/* Scrollable list of users */} +
+ {mockUsersList + .filter(usr => { + const q = userSearchQuery.toLowerCase(); + return usr.name.toLowerCase().includes(q) || usr.phone.includes(q) || usr.role.includes(q); + }) + .map(usr => { + const isSelected = composeTargetUsers.includes(usr.id); + return ( +
{ + setComposeTargetUsers(prev => + isSelected ? prev.filter(id => id !== usr.id) : [...prev, usr.id] + ); + }} + className={`flex items-center justify-between p-2 text-xs cursor-pointer hover:bg-slate-50 transition-colors ${ + isSelected ? 'bg-accent/5' : '' + }`} + > +
+
+ {usr.name.charAt(0)} +
+
+

{usr.name}

+

{usr.phone} • {usr.role}

+
+
+ {}} // toggled by parent div onClick + className="w-3.5 h-3.5 accent-accent" + /> +
+ ); + })} +
+
+ )} +
+ + {/* Subject Title */} +
+ + setComposeTitle(e.target.value)} + className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm bg-white focus:outline-none focus:ring-1 focus:ring-accent" + /> +
+ + {/* Message Content */} +
+
+ + {composeMessage.length} characters +
+