feat: visualizacion completa de mensajes en vivo por grupos de whatsapp con imagenes e IA

This commit is contained in:
AI Bot 2026-08-28 18:42:09 -05:00
parent e89c65c426
commit ac82c9aaae
16 changed files with 1034 additions and 730 deletions

View File

@ -3,3 +3,4 @@
- **Autonomía Total**: Ejecuta todas las acciones, ediciones de archivos, comandos de terminal, pruebas y diagnósticos de forma proactiva e inmediata sin solicitar confirmación previa ni esperar aprobaciones intermedias.
- **Flujo Completo**: Cuando se solicite un cambio o se reporte un problema, investiga, modifica el código, compila, prueba y entrega la solución terminada.
- **Sin Bloqueos**: No te detengas a pedir permisos antes de ejecutar herramientas, llamadas a bases de datos o scripts de verificación.
- **Despliegue y Envío de APK Continuo**: Tras cualquier cambio o ajuste en el código, SIEMPRE compilar el APK Android, desplegar en el servidor VPS de producción y enviar automáticamente el archivo APK actualizado al WhatsApp de Carlos (+57 316 745 5877) sin esperar confirmación.

View File

@ -23,6 +23,7 @@ import {
Edit3,
Trash2,
ArrowUpRight,
Eye,
} from 'lucide-react';
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
import { formatCOP } from '@/lib/pricing-engine';
@ -50,17 +51,19 @@ export default function SupplierFeedPage() {
// Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados y posts iniciales
// Cargar grupos monitoreados y posts iniciales en vivo
const loadInitialData = async () => {
setIsLoading(true);
try {
const grpRes = await fetch('/api/whatsapp-groups');
// 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(grpData.groups);
}
const feedRes = await fetch('/api/whatsapp-feed');
// 2. Posts en vivo desde los grupos de WhatsApp
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts);
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData();
}, []);
// Cambiar filtro de grupo y recargar mensajes
const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid);
setIsSyncingLive(true);
setActionMessage(null);
try {
const url =
groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
: '/api/whatsapp-feed?fetchLive=true&limit=50';
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 del grupo.`
);
}
} catch (err) {
console.error(err);
setActionMessage('❌ Error cargando mensajes del grupo.');
} finally {
setIsSyncingLive(false);
}
};
// Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => {
setIsSyncingLive(true);
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
: '/api/whatsapp-feed?fetchLive=true&limit=40';
: '/api/whatsapp-feed?fetchLive=true&limit=50';
const res = await fetch(url);
const data = await res.json();
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
}
};
// Manejo de Selección
const handleToggleSelect = (id: string) => {
setSelectedPostIds((prev) => {
const next = new Set(prev);
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
});
};
// Filtrado de posts por estado y grupo
const filteredPosts = posts.filter((p) => {
// Estado
if (tabStatus === 'pending' && p.status === 'published') return false;
if (tabStatus === 'pending' && p.status === 'discarded') return false;
if (tabStatus === 'published' && p.status !== 'published') return false;
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
// Grupo
if (selectedGroupFilter !== 'all') {
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
}
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-cyan-500/20 text-cyan-400 flex items-center justify-center border border-cyan-500/30 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
<Radio className="w-5 h-5 animate-pulse" />
</div>
<div>
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Almeira Store</h1>
<p className="text-xs text-slate-400">
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
<p className="text-xs text-neutral-400">
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
</p>
</div>
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2.5 flex-wrap">
<Link
href="/admin/grupos-whatsapp"
className="bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
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-xl flex items-center gap-1.5 transition-all"
>
<MessageSquare className="w-4 h-4 text-cyan-400" />
<MessageSquare className="w-4 h-4 text-purple-400" />
<span>Gestionar Grupos</span>
</Link>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
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-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
{/* Toast feedback */}
{actionMessage && (
<div className="bg-cyan-950/80 border border-cyan-500/50 text-cyan-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-cyan-400" />
<Sparkles className="w-4 h-4 text-purple-400" />
<span>{actionMessage}</span>
</div>
<button
onClick={() => setActionMessage(null)}
className="text-white hover:text-cyan-300 font-bold px-2 cursor-pointer"
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
>
✕
</button>
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
)}
{/* Filter & Tabs Bar */}
<div className="bg-slate-900/90 border border-slate-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
{/* Status Tabs */}
<div className="flex items-center gap-1 bg-slate-950 p-1 rounded-2xl border border-slate-800 text-xs font-bold">
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
<button
onClick={() => setTabStatus('pending')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending'
? 'bg-cyan-600 text-white shadow'
: 'text-slate-400 hover:text-white'
? 'bg-purple-600 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>📥 Pendientes</span>
<span className="bg-cyan-950 text-cyan-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{pendingCount}
</span>
</button>
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published'
? 'bg-emerald-600 text-white shadow'
: 'text-slate-400 hover:text-white'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>✅ Publicados en Tienda</span>
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
onClick={() => setTabStatus('discarded')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded'
? 'bg-slate-800 text-white shadow'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>🗑️ Descartados</span>
<span className="bg-slate-900 text-slate-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{discardedCount}
</span>
</button>
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
{/* Group Selector Filter */}
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-cyan-400 shrink-0" />
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
<select
value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)}
className="bg-slate-950 border border-slate-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
onChange={(e) => handleGroupFilterChange(e.target.value)}
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
>
<option value="all">Todos los Grupos de WhatsApp</option>
{monitoredGroups.map((g) => (
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
{/* Multi-Selection Bulk Actions Bar */}
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
<div className="bg-gradient-to-r from-cyan-950/90 to-slate-900 border border-cyan-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="flex items-center gap-2.5">
<span className="bg-cyan-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
{selectedPostIds.size} seleccionados
</span>
<span className="text-xs text-slate-300 font-semibold hidden sm:inline">
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
Listos para transferir al catálogo de la tienda
</span>
</div>
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedPostIds(new Set())}
className="text-slate-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
>
Desmarcar
</button>
<button
onClick={handleBulkPublish}
className="bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-4 h-4" />
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
{/* Posts List / Grid */}
{filteredPosts.length === 0 ? (
<div className="bg-slate-900/60 rounded-3xl p-12 text-center border border-slate-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 flex items-center justify-center mx-auto">
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
<MessageSquare className="w-8 h-8" />
</div>
<div className="space-y-1">
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
<p className="text-xs text-slate-400 max-w-md mx-auto">
<p className="text-xs text-neutral-400 max-w-md mx-auto">
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
</p>
</div>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Escanear Grupos Ahora</span>
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
return (
<div
key={post.id}
className={`bg-slate-900 border rounded-3xl p-5 space-y-4 transition-all ${
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
isSelected
? 'border-cyan-500 shadow-[0_0_20px_rgba(6,182,212,0.2)]'
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
: isPublished
? 'border-emerald-500/30 bg-emerald-950/10'
: 'border-slate-800 hover:border-slate-700'
: 'border-neutral-800 hover:border-neutral-700'
}`}
>
{/* Header info */}
<div className="flex items-center justify-between gap-2 border-b border-slate-800 pb-3">
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
<div className="flex items-center gap-2">
{tabStatus === 'pending' && (
<button
onClick={() => handleToggleSelect(post.id)}
className="text-cyan-400 hover:text-cyan-300 cursor-pointer"
className="text-purple-400 hover:text-purple-300 cursor-pointer"
>
{isSelected ? (
<CheckSquare className="w-5 h-5 fill-cyan-600 text-white" />
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
) : (
<Square className="w-5 h-5 text-slate-600" />
<Square className="w-5 h-5 text-neutral-600" />
)}
</button>
)}
<div>
<span className="bg-cyan-950 text-cyan-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-cyan-500/30 inline-block">
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
{post.channelName || 'Grupo WhatsApp'}
</span>
<span className="text-[10px] text-slate-500 ml-2 font-mono">{post.receivedAt}</span>
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
</div>
</div>
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
</span>
) : isDiscarded ? (
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
Descartado
</span>
) : (
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
{/* Sneaker Image & Raw text */}
<div className="sm:col-span-5 space-y-2">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-slate-950 border border-slate-800 flex items-center justify-center group">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
{post.rawImages && post.rawImages[0] ? (
<img
src={post.rawImages[0]}
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
className="w-full h-full object-cover group-hover:scale-105 transition-all"
/>
) : (
<div className="text-slate-600 text-xs">Sin foto</div>
<div className="text-neutral-600 text-xs">Sin foto</div>
)}
</div>
<div className="bg-slate-950 rounded-xl p-2.5 border border-slate-800/80">
<p className="text-[11px] text-slate-400 font-mono whitespace-pre-line line-clamp-3">
{/* Raw WhatsApp Message snippet */}
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
{post.rawMessage}
</p>
</div>
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
<div className="space-y-2">
<div>
<span className="text-[10px] font-black uppercase text-cyan-400 tracking-wider">
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
</span>
<h3 className="text-base font-black text-white leading-tight">
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
</div>
{/* Pricing breakdown */}
<div className="bg-slate-950 p-3 rounded-2xl border border-slate-800 grid grid-cols-2 gap-2">
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
<div>
<span className="text-[10px] text-slate-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-slate-300 font-mono">
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-neutral-300 font-mono">
{formatCOP(extracted?.supplierPrice || 90000)}
</span>
</div>
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
{/* Sizes badge pills */}
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase block mb-1">
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
Tallas Disponibles:
</span>
<div className="flex gap-1 flex-wrap">
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
extracted.sizes.map((s) => (
<span
key={s}
className="bg-slate-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-slate-800"
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
>
{s}
</span>
))
) : (
<span className="text-xs text-slate-500">37 a 43</span>
<span className="text-xs text-neutral-500">37 a 43</span>
)}
</div>
</div>
</div>
{/* Card Actions */}
<div className="pt-2 border-t border-slate-800 flex items-center gap-2">
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
{!isPublished ? (
<>
<button
onClick={() => handlePublishPost(post)}
className="flex-1 bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-cyan-600/30 cursor-pointer transition-all active:scale-95"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-3.5 h-3.5" />
<span>Publicar en la App</span>
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleOpenEdit(post)}
title="Editar antes de publicar"
className="bg-slate-800 hover:bg-slate-700 text-slate-300 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Edit3 className="w-4 h-4" />
</button>
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleDiscardPost(post)}
title="Descartar"
className="bg-slate-800 hover:bg-red-950/40 text-slate-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
<Link
href="/"
target="_blank"
className="text-cyan-400 hover:underline font-bold flex items-center gap-1"
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
>
<span>Ver en Tienda</span>
<ArrowUpRight className="w-3 h-3" />
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
{/* Edit Modal Before Publishing */}
{editingPost && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-slate-900 border border-cyan-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Edit3 className="w-5 h-5 text-cyan-400" />
<Edit3 className="w-5 h-5 text-purple-400" />
<span>Personalizar antes de Publicar</span>
</h3>
<button
onClick={() => setEditingPost(null)}
className="text-slate-400 hover:text-white font-bold"
className="text-neutral-400 hover:text-white font-bold"
>
✕
</button>
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
<div className="space-y-3.5 text-xs">
<div>
<label className="font-bold text-slate-300 block mb-1">Nombre Comercial del Modelo:</label>
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-cyan-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="font-bold text-slate-300 block mb-1">Marca:</label>
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
<input
type="text"
value={editBrand}
onChange={(e) => setEditBrand(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
type="number"
value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
/>
</div>
</div>
<div>
<label className="font-bold text-slate-300 block mb-1">Tallas (separadas por coma):</label>
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
<input
type="text"
value={editSizes}
onChange={(e) => setEditSizes(e.target.value)}
placeholder="38, 39, 40, 41, 42, 43"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
</div>
<div className="flex items-center gap-2.5 pt-3 border-t border-slate-800">
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
<button
onClick={() => setEditingPost(null)}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
>
Cancelar
</button>
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
sizes: sizesArray,
});
}}
className="flex-1 bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-cyan-600/30 cursor-pointer"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
>
Guardar y Publicar en la App
</button>

View File

@ -70,7 +70,6 @@ export default function AdminWhatsAppGroupsPage() {
data.groups.forEach((g: MonitoredWhatsAppGroup) => {
if (g.isMonitored) initialSelected.add(g.jid);
});
// Also add local groups
groups.forEach((g) => {
if (g.isMonitored) initialSelected.add(g.jid);
});
@ -198,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Radio className="w-6 h-6 text-cyan-400 animate-pulse" />
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
<span>Monitoreo de Grupos de WhatsApp</span>
</h1>
<p className="text-xs text-slate-400 mt-1">
<p className="text-xs text-neutral-400 mt-1">
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
</p>
</div>
@ -209,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */}
<button
onClick={handleOpenPicker}
className="bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-cyan-500/25 active:scale-95 transition-all cursor-pointer"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
>
<Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span>
@ -218,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */}
{toast && (
<div className="bg-emerald-950/90 border border-emerald-500 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕
@ -228,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-slate-400">Grupos en tu WhatsApp</p>
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-slate-300">
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
<Users className="w-5 h-5" />
</div>
</div>
<div className="bg-gradient-to-br from-slate-900 to-emerald-950/40 border border-emerald-500/30 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-emerald-400">Grupos Monitoreados (Activos)</p>
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<Radio className="w-5 h-5" />
</div>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-cyan-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-slate-300 mt-1 flex items-center gap-1.5">
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span>
</p>
</div>
<div className="w-10 h-10 rounded-xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
<Zap className="w-5 h-5" />
</div>
</div>
</div>
{/* Filter and Search Toolbar */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="relative w-full sm:w-80">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre tus grupos activos..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-cyan-400"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
/>
</div>
@ -280,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all'
? 'bg-slate-800 text-white border border-slate-700'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Todos ({groups.length})
@ -290,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
: 'text-slate-400 hover:text-white'
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-neutral-400 hover:text-white'
}`}
>
Monitoreados ({monitoredCount})
@ -300,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored'
? 'bg-slate-800 text-white border border-slate-700'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Inactivos ({groups.length - monitoredCount})
@ -312,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-cyan-400" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p>
</div>
) : filteredGroups.length === 0 ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-3 bg-slate-900 border border-slate-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-slate-600" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p>
<button
onClick={handleOpenPicker}
className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
>
Buscar y Seleccionar Grupos desde WhatsApp
</button>
@ -336,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored
? 'bg-gradient-to-br from-slate-900 to-slate-900/90 border-emerald-500/40 shadow-lg shadow-emerald-950/20 ring-1 ring-emerald-500/20'
: 'bg-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80'
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`}
>
{/* Header */}
@ -346,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
isMonitored ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
}`}
/>
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name}
</h3>
</div>
<p className="text-[11px] font-mono text-slate-400 pl-4 mt-0.5 truncate">
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
{group.jid}
</p>
</div>
@ -363,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
isMonitored
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black'
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white'
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`}
>
{isMonitored ? (
<>
<CheckCircle2 className="w-3.5 h-3.5 fill-slate-950 text-emerald-500" />
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
<span>Monitoreando</span>
</>
) : (
@ -383,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span>
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span>
</div>
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-emerald-300 flex items-center gap-1">
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo
</span>
</div>
</div>
{group.notes && (
<p className="text-[11px] text-slate-400 bg-slate-950/40 p-2 rounded-lg border border-slate-800/60">
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
ℹ️ {group.notes}
</p>
)}
{/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80">
<span className="text-[10px] text-slate-500 font-mono">
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span>
<button
onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id}
className="text-xs font-bold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
>
<Sparkles className={`w-3.5 h-3.5 text-cyan-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button>
</div>
@ -430,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */}
{isPickerOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
{/* Modal Header */}
<div className="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950">
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
<div className="flex items-center space-x-2.5">
<div className="w-10 h-10 rounded-2xl bg-cyan-500/20 text-cyan-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<MessageSquare className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
<p className="text-xs text-slate-400">
<p className="text-xs text-neutral-400">
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p>
</div>
</div>
<button
onClick={() => setIsPickerOpen(false)}
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Search and Action Toolbar in Modal */}
<div className="p-4 border-b border-slate-800 bg-slate-900/50 space-y-3">
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
<div className="relative">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-cyan-400 font-semibold"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
/>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-400 font-semibold">
<span className="text-neutral-400 font-semibold">
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span>
<div className="flex items-center gap-2">
<button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
className="text-cyan-400 hover:text-cyan-300 font-bold hover:underline cursor-pointer"
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
>
Seleccionar visibles
</button>
<span className="text-slate-600">•</span>
<span className="text-neutral-600">•</span>
<button
onClick={handleDeselectAll}
className="text-slate-400 hover:text-white font-bold hover:underline cursor-pointer"
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
>
Deseleccionar todos
</button>
@ -490,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? (
<div className="py-16 text-center text-xs text-slate-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-cyan-400" />
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
<p className="text-[11px] text-slate-500">Conectando con WhatsApp...</p>
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
</div>
) : filteredPickerGroups.length === 0 ? (
<div className="py-12 text-center text-xs text-slate-400 space-y-2">
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div>
) : (
@ -509,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected
? 'bg-cyan-950/30 border-cyan-500/50 shadow-md ring-1 ring-cyan-500/20'
: 'bg-slate-950 border-slate-800 hover:border-slate-700'
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
}`}
>
<div className="flex items-center space-x-3 flex-1 min-w-0">
<div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected
? 'bg-cyan-500 border-cyan-400 text-slate-950'
: 'border-slate-700 bg-slate-900'
? 'bg-purple-600 border-purple-400 text-white'
: 'border-neutral-700 bg-neutral-900'
}`}
>
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
@ -528,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
<h4 className="font-bold text-white text-xs truncate">
{group.name}
</h4>
<p className="text-[10px] font-mono text-slate-400 truncate">
<p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid}
</p>
</div>
@ -540,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear
</span>
) : (
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
Ignorar
</span>
)}
@ -552,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div>
{/* Modal Footer */}
<div className="p-4 border-t border-slate-800 bg-slate-950 flex items-center justify-between">
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
<div className="text-xs">
<span className="text-slate-400">Seleccionados: </span>
<strong className="text-cyan-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-slate-400"> grupos para monitorear</span>
<span className="text-neutral-400">Seleccionados: </span>
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-neutral-400"> grupos para monitorear</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setIsPickerOpen(false)}
className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer transition-all"
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
>
Cancelar
</button>
@ -571,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button"
onClick={handleSaveSelectedGroups}
disabled={isLoadingLive}
className="px-5 py-2.5 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span>

