feat(feed): eliminar tope de 60 mensajes, carga profunda de miles de publicaciones y paginacion avanzada
This commit is contained in:
parent
c1c39982ff
commit
4abc8d6342
@ -77,7 +77,7 @@ export default function SupplierFeedPage() {
|
||||
// Target brand selector for publishing
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||
|
||||
// Cargar grupos monitoreados, productos existentes y posts en vivo (por defecto PENDIENTES)
|
||||
// Cargar grupos monitoreados, productos existentes y posts en vivo (TODOS los mensajes)
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -95,8 +95,8 @@ export default function SupplierFeedPage() {
|
||||
setExistingProducts(prodData.products);
|
||||
}
|
||||
|
||||
// 3. Posts en vivo desde los grupos de WhatsApp (iniciando con PENDIENTES para que la lista avance)
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60&status=pending');
|
||||
// 3. Posts en vivo desde los grupos de WhatsApp (escaneando todo el historial profundo)
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&pages=35&limit=3500&status=pending');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -112,7 +112,7 @@ export default function SupplierFeedPage() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Cambiar filtro de grupo y recargar mensajes
|
||||
// Cambiar filtro de grupo y recargar todos los mensajes del grupo
|
||||
const handleGroupFilterChange = async (groupJid: string) => {
|
||||
setSelectedGroupFilter(groupJid);
|
||||
setIsSyncingLive(true);
|
||||
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}&pages=35&limit=3500`
|
||||
: `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=35&limit=3500`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes.`);
|
||||
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del historial.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -136,22 +136,24 @@ export default function SupplierFeedPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizar mensajes en vivo desde Evolution API
|
||||
const handleSyncLive = async () => {
|
||||
// Sincronizar mensajes en vivo desde Evolution API (con escaneo profundo opcional)
|
||||
const handleSyncLive = async (deep: boolean = false) => {
|
||||
setIsSyncingLive(true);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const pagesCount = deep ? 60 : 35;
|
||||
const limitCount = deep ? 5000 : 3500;
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`;
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(
|
||||
`✅ ¡Sincronización completa! Se escanearon los mensajes de WhatsApp y se omitieron los ya publicados y descartados.`
|
||||
`✅ ¡Sincronización completa! Se escanearon ${data.totalFound || data.posts.length} mensajes de WhatsApp sin límite.`
|
||||
);
|
||||
} else {
|
||||
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.');
|
||||
@ -352,7 +354,7 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Cambiar pestaña y actualizar vista
|
||||
// Cambiar pestaña y actualizar vista con todo el historial
|
||||
const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => {
|
||||
setTabStatus(status);
|
||||
setSelectedPostIds(new Set());
|
||||
@ -360,8 +362,8 @@ export default function SupplierFeedPage() {
|
||||
try {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${status}`;
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}&pages=35&limit=3500`
|
||||
: `/api/whatsapp-feed?fetchLive=true&status=${status}&pages=35&limit=3500`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
@ -455,12 +457,23 @@ export default function SupplierFeedPage() {
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
onClick={() => handleSyncLive(false)}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 text-neutral-300 font-bold text-xs px-3.5 py-2.5 rounded-2xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
title="Sincronizar mensajes recientes"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 text-purple-400 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>Sincronizar ({posts.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleSyncLive(true)}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-2xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
title="Escanear miles de mensajes en todo el historial"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
|
||||
<Layers className="w-4 h-4" />
|
||||
<span>Escanear Todo el Historial (+1,000)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -953,15 +966,54 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Feed Pagination Bar */}
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-neutral-950 px-5 py-3.5 rounded-2xl border border-neutral-800 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">
|
||||
<span className="text-neutral-400">
|
||||
Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '}
|
||||
<strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '}
|
||||
<strong className="text-white">{filteredPosts.length}</strong> mensajes de WhatsApp (Pág.{' '}
|
||||
<strong className="text-purple-400">{currentPage}</strong> de {totalPages})
|
||||
</span>
|
||||
<div className="bg-neutral-950 px-5 py-4 rounded-2xl border border-neutral-800 flex flex-col md:flex-row items-center justify-between gap-4 text-xs">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="text-neutral-400">
|
||||
Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '}
|
||||
<strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '}
|
||||
<strong className="text-purple-400 text-sm">{filteredPosts.length}</strong> mensajes (Página{' '}
|
||||
<strong className="text-white font-bold">{currentPage}</strong> de {totalPages})
|
||||
</span>
|
||||
|
||||
{/* Page Size Selector */}
|
||||
<div className="flex items-center gap-1.5 text-neutral-400 ml-2 pl-3 border-l border-neutral-800">
|
||||
<span>Por pág:</span>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
setPageSize(Number(e.target.value));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="bg-neutral-900 border border-neutral-700 text-white rounded-lg px-2 py-1 text-xs outline-hidden focus:border-purple-500 cursor-pointer"
|
||||
>
|
||||
<option value={12}>12</option>
|
||||
<option value={24}>24</option>
|
||||
<option value={48}>48</option>
|
||||
<option value={96}>96</option>
|
||||
<option value={150}>150</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* Page Jump Selector */}
|
||||
{totalPages > 5 && (
|
||||
<div className="flex items-center gap-1 text-neutral-400 mr-2">
|
||||
<span>Ir a:</span>
|
||||
<select
|
||||
value={currentPage}
|
||||
onChange={(e) => setCurrentPage(Number(e.target.value))}
|
||||
className="bg-neutral-900 border border-neutral-700 text-white rounded-lg px-2 py-1 text-xs outline-hidden focus:border-purple-500 cursor-pointer"
|
||||
>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
|
||||
<option key={p} value={p}>
|
||||
Pág {p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
@ -988,7 +1040,7 @@ export default function SupplierFeedPage() {
|
||||
<span>Anterior</span>
|
||||
</button>
|
||||
|
||||
<span className="px-2.5 font-bold text-white bg-purple-950/60 border border-purple-500/40 rounded-lg py-1">
|
||||
<span className="px-3 font-bold text-white bg-purple-950/60 border border-purple-500/40 rounded-lg py-1">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
|
||||
|
||||
@ -22,7 +22,8 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '60', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '3000', 10);
|
||||
const maxPages = parseInt(searchParams.get('pages') || '30', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||
|
||||
@ -128,26 +129,45 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Consultar mensajes de cada grupo en paralelo desde Evolution API
|
||||
// 4. Consultar mensajes de los grupos objetivo recorriendo todas las páginas en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const fetchLimit = Math.max(limit * 2, 80);
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
const allGroupRecords: any[] = [];
|
||||
const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
|
||||
|
||||
// Lotes concurrentes de 10 páginas para máxima velocidad y profundidad
|
||||
const BATCH_SIZE = 10;
|
||||
for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
|
||||
const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map(async (page) => {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=${page}&limit=50`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
})
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
|
||||
let hitEnd = false;
|
||||
for (const res of batchResults) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allGroupRecords.push(...res.value);
|
||||
if (res.value.length === 0) {
|
||||
hitEnd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hitEnd) break;
|
||||
}
|
||||
return allGroupRecords;
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
@ -229,25 +249,13 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
let imageUrls: string[] = [];
|
||||
const imgMsg = record.message?.imageMessage;
|
||||
|
||||
if (hasImage) {
|
||||
try {
|
||||
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`;
|
||||
const mediaRes = await fetch(mediaUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: record, convertToMp4: false }),
|
||||
});
|
||||
const mediaData = await mediaRes.json();
|
||||
if (mediaData.base64) {
|
||||
const mime = mediaData.mimetype || 'image/jpeg';
|
||||
imageUrls.push(`data:${mime};base64,${mediaData.base64}`);
|
||||
}
|
||||
} catch (mediaErr) {
|
||||
console.warn('Error descargando imagen de WhatsApp:', mediaErr);
|
||||
if (imgMsg) {
|
||||
if (imgMsg.jpegThumbnail) {
|
||||
imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
|
||||
} else if (imgMsg.url) {
|
||||
imageUrls.push(imgMsg.url);
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,7 +268,7 @@ export async function GET(request: Request) {
|
||||
let extracted = existingInAppwrite?.extractedProduct;
|
||||
if (!extracted) {
|
||||
const cleanInputText = text || '';
|
||||
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
|
||||
extracted = await parseSupplierMessage(cleanInputText, undefined, [groupName]);
|
||||
} else if (extracted && extracted.name) {
|
||||
extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand);
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '3000', 10);
|
||||
const maxPages = parseInt(searchParams.get('pages') || '30', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||
|
||||
@ -99,26 +100,44 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||
// 3. Consultar mensajes de cada grupo objetivo recorriendo múltiples páginas en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const fetchLimit = Math.max(limit * 2, 80);
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
const allGroupRecords: any[] = [];
|
||||
const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
|
||||
|
||||
const BATCH_SIZE = 10;
|
||||
for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
|
||||
const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
|
||||
const batchResults = await Promise.allSettled(
|
||||
batch.map(async (page) => {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=${page}&limit=50`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
})
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
|
||||
let hitEnd = false;
|
||||
for (const res of batchResults) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allGroupRecords.push(...res.value);
|
||||
if (res.value.length === 0) {
|
||||
hitEnd = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hitEnd) break;
|
||||
}
|
||||
return allGroupRecords;
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
@ -132,7 +151,7 @@ export async function GET(request: Request) {
|
||||
// 4. Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
// 5. Procesar cada mensaje con control de identificador de WhatsApp
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
const seenMessageIds = new Set<string>();
|
||||
|
||||
@ -191,26 +210,13 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
let imageUrls: string[] = [];
|
||||
const imgMsg = record.message?.imageMessage;
|
||||
|
||||
// Descargar imagen en Base64 desde Evolution API si está presente
|
||||
if (hasImage) {
|
||||
try {
|
||||
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`;
|
||||
const mediaRes = await fetch(mediaUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: record, convertToMp4: false }),
|
||||
});
|
||||
const mediaData = await mediaRes.json();
|
||||
if (mediaData.base64) {
|
||||
const mime = mediaData.mimetype || 'image/jpeg';
|
||||
imageUrls.push(`data:${mime};base64,${mediaData.base64}`);
|
||||
}
|
||||
} catch (mediaErr) {
|
||||
console.warn('Error descargando imagen de WhatsApp:', mediaErr);
|
||||
if (imgMsg) {
|
||||
if (imgMsg.jpegThumbnail) {
|
||||
imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
|
||||
} else if (imgMsg.url) {
|
||||
imageUrls.push(imgMsg.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user