feat(feed): control por identificador de whatsapp para excluir mensajes publicados y descartados y avanzar lista

This commit is contained in:
AI Bot 2026-09-03 11:51:47 -05:00
parent 2e5ea56007
commit b7e920dcaf
8 changed files with 439 additions and 84 deletions

View File

@ -44,7 +44,7 @@ export default function SupplierFeedPage() {
const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]); const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]);
const [existingProducts, setExistingProducts] = useState<Product[]>([]); const [existingProducts, setExistingProducts] = useState<Product[]>([]);
const [selectedGroupFilter, setSelectedGroupFilter] = useState<string>('all'); 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 [isLoading, setIsLoading] = useState(false);
const [isSyncingLive, setIsSyncingLive] = useState(false); const [isSyncingLive, setIsSyncingLive] = useState(false);
const [actionMessage, setActionMessage] = useState<string | null>(null); const [actionMessage, setActionMessage] = useState<string | null>(null);
@ -77,7 +77,7 @@ export default function SupplierFeedPage() {
// Target brand selector for publishing // Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira'); 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 () => { const loadInitialData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -95,8 +95,8 @@ export default function SupplierFeedPage() {
setExistingProducts(prodData.products); setExistingProducts(prodData.products);
} }
// 3. Posts en vivo desde los grupos de WhatsApp (todos los mensajes: leídos y sin leer) // 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'); const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60&status=pending');
const feedData = await feedRes.json(); const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) { if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts); setPosts(feedData.posts);
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
try { try {
const url = const url =
groupJid !== 'all' groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}` ? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}`
: '/api/whatsapp-feed?fetchLive=true&limit=60'; : `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.posts)) { if (data.success && Array.isArray(data.posts)) {
setPosts(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) { } catch (err) {
console.error(err); console.error(err);
@ -143,15 +143,15 @@ export default function SupplierFeedPage() {
try { try {
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}` ? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}`
: '/api/whatsapp-feed?fetchLive=true&limit=60'; : `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.posts)) { if (data.success && Array.isArray(data.posts)) {
setPosts(data.posts); setPosts(data.posts);
setActionMessage( 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 { } else {
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.'); 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) => { const handleDiscardPost = async (post: SupplierPost) => {
try { try {
await fetch('/api/whatsapp-feed', { await fetch('/api/whatsapp-feed', {
@ -300,8 +300,45 @@ export default function SupplierFeedPage() {
next.delete(post.id); next.delete(post.id);
return next; return next;
}); });
setActionMessage(`🗑️ Mensaje #${post.whatsappMessageId || post.id.replace('post-', '')} descartado y archivado. No se volverá a mostrar en pendientes.`);
} catch (e) { } catch (e) {
console.error(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 // Filtrado de posts por estado y grupo
const filteredPosts = useMemo(() => { const filteredPosts = useMemo(() => {
return posts.filter((p) => { return posts.filter((p) => {
@ -445,7 +500,7 @@ export default function SupplierFeedPage() {
</button> </button>
<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 ${ className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending' tabStatus === 'pending'
? 'bg-purple-600 text-white shadow' ? 'bg-purple-600 text-white shadow'
@ -459,7 +514,7 @@ export default function SupplierFeedPage() {
</button> </button>
<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 ${ className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published' tabStatus === 'published'
? 'bg-emerald-600 text-white shadow' ? 'bg-emerald-600 text-white shadow'
@ -473,7 +528,7 @@ export default function SupplierFeedPage() {
</button> </button>
<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 ${ className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded' tabStatus === 'discarded'
? 'bg-rose-600 text-white shadow' ? 'bg-rose-600 text-white shadow'
@ -518,10 +573,18 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setSelectedPostIds(new Set())} 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 Deseleccionar
</button> </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 <button
onClick={handleBulkPublish} onClick={handleBulkPublish}
disabled={isLoading} disabled={isLoading}
@ -643,12 +706,20 @@ export default function SupplierFeedPage() {
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider"> <span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
{ext?.brand || 'Sneakers'} {ext?.brand || 'Sneakers'}
</span> </span>
<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 && ( {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"> <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} SKU: {ext.styleCode}
</span> </span>
)} )}
</div> </div>
</div>
<h3 className="font-bold text-white text-sm mt-0.5 line-clamp-1"> <h3 className="font-bold text-white text-sm mt-0.5 line-clamp-1">
{ext?.name || 'Zapatilla Urbana'} {ext?.name || 'Zapatilla Urbana'}
</h3> </h3>

View File

@ -10,6 +10,7 @@ import {
saveSupplierPostToAppwrite, saveSupplierPostToAppwrite,
updateSupplierPostStatusInAppwrite, updateSupplierPostStatusInAppwrite,
saveProductToAppwrite, saveProductToAppwrite,
getProductsFromAppwrite,
} from '@/lib/server-appwrite'; } from '@/lib/server-appwrite';
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types'; import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
@ -23,9 +24,41 @@ export async function GET(request: Request) {
const fetchLive = searchParams.get('fetchLive') !== 'false'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '60', 10); const limit = parseInt(searchParams.get('limit') || '60', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
// 1. Obtener posts guardados en Appwrite // 1. Obtener posts y productos existentes en Appwrite para control estricto de IDs de mensajes de WhatsApp
const savedPosts = await getSupplierPostsFromAppwrite(); 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 // 2. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
@ -57,7 +90,7 @@ export async function GET(request: Request) {
console.warn('Error resolviendo chats de WhatsApp:', e); 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[] = []; let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') { if (filterJid && filterJid !== 'all') {
targetJids = [filterJid]; targetJids = [filterJid];
@ -73,20 +106,34 @@ export async function GET(request: Request) {
} }
if (!fetchLive || targetJids.length === 0) { 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({ return NextResponse.json({
success: true, success: true,
isLive: false, isLive: false,
totalFound: savedPosts.length, totalFound: filteredSaved.length,
posts: savedPosts.length > 0 ? savedPosts : dataService.getSupplierPosts(), 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 })), 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) => { const groupFetchPromises = targetJids.map(async (jid) => {
try { try {
const fetchLimit = Math.max(limit * 2, 80);
const msgRes = await fetch( 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', method: 'POST',
headers: { 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)); 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 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 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -130,16 +187,47 @@ export async function GET(request: Request) {
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista'; const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
const hasImage = Boolean(record.message?.imageMessage); const hasImage = Boolean(record.message?.imageMessage);
// Si no tiene texto ni imagen, omitir
if (!text && !hasImage) continue; if (!text && !hasImage) continue;
const postId = `post-${record.key?.id || Date.now()}`; const existingInAppwrite =
appwritePostMap.get(postId) ||
// Verificar si ya existe en Appwrite para preservar su estado de publicación o descarte savedPosts.find(
const existingInAppwrite = savedPosts.find( (p) =>
(p) => p.id === postId || (p.rawMessage === text && (p as any).channelJid === remoteJid) p.id === postId ||
p.whatsappMessageId === rawMsgId ||
p.whatsappMessageId === cleanMsgId ||
(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[] = []; let imageUrls: string[] = [];
if (hasImage) { if (hasImage) {
@ -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; let extracted = existingInAppwrite?.extractedProduct;
if (!extracted) { 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 || ''; const cleanInputText = text || '';
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]); extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
} else if (extracted && extracted.name) { } else if (extracted && extracted.name) {
@ -190,12 +276,13 @@ export async function GET(request: Request) {
const post: SupplierPost = { const post: SupplierPost = {
id: postId, id: postId,
whatsappMessageId: rawMsgId || cleanMsgId,
channelName: groupName, channelName: groupName,
channelJid: remoteJid, channelJid: remoteJid,
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`, rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
rawImages: imageUrls, rawImages: imageUrls,
extractedProduct: extracted, extractedProduct: extracted,
status: existingInAppwrite?.status || 'pending_review', status: effectiveStatus,
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
@ -203,26 +290,23 @@ export async function GET(request: Request) {
}; };
processedPosts.push(post); processedPosts.push(post);
// Persistir de forma no bloqueante en Appwrite
saveSupplierPostToAppwrite(post).catch(() => {}); saveSupplierPostToAppwrite(post).catch(() => {});
}
const mergedPosts = [...processedPosts]; if (processedPosts.length >= limit) break;
// 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({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
totalFound: mergedPosts.length, statusFilter,
posts: mergedPosts, 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 })), groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
}); });
} catch (error: any) { } catch (error: any) {
@ -246,7 +330,6 @@ export async function POST(request: Request) {
marketIntelligence, marketIntelligence,
} = body; } = body;
// 1. PUBLICAR UN PRODUCTO A LA TIENDA (APP MÓVIL Y WEB)
if (action === 'publish' && post) { if (action === 'publish' && post) {
const retailPrice = const retailPrice =
customRetailPrice || customRetailPrice ||
@ -267,6 +350,8 @@ export async function POST(request: Request) {
forbiddenTerms: forbiddenContext, forbiddenTerms: forbiddenContext,
}); });
const whatsappMsgId = post.whatsappMessageId || post.id.replace('post-', '');
const newProduct = { const newProduct = {
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
name: prodName, name: prodName,
@ -292,6 +377,7 @@ export async function POST(request: Request) {
originBadge: proof.badge, originBadge: proof.badge,
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista', supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
supplierPhone: post.supplierPhone || '573167455877', supplierPhone: post.supplierPhone || '573167455877',
whatsappMessageId: whatsappMsgId,
}; };
const added = dataService.addProduct(newProduct as any); const added = dataService.addProduct(newProduct as any);
@ -301,12 +387,11 @@ export async function POST(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `¡${prodName} publicado y sincronizado en Appwrite, App Móvil y Web!`, message: `¡${prodName} publicado!`,
product: added, product: added,
}); });
} }
// 2. PUBLICACIÓN MASIVA (BULK)
if (action === 'bulk-publish' && Array.isArray(posts)) { if (action === 'bulk-publish' && Array.isArray(posts)) {
const createdProducts = []; const createdProducts = [];
for (const p of posts) { for (const p of posts) {
@ -328,6 +413,8 @@ export async function POST(request: Request) {
forbiddenTerms: forbiddenContext, forbiddenTerms: forbiddenContext,
}); });
const whatsappMsgId = p.whatsappMessageId || p.id.replace('post-', '');
const newProd = { const newProd = {
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
name: prodName, name: prodName,
@ -352,6 +439,7 @@ export async function POST(request: Request) {
originBadge: proof.badge, originBadge: proof.badge,
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista', supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
supplierPhone: p.supplierPhone || '573167455877', supplierPhone: p.supplierPhone || '573167455877',
whatsappMessageId: whatsappMsgId,
}; };
const added = dataService.addProduct(newProd as any); const added = dataService.addProduct(newProd as any);
@ -363,19 +451,31 @@ export async function POST(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `¡Se publicaron ${createdProducts.length} productos con persistencia en Appwrite!`, message: `¡Se publicaron ${createdProducts.length} productos!`,
products: createdProducts, products: createdProducts,
}); });
} }
// 3. DESCARTAR POST
if (action === 'discard' && post?.id) { if (action === 'discard' && post?.id) {
await updateSupplierPostStatusInAppwrite(post.id, 'discarded'); await updateSupplierPostStatusInAppwrite(post.id, 'discarded');
dataService.updateSupplierPostStatus(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) { if (action === 'market-research' && post) {
const img = post.rawImages && post.rawImages[0]; const img = post.rawImages && post.rawImages[0];
const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice); const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice);

View File

@ -543,6 +543,7 @@ export async function getProductsFromAppwrite(): Promise<Product[]> {
isActive: d.isActive !== false, isActive: d.isActive !== false,
supplierName: d.supplierName || '', supplierName: d.supplierName || '',
supplierPhone: d.supplierPhone || '', supplierPhone: d.supplierPhone || '',
whatsappMessageId: d.whatsappMessageId || '',
}; };
}); });
} }
@ -574,6 +575,7 @@ export async function saveProductToAppwrite(product: Product): Promise<Product>
isActive: product.isActive !== false, isActive: product.isActive !== false,
supplierName: product.supplierName || '', supplierName: product.supplierName || '',
supplierPhone: product.supplierPhone || '', supplierPhone: product.supplierPhone || '',
whatsappMessageId: product.whatsappMessageId || '',
}; };
try { try {
@ -620,6 +622,7 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
return { return {
id: d.$id, id: d.$id,
whatsappMessageId: d.whatsappMessageId || d.$id.replace('post-', ''),
channelName: d.channelName || '', channelName: d.channelName || '',
channelJid: d.channelJid || '', channelJid: d.channelJid || '',
supplierName: d.supplierName || '', supplierName: d.supplierName || '',
@ -639,7 +642,9 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
} }
export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> { export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> {
const whatsappMessageId = post.whatsappMessageId || post.id.replace('post-', '');
const payload = { const payload = {
whatsappMessageId,
channelName: post.channelName || 'Grupo WhatsApp', channelName: post.channelName || 'Grupo WhatsApp',
channelJid: post.channelJid || '', channelJid: post.channelJid || '',
supplierName: post.supplierName || '', supplierName: post.supplierName || '',
@ -653,22 +658,39 @@ export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<Su
try { try {
await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload); await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload);
return post; return { ...post, whatsappMessageId };
} catch (e: any) { } catch (e: any) {
if (e.code === 409) { if (e.code === 409) {
try { try {
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload); await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload);
} catch (_) {} } 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 { try {
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', id, { status }); await serverDatabases.updateDocument(DB_ID, 'supplier_posts', cleanId, {
status,
whatsappMessageId,
});
return true; return true;
} catch (e) { } catch (e) {
try {
await serverDatabases.createDocument(DB_ID, 'supplier_posts', cleanId, {
status,
whatsappMessageId,
receivedAt: new Date().toISOString(),
});
return true;
} catch (_) {
return false; return false;
} }
} }
}

View File

@ -78,6 +78,7 @@ export interface Product {
supplierName?: string; supplierName?: string;
supplierPhone?: string; supplierPhone?: string;
supplierSku?: string; supplierSku?: string;
whatsappMessageId?: string;
} }
@ -145,6 +146,7 @@ export interface Order {
export interface SupplierPost { export interface SupplierPost {
id: string; id: string;
whatsappMessageId?: string;
channelName: string; channelName: string;
channelJid?: string; channelJid?: string;
supplierName?: string; supplierName?: string;
@ -154,6 +156,7 @@ export interface SupplierPost {
extractedProduct?: ExtractedProductData; extractedProduct?: ExtractedProductData;
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored'; status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
receivedAt: string; receivedAt: string;
publishedAt?: string;
isRead?: boolean; isRead?: boolean;
marketResearchDone?: boolean; marketResearchDone?: boolean;
} }

View File

@ -16,7 +16,7 @@ async function sendApkToCarlos() {
sizeMB = (stat.size / (1024 * 1024)).toFixed(2); 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})...`); console.log(`📤 [1/2] Enviando notificación oficial a WhatsApp (+${TARGET_PHONE})...`);
try { try {

View 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()

View File

@ -15,12 +15,33 @@ export async function GET(request: Request) {
const fetchLive = searchParams.get('fetchLive') !== 'false'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10); const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API // 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const groupsMap = new Map<string, string>(); const groupsMap = new Map<string, string>();
localGroups.forEach((g) => groupsMap.set(g.jid, g.name)); 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[] = []; let liveGroups: any[] = [];
try { try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, { 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({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: false,
totalFound: 0, totalFound: filtered.length,
posts: dataService.getSupplierPosts(), posts: filtered,
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })), 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 // 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => { const groupFetchPromises = targetJids.map(async (jid) => {
try { try {
const fetchLimit = Math.max(limit * 2, 80);
const msgRes = await fetch( 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', method: 'POST',
headers: { headers: {
@ -105,12 +134,20 @@ export async function GET(request: Request) {
// 5. Procesar cada mensaje y descargar imágenes/IA // 5. Procesar cada mensaje y descargar imágenes/IA
const processedPosts: SupplierPost[] = []; const processedPosts: SupplierPost[] = [];
const seenMessageIds = new Set<string>();
// Limitar a los 60 más relevantes para rendimiento óptimo for (const record of allRecords) {
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || ''; 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -121,9 +158,38 @@ export async function GET(request: Request) {
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista'; const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
const hasImage = Boolean(record.message?.imageMessage); const hasImage = Boolean(record.message?.imageMessage);
// Si no tiene texto ni imagen, omitir
if (!text && !hasImage) continue; 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[] = []; let imageUrls: string[] = [];
// Descargar imagen en Base64 desde Evolution API si está presente // Descargar imagen en Base64 desde Evolution API si está presente
@ -167,31 +233,39 @@ export async function GET(request: Request) {
: 'Reciente'; : 'Reciente';
const post: SupplierPost = { const post: SupplierPost = {
id: `post-${record.key?.id || Date.now()}-${Math.random().toString(36).slice(2, 6)}`, id: postId,
whatsappMessageId: rawMsgId || cleanMsgId,
channelName: groupName, channelName: groupName,
channelJid: remoteJid, channelJid: remoteJid,
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`, rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
rawImages: imageUrls, rawImages: imageUrls,
extractedProduct: extracted, extractedProduct: extracted,
status: 'pending_review', status: effectiveStatus,
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
}; };
processedPosts.push(post); processedPosts.push(post);
} dataService.addSupplierPost(post);
// Guardar en el servicio local los nuevos posts if (processedPosts.length >= limit) {
if (processedPosts.length > 0) { break;
dataService.saveSupplierPosts(processedPosts); }
} }
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
statusFilter,
totalFound: processedPosts.length, 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 })), groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
}); });
} catch (error: any) { } catch (error: any) {
@ -298,7 +372,15 @@ export async function POST(request: Request) {
// 3. DESCARTAR POST // 3. DESCARTAR POST
if (action === 'discard' && post?.id) { if (action === 'discard' && post?.id) {
dataService.updateSupplierPostStatus(post.id, 'discarded'); 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 }); return NextResponse.json({ success: false, error: 'Acción no válida' }, { status: 400 });

View File

@ -72,6 +72,7 @@ export interface Product {
supplierName?: string; supplierName?: string;
supplierPhone?: string; supplierPhone?: string;
supplierSku?: string; supplierSku?: string;
whatsappMessageId?: string;
} }
export interface City { export interface City {
@ -138,6 +139,7 @@ export interface Order {
export interface SupplierPost { export interface SupplierPost {
id: string; id: string;
whatsappMessageId?: string;
channelName: string; channelName: string;
channelJid?: string; channelJid?: string;
supplierName?: string; supplierName?: string;
@ -147,6 +149,7 @@ export interface SupplierPost {
extractedProduct?: ExtractedProductData; extractedProduct?: ExtractedProductData;
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored'; status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
receivedAt: string; receivedAt: string;
publishedAt?: string;
} }
export interface ExtractedProductData { export interface ExtractedProductData {