View File

@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true';
const limit = parseInt(searchParams.get('limit') || '30', 10);
const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set(
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
);
// 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));
// Si no se pide en vivo, retornar lo almacenado localmente
if (!fetchLive) {
let posts = dataService.getSupplierPosts();
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
let liveGroups: any[] = [];
try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const allChats = await chatsRes.json();
if (Array.isArray(allChats)) {
liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
liveGroups.forEach((g: any) => {
const jid = g.remoteJid || g.id || g.jid;
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
if (jid) groupsMap.set(jid, name);
});
}
} catch (e) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 2. Determinar JIDs objetivo a consultar
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
} else {
targetJids = Array.from(groupsMap.keys());
}
}
if (targetJids.length === 0) {
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
const msgRes = await fetch(msgUrl, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
// 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => {
try {
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
{
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 : []);
} catch (err) {
return [];
}
});
const msgData = await msgRes.json();
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
// 3. Mapear grupos para resolver nombres
const groupsMap = new Map<string, string>();
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
// Si faltan nombres, intentar traerlos de Evolution
if (groupsMap.size === 0) {
try {
const grpRes = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
{ headers: { apikey: EVOLUTION_API_KEY } }
);
const grpData = await grpRes.json();
if (Array.isArray(grpData)) {
grpData.forEach((g: any) => {
const jid = g.id || g.jid;
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
});
}
} catch (e) {}
const results = await Promise.allSettled(groupFetchPromises);
const allRecords: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allRecords.push(...res.value);
}
}
// 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
const processedPosts: SupplierPost[] = [];
for (const record of records) {
// Limitar a los 60 más relevantes para rendimiento óptimo
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || '';
// Filtrar solo grupos de WhatsApp (@g.us)
if (!remoteJid.includes('@g.us')) continue;
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
// Si el grupo no está monitoreado, omitir
continue;
}
const text =
record.message?.conversation ||
record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
}
if (imageUrls.length === 0) {
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
imageUrls.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
hour: '2-digit',
minute: '2-digit',
day: '2-digit',
month: 'short',
})
: 'Reciente';
const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review',
receivedAt: timestamp,
supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
};
processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true,
isLive: true,
totalFound: processedPosts.length,
posts: dataService.getSupplierPosts(),
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
} catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error);

