From 8ba3d2582b5e8f7ff45096867db92afe5d02d1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Consultor=C3=ADa=20Alcaraz=20Salazar?= Date: Fri, 11 Sep 2026 10:58:57 +0000 Subject: [PATCH] WACRM: historial completo con paginacion desde el origen, sync refleja eliminaciones y trae todo el historial --- frontend/src/pages/WacrmMessages.tsx | 64 +++++++++++++++++-- frontend/src/services/odoo.ts | 5 ++ .../skeen_whatsapp/controllers/wacrm_proxy.py | 64 ++++++++++++++++++- 3 files changed, 126 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/WacrmMessages.tsx b/frontend/src/pages/WacrmMessages.tsx index edd735d..32fe45f 100644 --- a/frontend/src/pages/WacrmMessages.tsx +++ b/frontend/src/pages/WacrmMessages.tsx @@ -17,6 +17,11 @@ interface Member { const WacrmMessages: FC = () => { const [conversations, setConversations] = useState([]); const [messages, setMessages] = useState([]); + const [msgPage, setMsgPage] = useState(1); + const [msgHasMore, setMsgHasMore] = useState(false); + const [loadingOlder, setLoadingOlder] = useState(false); + const threadRef = useRef(null); + const convIdRef = useRef(null); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); 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 { - 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') { - 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) { toast.error('Error al cargar mensajes'); console.error(err); } - }, []); + }, [messages]); const loadMembers = useCallback(async () => { try { @@ -461,7 +500,22 @@ const WacrmMessages: FC = () => { )} )} -
+
+ {messages.length > 0 && msgHasMore && ( +
+ +
+ )} {messages.length === 0 ? (

No hay mensajes en esta conversación

) : ( diff --git a/frontend/src/services/odoo.ts b/frontend/src/services/odoo.ts index bc40978..e481dbb 100644 --- a/frontend/src/services/odoo.ts +++ b/frontend/src/services/odoo.ts @@ -1461,6 +1461,11 @@ export const odooApi = { 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[] }> { const { data } = await axios.get(`${ODOO_BASE}/skeen/frontend/v1/reports/inventory`); return data; diff --git a/odoo-addons/skeen_whatsapp/controllers/wacrm_proxy.py b/odoo-addons/skeen_whatsapp/controllers/wacrm_proxy.py index ce89ba8..9f639ca 100644 --- a/odoo-addons/skeen_whatsapp/controllers/wacrm_proxy.py +++ b/odoo-addons/skeen_whatsapp/controllers/wacrm_proxy.py @@ -162,6 +162,9 @@ class SkeenWacrmProxyController(http.Controller): contacts_map[item['id']] = request.env['skeen.wacrm.contact'].sudo().create(vals) # 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_map = {} for item in conversations_data: @@ -184,12 +187,25 @@ class SkeenWacrmProxyController(http.Controller): else: 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', {'select': '*', 'order': 'created_at.desc'}, 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: conversation = conversations_map.get(item.get('conversation_id')) if not conversation: @@ -418,6 +434,50 @@ class SkeenWacrmProxyController(http.Controller): _logger.exception('Error listando mensajes WACRM') 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//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) def list_conversations(self, **kw): try: