import React, { useState, useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../api/client'; import { MessageSquare, Send, Users, Mail, Smartphone, Bell, CheckCircle2, Clock, Eye, AlertCircle, X, Loader2, Search, UserPlus, Filter } from 'lucide-react'; import toast from 'react-hot-toast'; // UI Components import Card, { CardHeader, CardBody, CardFooter } from '../components/ui/Card'; import Button, { SubmitButton } from '../components/ui/Button'; import FormInput from '../components/ui/FormInput'; import FormTextarea from '../components/ui/FormTextarea'; import FormSelect from '../components/ui/FormSelect'; import TabGroup from '../components/ui/TabGroup'; import Badge from '../components/ui/Badge'; import EmptyState from '../components/ui/EmptyState'; // Types interface BroadcastMessage { id: string; subject: string; body: string; target_roles: string[]; target_users: string[]; delivery_method: 'IN_APP' | 'EMAIL' | 'SMS' | 'ALL'; status: 'DRAFT' | 'SENDING' | 'SENT' | 'FAILED'; total_recipients: number; delivered_count: number; read_count: number; created_at: string; sent_at: string | null; created_by: string; created_by_name: string; } interface RecipientOption { id: string; name: string; email: string; role: string; } interface RecipientOptionsResponse { users: RecipientOption[]; } type TabType = 'compose' | 'sent'; // Local Component for Selection Tiles interface SelectionTileProps { selected: boolean; onClick: () => void; icon: React.ElementType; label: string; description?: string; } const SelectionTile: React.FC = ({ selected, onClick, icon: Icon, label, description }) => (
{label} {description && ( {description} )} {selected && (
)}
); const Messages: React.FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); // State const [activeTab, setActiveTab] = useState('compose'); const [subject, setSubject] = useState(''); const [body, setBody] = useState(''); const [selectedRoles, setSelectedRoles] = useState([]); const [selectedUsers, setSelectedUsers] = useState([]); const [deliveryMethod, setDeliveryMethod] = useState<'IN_APP' | 'EMAIL' | 'SMS' | 'ALL'>('IN_APP'); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState<'ALL' | 'SENT' | 'SENDING' | 'FAILED'>('ALL'); const [selectedMessage, setSelectedMessage] = useState(null); const [recipientSearchTerm, setRecipientSearchTerm] = useState(''); const [isRecipientDropdownOpen, setIsRecipientDropdownOpen] = useState(false); const [visibleRecipientCount, setVisibleRecipientCount] = useState(20); const dropdownRef = useRef(null); // Queries const { data: messages = [], isLoading: messagesLoading } = useQuery({ queryKey: ['broadcast-messages'], queryFn: async () => { const response = await api.get('/messages/broadcast-messages/'); return response.data; }, }); const { data: recipientOptions, isLoading: recipientsLoading } = useQuery({ queryKey: ['message-recipient-options'], queryFn: async () => { const response = await api.get('/messages/broadcast-messages/recipient_options/'); return response.data; }, }); // Mutations const createMessage = useMutation({ mutationFn: async (data: { subject: string; body: string; target_roles: string[]; target_users: string[]; delivery_method: string; }) => { const response = await api.post('/messages/broadcast-messages/', data); return response.data; }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['broadcast-messages'] }); // Auto-send the message sendMessage.mutate(data.id); }, onError: (error: any) => { toast.error(error.response?.data?.error || 'Failed to create message'); }, }); const sendMessage = useMutation({ mutationFn: async (messageId: string) => { const response = await api.post(`/messages/broadcast-messages/${messageId}/send/`); return response.data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['broadcast-messages'] }); toast.success('Message sent successfully!'); resetForm(); setActiveTab('sent'); }, onError: (error: any) => { toast.error(error.response?.data?.error || 'Failed to send message'); }, }); // All available target roles (excluding 'everyone' which is a meta-option) const allRoles = ['owner', 'staff', 'customer']; // Handlers const handleRoleToggle = (role: string) => { if (role === 'everyone') { // Toggle all roles on/off setSelectedRoles((prev) => prev.length === allRoles.length ? [] : [...allRoles] ); } else { setSelectedRoles((prev) => prev.includes(role) ? prev.filter((r) => r !== role) : [...prev, role] ); } }; // Check if all roles are selected (for "Everyone" tile) const isEveryoneSelected = allRoles.every(role => selectedRoles.includes(role)); const handleAddUser = (user: RecipientOption) => { if (!selectedUsers.find(u => u.id === user.id)) { setSelectedUsers((prev) => [...prev, user]); } // Clear search and close dropdown for tag-style input setRecipientSearchTerm(''); setIsRecipientDropdownOpen(false); }; const handleRemoveUser = (userId: string) => { setSelectedUsers((prev) => prev.filter((u) => u.id !== userId)); }; const handleDropdownScroll = useCallback((e: React.UIEvent) => { const target = e.target as HTMLDivElement; const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight; // Load more when within 50px of bottom if (scrollBottom < 50) { setVisibleRecipientCount((prev) => prev + 20); } }, []); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Validation if (!subject.trim()) { toast.error('Subject is required'); return; } if (!body.trim()) { toast.error('Message body is required'); return; } if (selectedRoles.length === 0 && selectedUsers.length === 0) { toast.error('Please select at least one recipient'); return; } createMessage.mutate({ subject, body, target_roles: selectedRoles, target_users: selectedUsers.map(u => u.id), delivery_method: deliveryMethod, }); }; const resetForm = () => { setSubject(''); setBody(''); setSelectedRoles([]); setSelectedUsers([]); setDeliveryMethod('IN_APP'); setRecipientSearchTerm(''); setIsRecipientDropdownOpen(false); setVisibleRecipientCount(20); }; // Computed const roleOptions = [ { value: 'owner', label: 'Owners', icon: Users, description: 'Business owners' }, { value: 'staff', label: 'Staff', icon: Users, description: 'Employees' }, { value: 'customer', label: 'Customers', icon: Users, description: 'Clients' }, { value: 'everyone', label: 'Everyone', icon: Users, description: 'All users' }, ]; const deliveryMethodOptions = [ { value: 'IN_APP' as const, label: 'In-App', icon: Bell, description: 'Notifications only' }, { value: 'EMAIL' as const, label: 'Email', icon: Mail, description: 'Send via email' }, { value: 'SMS' as const, label: 'SMS', icon: Smartphone, description: 'Text message' }, { value: 'ALL' as const, label: 'All Channels', icon: MessageSquare, description: 'Maximum reach' }, ]; const filteredMessages = useMemo(() => { let filtered = messages; // Status filter if (statusFilter !== 'ALL') { filtered = filtered.filter((msg) => msg.status === statusFilter); } // Search filter if (searchTerm) { const term = searchTerm.toLowerCase(); filtered = filtered.filter( (msg) => msg.subject.toLowerCase().includes(term) || msg.body.toLowerCase().includes(term) || msg.created_by_name.toLowerCase().includes(term) ); } return filtered; }, [messages, statusFilter, searchTerm]); const filteredRecipients = useMemo(() => { if (!recipientOptions) return []; // Filter out already selected users const selectedIds = selectedUsers.map(u => u.id); let filtered = recipientOptions.users.filter(u => !selectedIds.includes(u.id)); // Apply search filter if (recipientSearchTerm) { const term = recipientSearchTerm.toLowerCase(); filtered = filtered.filter( (user) => user.name.toLowerCase().includes(term) || user.email.toLowerCase().includes(term) ); } return filtered; }, [recipientOptions, recipientSearchTerm, selectedUsers]); const recipientCount = useMemo(() => { let count = 0; // Count users from selected roles if (recipientOptions) { selectedRoles.forEach((role) => { count += recipientOptions.users.filter((u) => u.role === role).length; }); } // Add individually selected users (avoid double counting) const roleUserIds = recipientOptions?.users .filter((u) => selectedRoles.includes(u.role)) .map((u) => u.id) || []; count += selectedUsers.filter((u) => !roleUserIds.includes(u.id)).length; return count; }, [selectedRoles, selectedUsers, recipientOptions]); const getDeliveryMethodIcon = (method: string) => { switch (method) { case 'IN_APP': return ; case 'EMAIL': return ; case 'SMS': return ; case 'ALL': return ; default: return ; } }; const getStatusBadge = (status: string) => { switch (status) { case 'SENT': return Sent; case 'SENDING': return Sending; case 'FAILED': return Failed; default: return Draft; } }; const formatDate = (dateStr: string | null) => { if (!dateStr) return 'Not sent'; return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', }).format(new Date(dateStr)); }; const getTargetDescription = (message: BroadcastMessage) => { const parts: string[] = []; if (message.target_roles.length > 0) { const roleLabels = message.target_roles.map((role) => { const option = roleOptions.find((opt) => opt.value === role); return option?.label || role; }); parts.push(...roleLabels); } if (message.target_users.length > 0) { parts.push(`${message.target_users.length} user(s)`); } return parts.join(', '); }; return (
{/* Header */}

