WACRM: historial completo con paginacion desde el origen, sync refleja eliminaciones y trae todo el historial
This commit is contained in:
@@ -17,6 +17,11 @@ interface Member {
|
|||||||
const WacrmMessages: FC = () => {
|
const WacrmMessages: FC = () => {
|
||||||
const [conversations, setConversations] = useState<WacrmConversation[]>([]);
|
const [conversations, setConversations] = useState<WacrmConversation[]>([]);
|
||||||
const [messages, setMessages] = useState<WacrmMessage[]>([]);
|
const [messages, setMessages] = useState<WacrmMessage[]>([]);
|
||||||
|
const [msgPage, setMsgPage] = useState(1);
|
||||||
|
const [msgHasMore, setMsgHasMore] = useState(false);
|
||||||
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||||
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const convIdRef = useRef<string | null>(null);
|
||||||
const [members, setMembers] = useState<Member[]>([]);
|
const [members, setMembers] = useState<Member[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
@@ -85,17 +90,51 @@ const WacrmMessages: FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMessages = useCallback(async (conversationId: string) => {
|
const scrollThreadToBottom = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const el = threadRef.current;
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Historial de la conversación, leído del origen (Supabase) vía el proxy,
|
||||||
|
// paginado: página 1 = últimos 50; con "Cargar mensajes anteriores" se
|
||||||
|
// anteponen páginas más antiguas conservando la posición del scroll.
|
||||||
|
const loadMessages = useCallback(async (conversationId: string, page = 1) => {
|
||||||
try {
|
try {
|
||||||
const res = await odooApi.getWacrmMessages(undefined, conversationId);
|
const keepScroll = page > 1;
|
||||||
|
const el = threadRef.current;
|
||||||
|
const prevHeight = keepScroll && el ? el.scrollHeight : 0;
|
||||||
|
const prevTop = keepScroll && el ? el.scrollTop : 0;
|
||||||
|
const res = await odooApi.getWacrmConversationMessages(conversationId, page, 50);
|
||||||
if (res.status === 'success') {
|
if (res.status === 'success') {
|
||||||
setMessages(res.messages.reverse());
|
convIdRef.current = conversationId;
|
||||||
|
setMsgPage(page);
|
||||||
|
setMsgHasMore(res.has_more);
|
||||||
|
if (page === 1) {
|
||||||
|
// Merge: conserva el historial antiguo ya cargado (scroll manual)
|
||||||
|
const known = new Set(res.messages.map((m) => m.external_id));
|
||||||
|
const minTs = res.messages.length ? res.messages[0].created_at : '';
|
||||||
|
const older = messages.filter((m) => !known.has(m.external_id) && (m.created_at ?? '') < (minTs ?? ''));
|
||||||
|
setMessages([...older, ...res.messages]);
|
||||||
|
if (convIdRef.current === conversationId) scrollThreadToBottom();
|
||||||
|
} else {
|
||||||
|
const prev = messages;
|
||||||
|
const known = new Set(prev.map((m) => m.external_id));
|
||||||
|
const older = res.messages.filter((m) => !known.has(m.external_id));
|
||||||
|
setMessages([...older, ...prev]);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (threadRef.current) {
|
||||||
|
threadRef.current.scrollTop = threadRef.current.scrollHeight - prevHeight + prevTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Error al cargar mensajes');
|
toast.error('Error al cargar mensajes');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [messages]);
|
||||||
|
|
||||||
const loadMembers = useCallback(async () => {
|
const loadMembers = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -461,7 +500,22 @@ const WacrmMessages: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
<div ref={threadRef} className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||||
|
{messages.length > 0 && msgHasMore && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
loading={loadingOlder}
|
||||||
|
onClick={() => {
|
||||||
|
setLoadingOlder(true);
|
||||||
|
loadMessages(selected.external_id, msgPage + 1).finally(() => setLoadingOlder(false));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cargar mensajes anteriores
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<p className="text-center text-sm text-theme-muted py-8">No hay mensajes en esta conversación</p>
|
<p className="text-center text-sm text-theme-muted py-8">No hay mensajes en esta conversación</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1461,6 +1461,11 @@ export const odooApi = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getWacrmConversationMessages(conversationId: string, page = 1, pageSize = 50): Promise<{ status: string; messages: WacrmMessage[]; page: number; page_size: number; has_more: boolean }> {
|
||||||
|
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/wacrm/conversations/${conversationId}/messages`, { params: { page, page_size: pageSize } });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
async getInventoryReport(): Promise<{ status: string; summary: InventoryReportSummary; items: InventoryItem[] }> {
|
async getInventoryReport(): Promise<{ status: string; summary: InventoryReportSummary; items: InventoryItem[] }> {
|
||||||
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/inventory`);
|
const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/inventory`);
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ class SkeenWacrmProxyController(http.Controller):
|
|||||||
contacts_map[item['id']] = request.env['skeen.wacrm.contact'].sudo().create(vals)
|
contacts_map[item['id']] = request.env['skeen.wacrm.contact'].sudo().create(vals)
|
||||||
|
|
||||||
# 2. Conversations
|
# 2. Conversations
|
||||||
|
# Además del upsert, se REFLEJAN ELIMINACIONES: lo que ya no exista
|
||||||
|
# en Supabase se borra del caché local (antes el caché acumulaba
|
||||||
|
# conversaciones borradas y la bandeja de SKEEN mostraba chats muertos).
|
||||||
conversations_data = _supabase_request_paginated('conversations', {'select': '*'})
|
conversations_data = _supabase_request_paginated('conversations', {'select': '*'})
|
||||||
conversations_map = {}
|
conversations_map = {}
|
||||||
for item in conversations_data:
|
for item in conversations_data:
|
||||||
@@ -184,12 +187,25 @@ class SkeenWacrmProxyController(http.Controller):
|
|||||||
else:
|
else:
|
||||||
conversations_map[item['id']] = request.env['skeen.wacrm.conversation'].sudo().create(vals)
|
conversations_map[item['id']] = request.env['skeen.wacrm.conversation'].sudo().create(vals)
|
||||||
|
|
||||||
# 3. Messages (últimos 5000 paginados)
|
remote_conv_ids = set(conversations_map.keys())
|
||||||
|
stale_convs = request.env['skeen.wacrm.conversation'].sudo().search([
|
||||||
|
('external_id', 'not in', list(remote_conv_ids))]) if remote_conv_ids else \
|
||||||
|
request.env['skeen.wacrm.conversation'].sudo().search([])
|
||||||
|
if stale_convs:
|
||||||
|
stale_convs.unlink()
|
||||||
|
conversations_map = {k: v for k, v in conversations_map.items()}
|
||||||
|
|
||||||
|
# 3. Messages (todos los del origen, sin recorte)
|
||||||
messages_data = _supabase_request_paginated(
|
messages_data = _supabase_request_paginated(
|
||||||
'messages',
|
'messages',
|
||||||
{'select': '*', 'order': 'created_at.desc'},
|
{'select': '*', 'order': 'created_at.desc'},
|
||||||
page_size=1000
|
page_size=1000
|
||||||
)[:5000]
|
)
|
||||||
|
remote_msg_ids = {item['id'] for item in messages_data}
|
||||||
|
stale_msgs = request.env['skeen.wacrm.message'].sudo().search([
|
||||||
|
('external_id', 'not in', list(remote_msg_ids))]) if remote_msg_ids else \
|
||||||
|
request.env['skeen.wacrm.message'].sudo().search([])
|
||||||
|
stale_msgs.unlink()
|
||||||
for item in messages_data:
|
for item in messages_data:
|
||||||
conversation = conversations_map.get(item.get('conversation_id'))
|
conversation = conversations_map.get(item.get('conversation_id'))
|
||||||
if not conversation:
|
if not conversation:
|
||||||
@@ -418,6 +434,50 @@ class SkeenWacrmProxyController(http.Controller):
|
|||||||
_logger.exception('Error listando mensajes WACRM')
|
_logger.exception('Error listando mensajes WACRM')
|
||||||
return json_response({'status': 'error', 'message': str(e)}, 500)
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||||
|
|
||||||
|
def _supabase_message_to_dict(self, m):
|
||||||
|
"""Mensaje de Supabase en el formato que consume el frontend."""
|
||||||
|
return {
|
||||||
|
'id': m.get('id'),
|
||||||
|
'external_id': m.get('id'),
|
||||||
|
'conversation_id': m.get('conversation_id'),
|
||||||
|
'sender_type': {'contact': 'customer', 'user': 'agent'}.get(m.get('sender_type', 'customer'), m.get('sender_type', 'customer')),
|
||||||
|
'content_type': m.get('content_type', 'text'),
|
||||||
|
'content_text': m.get('content_text') or m.get('content') or '',
|
||||||
|
'media_url': m.get('media_url') or '',
|
||||||
|
'template_name': m.get('template_name') or '',
|
||||||
|
'status': m.get('status', 'sent'),
|
||||||
|
'created_at': (m.get('created_at') or '')[:19].replace('T', ' '),
|
||||||
|
}
|
||||||
|
|
||||||
|
@http.route('/skeen/frontend/v1/wacrm/conversations/<string:external_id>/messages', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||||
|
def list_conversation_messages_origin(self, external_id, **kw):
|
||||||
|
"""Historial COMPLETO de una conversación, leído directo del origen
|
||||||
|
(Supabase del WACRM), paginado. Devuelve los mensajes en orden
|
||||||
|
ascendente dentro de la página; `has_more` indica si hay página previa
|
||||||
|
(mensajes más antiguos)."""
|
||||||
|
try:
|
||||||
|
page = max(int(kw.get('page', 1) or 1), 1)
|
||||||
|
page_size = min(max(int(kw.get('page_size', 50) or 50), 1), 200)
|
||||||
|
data = _supabase_request('GET', 'messages', params={
|
||||||
|
'select': '*',
|
||||||
|
'conversation_id': f'eq.{external_id}',
|
||||||
|
'order': 'created_at.desc',
|
||||||
|
'limit': page_size,
|
||||||
|
'offset': (page - 1) * page_size,
|
||||||
|
})
|
||||||
|
msgs = [self._supabase_message_to_dict(m) for m in data]
|
||||||
|
msgs.reverse() # ascendente dentro de la página
|
||||||
|
return json_response({
|
||||||
|
'status': 'success',
|
||||||
|
'messages': msgs,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'has_more': len(data) == page_size,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception('Error listando mensajes del origen WACRM')
|
||||||
|
return json_response({'status': 'error', 'message': str(e)}, 500)
|
||||||
|
|
||||||
@http.route('/skeen/frontend/v1/wacrm/conversations', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
@http.route('/skeen/frontend/v1/wacrm/conversations', type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
||||||
def list_conversations(self, **kw):
|
def list_conversations(self, **kw):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user