diff --git a/frontend/src/pages/Inbox.tsx b/frontend/src/pages/Inbox.tsx index a30c826..47d4d89 100644 --- a/frontend/src/pages/Inbox.tsx +++ b/frontend/src/pages/Inbox.tsx @@ -11,8 +11,22 @@ import { Spin, Space, Badge, + Dropdown, + Select, + Tooltip, + Modal, + message, } from 'antd'; -import { SendOutlined, UserOutlined } from '@ant-design/icons'; +import { + SendOutlined, + UserOutlined, + MoreOutlined, + SwapOutlined, + RobotOutlined, + CheckCircleOutlined, + MessageOutlined, + FileTextOutlined, +} from '@ant-design/icons'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '../api/client'; import dayjs from 'dayjs'; @@ -36,25 +50,94 @@ interface Message { type: string; content: string | null; created_at: string; + is_internal_note: boolean; + sent_by: string | null; } interface Conversation { id: string; contact: Contact; status: string; + priority: string; + assigned_to: string | null; + queue_id: string | null; last_message_at: string | null; messages?: Message[]; } -export default function Inbox() { +interface Queue { + id: string; + name: string; +} + +interface Agent { + id: string; + name: string; + status: string; +} + +interface QuickReply { + shortcut: string; + content: string; +} + +const STATUS_COLORS: Record = { + bot: 'blue', + waiting: 'orange', + active: 'green', + resolved: 'default', +}; + +const PRIORITY_COLORS: Record = { + low: 'default', + normal: 'blue', + high: 'orange', + urgent: 'red', +}; + +function getMessageBackground(msg: Message): string { + if (msg.is_internal_note) { + return '#fff7e6'; + } + if (msg.direction === 'outbound') { + return '#25D366'; + } + return '#f0f0f0'; +} + +function getMessageColor(msg: Message): string { + if (msg.is_internal_note) { + return '#d46b08'; + } + if (msg.direction === 'outbound') { + return 'white'; + } + return 'inherit'; +} + +function getMessageBorder(msg: Message): string { + if (msg.is_internal_note) { + return '1px dashed #ffc069'; + } + return 'none'; +} + +export default function Inbox(): JSX.Element { const [selectedId, setSelectedId] = useState(null); const [messageText, setMessageText] = useState(''); + const [statusFilter, setStatusFilter] = useState(null); + const [isNoteMode, setIsNoteMode] = useState(false); + const [transferModalOpen, setTransferModalOpen] = useState(false); + const [transferType, setTransferType] = useState<'queue' | 'agent'>('queue'); const queryClient = useQueryClient(); const { data: conversations, isLoading } = useQuery({ - queryKey: ['conversations'], + queryKey: ['conversations', statusFilter], queryFn: async () => { - return apiClient.get('/api/whatsapp/conversations'); + const url = statusFilter + ? `/api/whatsapp/conversations?status=${statusFilter}` + : '/api/whatsapp/conversations'; + return apiClient.get(url); }, refetchInterval: 3000, }); @@ -69,6 +152,21 @@ export default function Inbox() { refetchInterval: 2000, }); + const { data: queues } = useQuery({ + queryKey: ['queues'], + queryFn: () => apiClient.get('/api/queues'), + }); + + const { data: agents } = useQuery({ + queryKey: ['agents'], + queryFn: () => apiClient.get('/api/auth/agents'), + }); + + const { data: quickReplies } = useQuery({ + queryKey: ['quick-replies'], + queryFn: () => apiClient.get('/api/queues/quick-replies'), + }); + const sendMutation = useMutation({ mutationFn: async (data: { conversationId: string; content: string }) => { await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/messages`, { @@ -83,177 +181,441 @@ export default function Inbox() { }, }); - const handleSend = () => { - if (!messageText.trim() || !selectedId) return; - sendMutation.mutate({ conversationId: selectedId, content: messageText }); - }; + const noteMutation = useMutation({ + mutationFn: async (data: { conversationId: string; content: string }) => { + await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/notes`, { + content: data.content, + }); + }, + onSuccess: () => { + setMessageText(''); + setIsNoteMode(false); + message.success('Nota agregada'); + queryClient.invalidateQueries({ queryKey: ['conversation', selectedId] }); + }, + }); - const statusColors: Record = { - bot: 'blue', - waiting: 'orange', - active: 'green', - resolved: 'default', - }; + const transferQueueMutation = useMutation({ + mutationFn: async (data: { conversationId: string; queueId: string }) => { + await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/transfer-to-queue`, { + queue_id: data.queueId, + }); + }, + onSuccess: () => { + message.success('Transferido a cola'); + setTransferModalOpen(false); + queryClient.invalidateQueries({ queryKey: ['conversations'] }); + }, + }); + + const transferAgentMutation = useMutation({ + mutationFn: async (data: { conversationId: string; agentId: string }) => { + await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/transfer-to-agent`, { + agent_id: data.agentId, + }); + }, + onSuccess: () => { + message.success('Transferido a agente'); + setTransferModalOpen(false); + queryClient.invalidateQueries({ queryKey: ['conversations'] }); + }, + }); + + const transferBotMutation = useMutation({ + mutationFn: async (conversationId: string) => { + await apiClient.post(`/api/whatsapp/conversations/${conversationId}/transfer-to-bot`, {}); + }, + onSuccess: () => { + message.success('Transferido a bot'); + queryClient.invalidateQueries({ queryKey: ['conversations'] }); + }, + }); + + const resolveMutation = useMutation({ + mutationFn: async (conversationId: string) => { + await apiClient.post(`/api/whatsapp/conversations/${conversationId}/resolve`, {}); + }, + onSuccess: () => { + message.success('Conversacion resuelta'); + queryClient.invalidateQueries({ queryKey: ['conversations'] }); + }, + }); + + function handleSend(): void { + if (!messageText.trim() || !selectedId) return; + if (isNoteMode) { + noteMutation.mutate({ conversationId: selectedId, content: messageText }); + } else { + sendMutation.mutate({ conversationId: selectedId, content: messageText }); + } + } + + function handleTransferQueue(): void { + setTransferType('queue'); + setTransferModalOpen(true); + } + + function handleTransferAgent(): void { + setTransferType('agent'); + setTransferModalOpen(true); + } + + function handleTransferBot(): void { + if (selectedId) { + transferBotMutation.mutate(selectedId); + } + } + + function handleResolve(): void { + if (selectedId) { + resolveMutation.mutate(selectedId); + } + } + + function handleQueueSelect(value: string): void { + if (selectedId) { + transferQueueMutation.mutate({ conversationId: selectedId, queueId: value }); + } + } + + function handleAgentSelect(value: string): void { + if (selectedId) { + transferAgentMutation.mutate({ conversationId: selectedId, agentId: value }); + } + } + + const actionMenuItems = [ + { + key: 'transfer-queue', + icon: , + label: 'Transferir a cola', + onClick: handleTransferQueue, + }, + { + key: 'transfer-agent', + icon: , + label: 'Transferir a agente', + onClick: handleTransferAgent, + }, + { + key: 'transfer-bot', + icon: , + label: 'Transferir a bot', + onClick: handleTransferBot, + }, + { type: 'divider' as const }, + { + key: 'resolve', + icon: , + label: 'Resolver conversacion', + onClick: handleResolve, + }, + ]; + + function renderConversationList(): JSX.Element { + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!conversations || conversations.length === 0) { + return ; + } + + return ( + ( + setSelectedId(conv.id)} + style={{ + padding: '12px 16px', + cursor: 'pointer', + background: selectedId === conv.id ? '#f5f5f5' : 'transparent', + }} + > + + } /> + + } + title={ + + {conv.contact.name || conv.contact.phone_number} + + {conv.status} + + {conv.priority !== 'normal' && ( + + {conv.priority} + + )} + + } + description={ + + {conv.last_message_at ? dayjs(conv.last_message_at).fromNow() : 'Sin mensajes'} + + } + /> + + )} + /> + ); + } + + function renderMessage(msg: Message): JSX.Element { + return ( +
+
+ {msg.is_internal_note && ( + + Nota interna + + )} + {msg.content} +
+ + {dayjs(msg.created_at).format('HH:mm')} + +
+ ); + } + + function renderQuickReplies(): JSX.Element | null { + if (!quickReplies || quickReplies.length === 0) { + return null; + } + + return ( +
+ + {quickReplies.slice(0, 5).map((qr) => ( + + setMessageText(qr.content)}> + {qr.shortcut} + + + ))} + +
+ ); + } + + function renderMessageInput(): JSX.Element { + const inputStyle = isNoteMode ? { borderColor: '#ffc069' } : {}; + const buttonStyle = isNoteMode + ? { background: '#d46b08', borderColor: '#d46b08' } + : { background: '#25D366', borderColor: '#25D366' }; + + return ( +
+
+ + + + +
+
+ setMessageText(e.target.value)} + onPressEnter={handleSend} + style={inputStyle} + /> +
+
+ ); + } + + function renderChatContent(): JSX.Element { + if (!selectedConversation) { + return ( +
+ +
+ ); + } + + return ( + <> +
+
+ + {selectedConversation.contact.name || selectedConversation.contact.phone_number} + +
+ {selectedConversation.contact.phone_number} +
+ + + {selectedConversation.status} + + +
+ +
+ {selectedConversation.messages?.map(renderMessage)} +
+ + {renderQuickReplies()} + {renderMessageInput()} + + ); + } + + function renderTransferModal(): JSX.Element { + const title = transferType === 'queue' ? 'Transferir a Cola' : 'Transferir a Agente'; + + return ( + setTransferModalOpen(false)} + footer={null} + > + {transferType === 'queue' ? ( + + ) : ( + + )} + + ); + } return (
- {/* Lista de conversaciones */} - {isLoading ? ( -
- + title={ +
+ Conversaciones +
- ) : conversations?.length === 0 ? ( - - ) : ( - ( - setSelectedId(conv.id)} - style={{ - padding: '12px 16px', - cursor: 'pointer', - background: selectedId === conv.id ? '#f5f5f5' : 'transparent', - }} - > - - } /> - - } - title={ - - {conv.contact.name || conv.contact.phone_number} - - {conv.status} - - - } - description={ - - {conv.last_message_at - ? dayjs(conv.last_message_at).fromNow() - : 'Sin mensajes'} - - } - /> - - )} - /> - )} + } + > + {renderConversationList()} - {/* Chat */} - {selectedConversation ? ( - <> - {/* Header */} -
- - {selectedConversation.contact.name || selectedConversation.contact.phone_number} - -
- {selectedConversation.contact.phone_number} -
- - {/* Messages */} -
- {selectedConversation.messages?.map((msg) => ( -
-
- {msg.content} -
- - {dayjs(msg.created_at).format('HH:mm')} - -
- ))} -
- - {/* Input */} -
- setMessageText(e.target.value)} - onPressEnter={handleSend} - /> -
- - ) : ( -
- -
- )} + {renderChatContent()}
+ + {renderTransferModal()}
); }