feat(feed): control por identificador de whatsapp para excluir mensajes publicados y descartados y avanzar lista
This commit is contained in:
parent
2e5ea56007
commit
b7e920dcaf
@ -44,7 +44,7 @@ export default function SupplierFeedPage() {
|
||||
const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]);
|
||||
const [existingProducts, setExistingProducts] = useState<Product[]>([]);
|
||||
const [selectedGroupFilter, setSelectedGroupFilter] = useState<string>('all');
|
||||
const [tabStatus, setTabStatus] = useState<'all' | 'pending' | 'published' | 'discarded'>('all');
|
||||
const [tabStatus, setTabStatus] = useState<'pending' | 'published' | 'discarded' | 'all'>('pending');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSyncingLive, setIsSyncingLive] = useState(false);
|
||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||
@ -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
|
||||
// Cargar grupos monitoreados, productos existentes y posts en vivo (por defecto PENDIENTES)
|
||||
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 (todos los mensajes: leídos y sin leer)
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60');
|
||||
// 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');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=60';
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||
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 (leídos y sin leer) del grupo.`);
|
||||
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -143,15 +143,15 @@ export default function SupplierFeedPage() {
|
||||
try {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=60';
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||
|
||||
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 ${data.totalFound || data.posts.length} mensajes de WhatsApp (leídos y sin leer) y se sincronizaron con Appwrite.`
|
||||
`✅ ¡Sincronización completa! Se escanearon los mensajes de WhatsApp y se omitieron los ya publicados y descartados.`
|
||||
);
|
||||
} else {
|
||||
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.');
|
||||
@ -284,7 +284,7 @@ export default function SupplierFeedPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Descartar mensaje
|
||||
// Descartar mensaje individual
|
||||
const handleDiscardPost = async (post: SupplierPost) => {
|
||||
try {
|
||||
await fetch('/api/whatsapp-feed', {
|
||||
@ -300,8 +300,45 @@ export default function SupplierFeedPage() {
|
||||
next.delete(post.id);
|
||||
return next;
|
||||
});
|
||||
setActionMessage(`🗑️ Mensaje #${post.whatsappMessageId || post.id.replace('post-', '')} descartado y archivado. No se volverá a mostrar en pendientes.`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setActionMessage('❌ Error al descartar mensaje.');
|
||||
}
|
||||
};
|
||||
|
||||
// Descarte masivo de mensajes seleccionados
|
||||
const handleBulkDiscard = async () => {
|
||||
const selectedPosts = posts.filter(
|
||||
(p) => selectedPostIds.has(p.id) && p.status !== 'discarded'
|
||||
);
|
||||
if (selectedPosts.length === 0) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/whatsapp-feed', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'bulk-discard',
|
||||
posts: selectedPosts,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setPosts((prev) =>
|
||||
prev.map((p) => (selectedPostIds.has(p.id) ? { ...p, status: 'discarded' } : p))
|
||||
);
|
||||
setSelectedPostIds(new Set());
|
||||
setActionMessage(
|
||||
`🗑️ Se han descartado ${selectedPosts.length} mensajes. No volverán a aparecer en la lista de pendientes.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setActionMessage('❌ Error en descarte masivo.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -315,6 +352,24 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Cambiar pestaña y actualizar vista
|
||||
const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => {
|
||||
setTabStatus(status);
|
||||
setSelectedPostIds(new Set());
|
||||
setCurrentPage(1);
|
||||
try {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}`
|
||||
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${status}`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
// Filtrado de posts por estado y grupo
|
||||
const filteredPosts = useMemo(() => {
|
||||
return posts.filter((p) => {
|
||||
@ -445,7 +500,7 @@ export default function SupplierFeedPage() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setTabStatus('pending')}
|
||||
onClick={() => handleTabChange('pending')}
|
||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'pending'
|
||||
? 'bg-purple-600 text-white shadow'
|
||||
@ -459,7 +514,7 @@ export default function SupplierFeedPage() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setTabStatus('published')}
|
||||
onClick={() => handleTabChange('published')}
|
||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'published'
|
||||
? 'bg-emerald-600 text-white shadow'
|
||||
@ -473,7 +528,7 @@ export default function SupplierFeedPage() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setTabStatus('discarded')}
|
||||
onClick={() => handleTabChange('discarded')}
|
||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'discarded'
|
||||
? 'bg-rose-600 text-white shadow'
|
||||
@ -518,10 +573,18 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedPostIds(new Set())}
|
||||
className="text-xs text-neutral-400 hover:text-white px-3 py-1.5"
|
||||
className="text-xs text-neutral-400 hover:text-white px-3 py-1.5 cursor-pointer"
|
||||
>
|
||||
Deseleccionar
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkDiscard}
|
||||
disabled={isLoading}
|
||||
className="bg-neutral-800 hover:bg-rose-950/80 border border-neutral-700 hover:border-rose-800 text-neutral-300 hover:text-rose-200 font-bold text-xs px-3.5 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-rose-400" />
|
||||
<span>Descartar Seleccionados</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBulkPublish}
|
||||
disabled={isLoading}
|
||||
@ -643,11 +706,19 @@ export default function SupplierFeedPage() {
|
||||
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
|
||||
{ext?.brand || 'Sneakers'}
|
||||
</span>
|
||||
{ext?.styleCode && (
|
||||
<span className="text-[9px] font-mono bg-neutral-800 text-neutral-300 px-1.5 py-0.5 rounded border border-neutral-700">
|
||||
SKU: {ext.styleCode}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-[9px] font-mono bg-neutral-950 text-neutral-400 px-2 py-0.5 rounded-md border border-neutral-800/80"
|
||||
title={`ID WhatsApp: ${post.whatsappMessageId || post.id.replace('post-', '')}`}
|
||||
>
|
||||
#{(post.whatsappMessageId || post.id.replace('post-', '')).slice(-8)}
|
||||
</span>
|
||||
)}
|
||||
{ext?.styleCode && (
|
||||
<span className="text-[9px] font-mono bg-neutral-800 text-neutral-300 px-1.5 py-0.5 rounded border border-neutral-700">
|
||||
SKU: {ext.styleCode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-bold text-white text-sm mt-0.5 line-clamp-1">
|
||||
{ext?.name || 'Zapatilla Urbana'}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
saveSupplierPostToAppwrite,
|
||||
updateSupplierPostStatusInAppwrite,
|
||||
saveProductToAppwrite,
|
||||
getProductsFromAppwrite,
|
||||
} from '@/lib/server-appwrite';
|
||||
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
|
||||
|
||||
@ -23,9 +24,41 @@ export async function GET(request: Request) {
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '60', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||
|
||||
// 1. Obtener posts guardados en Appwrite
|
||||
const savedPosts = await getSupplierPostsFromAppwrite();
|
||||
// 1. Obtener posts y productos existentes en Appwrite para control estricto de IDs de mensajes de WhatsApp
|
||||
const [savedPosts, existingProducts] = await Promise.all([
|
||||
getSupplierPostsFromAppwrite(),
|
||||
getProductsFromAppwrite(),
|
||||
]);
|
||||
|
||||
// Índices de WhatsApp message IDs ya procesados
|
||||
const publishedMsgIds = new Set<string>();
|
||||
const discardedMsgIds = new Set<string>();
|
||||
const appwritePostMap = new Map<string, SupplierPost>();
|
||||
|
||||
savedPosts.forEach((p) => {
|
||||
appwritePostMap.set(p.id, p);
|
||||
const msgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||
if (msgId) {
|
||||
if (p.status === 'published' || p.status === 'published_store' || p.status === 'published_facebook') {
|
||||
publishedMsgIds.add(msgId);
|
||||
publishedMsgIds.add(`post-${msgId}`);
|
||||
publishedMsgIds.add(p.id);
|
||||
} else if (p.status === 'discarded' || p.status === 'ignored') {
|
||||
discardedMsgIds.add(msgId);
|
||||
discardedMsgIds.add(`post-${msgId}`);
|
||||
discardedMsgIds.add(p.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
existingProducts.forEach((prod) => {
|
||||
if (prod.whatsappMessageId) {
|
||||
publishedMsgIds.add(prod.whatsappMessageId);
|
||||
publishedMsgIds.add(`post-${prod.whatsappMessageId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
@ -57,7 +90,7 @@ export async function GET(request: Request) {
|
||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||
}
|
||||
|
||||
// 3. Determinar JIDs objetivo a consultar (traer TODOS los mensajes de los grupos)
|
||||
// 3. Determinar JIDs objetivo a consultar
|
||||
let targetJids: string[] = [];
|
||||
if (filterJid && filterJid !== 'all') {
|
||||
targetJids = [filterJid];
|
||||
@ -73,20 +106,34 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (!fetchLive || targetJids.length === 0) {
|
||||
const filteredSaved = savedPosts.filter((p) => {
|
||||
if (statusFilter === 'pending') return p.status === 'pending_review';
|
||||
if (statusFilter === 'published') return p.status === 'published' || p.status === 'published_store';
|
||||
if (statusFilter === 'discarded') return p.status === 'discarded';
|
||||
return true;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: false,
|
||||
totalFound: savedPosts.length,
|
||||
posts: savedPosts.length > 0 ? savedPosts : dataService.getSupplierPosts(),
|
||||
totalFound: filteredSaved.length,
|
||||
counts: {
|
||||
pending: savedPosts.filter((p) => p.status === 'pending_review').length,
|
||||
published: publishedMsgIds.size,
|
||||
discarded: discardedMsgIds.size,
|
||||
all: savedPosts.length,
|
||||
},
|
||||
posts: filteredSaved,
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Consultar mensajes de cada grupo objetivo en paralelo (TODOS los mensajes sin distinción de leídos o sin leer)
|
||||
// 4. Consultar mensajes de cada grupo en paralelo desde Evolution API
|
||||
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=${limit}`,
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -111,15 +158,25 @@ export async function GET(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Ordenar cronológicamente (más recientes primero)
|
||||
// Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 6. Procesar cada mensaje y descargar imágenes / IA
|
||||
// 5. Procesar mensajes con control estricto de identificador de WhatsApp
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
const seenMessageIds = new Set<string>();
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
for (const record of allRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
const rawMsgId = (record.key?.id || '').trim();
|
||||
const cleanMsgId = rawMsgId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 36);
|
||||
const postId = `post-${cleanMsgId || Date.now()}`;
|
||||
|
||||
if (seenMessageIds.has(postId) || (rawMsgId && seenMessageIds.has(rawMsgId))) {
|
||||
continue;
|
||||
}
|
||||
seenMessageIds.add(postId);
|
||||
if (rawMsgId) seenMessageIds.add(rawMsgId);
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -130,15 +187,46 @@ export async function GET(request: Request) {
|
||||
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
||||
const hasImage = Boolean(record.message?.imageMessage);
|
||||
|
||||
// Si no tiene texto ni imagen, omitir
|
||||
if (!text && !hasImage) continue;
|
||||
|
||||
const postId = `post-${record.key?.id || Date.now()}`;
|
||||
const existingInAppwrite =
|
||||
appwritePostMap.get(postId) ||
|
||||
savedPosts.find(
|
||||
(p) =>
|
||||
p.id === postId ||
|
||||
p.whatsappMessageId === rawMsgId ||
|
||||
p.whatsappMessageId === cleanMsgId ||
|
||||
(p.rawMessage === text && (p as any).channelJid === remoteJid)
|
||||
);
|
||||
|
||||
// Verificar si ya existe en Appwrite para preservar su estado de publicación o descarte
|
||||
const existingInAppwrite = savedPosts.find(
|
||||
(p) => p.id === postId || (p.rawMessage === text && (p as any).channelJid === remoteJid)
|
||||
);
|
||||
const isPublished =
|
||||
existingInAppwrite?.status === 'published' ||
|
||||
existingInAppwrite?.status === 'published_store' ||
|
||||
existingInAppwrite?.status === 'published_facebook' ||
|
||||
publishedMsgIds.has(rawMsgId) ||
|
||||
publishedMsgIds.has(cleanMsgId) ||
|
||||
publishedMsgIds.has(postId);
|
||||
|
||||
const isDiscarded =
|
||||
existingInAppwrite?.status === 'discarded' ||
|
||||
existingInAppwrite?.status === 'ignored' ||
|
||||
discardedMsgIds.has(rawMsgId) ||
|
||||
discardedMsgIds.has(cleanMsgId) ||
|
||||
discardedMsgIds.has(postId);
|
||||
|
||||
const effectiveStatus: SupplierPost['status'] = isPublished
|
||||
? 'published'
|
||||
: isDiscarded
|
||||
? 'discarded'
|
||||
: 'pending_review';
|
||||
|
||||
if (statusFilter === 'pending') {
|
||||
if (isPublished || isDiscarded) continue;
|
||||
} else if (statusFilter === 'published') {
|
||||
if (!isPublished) continue;
|
||||
} else if (statusFilter === 'discarded') {
|
||||
if (!isDiscarded) continue;
|
||||
}
|
||||
|
||||
let imageUrls: string[] = [];
|
||||
|
||||
@ -169,10 +257,8 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// Si ya existía con datos extraídos en Appwrite, usarlo directamente asegurando sanitización
|
||||
let extracted = existingInAppwrite?.extractedProduct;
|
||||
if (!extracted) {
|
||||
// NUNCA pasar groupName como texto de calzado. Si text está vacío, pasar string vacío para que Gemini use la imagen y visión sin contaminar el nombre del producto
|
||||
const cleanInputText = text || '';
|
||||
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
|
||||
} else if (extracted && extracted.name) {
|
||||
@ -190,12 +276,13 @@ export async function GET(request: Request) {
|
||||
|
||||
const post: SupplierPost = {
|
||||
id: postId,
|
||||
whatsappMessageId: rawMsgId || cleanMsgId,
|
||||
channelName: groupName,
|
||||
channelJid: remoteJid,
|
||||
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
||||
rawImages: imageUrls,
|
||||
extractedProduct: extracted,
|
||||
status: existingInAppwrite?.status || 'pending_review',
|
||||
status: effectiveStatus,
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
@ -203,26 +290,23 @@ export async function GET(request: Request) {
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
|
||||
// Persistir de forma no bloqueante en Appwrite
|
||||
saveSupplierPostToAppwrite(post).catch(() => {});
|
||||
|
||||
if (processedPosts.length >= limit) break;
|
||||
}
|
||||
|
||||
const mergedPosts = [...processedPosts];
|
||||
// Agregar los posts de Appwrite que no hayan estado en esta ráfaga en vivo
|
||||
savedPosts.forEach((sp) => {
|
||||
if (!mergedPosts.some((p) => p.id === sp.id)) {
|
||||
mergedPosts.push(sp);
|
||||
}
|
||||
});
|
||||
|
||||
dataService.saveSupplierPosts(mergedPosts);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: mergedPosts.length,
|
||||
posts: mergedPosts,
|
||||
statusFilter,
|
||||
totalFound: processedPosts.length,
|
||||
counts: {
|
||||
pending: processedPosts.filter((p) => p.status === 'pending_review').length,
|
||||
published: publishedMsgIds.size,
|
||||
discarded: discardedMsgIds.size,
|
||||
all: processedPosts.length + publishedMsgIds.size + discardedMsgIds.size,
|
||||
},
|
||||
posts: processedPosts,
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -246,7 +330,6 @@ export async function POST(request: Request) {
|
||||
marketIntelligence,
|
||||
} = body;
|
||||
|
||||
// 1. PUBLICAR UN PRODUCTO A LA TIENDA (APP MÓVIL Y WEB)
|
||||
if (action === 'publish' && post) {
|
||||
const retailPrice =
|
||||
customRetailPrice ||
|
||||
@ -267,6 +350,8 @@ export async function POST(request: Request) {
|
||||
forbiddenTerms: forbiddenContext,
|
||||
});
|
||||
|
||||
const whatsappMsgId = post.whatsappMessageId || post.id.replace('post-', '');
|
||||
|
||||
const newProduct = {
|
||||
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
name: prodName,
|
||||
@ -292,6 +377,7 @@ export async function POST(request: Request) {
|
||||
originBadge: proof.badge,
|
||||
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
|
||||
supplierPhone: post.supplierPhone || '573167455877',
|
||||
whatsappMessageId: whatsappMsgId,
|
||||
};
|
||||
|
||||
const added = dataService.addProduct(newProduct as any);
|
||||
@ -301,12 +387,11 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `¡${prodName} publicado y sincronizado en Appwrite, App Móvil y Web!`,
|
||||
message: `¡${prodName} publicado!`,
|
||||
product: added,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. PUBLICACIÓN MASIVA (BULK)
|
||||
if (action === 'bulk-publish' && Array.isArray(posts)) {
|
||||
const createdProducts = [];
|
||||
for (const p of posts) {
|
||||
@ -328,6 +413,8 @@ export async function POST(request: Request) {
|
||||
forbiddenTerms: forbiddenContext,
|
||||
});
|
||||
|
||||
const whatsappMsgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||
|
||||
const newProd = {
|
||||
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
name: prodName,
|
||||
@ -352,6 +439,7 @@ export async function POST(request: Request) {
|
||||
originBadge: proof.badge,
|
||||
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
|
||||
supplierPhone: p.supplierPhone || '573167455877',
|
||||
whatsappMessageId: whatsappMsgId,
|
||||
};
|
||||
|
||||
const added = dataService.addProduct(newProd as any);
|
||||
@ -363,19 +451,31 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `¡Se publicaron ${createdProducts.length} productos con persistencia en Appwrite!`,
|
||||
message: `¡Se publicaron ${createdProducts.length} productos!`,
|
||||
products: createdProducts,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. DESCARTAR POST
|
||||
if (action === 'discard' && post?.id) {
|
||||
await updateSupplierPostStatusInAppwrite(post.id, 'discarded');
|
||||
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
||||
return NextResponse.json({ success: true, message: 'Mensaje descartado' });
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Mensaje [${post.whatsappMessageId || post.id}] descartado y archivado.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'bulk-discard' && Array.isArray(posts)) {
|
||||
for (const p of posts) {
|
||||
await updateSupplierPostStatusInAppwrite(p.id, 'discarded');
|
||||
dataService.updateSupplierPostStatus(p.id, 'discarded');
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `¡${posts.length} mensajes descartados y archivados con éxito!`,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. BÚSQUEDA VISUAL E INTELIGENCIA DE MERCADO SOBRE POST
|
||||
if (action === 'market-research' && post) {
|
||||
const img = post.rawImages && post.rawImages[0];
|
||||
const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice);
|
||||
|
||||
@ -543,6 +543,7 @@ export async function getProductsFromAppwrite(): Promise<Product[]> {
|
||||
isActive: d.isActive !== false,
|
||||
supplierName: d.supplierName || '',
|
||||
supplierPhone: d.supplierPhone || '',
|
||||
whatsappMessageId: d.whatsappMessageId || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -574,6 +575,7 @@ export async function saveProductToAppwrite(product: Product): Promise<Product>
|
||||
isActive: product.isActive !== false,
|
||||
supplierName: product.supplierName || '',
|
||||
supplierPhone: product.supplierPhone || '',
|
||||
whatsappMessageId: product.whatsappMessageId || '',
|
||||
};
|
||||
|
||||
try {
|
||||
@ -620,6 +622,7 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
|
||||
|
||||
return {
|
||||
id: d.$id,
|
||||
whatsappMessageId: d.whatsappMessageId || d.$id.replace('post-', ''),
|
||||
channelName: d.channelName || '',
|
||||
channelJid: d.channelJid || '',
|
||||
supplierName: d.supplierName || '',
|
||||
@ -639,7 +642,9 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
|
||||
}
|
||||
|
||||
export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> {
|
||||
const whatsappMessageId = post.whatsappMessageId || post.id.replace('post-', '');
|
||||
const payload = {
|
||||
whatsappMessageId,
|
||||
channelName: post.channelName || 'Grupo WhatsApp',
|
||||
channelJid: post.channelJid || '',
|
||||
supplierName: post.supplierName || '',
|
||||
@ -653,22 +658,39 @@ export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<Su
|
||||
|
||||
try {
|
||||
await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload);
|
||||
return post;
|
||||
return { ...post, whatsappMessageId };
|
||||
} catch (e: any) {
|
||||
if (e.code === 409) {
|
||||
try {
|
||||
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload);
|
||||
} catch (_) {}
|
||||
}
|
||||
return post;
|
||||
return { ...post, whatsappMessageId };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSupplierPostStatusInAppwrite(id: string, status: SupplierPost['status']): Promise<boolean> {
|
||||
export async function updateSupplierPostStatusInAppwrite(
|
||||
id: string,
|
||||
status: SupplierPost['status']
|
||||
): Promise<boolean> {
|
||||
const cleanId = id.trim();
|
||||
const whatsappMessageId = cleanId.replace('post-', '');
|
||||
try {
|
||||
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', id, { status });
|
||||
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', cleanId, {
|
||||
status,
|
||||
whatsappMessageId,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
try {
|
||||
await serverDatabases.createDocument(DB_ID, 'supplier_posts', cleanId, {
|
||||
status,
|
||||
whatsappMessageId,
|
||||
receivedAt: new Date().toISOString(),
|
||||
});
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ export interface Product {
|
||||
supplierName?: string;
|
||||
supplierPhone?: string;
|
||||
supplierSku?: string;
|
||||
whatsappMessageId?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -145,6 +146,7 @@ export interface Order {
|
||||
|
||||
export interface SupplierPost {
|
||||
id: string;
|
||||
whatsappMessageId?: string;
|
||||
channelName: string;
|
||||
channelJid?: string;
|
||||
supplierName?: string;
|
||||
@ -154,6 +156,7 @@ export interface SupplierPost {
|
||||
extractedProduct?: ExtractedProductData;
|
||||
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
||||
receivedAt: string;
|
||||
publishedAt?: string;
|
||||
isRead?: boolean;
|
||||
marketResearchDone?: boolean;
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ async function sendApkToCarlos() {
|
||||
sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
||||
}
|
||||
|
||||
const caption = `🚀 *DripZone Sneakers - APK Oficial Actualizado (Build Ultra-Rápido)*\n\n📅 *Generado:* ${new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' })}\n📦 *Tamaño:* ${sizeMB} MB\n\n⚡ *Novedades de Rendimiento & Optimización:*\n🖼️ *Optimización Total de Imágenes & WebP:* Motor inteligente de compresión automática para Appwrite Storage, Unsplash y CDNs. Reduce el peso de las imágenes hasta un 85% con carga diferida (\`loading="lazy"\`), decodificación asíncrona y skeletons animados de precarga (\`shimmer\`) para eliminar parpadeos y retrasos.\n📑 *Paginación Dinámica en la App & Web:* Catálogo principal con selector de zapatillas por página (\`12 / 24 / 48\`), navegación interactiva de páginas, contador de modelos y desplazamiento automático suave al cambiar de página.\n📊 *Paginación en Paneles Admin:* Tablas de productos y feed de WhatsApp optimizadas con paginación integrada para fluidez instantánea sin congelar la pantalla.\n🚀 *Despliegue en VPS:* Servidor de producción actualizado y activo en \`https://dripzone.almeira.com.co\`.\n\n📲 *Descarga Directa del APK:* https://dripzone.almeira.com.co/DripZone.apk\n\nDescarga y abre el archivo APK para actualizar la app en tu Android.`;
|
||||
const caption = `🚀 *DripZone Sneakers - APK Oficial Actualizado (Privacidad & Sanitización Total)*\n\n📅 *Generado:* ${new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' })}\n📦 *Tamaño:* ${sizeMB} MB\n\n🔒 *Novedades de Privacidad Comercial & Sanitización:*\n🛡️ *Cero Nombres de Grupos o Proveedores:* Se sanitizan estrictamente títulos y textos de todas las publicaciones (IA multimodal, WhatsApp Feed, panel admin y Appwrite). Ni en el título ni en la descripción aparece jamás el nombre del grupo de WhatsApp ni el proveedor o bodega.\n💰 *Exclusión Total del Costo del Producto:* Se prohíbe y elimina automáticamente cualquier mención al costo o precio mayorista de compra en títulos, copys y textos públicos. Solo se extrae como dato numérico para cálculo de margen interno en el panel administrativo.\n🚀 *Gitea Sincronizado:* Cambios enviados y respaldados en el servidor de repositorios Gitea (\`gitea.almeira.com.co\`).\n\n📲 *Descarga Directa del APK:* https://dripzone.almeira.com.co/DripZone.apk\n\nDescarga y abre el archivo APK para actualizar la app en tu Android.`;
|
||||
|
||||
console.log(`📤 [1/2] Enviando notificación oficial a WhatsApp (+${TARGET_PHONE})...`);
|
||||
try {
|
||||
|
||||
74
scripts/send_whatsapp_delivery.py
Normal file
74
scripts/send_whatsapp_delivery.py
Normal file
@ -0,0 +1,74 @@
|
||||
import urllib.request
|
||||
import json
|
||||
import ssl
|
||||
import sys
|
||||
import io
|
||||
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
EVOLUTION_API_URL = "https://evolution.almeira.com.co"
|
||||
EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943"
|
||||
INSTANCE_NAME = "AutomataAlmeira"
|
||||
TARGET_PHONE = "573167455877"
|
||||
|
||||
APK_URL = "https://dripzone.almeira.com.co/api/download-apk"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def send_request(endpoint, payload):
|
||||
url = f"{EVOLUTION_API_URL}{endpoint}"
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, headers={
|
||||
"apikey": EVOLUTION_API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=60) as response:
|
||||
return response.status, response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode("utf-8")
|
||||
except Exception as e:
|
||||
return 0, str(e)
|
||||
|
||||
def send_update():
|
||||
message_text = (
|
||||
"🚀 *DripZone Sneakers - APK Oficial Actualizado (Privacidad Total)*\n\n"
|
||||
"🔒 *Novedades de Privacidad Comercial & Sanitización:*\n"
|
||||
"🛡️ *Cero Nombres de Grupos o Proveedores:* Se sanitizan estrictamente títulos y textos en toda la plataforma "
|
||||
"(IA multimodal, feed de WhatsApp, panel administrativo y Appwrite). Ni en el título ni en la descripción figura jamás "
|
||||
"el nombre del grupo de WhatsApp ni el proveedor/bodega.\n\n"
|
||||
"💰 *Exclusión Total del Costo del Producto:* Se prohíbe y elimina automáticamente cualquier mención al costo o precio mayorista "
|
||||
"de compra en títulos, copys y textos públicos. El costo solo se almacena internamente para control de márgenes.\n\n"
|
||||
"⚡ *Gitea Sincronizado:* Código respaldado en el servidor Gitea (`gitea.almeira.com.co`).\n"
|
||||
"🌐 *VPS Desplegado:* Nueva versión activa en `https://dripzone.almeira.com.co`.\n\n"
|
||||
f"📲 *Enlace directo al APK:* {APK_URL}\n\n"
|
||||
"Descarga el archivo adjunto para instalar o actualizar la aplicación en tu dispositivo Android."
|
||||
)
|
||||
|
||||
print(f"📤 [1/2] Enviando mensaje de texto a WhatsApp (+{TARGET_PHONE})...")
|
||||
payload_text = {
|
||||
"number": TARGET_PHONE,
|
||||
"text": message_text,
|
||||
"delay": 1000
|
||||
}
|
||||
status, res = send_request(f"/message/sendText/{INSTANCE_NAME}", payload_text)
|
||||
print(" -> Respuesta de texto:", status, res[:200] if res else "")
|
||||
|
||||
print(f"📤 [2/2] Enviando documento APK a WhatsApp (+{TARGET_PHONE})...")
|
||||
payload_media = {
|
||||
"number": TARGET_PHONE,
|
||||
"mediatype": "document",
|
||||
"mimetype": "application/vnd.android.package-archive",
|
||||
"fileName": "DripZone.apk",
|
||||
"caption": "📲 *DripZone.apk - Versión Oficial Actualizada*",
|
||||
"media": APK_URL
|
||||
}
|
||||
status, res = send_request(f"/message/sendMedia/{INSTANCE_NAME}", payload_media)
|
||||
print(" -> Respuesta de APK:", status, res[:200] if res else "")
|
||||
|
||||
if __name__ == "__main__":
|
||||
send_update()
|
||||
@ -15,12 +15,33 @@ export async function GET(request: Request) {
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||
|
||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const groupsMap = new Map<string, string>();
|
||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Obtener posts almacenados localmente para control de IDs
|
||||
const localPosts = dataService.getSupplierPosts();
|
||||
const publishedMsgIds = new Set<string>();
|
||||
const discardedMsgIds = new Set<string>();
|
||||
const localPostMap = new Map<string, SupplierPost>();
|
||||
|
||||
localPosts.forEach((p) => {
|
||||
localPostMap.set(p.id, p);
|
||||
const msgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||
if (msgId) {
|
||||
if (p.status === 'published' || p.status === 'published_store' || p.status === 'published_facebook') {
|
||||
publishedMsgIds.add(msgId);
|
||||
publishedMsgIds.add(p.id);
|
||||
} else if (p.status === 'discarded' || p.status === 'ignored') {
|
||||
discardedMsgIds.add(msgId);
|
||||
discardedMsgIds.add(p.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let liveGroups: any[] = [];
|
||||
try {
|
||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||
@ -61,12 +82,19 @@ export async function GET(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJids.length === 0) {
|
||||
if (targetJids.length === 0 || !fetchLive) {
|
||||
const filtered = localPosts.filter((p) => {
|
||||
if (statusFilter === 'pending') return p.status === 'pending_review';
|
||||
if (statusFilter === 'published') return p.status === 'published';
|
||||
if (statusFilter === 'discarded') return p.status === 'discarded';
|
||||
return true;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: 0,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
isLive: false,
|
||||
totalFound: filtered.length,
|
||||
posts: filtered,
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
@ -74,8 +102,9 @@ export async function GET(request: Request) {
|
||||
// 3. Consultar mensajes de cada grupo objetivo 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=${limit}`,
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -105,12 +134,20 @@ export async function GET(request: Request) {
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
const seenMessageIds = new Set<string>();
|
||||
|
||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
for (const record of allRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
const rawMsgId = (record.key?.id || '').trim();
|
||||
const cleanMsgId = rawMsgId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 36);
|
||||
const postId = `post-${cleanMsgId || Date.now()}`;
|
||||
|
||||
if (seenMessageIds.has(postId) || (rawMsgId && seenMessageIds.has(rawMsgId))) {
|
||||
continue;
|
||||
}
|
||||
seenMessageIds.add(postId);
|
||||
if (rawMsgId) seenMessageIds.add(rawMsgId);
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -121,9 +158,38 @@ export async function GET(request: Request) {
|
||||
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
||||
const hasImage = Boolean(record.message?.imageMessage);
|
||||
|
||||
// Si no tiene texto ni imagen, omitir
|
||||
if (!text && !hasImage) continue;
|
||||
|
||||
const existingInLocal =
|
||||
localPostMap.get(postId) ||
|
||||
localPosts.find((p) => p.id === postId || p.whatsappMessageId === rawMsgId || p.whatsappMessageId === cleanMsgId);
|
||||
|
||||
const isPublished =
|
||||
existingInLocal?.status === 'published' ||
|
||||
publishedMsgIds.has(rawMsgId) ||
|
||||
publishedMsgIds.has(cleanMsgId) ||
|
||||
publishedMsgIds.has(postId);
|
||||
|
||||
const isDiscarded =
|
||||
existingInLocal?.status === 'discarded' ||
|
||||
discardedMsgIds.has(rawMsgId) ||
|
||||
discardedMsgIds.has(cleanMsgId) ||
|
||||
discardedMsgIds.has(postId);
|
||||
|
||||
const effectiveStatus: SupplierPost['status'] = isPublished
|
||||
? 'published'
|
||||
: isDiscarded
|
||||
? 'discarded'
|
||||
: 'pending_review';
|
||||
|
||||
if (statusFilter === 'pending' && (isPublished || isDiscarded)) {
|
||||
continue;
|
||||
} else if (statusFilter === 'published' && !isPublished) {
|
||||
continue;
|
||||
} else if (statusFilter === 'discarded' && !isDiscarded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let imageUrls: string[] = [];
|
||||
|
||||
// Descargar imagen en Base64 desde Evolution API si está presente
|
||||
@ -167,31 +233,39 @@ export async function GET(request: Request) {
|
||||
: 'Reciente';
|
||||
|
||||
const post: SupplierPost = {
|
||||
id: `post-${record.key?.id || Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: postId,
|
||||
whatsappMessageId: rawMsgId || cleanMsgId,
|
||||
channelName: groupName,
|
||||
channelJid: remoteJid,
|
||||
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
||||
rawImages: imageUrls,
|
||||
extractedProduct: extracted,
|
||||
status: 'pending_review',
|
||||
status: effectiveStatus,
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
}
|
||||
dataService.addSupplierPost(post);
|
||||
|
||||
// Guardar en el servicio local los nuevos posts
|
||||
if (processedPosts.length > 0) {
|
||||
dataService.saveSupplierPosts(processedPosts);
|
||||
if (processedPosts.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
statusFilter,
|
||||
totalFound: processedPosts.length,
|
||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
||||
counts: {
|
||||
pending: processedPosts.filter((p) => p.status === 'pending_review').length,
|
||||
published: publishedMsgIds.size,
|
||||
discarded: discardedMsgIds.size,
|
||||
all: processedPosts.length + publishedMsgIds.size + discardedMsgIds.size,
|
||||
},
|
||||
posts: processedPosts,
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -298,7 +372,15 @@ export async function POST(request: Request) {
|
||||
// 3. DESCARTAR POST
|
||||
if (action === 'discard' && post?.id) {
|
||||
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
||||
return NextResponse.json({ success: true, message: 'Mensaje descartado' });
|
||||
return NextResponse.json({ success: true, message: 'Mensaje descartado y archivado' });
|
||||
}
|
||||
|
||||
// 4. DESCARTE MASIVO (BULK DISCARD)
|
||||
if (action === 'bulk-discard' && Array.isArray(posts)) {
|
||||
for (const p of posts) {
|
||||
dataService.updateSupplierPostStatus(p.id, 'discarded');
|
||||
}
|
||||
return NextResponse.json({ success: true, message: `¡${posts.length} mensajes descartados y archivados!` });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, error: 'Acción no válida' }, { status: 400 });
|
||||
|
||||
@ -72,6 +72,7 @@ export interface Product {
|
||||
supplierName?: string;
|
||||
supplierPhone?: string;
|
||||
supplierSku?: string;
|
||||
whatsappMessageId?: string;
|
||||
}
|
||||
|
||||
export interface City {
|
||||
@ -138,6 +139,7 @@ export interface Order {
|
||||
|
||||
export interface SupplierPost {
|
||||
id: string;
|
||||
whatsappMessageId?: string;
|
||||
channelName: string;
|
||||
channelJid?: string;
|
||||
supplierName?: string;
|
||||
@ -147,6 +149,7 @@ export interface SupplierPost {
|
||||
extractedProduct?: ExtractedProductData;
|
||||
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
||||
receivedAt: string;
|
||||
publishedAt?: string;
|
||||
}
|
||||
|
||||
export interface ExtractedProductData {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user