Broadcast Messages

Reach your staff and customers across multiple channels.

{/* Tabs */} }, { id: 'sent', label: `Sent History ${messages.length > 0 ? `(${messages.length})` : ''}`, icon: } ]} activeTab={activeTab} onChange={(id) => setActiveTab(id as TabType)} className="w-full sm:w-auto" /> {/* Compose Tab */} {activeTab === 'compose' && (

Message Details

{/* Target Selection */}
{roleOptions.map((role) => ( handleRoleToggle(role.value)} /> ))}
{/* Individual Recipients Search */}
{ setRecipientSearchTerm(e.target.value); setVisibleRecipientCount(20); setIsRecipientDropdownOpen(e.target.value.length > 0); }} onFocus={() => { if (recipientSearchTerm.length > 0) { setIsRecipientDropdownOpen(true); } }} placeholder="Search for specific people..." className="w-full pl-11 pr-4 py-3 border border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50 focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-all outline-none" /> {recipientsLoading && recipientSearchTerm && ( )} {/* Dropdown Results */} {isRecipientDropdownOpen && recipientSearchTerm && !recipientsLoading && ( <>
setIsRecipientDropdownOpen(false)} />
{filteredRecipients.length === 0 ? (

No matching users found

) : (
{filteredRecipients.slice(0, visibleRecipientCount).map((user) => ( ))}
)}
)}
{/* Selected Users Chips */} {selectedUsers.length > 0 && (
{selectedUsers.map((user) => (
{user.name} {user.role}
))}
)}

