feat(fase3): enhance Inbox with transfers, notes, and quick replies
Add comprehensive agent inbox functionality including: - Status filter dropdown to filter conversations by bot/waiting/active/resolved - Priority tags displayed in conversation list for non-normal priorities - Internal notes feature with visual distinction (orange styling, dashed border) - Quick replies panel showing shortcut tags with tooltip previews - Transfer modal for moving conversations to queues or agents - Transfer to bot action for returning conversations to bot handling - Resolve conversation action to close conversations - Helper functions for message styling (background, color, border) - Refactored render functions for improved code organization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,22 @@ import {
|
|||||||
Spin,
|
Spin,
|
||||||
Space,
|
Space,
|
||||||
Badge,
|
Badge,
|
||||||
|
Dropdown,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
} from 'antd';
|
} 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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -36,25 +50,94 @@ interface Message {
|
|||||||
type: string;
|
type: string;
|
||||||
content: string | null;
|
content: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
is_internal_note: boolean;
|
||||||
|
sent_by: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Conversation {
|
interface Conversation {
|
||||||
id: string;
|
id: string;
|
||||||
contact: Contact;
|
contact: Contact;
|
||||||
status: string;
|
status: string;
|
||||||
|
priority: string;
|
||||||
|
assigned_to: string | null;
|
||||||
|
queue_id: string | null;
|
||||||
last_message_at: string | null;
|
last_message_at: string | null;
|
||||||
messages?: Message[];
|
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<string, string> = {
|
||||||
|
bot: 'blue',
|
||||||
|
waiting: 'orange',
|
||||||
|
active: 'green',
|
||||||
|
resolved: 'default',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRIORITY_COLORS: Record<string, string> = {
|
||||||
|
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<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||||
|
const [isNoteMode, setIsNoteMode] = useState(false);
|
||||||
|
const [transferModalOpen, setTransferModalOpen] = useState(false);
|
||||||
|
const [transferType, setTransferType] = useState<'queue' | 'agent'>('queue');
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: conversations, isLoading } = useQuery({
|
const { data: conversations, isLoading } = useQuery({
|
||||||
queryKey: ['conversations'],
|
queryKey: ['conversations', statusFilter],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
return apiClient.get<Conversation[]>('/api/whatsapp/conversations');
|
const url = statusFilter
|
||||||
|
? `/api/whatsapp/conversations?status=${statusFilter}`
|
||||||
|
: '/api/whatsapp/conversations';
|
||||||
|
return apiClient.get<Conversation[]>(url);
|
||||||
},
|
},
|
||||||
refetchInterval: 3000,
|
refetchInterval: 3000,
|
||||||
});
|
});
|
||||||
@@ -69,6 +152,21 @@ export default function Inbox() {
|
|||||||
refetchInterval: 2000,
|
refetchInterval: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: queues } = useQuery({
|
||||||
|
queryKey: ['queues'],
|
||||||
|
queryFn: () => apiClient.get<Queue[]>('/api/queues'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: agents } = useQuery({
|
||||||
|
queryKey: ['agents'],
|
||||||
|
queryFn: () => apiClient.get<Agent[]>('/api/auth/agents'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: quickReplies } = useQuery({
|
||||||
|
queryKey: ['quick-replies'],
|
||||||
|
queryFn: () => apiClient.get<QuickReply[]>('/api/queues/quick-replies'),
|
||||||
|
});
|
||||||
|
|
||||||
const sendMutation = useMutation({
|
const sendMutation = useMutation({
|
||||||
mutationFn: async (data: { conversationId: string; content: string }) => {
|
mutationFn: async (data: { conversationId: string; content: string }) => {
|
||||||
await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/messages`, {
|
await apiClient.post(`/api/whatsapp/conversations/${data.conversationId}/messages`, {
|
||||||
@@ -83,36 +181,151 @@ export default function Inbox() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSend = () => {
|
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 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 (!messageText.trim() || !selectedId) return;
|
||||||
|
if (isNoteMode) {
|
||||||
|
noteMutation.mutate({ conversationId: selectedId, content: messageText });
|
||||||
|
} else {
|
||||||
sendMutation.mutate({ conversationId: selectedId, content: messageText });
|
sendMutation.mutate({ conversationId: selectedId, content: messageText });
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const statusColors: Record<string, string> = {
|
function handleTransferQueue(): void {
|
||||||
bot: 'blue',
|
setTransferType('queue');
|
||||||
waiting: 'orange',
|
setTransferModalOpen(true);
|
||||||
active: 'green',
|
}
|
||||||
resolved: 'default',
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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: <SwapOutlined />,
|
||||||
|
label: 'Transferir a cola',
|
||||||
|
onClick: handleTransferQueue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'transfer-agent',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: 'Transferir a agente',
|
||||||
|
onClick: handleTransferAgent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'transfer-bot',
|
||||||
|
icon: <RobotOutlined />,
|
||||||
|
label: 'Transferir a bot',
|
||||||
|
onClick: handleTransferBot,
|
||||||
|
},
|
||||||
|
{ type: 'divider' as const },
|
||||||
|
{
|
||||||
|
key: 'resolve',
|
||||||
|
icon: <CheckCircleOutlined />,
|
||||||
|
label: 'Resolver conversacion',
|
||||||
|
onClick: handleResolve,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderConversationList(): JSX.Element {
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', height: 'calc(100vh - 180px)', gap: 16 }}>
|
|
||||||
{/* Lista de conversaciones */}
|
|
||||||
<Card
|
|
||||||
style={{ width: 350, overflow: 'auto' }}
|
|
||||||
styles={{ body: { padding: 0 } }}
|
|
||||||
title="Conversaciones"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div style={{ padding: 40, textAlign: 'center' }}>
|
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||||
<Spin />
|
<Spin />
|
||||||
</div>
|
</div>
|
||||||
) : conversations?.length === 0 ? (
|
);
|
||||||
<Empty
|
}
|
||||||
description="Sin conversaciones"
|
|
||||||
style={{ padding: 40 }}
|
if (!conversations || conversations.length === 0) {
|
||||||
/>
|
return <Empty description="Sin conversaciones" style={{ padding: 40 }} />;
|
||||||
) : (
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<List
|
<List
|
||||||
dataSource={conversations}
|
dataSource={conversations}
|
||||||
renderItem={(conv) => (
|
renderItem={(conv) => (
|
||||||
@@ -126,65 +339,37 @@ export default function Inbox() {
|
|||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
<Badge dot={conv.status !== 'resolved'} color={statusColors[conv.status]}>
|
<Badge dot={conv.status !== 'resolved'} color={STATUS_COLORS[conv.status]}>
|
||||||
<Avatar icon={<UserOutlined />} />
|
<Avatar icon={<UserOutlined />} />
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<Text strong>{conv.contact.name || conv.contact.phone_number}</Text>
|
<Text strong>{conv.contact.name || conv.contact.phone_number}</Text>
|
||||||
<Tag color={statusColors[conv.status]} style={{ fontSize: 10 }}>
|
<Tag color={STATUS_COLORS[conv.status]} style={{ fontSize: 10 }}>
|
||||||
{conv.status}
|
{conv.status}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{conv.priority !== 'normal' && (
|
||||||
|
<Tag color={PRIORITY_COLORS[conv.priority]} style={{ fontSize: 10 }}>
|
||||||
|
{conv.priority}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
{conv.last_message_at
|
{conv.last_message_at ? dayjs(conv.last_message_at).fromNow() : 'Sin mensajes'}
|
||||||
? dayjs(conv.last_message_at).fromNow()
|
|
||||||
: 'Sin mensajes'}
|
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
</Card>
|
}
|
||||||
|
|
||||||
{/* Chat */}
|
function renderMessage(msg: Message): JSX.Element {
|
||||||
<Card
|
return (
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
|
||||||
styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', padding: 0 } }}
|
|
||||||
>
|
|
||||||
{selectedConversation ? (
|
|
||||||
<>
|
|
||||||
{/* Header */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: 16,
|
|
||||||
borderBottom: '1px solid #f0f0f0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text strong style={{ fontSize: 16 }}>
|
|
||||||
{selectedConversation.contact.name || selectedConversation.contact.phone_number}
|
|
||||||
</Text>
|
|
||||||
<br />
|
|
||||||
<Text type="secondary">{selectedConversation.contact.phone_number}</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
overflow: 'auto',
|
|
||||||
padding: 16,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedConversation.messages?.map((msg) => (
|
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
style={{
|
style={{
|
||||||
@@ -196,10 +381,16 @@ export default function Inbox() {
|
|||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
background: msg.direction === 'outbound' ? '#25D366' : '#f0f0f0',
|
background: getMessageBackground(msg),
|
||||||
color: msg.direction === 'outbound' ? 'white' : 'inherit',
|
color: getMessageColor(msg),
|
||||||
|
border: getMessageBorder(msg),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{msg.is_internal_note && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 10, display: 'block', marginBottom: 4 }}>
|
||||||
|
<FileTextOutlined /> Nota interna
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<Text style={{ color: 'inherit' }}>{msg.content}</Text>
|
<Text style={{ color: 'inherit' }}>{msg.content}</Text>
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Text
|
||||||
@@ -214,34 +405,81 @@ export default function Inbox() {
|
|||||||
{dayjs(msg.created_at).format('HH:mm')}
|
{dayjs(msg.created_at).format('HH:mm')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
}
|
||||||
|
|
||||||
{/* Input */}
|
function renderQuickReplies(): JSX.Element | null {
|
||||||
<div
|
if (!quickReplies || quickReplies.length === 0) {
|
||||||
style={{
|
return null;
|
||||||
padding: 16,
|
}
|
||||||
borderTop: '1px solid #f0f0f0',
|
|
||||||
display: 'flex',
|
return (
|
||||||
gap: 8,
|
<div style={{ padding: '8px 16px', borderTop: '1px solid #f0f0f0' }}>
|
||||||
}}
|
<Space wrap size={4}>
|
||||||
|
{quickReplies.slice(0, 5).map((qr) => (
|
||||||
|
<Tooltip key={qr.shortcut} title={qr.content}>
|
||||||
|
<Tag style={{ cursor: 'pointer' }} onClick={() => setMessageText(qr.content)}>
|
||||||
|
{qr.shortcut}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMessageInput(): JSX.Element {
|
||||||
|
const inputStyle = isNoteMode ? { borderColor: '#ffc069' } : {};
|
||||||
|
const buttonStyle = isNoteMode
|
||||||
|
? { background: '#d46b08', borderColor: '#d46b08' }
|
||||||
|
: { background: '#25D366', borderColor: '#25D366' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16, borderTop: '1px solid #f0f0f0' }}>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type={isNoteMode ? 'default' : 'primary'}
|
||||||
|
icon={<MessageOutlined />}
|
||||||
|
onClick={() => setIsNoteMode(false)}
|
||||||
>
|
>
|
||||||
|
Mensaje
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type={isNoteMode ? 'primary' : 'default'}
|
||||||
|
icon={<FileTextOutlined />}
|
||||||
|
onClick={() => setIsNoteMode(true)}
|
||||||
|
style={isNoteMode ? { background: '#d46b08', borderColor: '#d46b08' } : {}}
|
||||||
|
>
|
||||||
|
Nota interna
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Escribe un mensaje..."
|
placeholder={isNoteMode ? 'Escribe una nota interna...' : 'Escribe un mensaje...'}
|
||||||
value={messageText}
|
value={messageText}
|
||||||
onChange={(e) => setMessageText(e.target.value)}
|
onChange={(e) => setMessageText(e.target.value)}
|
||||||
onPressEnter={handleSend}
|
onPressEnter={handleSend}
|
||||||
|
style={inputStyle}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
loading={sendMutation.isPending}
|
loading={sendMutation.isPending || noteMutation.isPending}
|
||||||
style={{ background: '#25D366', borderColor: '#25D366' }}
|
style={buttonStyle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatContent(): JSX.Element {
|
||||||
|
if (!selectedConversation) {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -250,10 +488,134 @@ export default function Inbox() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Empty description="Selecciona una conversación" />
|
<Empty description="Selecciona una conversacion" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
</Card>
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Text strong style={{ fontSize: 16 }}>
|
||||||
|
{selectedConversation.contact.name || selectedConversation.contact.phone_number}
|
||||||
|
</Text>
|
||||||
|
<br />
|
||||||
|
<Text type="secondary">{selectedConversation.contact.phone_number}</Text>
|
||||||
|
</div>
|
||||||
|
<Space>
|
||||||
|
<Tag color={STATUS_COLORS[selectedConversation.status]}>
|
||||||
|
{selectedConversation.status}
|
||||||
|
</Tag>
|
||||||
|
<Dropdown menu={{ items: actionMenuItems }} trigger={['click']}>
|
||||||
|
<Button icon={<MoreOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
padding: 16,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedConversation.messages?.map(renderMessage)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderQuickReplies()}
|
||||||
|
{renderMessageInput()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTransferModal(): JSX.Element {
|
||||||
|
const title = transferType === 'queue' ? 'Transferir a Cola' : 'Transferir a Agente';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={transferModalOpen}
|
||||||
|
onCancel={() => setTransferModalOpen(false)}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{transferType === 'queue' ? (
|
||||||
|
<Select
|
||||||
|
placeholder="Seleccionar cola"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={handleQueueSelect}
|
||||||
|
>
|
||||||
|
{queues?.map((q) => (
|
||||||
|
<Select.Option key={q.id} value={q.id}>
|
||||||
|
{q.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
placeholder="Seleccionar agente"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={handleAgentSelect}
|
||||||
|
>
|
||||||
|
{agents
|
||||||
|
?.filter((a) => a.status === 'online')
|
||||||
|
.map((a) => (
|
||||||
|
<Select.Option key={a.id} value={a.id}>
|
||||||
|
{a.name} ({a.status})
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', height: 'calc(100vh - 180px)', gap: 16 }}>
|
||||||
|
<Card
|
||||||
|
style={{ width: 380, overflow: 'auto' }}
|
||||||
|
styles={{ body: { padding: 0 } }}
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span>Conversaciones</span>
|
||||||
|
<Select
|
||||||
|
placeholder="Filtrar"
|
||||||
|
allowClear
|
||||||
|
size="small"
|
||||||
|
style={{ width: 120 }}
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={setStatusFilter}
|
||||||
|
>
|
||||||
|
<Select.Option value="bot">Bot</Select.Option>
|
||||||
|
<Select.Option value="waiting">En espera</Select.Option>
|
||||||
|
<Select.Option value="active">Activas</Select.Option>
|
||||||
|
<Select.Option value="resolved">Resueltas</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderConversationList()}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
style={{ flex: 1, display: 'flex', flexDirection: 'column' }}
|
||||||
|
styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', padding: 0 } }}
|
||||||
|
>
|
||||||
|
{renderChatContent()}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{renderTransferModal()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user