diff --git a/admin/src/pages/sms/NewConversationModal.tsx b/admin/src/pages/sms/NewConversationModal.tsx new file mode 100644 index 00000000..d3748165 --- /dev/null +++ b/admin/src/pages/sms/NewConversationModal.tsx @@ -0,0 +1,231 @@ +import { useState, useEffect } from 'react'; +import { Modal, Input, List, Tag, Typography, Space, Button, App } from 'antd'; +import { SendOutlined, PhoneOutlined, UserOutlined } from '@ant-design/icons'; +import { api } from '@/lib/api'; +import { useDebounce } from '@/hooks/useDebounce'; +import type { SmsContactSearchResult, SmsConversation } from '@/types/sms'; + +const { Text } = Typography; +const { TextArea } = Input; + +const SOURCE_LABELS: Record = { + sms_contact: { label: 'SMS List', color: 'purple' }, + crm_contact: { label: 'CRM', color: 'blue' }, + conversation: { label: 'Previous', color: 'default' }, +}; + +interface Props { + open: boolean; + onClose: () => void; + onCreated: (conv: SmsConversation) => void; +} + +export default function NewConversationModal({ open, onClose, onCreated }: Props) { + const { message } = App.useApp(); + + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [searching, setSearching] = useState(false); + const [selectedContact, setSelectedContact] = useState(null); + const [msgText, setMsgText] = useState(''); + const [sending, setSending] = useState(false); + + const debouncedQuery = useDebounce(query, 300); + + // Search contacts when debounced query changes + useEffect(() => { + if (!debouncedQuery || debouncedQuery.length < 2) { + setResults([]); + return; + } + let cancelled = false; + setSearching(true); + api.get<{ results: SmsContactSearchResult[] }>('/sms/conversations/contact-search', { + params: { q: debouncedQuery }, + }).then(({ data }) => { + if (!cancelled) setResults(data.results); + }).catch(() => { + if (!cancelled) setResults([]); + }).finally(() => { + if (!cancelled) setSearching(false); + }); + return () => { cancelled = true; }; + }, [debouncedQuery]); + + // Check if query looks like a phone number (mostly digits, 7+) + const digitsOnly = query.replace(/\D/g, ''); + const isPhoneQuery = digitsOnly.length >= 7; + const phoneAlreadyInResults = isPhoneQuery && results.some((r) => r.phone.includes(digitsOnly)); + + const handleSelect = (contact: SmsContactSearchResult) => { + setSelectedContact(contact); + }; + + const handleManualPhone = () => { + setSelectedContact({ + phone: digitsOnly, + name: null, + source: 'sms_contact', + sourceId: '', + }); + }; + + const handleSend = async () => { + if (!selectedContact || !msgText.trim()) return; + setSending(true); + try { + const { data } = await api.post('/sms/conversations', { + phone: selectedContact.phone, + message: msgText.trim(), + contactName: selectedContact.name || undefined, + contactId: selectedContact.contactId || undefined, + }); + message.success('Message queued'); + onCreated(data); + handleReset(); + } catch (err: any) { + const errMsg = err.response?.data?.error || 'Failed to send message'; + message.error(errMsg); + } finally { + setSending(false); + } + }; + + const handleReset = () => { + setQuery(''); + setResults([]); + setSelectedContact(null); + setMsgText(''); + onClose(); + }; + + const handleBack = () => { + setSelectedContact(null); + setMsgText(''); + }; + + return ( + + {!selectedContact ? ( + // Step 1: Contact Search +
+ } + value={query} + onChange={(e) => setQuery(e.target.value)} + allowClear + autoFocus + /> + +
+ {/* Manual phone entry option */} + {isPhoneQuery && !phoneAlreadyInResults && ( +
(e.currentTarget.style.background = 'rgba(255,255,255,0.06)')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + + + Use number: {digitsOnly} + Manual + +
+ )} + + = 2 ? 'No contacts found' : 'Type to search...' }} + renderItem={(item) => ( + handleSelect(item)} + style={{ cursor: 'pointer', padding: '8px 12px', borderRadius: 6 }} + onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,0.06)')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > +
+ + +
+ {item.name || item.phone} + {item.name && {item.phone}} +
+
+ + {SOURCE_LABELS[item.source]?.label} + +
+
+ )} + /> +
+
+ ) : ( + // Step 2: Compose Message +
+ + +
+ + +
+ {selectedContact.name && {selectedContact.name}} + + {selectedContact.phone} + +
+
+
+ +