{/* Message Content */}
setSubject(e.target.value)} placeholder="Brief summary of your message..." required fullWidth /> setBody(e.target.value)} rows={6} placeholder="Write your message here..." required fullWidth hint="You can use plain text. Links will be automatically detected." />

{/* Delivery Method */}
{deliveryMethodOptions.map((option) => ( setDeliveryMethod(option.value)} /> ))}
{/* Recipient Count Summary */} {recipientCount > 0 && (

Ready to Broadcast

This message will be sent to approximately {recipientCount} recipient{recipientCount !== 1 ? 's' : ''} via {deliveryMethodOptions.find(o => o.value === deliveryMethod)?.label}.

)} } variant="primary" size="lg" > Send Broadcast )} {/* Sent Messages Tab */} {activeTab === 'sent' && (
{/* Filters Bar */}
setSearchTerm(e.target.value)} placeholder="Search subject, body, or sender..." className="w-full pl-10 pr-4 py-2 border-none bg-transparent focus:ring-0 text-gray-900 dark:text-white placeholder-gray-400" />
{/* Messages List */} {messagesLoading ? (

Loading messages...

) : filteredMessages.length === 0 ? ( } title="No messages found" description={searchTerm || statusFilter !== 'ALL' ? "Try adjusting your filters to see more results." : "You haven't sent any broadcast messages yet."} action={ statusFilter === 'ALL' && !searchTerm ? ( ) : undefined } /> ) : (
{filteredMessages.map((message) => ( setSelectedMessage(message)} className="group transition-all duration-200 border-l-4 border-l-transparent hover:border-l-brand-500" padding="lg" >
{getStatusBadge(message.status)}

{message.subject}

{message.body}

{getTargetDescription(message)}
{getDeliveryMethodIcon(message.delivery_method)} {message.delivery_method.toLowerCase().replace('_', ' ')}
{formatDate(message.sent_at || message.created_at)}
{message.status === 'SENT' ? (
Sent
{message.total_recipients}
Read
{message.read_count}
) : (
Draft
)}
by {message.created_by_name}
))}
)}
)} {/* Message Detail Modal - Using simple fixed overlay for now since Modal component wasn't in list but likely exists. keeping existing logic with better styling */} {selectedMessage && (
{getStatusBadge(selectedMessage.status)} {formatDate(selectedMessage.sent_at || selectedMessage.created_at)}

{selectedMessage.subject}

{/* Stats Cards */} {selectedMessage.status === 'SENT' && (
{selectedMessage.total_recipients}
Recipients
{selectedMessage.delivered_count}
Delivered
{selectedMessage.read_count}
Read
)} {/* Message Body */}

Message Content

{selectedMessage.body}
{/* Meta Info */}

Recipients

{getTargetDescription(selectedMessage)}

Delivery Method

{getDeliveryMethodIcon(selectedMessage.delivery_method)} {selectedMessage.delivery_method.toLowerCase().replace('_', ' ')}
Sent by {selectedMessage.created_by_name}
)}
); }; export default Messages;