View File

@ -20,41 +20,50 @@ export async function GET(request: Request) {
});
}
// Consultar grupos en vivo desde Evolution API
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
try {
const response = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const liveGroups = await response.json();
const allChats = await response.json();
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
if (Array.isArray(liveGroups)) {
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: existing ? existing.isMonitored : false,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
// Asegurar que los locales que no vinieron en live sigan presentes
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({
success: true,
isLive: true,
totalLive: liveGroups.length,
totalLive: merged.length,
groups: merged,
});
}

View File

@ -51,19 +51,19 @@ export default function SupplierFeedPage() {
// Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados y posts iniciales
// Cargar grupos monitoreados y posts iniciales en vivo
const loadInitialData = async () => {
setIsLoading(true);
try {
// 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups');
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(grpData.groups);
}
// 2. Posts almacenados
const feedRes = await fetch('/api/whatsapp-feed');
// 2. Posts en vivo desde los grupos de WhatsApp
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts);
@ -79,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData();
}, []);
// Cambiar filtro de grupo y recargar mensajes
const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid);
setIsSyncingLive(true);
setActionMessage(null);
try {
const url =
groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
: '/api/whatsapp-feed?fetchLive=true&limit=50';
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 del grupo.`
);
}
} catch (err) {
console.error(err);
setActionMessage('❌ Error cargando mensajes del grupo.');
} finally {
setIsSyncingLive(false);
}
};
// Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => {
setIsSyncingLive(true);
@ -87,7 +113,7 @@ export default function SupplierFeedPage() {
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
: '/api/whatsapp-feed?fetchLive=true&limit=40';
: '/api/whatsapp-feed?fetchLive=true&limit=50';
const res = await fetch(url);
const data = await res.json();
@ -341,7 +367,7 @@ export default function SupplierFeedPage() {
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
<select
value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)}
onChange={(e) => handleGroupFilterChange(e.target.value)}
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
>
<option value="all">Todos los Grupos de WhatsApp</option>

View File

@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true';
const limit = parseInt(searchParams.get('limit') || '30', 10);
const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set(
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
);
// 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));
// Si no se pide en vivo, retornar lo almacenado localmente
if (!fetchLive) {
let posts = dataService.getSupplierPosts();
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
let liveGroups: any[] = [];
try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const allChats = await chatsRes.json();
if (Array.isArray(allChats)) {
liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
liveGroups.forEach((g: any) => {
const jid = g.remoteJid || g.id || g.jid;
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
if (jid) groupsMap.set(jid, name);
});
}
} catch (e) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 2. Determinar JIDs objetivo a consultar
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
} else {
targetJids = Array.from(groupsMap.keys());
}
}
if (targetJids.length === 0) {
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
const msgRes = await fetch(msgUrl, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
// 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => {
try {
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
{
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 : []);
} catch (err) {
return [];
}
});
const msgData = await msgRes.json();
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
// 3. Mapear grupos para resolver nombres
const groupsMap = new Map<string, string>();
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
// Si faltan nombres, intentar traerlos de Evolution
if (groupsMap.size === 0) {
try {
const grpRes = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
{ headers: { apikey: EVOLUTION_API_KEY } }
);
const grpData = await grpRes.json();
if (Array.isArray(grpData)) {
grpData.forEach((g: any) => {
const jid = g.id || g.jid;
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
});
}
} catch (e) {}
const results = await Promise.allSettled(groupFetchPromises);
const allRecords: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allRecords.push(...res.value);
}
}
// 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
const processedPosts: SupplierPost[] = [];
for (const record of records) {
// Limitar a los 60 más relevantes para rendimiento óptimo
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || '';
// Filtrar solo grupos de WhatsApp (@g.us)
if (!remoteJid.includes('@g.us')) continue;
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
// Si el grupo no está monitoreado, omitir
continue;
}
const text =
record.message?.conversation ||
record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
}
if (imageUrls.length === 0) {
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
imageUrls.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
hour: '2-digit',
minute: '2-digit',
day: '2-digit',
month: 'short',
})
: 'Reciente';
const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review',
receivedAt: timestamp,
supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
};
processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true,
isLive: true,
totalFound: processedPosts.length,
posts: dataService.getSupplierPosts(),
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
} catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error);

View File

@ -20,41 +20,50 @@ export async function GET(request: Request) {
});
}
// Consultar grupos en vivo desde Evolution API
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
try {
const response = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const liveGroups = await response.json();
const allChats = await response.json();
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
if (Array.isArray(liveGroups)) {
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: existing ? existing.isMonitored : false,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
// Asegurar que los locales que no vinieron en live sigan presentes
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({
success: true,
isLive: true,
totalLive: liveGroups.length,
totalLive: merged.length,
groups: merged,
});
}

View File

@ -23,6 +23,7 @@ import {
Edit3,
Trash2,
ArrowUpRight,
Eye,
} from 'lucide-react';
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
import { formatCOP } from '@/lib/pricing-engine';
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
const [editSizes, setEditSizes] = useState('');
// Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('sneaky');
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados y posts iniciales
// Cargar grupos monitoreados y posts iniciales en vivo
const loadInitialData = async () => {
setIsLoading(true);
try {
const grpRes = await fetch('/api/whatsapp-groups');
// 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(grpData.groups);
}
const feedRes = await fetch('/api/whatsapp-feed');
// 2. Posts en vivo desde los grupos de WhatsApp
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts);
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData();
}, []);
// Cambiar filtro de grupo y recargar mensajes
const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid);
setIsSyncingLive(true);
setActionMessage(null);
try {
const url =
groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
: '/api/whatsapp-feed?fetchLive=true&limit=50';
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 del grupo.`
);
}
} catch (err) {
console.error(err);
setActionMessage('❌ Error cargando mensajes del grupo.');
} finally {
setIsSyncingLive(false);
}
};
// Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => {
setIsSyncingLive(true);
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
: '/api/whatsapp-feed?fetchLive=true&limit=40';
: '/api/whatsapp-feed?fetchLive=true&limit=50';
const res = await fetch(url);
const data = await res.json();
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
}
};
// Manejo de Selección
const handleToggleSelect = (id: string) => {
setSelectedPostIds((prev) => {
const next = new Set(prev);
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
});
};
// Filtrado de posts por estado y grupo
const filteredPosts = posts.filter((p) => {
// Estado
if (tabStatus === 'pending' && p.status === 'published') return false;
if (tabStatus === 'pending' && p.status === 'discarded') return false;
if (tabStatus === 'published' && p.status !== 'published') return false;
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
// Grupo
if (selectedGroupFilter !== 'all') {
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
}
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-orange-500/20 text-orange-400 flex items-center justify-center border border-orange-500/30 shadow-[0_0_15px_rgba(249,115,22,0.3)]">
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
<Radio className="w-5 h-5 animate-pulse" />
</div>
<div>
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Sneaky</h1>
<p className="text-xs text-zinc-400">
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
<p className="text-xs text-neutral-400">
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
</p>
</div>
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2.5 flex-wrap">
<Link
href="/admin/grupos-whatsapp"
className="bg-zinc-900 hover:bg-zinc-800 border border-zinc-700 text-zinc-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
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-xl flex items-center gap-1.5 transition-all"
>
<MessageSquare className="w-4 h-4 text-orange-400" />
<MessageSquare className="w-4 h-4 text-purple-400" />
<span>Gestionar Grupos</span>
</Link>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-orange-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
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-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
{/* Toast feedback */}
{actionMessage && (
<div className="bg-orange-950/80 border border-orange-500/50 text-orange-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-orange-400" />
<Sparkles className="w-4 h-4 text-purple-400" />
<span>{actionMessage}</span>
</div>
<button
onClick={() => setActionMessage(null)}
className="text-white hover:text-orange-300 font-bold px-2 cursor-pointer"
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
>
✕
</button>
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
)}
{/* Filter & Tabs Bar */}
<div className="bg-zinc-900/90 border border-zinc-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
{/* Status Tabs */}
<div className="flex items-center gap-1 bg-zinc-950 p-1 rounded-2xl border border-zinc-800 text-xs font-bold">
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
<button
onClick={() => setTabStatus('pending')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending'
? 'bg-orange-600 text-white shadow'
: 'text-zinc-400 hover:text-white'
? 'bg-purple-600 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>📥 Pendientes</span>
<span className="bg-orange-950 text-orange-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{pendingCount}
</span>
</button>
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published'
? 'bg-emerald-600 text-white shadow'
: 'text-zinc-400 hover:text-white'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>✅ Publicados en Tienda</span>
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
onClick={() => setTabStatus('discarded')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded'
? 'bg-zinc-800 text-white shadow'
: 'text-zinc-400 hover:text-white'
? 'bg-neutral-800 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>🗑️ Descartados</span>
<span className="bg-zinc-900 text-zinc-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{discardedCount}
</span>
</button>
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
{/* Group Selector Filter */}
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-orange-400 shrink-0" />
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
<select
value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)}
className="bg-zinc-950 border border-zinc-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
onChange={(e) => handleGroupFilterChange(e.target.value)}
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
>
<option value="all">Todos los Grupos de WhatsApp</option>
{monitoredGroups.map((g) => (
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
{/* Multi-Selection Bulk Actions Bar */}
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
<div className="bg-gradient-to-r from-orange-950/90 to-zinc-900 border border-orange-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="flex items-center gap-2.5">
<span className="bg-orange-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
{selectedPostIds.size} seleccionados
</span>
<span className="text-xs text-zinc-300 font-semibold hidden sm:inline">
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
Listos para transferir al catálogo de la tienda
</span>
</div>
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedPostIds(new Set())}
className="text-zinc-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
>
Desmarcar
</button>
<button
onClick={handleBulkPublish}
className="bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-orange-600/30 cursor-pointer transition-all active:scale-95"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-4 h-4" />
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
{/* Posts List / Grid */}
{filteredPosts.length === 0 ? (
<div className="bg-zinc-900/60 rounded-3xl p-12 text-center border border-zinc-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 text-orange-400 flex items-center justify-center mx-auto">
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
<MessageSquare className="w-8 h-8" />
</div>
<div className="space-y-1">
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
<p className="text-xs text-zinc-400 max-w-md mx-auto">
<p className="text-xs text-neutral-400 max-w-md mx-auto">
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
</p>
</div>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-orange-600 hover:bg-orange-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Escanear Grupos Ahora</span>
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
return (
<div
key={post.id}
className={`bg-zinc-900 border rounded-3xl p-5 space-y-4 transition-all ${
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
isSelected
? 'border-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)]'
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
: isPublished
? 'border-emerald-500/30 bg-emerald-950/10'
: 'border-zinc-800 hover:border-zinc-700'
: 'border-neutral-800 hover:border-neutral-700'
}`}
>
{/* Header info */}
<div className="flex items-center justify-between gap-2 border-b border-zinc-800 pb-3">
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
<div className="flex items-center gap-2">
{tabStatus === 'pending' && (
<button
onClick={() => handleToggleSelect(post.id)}
className="text-orange-400 hover:text-orange-300 cursor-pointer"
className="text-purple-400 hover:text-purple-300 cursor-pointer"
>
{isSelected ? (
<CheckSquare className="w-5 h-5 fill-orange-600 text-white" />
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
) : (
<Square className="w-5 h-5 text-zinc-600" />
<Square className="w-5 h-5 text-neutral-600" />
)}
</button>
)}
<div>
<span className="bg-orange-950 text-orange-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-orange-500/30 inline-block">
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
{post.channelName || 'Grupo WhatsApp'}
</span>
<span className="text-[10px] text-zinc-500 ml-2 font-mono">{post.receivedAt}</span>
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
</div>
</div>
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
</span>
) : isDiscarded ? (
<span className="bg-zinc-800 text-zinc-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
Descartado
</span>
) : (
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
{/* Sneaker Image & Raw text */}
<div className="sm:col-span-5 space-y-2">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-zinc-950 border border-zinc-800 flex items-center justify-center group">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
{post.rawImages && post.rawImages[0] ? (
<img
src={post.rawImages[0]}
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
className="w-full h-full object-cover group-hover:scale-105 transition-all"
/>
) : (
<div className="text-zinc-600 text-xs">Sin foto</div>
<div className="text-neutral-600 text-xs">Sin foto</div>
)}
</div>
<div className="bg-zinc-950 rounded-xl p-2.5 border border-zinc-800/80">
<p className="text-[11px] text-zinc-400 font-mono whitespace-pre-line line-clamp-3">
{/* Raw WhatsApp Message snippet */}
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
{post.rawMessage}
</p>
</div>
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
<div className="space-y-2">
<div>
<span className="text-[10px] font-black uppercase text-orange-400 tracking-wider">
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
</span>
<h3 className="text-base font-black text-white leading-tight">
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
</div>
{/* Pricing breakdown */}
<div className="bg-zinc-950 p-3 rounded-2xl border border-zinc-800 grid grid-cols-2 gap-2">
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
<div>
<span className="text-[10px] text-zinc-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-zinc-300 font-mono">
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-neutral-300 font-mono">
{formatCOP(extracted?.supplierPrice || 90000)}
</span>
</div>
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
{/* Sizes badge pills */}
<div>
<span className="text-[10px] font-bold text-zinc-400 uppercase block mb-1">
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
Tallas Disponibles:
</span>
<div className="flex gap-1 flex-wrap">
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
extracted.sizes.map((s) => (
<span
key={s}
className="bg-zinc-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-zinc-800"
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
>
{s}
</span>
))
) : (
<span className="text-xs text-zinc-500">37 a 43</span>
<span className="text-xs text-neutral-500">37 a 43</span>
)}
</div>
</div>
</div>
{/* Card Actions */}
<div className="pt-2 border-t border-zinc-800 flex items-center gap-2">
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
{!isPublished ? (
<>
<button
onClick={() => handlePublishPost(post)}
className="flex-1 bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-orange-600/30 cursor-pointer transition-all active:scale-95"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-3.5 h-3.5" />
<span>Publicar en la App</span>
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleOpenEdit(post)}
title="Editar antes de publicar"
className="bg-zinc-800 hover:bg-zinc-700 text-zinc-300 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Edit3 className="w-4 h-4" />
</button>
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleDiscardPost(post)}
title="Descartar"
className="bg-zinc-800 hover:bg-red-950/40 text-zinc-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
<Link
href="/"
target="_blank"
className="text-orange-400 hover:underline font-bold flex items-center gap-1"
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
>
<span>Ver en Tienda</span>
<ArrowUpRight className="w-3 h-3" />
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
{/* Edit Modal Before Publishing */}
{editingPost && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-zinc-900 border border-orange-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-zinc-800 pb-3">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Edit3 className="w-5 h-5 text-orange-400" />
<Edit3 className="w-5 h-5 text-purple-400" />
<span>Personalizar antes de Publicar</span>
</h3>
<button
onClick={() => setEditingPost(null)}
className="text-zinc-400 hover:text-white font-bold"
className="text-neutral-400 hover:text-white font-bold"
>
✕
</button>
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
<div className="space-y-3.5 text-xs">
<div>
<label className="font-bold text-zinc-300 block mb-1">Nombre Comercial del Modelo:</label>
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-orange-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="font-bold text-zinc-300 block mb-1">Marca:</label>
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
<input
type="text"
value={editBrand}
onChange={(e) => setEditBrand(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
type="number"
value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
/>
</div>
</div>
<div>
<label className="font-bold text-zinc-300 block mb-1">Tallas (separadas por coma):</label>
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
<input
type="text"
value={editSizes}
onChange={(e) => setEditSizes(e.target.value)}
placeholder="38, 39, 40, 41, 42, 43"
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
</div>
<div className="flex items-center gap-2.5 pt-3 border-t border-zinc-800">
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
<button
onClick={() => setEditingPost(null)}
className="flex-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
>
Cancelar
</button>
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
sizes: sizesArray,
});
}}
className="flex-1 bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-orange-600/30 cursor-pointer"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
>
Guardar y Publicar en la App
</button>

View File

@ -197,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Radio className="w-6 h-6 text-orange-500 animate-pulse" />
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
<span>Monitoreo de Grupos de WhatsApp</span>
</h1>
<p className="text-xs text-zinc-400 mt-1">
<p className="text-xs text-neutral-400 mt-1">
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
</p>
</div>
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */}
<button
onClick={handleOpenPicker}
className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-400 hover:to-amber-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-orange-500/25 active:scale-95 transition-all cursor-pointer"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
>
<Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span>
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */}
{toast && (
<div className="bg-orange-950/90 border border-orange-500 text-orange-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-zinc-400">Grupos en tu WhatsApp</p>
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center text-zinc-300">
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
<Users className="w-5 h-5" />
</div>
</div>
<div className="bg-gradient-to-br from-zinc-900 to-orange-950/40 border border-orange-500/30 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-orange-400">Grupos Monitoreados (Activos)</p>
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-orange-500/20 text-orange-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<Radio className="w-5 h-5" />
</div>
</div>
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-orange-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-zinc-300 mt-1 flex items-center gap-1.5">
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span>
</p>
</div>
<div className="w-10 h-10 rounded-xl bg-orange-500/10 text-orange-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
<Zap className="w-5 h-5" />
</div>
</div>
</div>
{/* Filter and Search Toolbar */}
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="relative w-full sm:w-80">
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre tus grupos activos..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-orange-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
/>
</div>
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all'
? 'bg-zinc-800 text-white border border-zinc-700'
: 'text-zinc-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Todos ({groups.length})
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored'
? 'bg-orange-500/20 text-orange-300 border border-orange-500/30'
: 'text-zinc-400 hover:text-white'
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-neutral-400 hover:text-white'
}`}
>
Monitoreados ({monitoredCount})
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored'
? 'bg-zinc-800 text-white border border-zinc-700'
: 'text-zinc-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Inactivos ({groups.length - monitoredCount})
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-zinc-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-orange-400" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p>
</div>
) : filteredGroups.length === 0 ? (
<div className="col-span-full py-12 text-center text-xs text-zinc-400 space-y-3 bg-zinc-900 border border-zinc-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-zinc-600" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p>
<button
onClick={handleOpenPicker}
className="bg-orange-500 hover:bg-orange-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
>
Buscar y Seleccionar Grupos desde WhatsApp
</button>
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored
? 'bg-gradient-to-br from-zinc-900 to-zinc-900/90 border-orange-500/40 shadow-lg shadow-orange-950/20 ring-1 ring-orange-500/20'
: 'bg-zinc-900/60 border-zinc-800 hover:border-zinc-700 opacity-80'
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`}
>
{/* Header */}
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
isMonitored ? 'bg-orange-400 animate-pulse' : 'bg-zinc-600'
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
}`}
/>
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name}
</h3>
</div>
<p className="text-[11px] font-mono text-zinc-400 pl-4 mt-0.5 truncate">
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
{group.jid}
</p>
</div>
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
isMonitored
? 'bg-orange-500 text-zinc-950 border-orange-400 shadow-md font-black'
: 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-white'
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`}
>
{isMonitored ? (
<>
<CheckCircle2 className="w-3.5 h-3.5 fill-zinc-950 text-orange-500" />
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
<span>Monitoreando</span>
</>
) : (
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
<div className="bg-zinc-950/80 p-2.5 rounded-xl border border-zinc-800">
<span className="text-zinc-400 block text-[10px]">Margen Aplicado</span>
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span>
</div>
<div className="bg-zinc-950/80 p-2.5 rounded-xl border border-zinc-800">
<span className="text-zinc-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-orange-300 flex items-center gap-1">
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo
</span>
</div>
</div>
{group.notes && (
<p className="text-[11px] text-zinc-400 bg-zinc-950/40 p-2 rounded-lg border border-zinc-800/60">
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
ℹ️ {group.notes}
</p>
)}
{/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-zinc-800/80">
<span className="text-[10px] text-zinc-500 font-mono">
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span>
<button
onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id}
className="text-xs font-bold text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
>
<Sparkles className={`w-3.5 h-3.5 text-orange-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button>
</div>
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */}
{isPickerOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
{/* Modal Header */}
<div className="p-5 border-b border-zinc-800 flex items-center justify-between bg-zinc-950">
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
<div className="flex items-center space-x-2.5">
<div className="w-10 h-10 rounded-2xl bg-orange-500/20 text-orange-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<MessageSquare className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
<p className="text-xs text-zinc-400">
<p className="text-xs text-neutral-400">
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p>
</div>
</div>
<button
onClick={() => setIsPickerOpen(false)}
className="w-8 h-8 rounded-full bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Search and Action Toolbar in Modal */}
<div className="p-4 border-b border-zinc-800 bg-zinc-900/50 space-y-3">
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
<div className="relative">
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-orange-500 font-semibold"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
/>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-zinc-400 font-semibold">
<span className="text-neutral-400 font-semibold">
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span>
<div className="flex items-center gap-2">
<button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
className="text-orange-400 hover:text-orange-300 font-bold hover:underline cursor-pointer"
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
>
Seleccionar visibles
</button>
<span className="text-zinc-600">•</span>
<span className="text-neutral-600">•</span>
<button
onClick={handleDeselectAll}
className="text-zinc-400 hover:text-white font-bold hover:underline cursor-pointer"
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
>
Deseleccionar todos
</button>
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? (
<div className="py-16 text-center text-xs text-zinc-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-orange-400" />
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
<p className="text-[11px] text-zinc-500">Conectando con WhatsApp...</p>
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
</div>
) : filteredPickerGroups.length === 0 ? (
<div className="py-12 text-center text-xs text-zinc-400 space-y-2">
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div>
) : (
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected
? 'bg-orange-950/30 border-orange-500/50 shadow-md ring-1 ring-orange-500/20'
: 'bg-zinc-950 border-zinc-800 hover:border-zinc-700'
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
}`}
>
<div className="flex items-center space-x-3 flex-1 min-w-0">
<div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected
? 'bg-orange-500 border-orange-400 text-zinc-950'
: 'border-zinc-700 bg-zinc-900'
? 'bg-purple-600 border-purple-400 text-white'
: 'border-neutral-700 bg-neutral-900'
}`}
>
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
@ -527,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
<h4 className="font-bold text-white text-xs truncate">
{group.name}
</h4>
<p className="text-[10px] font-mono text-zinc-400 truncate">
<p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid}
</p>
</div>
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear
</span>
) : (
<span className="bg-zinc-800 text-zinc-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
Ignorar
</span>
)}
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div>
{/* Modal Footer */}
<div className="p-4 border-t border-zinc-800 bg-zinc-950 flex items-center justify-between">
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
<div className="text-xs">
<span className="text-zinc-400">Seleccionados: </span>
<strong className="text-orange-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-zinc-400"> grupos para monitorear</span>
<span className="text-neutral-400">Seleccionados: </span>
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-neutral-400"> grupos para monitorear</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setIsPickerOpen(false)}
className="px-4 py-2.5 rounded-xl bg-zinc-800 hover:bg-zinc-700 text-zinc-300 font-bold text-xs cursor-pointer transition-all"
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
>
Cancelar
</button>
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button"
onClick={handleSaveSelectedGroups}
disabled={isLoadingLive}
className="px-5 py-2.5 rounded-xl bg-orange-500 hover:bg-orange-400 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-orange-500/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span>

View File

@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true';
const limit = parseInt(searchParams.get('limit') || '30', 10);
const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set(
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
);
// 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));
// Si no se pide en vivo, retornar lo almacenado localmente
if (!fetchLive) {
let posts = dataService.getSupplierPosts();
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
let liveGroups: any[] = [];
try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const allChats = await chatsRes.json();
if (Array.isArray(allChats)) {
liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
liveGroups.forEach((g: any) => {
const jid = g.remoteJid || g.id || g.jid;
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
if (jid) groupsMap.set(jid, name);
});
}
} catch (e) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 2. Determinar JIDs objetivo a consultar
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
} else {
targetJids = Array.from(groupsMap.keys());
}
}
if (targetJids.length === 0) {
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
const msgRes = await fetch(msgUrl, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
// 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => {
try {
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
{
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 : []);
} catch (err) {
return [];
}
});
const msgData = await msgRes.json();
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
// 3. Mapear grupos para resolver nombres
const groupsMap = new Map<string, string>();
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
// Si faltan nombres, intentar traerlos de Evolution
if (groupsMap.size === 0) {
try {
const grpRes = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
{ headers: { apikey: EVOLUTION_API_KEY } }
);
const grpData = await grpRes.json();
if (Array.isArray(grpData)) {
grpData.forEach((g: any) => {
const jid = g.id || g.jid;
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
});
}
} catch (e) {}
const results = await Promise.allSettled(groupFetchPromises);
const allRecords: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allRecords.push(...res.value);
}
}
// 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
const processedPosts: SupplierPost[] = [];
for (const record of records) {
// Limitar a los 60 más relevantes para rendimiento óptimo
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || '';
// Filtrar solo grupos de WhatsApp (@g.us)
if (!remoteJid.includes('@g.us')) continue;
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
// Si el grupo no está monitoreado, omitir
continue;
}
const text =
record.message?.conversation ||
record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
}
if (imageUrls.length === 0) {
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
imageUrls.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
hour: '2-digit',
minute: '2-digit',
day: '2-digit',
month: 'short',
})
: 'Reciente';
const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review',
receivedAt: timestamp,
supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
};
processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true,
isLive: true,
totalFound: processedPosts.length,
posts: dataService.getSupplierPosts(),
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
} catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error);

View File

@ -20,41 +20,50 @@ export async function GET(request: Request) {
});
}
// Consultar grupos en vivo desde Evolution API
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
try {
const response = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const liveGroups = await response.json();
const allChats = await response.json();
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
if (Array.isArray(liveGroups)) {
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: existing ? existing.isMonitored : false,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
// Asegurar que los locales que no vinieron en live sigan presentes
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({
success: true,
isLive: true,
totalLive: liveGroups.length,
totalLive: merged.length,
groups: merged,
});
}

View File

@ -23,6 +23,7 @@ import {
Edit3,
Trash2,
ArrowUpRight,
Eye,
} from 'lucide-react';
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
import { formatCOP } from '@/lib/pricing-engine';
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
const [editSizes, setEditSizes] = useState('');
// Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('zapply');
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados y posts iniciales
// Cargar grupos monitoreados y posts iniciales en vivo
const loadInitialData = async () => {
setIsLoading(true);
try {
const grpRes = await fetch('/api/whatsapp-groups');
// 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(grpData.groups);
}
const feedRes = await fetch('/api/whatsapp-feed');
// 2. Posts en vivo desde los grupos de WhatsApp
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts);
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData();
}, []);
// Cambiar filtro de grupo y recargar mensajes
const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid);
setIsSyncingLive(true);
setActionMessage(null);
try {
const url =
groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
: '/api/whatsapp-feed?fetchLive=true&limit=50';
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 del grupo.`
);
}
} catch (err) {
console.error(err);
setActionMessage('❌ Error cargando mensajes del grupo.');
} finally {
setIsSyncingLive(false);
}
};
// Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => {
setIsSyncingLive(true);
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
: '/api/whatsapp-feed?fetchLive=true&limit=40';
: '/api/whatsapp-feed?fetchLive=true&limit=50';
const res = await fetch(url);
const data = await res.json();
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
}
};
// Manejo de Selección
const handleToggleSelect = (id: string) => {
setSelectedPostIds((prev) => {
const next = new Set(prev);
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
});
};
// Filtrado de posts por estado y grupo
const filteredPosts = posts.filter((p) => {
// Estado
if (tabStatus === 'pending' && p.status === 'published') return false;
if (tabStatus === 'pending' && p.status === 'discarded') return false;
if (tabStatus === 'published' && p.status !== 'published') return false;
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
// Grupo
if (selectedGroupFilter !== 'all') {
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
}
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center border border-emerald-500/30 shadow-[0_0_15px_rgba(16,185,129,0.3)]">
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
<Radio className="w-5 h-5 animate-pulse" />
</div>
<div>
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Zapply</h1>
<p className="text-xs text-slate-400">
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
<p className="text-xs text-neutral-400">
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
</p>
</div>
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2.5 flex-wrap">
<Link
href="/admin/grupos-whatsapp"
className="bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
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-xl flex items-center gap-1.5 transition-all"
>
<MessageSquare className="w-4 h-4 text-emerald-400" />
<MessageSquare className="w-4 h-4 text-purple-400" />
<span>Gestionar Grupos</span>
</Link>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
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-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
{/* Toast feedback */}
{actionMessage && (
<div className="bg-emerald-950/80 border border-emerald-500/50 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-emerald-400" />
<Sparkles className="w-4 h-4 text-purple-400" />
<span>{actionMessage}</span>
</div>
<button
onClick={() => setActionMessage(null)}
className="text-white hover:text-emerald-300 font-bold px-2 cursor-pointer"
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
>
✕
</button>
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
)}
{/* Filter & Tabs Bar */}
<div className="bg-slate-900/90 border border-slate-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
{/* Status Tabs */}
<div className="flex items-center gap-1 bg-slate-950 p-1 rounded-2xl border border-slate-800 text-xs font-bold">
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
<button
onClick={() => setTabStatus('pending')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending'
? 'bg-emerald-600 text-white shadow'
: 'text-slate-400 hover:text-white'
? 'bg-purple-600 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>📥 Pendientes</span>
<span className="bg-emerald-950 text-emerald-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{pendingCount}
</span>
</button>
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published'
? 'bg-emerald-600 text-white shadow'
: 'text-slate-400 hover:text-white'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>✅ Publicados en Tienda</span>
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
onClick={() => setTabStatus('discarded')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded'
? 'bg-slate-800 text-white shadow'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white shadow'
: 'text-neutral-400 hover:text-white'
}`}
>
<span>🗑️ Descartados</span>
<span className="bg-slate-900 text-slate-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
{discardedCount}
</span>
</button>
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
{/* Group Selector Filter */}
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-emerald-400 shrink-0" />
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
<select
value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)}
className="bg-slate-950 border border-slate-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
onChange={(e) => handleGroupFilterChange(e.target.value)}
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
>
<option value="all">Todos los Grupos de WhatsApp</option>
{monitoredGroups.map((g) => (
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
{/* Multi-Selection Bulk Actions Bar */}
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
<div className="bg-gradient-to-r from-emerald-950/90 to-slate-900 border border-emerald-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
<div className="flex items-center gap-2.5">
<span className="bg-emerald-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
{selectedPostIds.size} seleccionados
</span>
<span className="text-xs text-slate-300 font-semibold hidden sm:inline">
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
Listos para transferir al catálogo de la tienda
</span>
</div>
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2">
<button
onClick={() => setSelectedPostIds(new Set())}
className="text-slate-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
>
Desmarcar
</button>
<button
onClick={handleBulkPublish}
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-4 h-4" />
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
{/* Posts List / Grid */}
{filteredPosts.length === 0 ? (
<div className="bg-slate-900/60 rounded-3xl p-12 text-center border border-slate-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto">
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
<MessageSquare className="w-8 h-8" />
</div>
<div className="space-y-1">
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
<p className="text-xs text-slate-400 max-w-md mx-auto">
<p className="text-xs text-neutral-400 max-w-md mx-auto">
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
</p>
</div>
<button
onClick={handleSyncLive}
disabled={isSyncingLive}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
>
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Escanear Grupos Ahora</span>
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
return (
<div
key={post.id}
className={`bg-slate-900 border rounded-3xl p-5 space-y-4 transition-all ${
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
isSelected
? 'border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.2)]'
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
: isPublished
? 'border-emerald-500/30 bg-emerald-950/10'
: 'border-slate-800 hover:border-slate-700'
: 'border-neutral-800 hover:border-neutral-700'
}`}
>
{/* Header info */}
<div className="flex items-center justify-between gap-2 border-b border-slate-800 pb-3">
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
<div className="flex items-center gap-2">
{tabStatus === 'pending' && (
<button
onClick={() => handleToggleSelect(post.id)}
className="text-emerald-400 hover:text-emerald-300 cursor-pointer"
className="text-purple-400 hover:text-purple-300 cursor-pointer"
>
{isSelected ? (
<CheckSquare className="w-5 h-5 fill-emerald-600 text-white" />
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
) : (
<Square className="w-5 h-5 text-slate-600" />
<Square className="w-5 h-5 text-neutral-600" />
)}
</button>
)}
<div>
<span className="bg-emerald-950 text-emerald-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-emerald-500/30 inline-block">
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
{post.channelName || 'Grupo WhatsApp'}
</span>
<span className="text-[10px] text-slate-500 ml-2 font-mono">{post.receivedAt}</span>
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
</div>
</div>
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
</span>
) : isDiscarded ? (
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
Descartado
</span>
) : (
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
{/* Sneaker Image & Raw text */}
<div className="sm:col-span-5 space-y-2">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-slate-950 border border-slate-800 flex items-center justify-center group">
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
{post.rawImages && post.rawImages[0] ? (
<img
src={post.rawImages[0]}
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
className="w-full h-full object-cover group-hover:scale-105 transition-all"
/>
) : (
<div className="text-slate-600 text-xs">Sin foto</div>
<div className="text-neutral-600 text-xs">Sin foto</div>
)}
</div>
<div className="bg-slate-950 rounded-xl p-2.5 border border-slate-800/80">
<p className="text-[11px] text-slate-400 font-mono whitespace-pre-line line-clamp-3">
{/* Raw WhatsApp Message snippet */}
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
{post.rawMessage}
</p>
</div>
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
<div className="space-y-2">
<div>
<span className="text-[10px] font-black uppercase text-emerald-400 tracking-wider">
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
</span>
<h3 className="text-base font-black text-white leading-tight">
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
</div>
{/* Pricing breakdown */}
<div className="bg-slate-950 p-3 rounded-2xl border border-slate-800 grid grid-cols-2 gap-2">
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
<div>
<span className="text-[10px] text-slate-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-slate-300 font-mono">
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-neutral-300 font-mono">
{formatCOP(extracted?.supplierPrice || 90000)}
</span>
</div>
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
{/* Sizes badge pills */}
<div>
<span className="text-[10px] font-bold text-slate-400 uppercase block mb-1">
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
Tallas Disponibles:
</span>
<div className="flex gap-1 flex-wrap">
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
extracted.sizes.map((s) => (
<span
key={s}
className="bg-slate-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-slate-800"
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
>
{s}
</span>
))
) : (
<span className="text-xs text-slate-500">37 a 43</span>
<span className="text-xs text-neutral-500">37 a 43</span>
)}
</div>
</div>
</div>
{/* Card Actions */}
<div className="pt-2 border-t border-slate-800 flex items-center gap-2">
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
{!isPublished ? (
<>
<button
onClick={() => handlePublishPost(post)}
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-emerald-600/30 cursor-pointer transition-all active:scale-95"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
>
<Plus className="w-3.5 h-3.5" />
<span>Publicar en la App</span>
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleOpenEdit(post)}
title="Editar antes de publicar"
className="bg-slate-800 hover:bg-slate-700 text-slate-300 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Edit3 className="w-4 h-4" />
</button>
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
<button
onClick={() => handleDiscardPost(post)}
title="Descartar"
className="bg-slate-800 hover:bg-red-950/40 text-slate-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
<Link
href="/"
target="_blank"
className="text-emerald-400 hover:underline font-bold flex items-center gap-1"
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
>
<span>Ver en Tienda</span>
<ArrowUpRight className="w-3 h-3" />
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
{/* Edit Modal Before Publishing */}
{editingPost && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-slate-900 border border-emerald-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Edit3 className="w-5 h-5 text-emerald-400" />
<Edit3 className="w-5 h-5 text-purple-400" />
<span>Personalizar antes de Publicar</span>
</h3>
<button
onClick={() => setEditingPost(null)}
className="text-slate-400 hover:text-white font-bold"
className="text-neutral-400 hover:text-white font-bold"
>
✕
</button>
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
<div className="space-y-3.5 text-xs">
<div>
<label className="font-bold text-slate-300 block mb-1">Nombre Comercial del Modelo:</label>
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="font-bold text-slate-300 block mb-1">Marca:</label>
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
<input
type="text"
value={editBrand}
onChange={(e) => setEditBrand(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
type="number"
value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
/>
</div>
</div>
<div>
<label className="font-bold text-slate-300 block mb-1">Tallas (separadas por coma):</label>
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
<input
type="text"
value={editSizes}
onChange={(e) => setEditSizes(e.target.value)}
placeholder="38, 39, 40, 41, 42, 43"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-500"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
/>
</div>
</div>
<div className="flex items-center gap-2.5 pt-3 border-t border-slate-800">
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
<button
onClick={() => setEditingPost(null)}
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
>
Cancelar
</button>
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
sizes: sizesArray,
});
}}
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-emerald-600/30 cursor-pointer"
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
>
Guardar y Publicar en la App
</button>

View File

@ -197,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-white flex items-center gap-2">
<Radio className="w-6 h-6 text-emerald-400 animate-pulse" />
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
<span>Monitoreo de Grupos de WhatsApp</span>
</h1>
<p className="text-xs text-slate-400 mt-1">
<p className="text-xs text-neutral-400 mt-1">
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
</p>
</div>
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */}
<button
onClick={handleOpenPicker}
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/25 active:scale-95 transition-all cursor-pointer"
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
>
<Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span>
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */}
{toast && (
<div className="bg-emerald-950/90 border border-emerald-500 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
<span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-slate-400">Grupos en tu WhatsApp</p>
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-slate-300">
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
<Users className="w-5 h-5" />
</div>
</div>
<div className="bg-gradient-to-br from-slate-900 to-emerald-950/40 border border-emerald-500/30 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-emerald-400">Grupos Monitoreados (Activos)</p>
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div>
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<Radio className="w-5 h-5" />
</div>
</div>
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
<div>
<p className="text-[11px] font-bold text-emerald-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-slate-300 mt-1 flex items-center gap-1.5">
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span>
</p>
</div>
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
<Zap className="w-5 h-5" />
</div>
</div>
</div>
{/* Filter and Search Toolbar */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
<div className="relative w-full sm:w-80">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre tus grupos activos..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-emerald-400"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
/>
</div>
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all'
? 'bg-slate-800 text-white border border-slate-700'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Todos ({groups.length})
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
: 'text-slate-400 hover:text-white'
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-neutral-400 hover:text-white'
}`}
>
Monitoreados ({monitoredCount})
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored'
? 'bg-slate-800 text-white border border-slate-700'
: 'text-slate-400 hover:text-white'
? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-neutral-400 hover:text-white'
}`}
>
Inactivos ({groups.length - monitoredCount})
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-emerald-400" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p>
</div>
) : filteredGroups.length === 0 ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-3 bg-slate-900 border border-slate-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-slate-600" />
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p>
<button
onClick={handleOpenPicker}
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
>
Buscar y Seleccionar Grupos desde WhatsApp
</button>
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored
? 'bg-gradient-to-br from-slate-900 to-slate-900/90 border-emerald-500/40 shadow-lg shadow-emerald-950/20 ring-1 ring-emerald-500/20'
: 'bg-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80'
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`}
>
{/* Header */}
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
isMonitored ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
}`}
/>
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name}
</h3>
</div>
<p className="text-[11px] font-mono text-slate-400 pl-4 mt-0.5 truncate">
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
{group.jid}
</p>
</div>
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
isMonitored
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black'
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white'
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`}
>
{isMonitored ? (
<>
<CheckCircle2 className="w-3.5 h-3.5 fill-slate-950 text-emerald-500" />
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
<span>Monitoreando</span>
</>
) : (
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span>
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span>
</div>
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-emerald-300 flex items-center gap-1">
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo
</span>
</div>
</div>
{group.notes && (
<p className="text-[11px] text-slate-400 bg-slate-950/40 p-2 rounded-lg border border-slate-800/60">
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
ℹ️ {group.notes}
</p>
)}
{/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80">
<span className="text-[10px] text-slate-500 font-mono">
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span>
<button
onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id}
className="text-xs font-bold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
>
<Sparkles className={`w-3.5 h-3.5 text-emerald-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button>
</div>
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */}
{isPickerOpen && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
{/* Modal Header */}
<div className="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950">
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
<div className="flex items-center space-x-2.5">
<div className="w-10 h-10 rounded-2xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
<MessageSquare className="w-5 h-5" />
</div>
<div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
<p className="text-xs text-slate-400">
<p className="text-xs text-neutral-400">
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p>
</div>
</div>
<button
onClick={() => setIsPickerOpen(false)}
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Search and Action Toolbar in Modal */}
<div className="p-4 border-b border-slate-800 bg-slate-900/50 space-y-3">
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
<div className="relative">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-emerald-400 font-semibold"
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
/>
</div>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-400 font-semibold">
<span className="text-neutral-400 font-semibold">
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span>
<div className="flex items-center gap-2">
<button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
className="text-emerald-400 hover:text-emerald-300 font-bold hover:underline cursor-pointer"
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
>
Seleccionar visibles
</button>
<span className="text-slate-600">•</span>
<span className="text-neutral-600">•</span>
<button
onClick={handleDeselectAll}
className="text-slate-400 hover:text-white font-bold hover:underline cursor-pointer"
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
>
Deseleccionar todos
</button>
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? (
<div className="py-16 text-center text-xs text-slate-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-emerald-400" />
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
<p className="text-[11px] text-slate-500">Conectando con WhatsApp...</p>
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
</div>
) : filteredPickerGroups.length === 0 ? (
<div className="py-12 text-center text-xs text-slate-400 space-y-2">
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div>
) : (
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected
? 'bg-emerald-950/30 border-emerald-500/50 shadow-md ring-1 ring-emerald-500/20'
: 'bg-slate-950 border-slate-800 hover:border-slate-700'
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
}`}
>
<div className="flex items-center space-x-3 flex-1 min-w-0">
<div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected
? 'bg-emerald-500 border-emerald-400 text-slate-950'
: 'border-slate-700 bg-slate-900'
? 'bg-purple-600 border-purple-400 text-white'
: 'border-neutral-700 bg-neutral-900'
}`}
>
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
@ -527,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
<h4 className="font-bold text-white text-xs truncate">
{group.name}
</h4>
<p className="text-[10px] font-mono text-slate-400 truncate">
<p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid}
</p>
</div>
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear
</span>
) : (
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
Ignorar
</span>
)}
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div>
{/* Modal Footer */}
<div className="p-4 border-t border-slate-800 bg-slate-950 flex items-center justify-between">
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
<div className="text-xs">
<span className="text-slate-400">Seleccionados: </span>
<strong className="text-emerald-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-slate-400"> grupos para monitorear</span>
<span className="text-neutral-400">Seleccionados: </span>
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-neutral-400"> grupos para monitorear</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setIsPickerOpen(false)}
className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer transition-all"
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
>
Cancelar
</button>
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button"
onClick={handleSaveSelectedGroups}
disabled={isLoadingLive}
className="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
>
<CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span>

View File

@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true';
const limit = parseInt(searchParams.get('limit') || '30', 10);
const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set(
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
);
// 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));
// Si no se pide en vivo, retornar lo almacenado localmente
if (!fetchLive) {
let posts = dataService.getSupplierPosts();
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
let liveGroups: any[] = [];
try {
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const allChats = await chatsRes.json();
if (Array.isArray(allChats)) {
liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
liveGroups.forEach((g: any) => {
const jid = g.remoteJid || g.id || g.jid;
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
if (jid) groupsMap.set(jid, name);
});
}
} catch (e) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 2. Determinar JIDs objetivo a consultar
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
} else {
targetJids = Array.from(groupsMap.keys());
}
}
if (targetJids.length === 0) {
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
const msgRes = await fetch(msgUrl, {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
// 3. Consultar mensajes de cada grupo objetivo en paralelo
const groupFetchPromises = targetJids.map(async (jid) => {
try {
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
{
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 : []);
} catch (err) {
return [];
}
});
const msgData = await msgRes.json();
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
// 3. Mapear grupos para resolver nombres
const groupsMap = new Map<string, string>();
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
// Si faltan nombres, intentar traerlos de Evolution
if (groupsMap.size === 0) {
try {
const grpRes = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
{ headers: { apikey: EVOLUTION_API_KEY } }
);
const grpData = await grpRes.json();
if (Array.isArray(grpData)) {
grpData.forEach((g: any) => {
const jid = g.id || g.jid;
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
});
}
} catch (e) {}
const results = await Promise.allSettled(groupFetchPromises);
const allRecords: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allRecords.push(...res.value);
}
}
// 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
const processedPosts: SupplierPost[] = [];
for (const record of records) {
// Limitar a los 60 más relevantes para rendimiento óptimo
const sliceRecords = allRecords.slice(0, 60);
for (const record of sliceRecords) {
const remoteJid = record.key?.remoteJid || '';
// Filtrar solo grupos de WhatsApp (@g.us)
if (!remoteJid.includes('@g.us')) continue;
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
// Si el grupo no está monitoreado, omitir
continue;
}
const text =
record.message?.conversation ||
record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
}
if (imageUrls.length === 0) {
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
imageUrls.push(
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
);
}
// Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
hour: '2-digit',
minute: '2-digit',
day: '2-digit',
month: 'short',
})
: 'Reciente';
const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review',
receivedAt: timestamp,
supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
};
processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true,
isLive: true,
totalFound: processedPosts.length,
posts: dataService.getSupplierPosts(),
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
} catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error);

View File

@ -20,41 +20,50 @@ export async function GET(request: Request) {
});
}
// Consultar grupos en vivo desde Evolution API
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
try {
const response = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const liveGroups = await response.json();
const allChats = await response.json();
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
if (Array.isArray(liveGroups)) {
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: existing ? existing.isMonitored : false,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
// Asegurar que los locales que no vinieron en live sigan presentes
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({
success: true,
isLive: true,
totalLive: liveGroups.length,
totalLive: merged.length,
groups: merged,
});
}