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

View File

@ -70,7 +70,6 @@ export default function AdminWhatsAppGroupsPage() {
data.groups.forEach((g: MonitoredWhatsAppGroup) => { data.groups.forEach((g: MonitoredWhatsAppGroup) => {
if (g.isMonitored) initialSelected.add(g.jid); if (g.isMonitored) initialSelected.add(g.jid);
}); });
// Also add local groups
groups.forEach((g) => { groups.forEach((g) => {
if (g.isMonitored) initialSelected.add(g.jid); 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 className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-black text-white flex items-center gap-2"> <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> <span>Monitoreo de Grupos de WhatsApp</span>
</h1> </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. 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> </p>
</div> </div>
@ -209,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */} {/* Big Search and Select Button */}
<button <button
onClick={handleOpenPicker} 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" /> <Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span> <span>Buscar y Seleccionar Grupos</span>
@ -218,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */} {/* Toast Notification */}
{toast && ( {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> <span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer"> <button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕ ✕
@ -228,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */} {/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <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> <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> <p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div> </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" /> <Users className="w-5 h-5" />
</div> </div>
</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> <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> <p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div> </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" /> <Radio className="w-5 h-5" />
</div> </div>
</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> <div>
<p className="text-[11px] font-bold text-cyan-400">Evolution API (WhatsApp)</p> <p className="text-[11px] font-bold text-purple-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-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 className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span> <span>AutomataAlmeira (En línea)</span>
</p> </p>
</div> </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" /> <Zap className="w-5 h-5" />
</div> </div>
</div> </div>
</div> </div>
{/* Filter and Search Toolbar */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre tus grupos activos..." placeholder="Buscar entre tus grupos activos..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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> </div>
@ -280,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')} onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all' filterMode === 'all'
? 'bg-slate-800 text-white border border-slate-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Todos ({groups.length}) Todos ({groups.length})
@ -290,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')} onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored' filterMode === 'monitored'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Monitoreados ({monitoredCount}) Monitoreados ({monitoredCount})
@ -300,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')} onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored' filterMode === 'unmonitored'
? 'bg-slate-800 text-white border border-slate-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Inactivos ({groups.length - monitoredCount}) Inactivos ({groups.length - monitoredCount})
@ -312,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */} {/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? ( {isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2"> <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-cyan-400" /> <RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p> <p>Cargando grupos de proveedores...</p>
</div> </div>
) : filteredGroups.length === 0 ? ( ) : 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"> <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-slate-600" /> <MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p> <p>No tienes grupos seleccionados para monitorear.</p>
<button <button
onClick={handleOpenPicker} 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 Buscar y Seleccionar Grupos desde WhatsApp
</button> </button>
@ -336,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid} key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${ className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored 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-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-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80' : 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`} }`}
> >
{/* Header */} {/* Header */}
@ -346,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${ 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"> <h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name} {group.name}
</h3> </h3>
</div> </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} {group.jid}
</p> </p>
</div> </div>
@ -363,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)} 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 ${ 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 isMonitored
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black' ? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white' : 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`} }`}
> >
{isMonitored ? ( {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> <span>Monitoreando</span>
</> </>
) : ( ) : (
@ -383,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */} {/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1"> <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"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span> <span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white"> <span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000) +{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span> </span>
</div> </div>
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span> <span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-emerald-300 flex items-center gap-1"> <span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo <Zap className="w-3 h-3" /> Auto-Publicar Directo
</span> </span>
</div> </div>
</div> </div>
{group.notes && ( {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} ℹ️ {group.notes}
</p> </p>
)} )}
{/* Action footer */} {/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80"> <div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-slate-500 font-mono"> <span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'} {group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span> </span>
<button <button
onClick={() => handleScanGroup(group)} onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id} 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> <span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button> </button>
</div> </div>
@ -430,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */} {/* ========================================================= */}
{isPickerOpen && ( {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="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 */} {/* 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="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" /> <MessageSquare className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2> <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. Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p> </p>
</div> </div>
</div> </div>
<button <button
onClick={() => setIsPickerOpen(false)} 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" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Search and Action Toolbar in Modal */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..." placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch} value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)} 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>
<div className="flex items-center justify-between text-xs"> <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 Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)} 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 Seleccionar visibles
</button> </button>
<span className="text-slate-600">•</span> <span className="text-neutral-600">•</span>
<button <button
onClick={handleDeselectAll} 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 Deseleccionar todos
</button> </button>
@ -490,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */} {/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2"> <div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? ( {isLoadingLive ? (
<div className="py-16 text-center text-xs text-slate-400 space-y-3"> <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-cyan-400" /> <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="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> </div>
) : filteredPickerGroups.length === 0 ? ( ) : 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> <p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div> </div>
) : ( ) : (
@ -509,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)} onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${ className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected isSelected
? 'bg-cyan-950/30 border-cyan-500/50 shadow-md ring-1 ring-cyan-500/20' ? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-slate-950 border-slate-800 hover:border-slate-700' : '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="flex items-center space-x-3 flex-1 min-w-0">
<div <div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${ className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected isSelected
? 'bg-cyan-500 border-cyan-400 text-slate-950' ? 'bg-purple-600 border-purple-400 text-white'
: 'border-slate-700 bg-slate-900' : 'border-neutral-700 bg-neutral-900'
}`} }`}
> >
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />} {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"> <h4 className="font-bold text-white text-xs truncate">
{group.name} {group.name}
</h4> </h4>
<p className="text-[10px] font-mono text-slate-400 truncate"> <p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid} {group.jid}
</p> </p>
</div> </div>
@ -540,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear Monitorear
</span> </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 Ignorar
</span> </span>
)} )}
@ -552,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div> </div>
{/* Modal Footer */} {/* 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"> <div className="text-xs">
<span className="text-slate-400">Seleccionados: </span> <span className="text-neutral-400">Seleccionados: </span>
<strong className="text-cyan-400 font-mono text-sm">{selectedJids.size}</strong> <strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-slate-400"> grupos para monitorear</span> <span className="text-neutral-400"> grupos para monitorear</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setIsPickerOpen(false)} 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 Cancelar
</button> </button>
@ -571,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button" type="button"
onClick={handleSaveSelectedGroups} onClick={handleSaveSelectedGroups}
disabled={isLoadingLive} 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" /> <CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span> <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) { export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '30', 10); const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales // 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set( const groupsMap = new Map<string, string>();
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid) localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
);
// Si no se pide en vivo, retornar lo almacenado localmente let liveGroups: any[] = [];
if (!fetchLive) { try {
let posts = dataService.getSupplierPosts(); const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
}
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
});
}
// 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', method: 'POST',
headers: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}), body: JSON.stringify({}),
}); });
const allChats = await chatsRes.json();
const msgData = await msgRes.json(); if (Array.isArray(allChats)) {
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
// 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(); liveGroups.forEach((g: any) => {
if (Array.isArray(grpData)) { const jid = g.remoteJid || g.id || g.jid;
grpData.forEach((g: any) => { const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
const jid = g.id || g.jid; if (jid) groupsMap.set(jid, name);
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
}); });
} }
} catch (e) {} } 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,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 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 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[] = []; 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 || ''; 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
} }
if (imageUrls.length === 0) { 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 // Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName); const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp 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'; : 'Reciente';
const post: SupplierPost = { const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review', status: 'pending_review',
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
}; };
processedPosts.push(post); processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true, success: true,
isLive: true, isLive: true,
totalFound: processedPosts.length, 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) { } catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error); 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 { try {
const response = await fetch( 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: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', '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 merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid; const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre'; const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase()); 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 { return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`, id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid, jid,
name, name,
isMonitored: existing ? existing.isMonitored : false, isMonitored,
supplierId: existing?.supplierId, supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name, supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35, profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true, autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined), participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '', notes: existing?.notes || '',
}; };
}); });
// Asegurar que los locales que no vinieron en live sigan presentes // Asegurar que los locales sigan presentes
localGroups.forEach((lg) => { localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) { if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg); merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
totalLive: liveGroups.length, totalLive: merged.length,
groups: merged, groups: merged,
}); });
} }

View File

@ -51,19 +51,19 @@ export default function SupplierFeedPage() {
// Target brand selector for publishing // Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira'); const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados y posts iniciales // Cargar grupos monitoreados y posts iniciales en vivo
const loadInitialData = async () => { const loadInitialData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
// 1. Grupos // 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups'); const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json(); const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) { if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(grpData.groups); setMonitoredGroups(grpData.groups);
} }
// 2. Posts almacenados // 2. Posts en vivo desde los grupos de WhatsApp
const feedRes = await fetch('/api/whatsapp-feed'); const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
const feedData = await feedRes.json(); const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) { if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts); setPosts(feedData.posts);
@ -79,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData(); 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 // Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => { const handleSyncLive = async () => {
setIsSyncingLive(true); setIsSyncingLive(true);
@ -87,7 +113,7 @@ export default function SupplierFeedPage() {
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}` ? `/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 res = await fetch(url);
const data = await res.json(); const data = await res.json();
@ -341,7 +367,7 @@ export default function SupplierFeedPage() {
<Filter className="w-4 h-4 text-purple-400 shrink-0" /> <Filter className="w-4 h-4 text-purple-400 shrink-0" />
<select <select
value={selectedGroupFilter} 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" 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> <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) { export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '30', 10); const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales // 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set( const groupsMap = new Map<string, string>();
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid) localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
);
// Si no se pide en vivo, retornar lo almacenado localmente let liveGroups: any[] = [];
if (!fetchLive) { try {
let posts = dataService.getSupplierPosts(); const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
}
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
});
}
// 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', method: 'POST',
headers: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}), body: JSON.stringify({}),
}); });
const allChats = await chatsRes.json();
const msgData = await msgRes.json(); if (Array.isArray(allChats)) {
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
// 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(); liveGroups.forEach((g: any) => {
if (Array.isArray(grpData)) { const jid = g.remoteJid || g.id || g.jid;
grpData.forEach((g: any) => { const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
const jid = g.id || g.jid; if (jid) groupsMap.set(jid, name);
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
}); });
} }
} catch (e) {} } 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,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 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 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[] = []; 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 || ''; 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
} }
if (imageUrls.length === 0) { 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 // Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName); const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp 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'; : 'Reciente';
const post: SupplierPost = { const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review', status: 'pending_review',
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
}; };
processedPosts.push(post); processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true, success: true,
isLive: true, isLive: true,
totalFound: processedPosts.length, 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) { } catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error); 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 { try {
const response = await fetch( 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: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', '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 merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid; const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre'; const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase()); 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 { return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`, id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid, jid,
name, name,
isMonitored: existing ? existing.isMonitored : false, isMonitored,
supplierId: existing?.supplierId, supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name, supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35, profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true, autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined), participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '', notes: existing?.notes || '',
}; };
}); });
// Asegurar que los locales que no vinieron en live sigan presentes // Asegurar que los locales sigan presentes
localGroups.forEach((lg) => { localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) { if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg); merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
totalLive: liveGroups.length, totalLive: merged.length,
groups: merged, groups: merged,
}); });
} }

View File

@ -23,6 +23,7 @@ import {
Edit3, Edit3,
Trash2, Trash2,
ArrowUpRight, ArrowUpRight,
Eye,
} from 'lucide-react'; } from 'lucide-react';
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types'; import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
import { formatCOP } from '@/lib/pricing-engine'; import { formatCOP } from '@/lib/pricing-engine';
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
const [editSizes, setEditSizes] = useState(''); const [editSizes, setEditSizes] = useState('');
// Target brand selector for publishing // 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 () => { const loadInitialData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const grpRes = await fetch('/api/whatsapp-groups'); // 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json(); const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) { if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(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(); const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) { if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts); setPosts(feedData.posts);
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData(); 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 // Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => { const handleSyncLive = async () => {
setIsSyncingLive(true); setIsSyncingLive(true);
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}` ? `/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 res = await fetch(url);
const data = await res.json(); const data = await res.json();
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
} }
}; };
// Manejo de Selección
const handleToggleSelect = (id: string) => { const handleToggleSelect = (id: string) => {
setSelectedPostIds((prev) => { setSelectedPostIds((prev) => {
const next = new Set(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) => { const filteredPosts = posts.filter((p) => {
// Estado
if (tabStatus === 'pending' && p.status === 'published') return false; if (tabStatus === 'pending' && p.status === 'published') return false;
if (tabStatus === 'pending' && p.status === 'discarded') return false; if (tabStatus === 'pending' && p.status === 'discarded') return false;
if (tabStatus === 'published' && p.status !== 'published') return false; if (tabStatus === 'published' && p.status !== 'published') return false;
if (tabStatus === 'discarded' && p.status !== 'discarded') return false; if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
// Grupo
if (selectedGroupFilter !== 'all') { if (selectedGroupFilter !== 'all') {
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter; 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 className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div> <div>
<div className="flex items-center gap-2"> <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" /> <Radio className="w-5 h-5 animate-pulse" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Sneaky</h1> <h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
<p className="text-xs text-zinc-400"> <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. Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
</p> </p>
</div> </div>
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2.5 flex-wrap"> <div className="flex items-center gap-2.5 flex-wrap">
<Link <Link
href="/admin/grupos-whatsapp" 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> <span>Gestionar Grupos</span>
</Link> </Link>
<button <button
onClick={handleSyncLive} onClick={handleSyncLive}
disabled={isSyncingLive} 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' : ''}`} /> <RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span> <span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
{/* Toast feedback */} {/* Toast feedback */}
{actionMessage && ( {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"> <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> <span>{actionMessage}</span>
</div> </div>
<button <button
onClick={() => setActionMessage(null)} 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> </button>
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
)} )}
{/* Filter & Tabs Bar */} {/* 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 */} {/* 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 <button
onClick={() => setTabStatus('pending')} onClick={() => setTabStatus('pending')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending' tabStatus === 'pending'
? 'bg-orange-600 text-white shadow' ? 'bg-purple-600 text-white shadow'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>📥 Pendientes</span> <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} {pendingCount}
</span> </span>
</button> </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 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published' tabStatus === 'published'
? 'bg-emerald-600 text-white shadow' ? 'bg-emerald-600 text-white shadow'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>✅ Publicados en Tienda</span> <span>✅ Publicados en Tienda</span>
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
onClick={() => setTabStatus('discarded')} onClick={() => setTabStatus('discarded')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded' tabStatus === 'discarded'
? 'bg-zinc-800 text-white shadow' ? 'bg-neutral-800 text-white shadow'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>🗑️ Descartados</span> <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} {discardedCount}
</span> </span>
</button> </button>
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
{/* Group Selector Filter */} {/* Group Selector Filter */}
<div className="flex items-center gap-2"> <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 <select
value={selectedGroupFilter} value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)} onChange={(e) => handleGroupFilterChange(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" 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> <option value="all">Todos los Grupos de WhatsApp</option>
{monitoredGroups.map((g) => ( {monitoredGroups.map((g) => (
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
{/* Multi-Selection Bulk Actions Bar */} {/* Multi-Selection Bulk Actions Bar */}
{selectedPostIds.size > 0 && tabStatus === 'pending' && ( {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"> <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 {selectedPostIds.size} seleccionados
</span> </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 Listos para transferir al catálogo de la tienda
</span> </span>
</div> </div>
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setSelectedPostIds(new Set())} onClick={() => setSelectedPostIds(new Set())}
className="text-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 Desmarcar
</button> </button>
<button <button
onClick={handleBulkPublish} 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" /> <Plus className="w-4 h-4" />
<span>Publicar Seleccionados ({selectedPostIds.size})</span> <span>Publicar Seleccionados ({selectedPostIds.size})</span>
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
{/* Posts List / Grid */} {/* Posts List / Grid */}
{filteredPosts.length === 0 ? ( {filteredPosts.length === 0 ? (
<div className="bg-zinc-900/60 rounded-3xl p-12 text-center border border-zinc-800 space-y-4"> <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-orange-500/10 border border-orange-500/20 text-orange-400 flex items-center justify-center mx-auto"> <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" /> <MessageSquare className="w-8 h-8" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3> <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. Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
</p> </p>
</div> </div>
<button <button
onClick={handleSyncLive} onClick={handleSyncLive}
disabled={isSyncingLive} 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' : ''}`} /> <RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Escanear Grupos Ahora</span> <span>Escanear Grupos Ahora</span>
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
return ( return (
<div <div
key={post.id} 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 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 : isPublished
? 'border-emerald-500/30 bg-emerald-950/10' ? 'border-emerald-500/30 bg-emerald-950/10'
: 'border-zinc-800 hover:border-zinc-700' : 'border-neutral-800 hover:border-neutral-700'
}`} }`}
> >
{/* Header info */} {/* 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"> <div className="flex items-center gap-2">
{tabStatus === 'pending' && ( {tabStatus === 'pending' && (
<button <button
onClick={() => handleToggleSelect(post.id)} 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 ? ( {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> </button>
)} )}
<div> <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'} {post.channelName || 'Grupo WhatsApp'}
</span> </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>
</div> </div>
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda <CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
</span> </span>
) : isDiscarded ? ( ) : 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 Descartado
</span> </span>
) : ( ) : (
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
{/* Sneaker Image & Raw text */} {/* Sneaker Image & Raw text */}
<div className="sm:col-span-5 space-y-2"> <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] ? ( {post.rawImages && post.rawImages[0] ? (
<img <img
src={post.rawImages[0]} 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" 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>
<div className="bg-zinc-950 rounded-xl p-2.5 border border-zinc-800/80"> {/* Raw WhatsApp Message snippet */}
<p className="text-[11px] text-zinc-400 font-mono whitespace-pre-line line-clamp-3"> <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} {post.rawMessage}
</p> </p>
</div> </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="sm:col-span-7 space-y-3 flex flex-col justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div> <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'} {extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
</span> </span>
<h3 className="text-base font-black text-white leading-tight"> <h3 className="text-base font-black text-white leading-tight">
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
</div> </div>
{/* Pricing breakdown */} {/* 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> <div>
<span className="text-[10px] text-zinc-400 block font-bold">Costo Proveedor:</span> <span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-zinc-300 font-mono"> <span className="text-xs font-bold text-neutral-300 font-mono">
{formatCOP(extracted?.supplierPrice || 90000)} {formatCOP(extracted?.supplierPrice || 90000)}
</span> </span>
</div> </div>
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
{/* Sizes badge pills */} {/* Sizes badge pills */}
<div> <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: Tallas Disponibles:
</span> </span>
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
extracted.sizes.map((s) => ( extracted.sizes.map((s) => (
<span <span
key={s} 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} {s}
</span> </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> </div>
</div> </div>
{/* Card Actions */} {/* 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 ? ( {!isPublished ? (
<> <>
<button <button
onClick={() => handlePublishPost(post)} 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" /> <Plus className="w-3.5 h-3.5" />
<span>Publicar en la App</span> <span>Publicar en la App</span>
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
<button <button
onClick={() => handleOpenEdit(post)} onClick={() => handleOpenEdit(post)}
title="Editar antes de publicar" 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" /> <Edit3 className="w-4 h-4" />
</button> </button>
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
<button <button
onClick={() => handleDiscardPost(post)} onClick={() => handleDiscardPost(post)}
title="Descartar" 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" /> <Trash2 className="w-4 h-4" />
</button> </button>
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
<Link <Link
href="/" href="/"
target="_blank" 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> <span>Ver en Tienda</span>
<ArrowUpRight className="w-3 h-3" /> <ArrowUpRight className="w-3 h-3" />
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
{/* Edit Modal Before Publishing */} {/* Edit Modal Before Publishing */}
{editingPost && ( {editingPost && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> <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="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-zinc-800 pb-3"> <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"> <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> <span>Personalizar antes de Publicar</span>
</h3> </h3>
<button <button
onClick={() => setEditingPost(null)} onClick={() => setEditingPost(null)}
className="text-zinc-400 hover:text-white font-bold" className="text-neutral-400 hover:text-white font-bold"
> >
✕ ✕
</button> </button>
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
<div className="space-y-3.5 text-xs"> <div className="space-y-3.5 text-xs">
<div> <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 <input
type="text" type="text"
value={editName} value={editName}
onChange={(e) => setEditName(e.target.value)} 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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <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 <input
type="text" type="text"
value={editBrand} value={editBrand}
onChange={(e) => setEditBrand(e.target.value)} 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> </div>
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
type="number" type="number"
value={editPrice} value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))} 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> </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 <input
type="text" type="text"
value={editSizes} value={editSizes}
onChange={(e) => setEditSizes(e.target.value)} onChange={(e) => setEditSizes(e.target.value)}
placeholder="38, 39, 40, 41, 42, 43" 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> </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 <button
onClick={() => setEditingPost(null)} 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 Cancelar
</button> </button>
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
sizes: sizesArray, 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 Guardar y Publicar en la App
</button> </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 className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-black text-white flex items-center gap-2"> <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> <span>Monitoreo de Grupos de WhatsApp</span>
</h1> </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. 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> </p>
</div> </div>
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */} {/* Big Search and Select Button */}
<button <button
onClick={handleOpenPicker} 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" /> <Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span> <span>Buscar y Seleccionar Grupos</span>
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */} {/* Toast Notification */}
{toast && ( {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> <span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer"> <button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕ ✕
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */} {/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <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> <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> <p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div> </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" /> <Users className="w-5 h-5" />
</div> </div>
</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> <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> <p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div> </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" /> <Radio className="w-5 h-5" />
</div> </div>
</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> <div>
<p className="text-[11px] font-bold text-orange-400">Evolution API (WhatsApp)</p> <p className="text-[11px] font-bold text-purple-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-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 className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span> <span>AutomataAlmeira (En línea)</span>
</p> </p>
</div> </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" /> <Zap className="w-5 h-5" />
</div> </div>
</div> </div>
</div> </div>
{/* Filter and Search Toolbar */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre tus grupos activos..." placeholder="Buscar entre tus grupos activos..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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> </div>
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')} onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all' filterMode === 'all'
? 'bg-zinc-800 text-white border border-zinc-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Todos ({groups.length}) Todos ({groups.length})
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')} onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored' filterMode === 'monitored'
? 'bg-orange-500/20 text-orange-300 border border-orange-500/30' ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Monitoreados ({monitoredCount}) Monitoreados ({monitoredCount})
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')} onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored' filterMode === 'unmonitored'
? 'bg-zinc-800 text-white border border-zinc-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-zinc-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Inactivos ({groups.length - monitoredCount}) Inactivos ({groups.length - monitoredCount})
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */} {/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? ( {isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-zinc-400 space-y-2"> <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-orange-400" /> <RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p> <p>Cargando grupos de proveedores...</p>
</div> </div>
) : filteredGroups.length === 0 ? ( ) : 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"> <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-zinc-600" /> <MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p> <p>No tienes grupos seleccionados para monitorear.</p>
<button <button
onClick={handleOpenPicker} 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 Buscar y Seleccionar Grupos desde WhatsApp
</button> </button>
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid} key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${ className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored 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-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-zinc-900/60 border-zinc-800 hover:border-zinc-700 opacity-80' : 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`} }`}
> >
{/* Header */} {/* Header */}
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${ 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"> <h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name} {group.name}
</h3> </h3>
</div> </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} {group.jid}
</p> </p>
</div> </div>
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)} 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 ${ 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 isMonitored
? 'bg-orange-500 text-zinc-950 border-orange-400 shadow-md font-black' ? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-white' : 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`} }`}
> >
{isMonitored ? ( {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> <span>Monitoreando</span>
</> </>
) : ( ) : (
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */} {/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1"> <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"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-zinc-400 block text-[10px]">Margen Aplicado</span> <span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white"> <span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000) +{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span> </span>
</div> </div>
<div className="bg-zinc-950/80 p-2.5 rounded-xl border border-zinc-800"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-zinc-400 block text-[10px]">Modo de Ingreso</span> <span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-orange-300 flex items-center gap-1"> <span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo <Zap className="w-3 h-3" /> Auto-Publicar Directo
</span> </span>
</div> </div>
</div> </div>
{group.notes && ( {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} ℹ️ {group.notes}
</p> </p>
)} )}
{/* Action footer */} {/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-zinc-800/80"> <div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-zinc-500 font-mono"> <span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'} {group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span> </span>
<button <button
onClick={() => handleScanGroup(group)} onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id} 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> <span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button> </button>
</div> </div>
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */} {/* ========================================================= */}
{isPickerOpen && ( {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="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 */} {/* 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="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" /> <MessageSquare className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2> <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. Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p> </p>
</div> </div>
</div> </div>
<button <button
onClick={() => setIsPickerOpen(false)} 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" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Search and Action Toolbar in Modal */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..." placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch} value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)} 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>
<div className="flex items-center justify-between text-xs"> <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 Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)} 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 Seleccionar visibles
</button> </button>
<span className="text-zinc-600">•</span> <span className="text-neutral-600">•</span>
<button <button
onClick={handleDeselectAll} 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 Deseleccionar todos
</button> </button>
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */} {/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2"> <div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? ( {isLoadingLive ? (
<div className="py-16 text-center text-xs text-zinc-400 space-y-3"> <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-orange-400" /> <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="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> </div>
) : filteredPickerGroups.length === 0 ? ( ) : 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> <p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div> </div>
) : ( ) : (
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)} onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${ className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected isSelected
? 'bg-orange-950/30 border-orange-500/50 shadow-md ring-1 ring-orange-500/20' ? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-zinc-950 border-zinc-800 hover:border-zinc-700' : '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="flex items-center space-x-3 flex-1 min-w-0">
<div <div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${ className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected isSelected
? 'bg-orange-500 border-orange-400 text-zinc-950' ? 'bg-purple-600 border-purple-400 text-white'
: 'border-zinc-700 bg-zinc-900' : 'border-neutral-700 bg-neutral-900'
}`} }`}
> >
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />} {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"> <h4 className="font-bold text-white text-xs truncate">
{group.name} {group.name}
</h4> </h4>
<p className="text-[10px] font-mono text-zinc-400 truncate"> <p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid} {group.jid}
</p> </p>
</div> </div>
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear Monitorear
</span> </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 Ignorar
</span> </span>
)} )}
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div> </div>
{/* Modal Footer */} {/* 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"> <div className="text-xs">
<span className="text-zinc-400">Seleccionados: </span> <span className="text-neutral-400">Seleccionados: </span>
<strong className="text-orange-400 font-mono text-sm">{selectedJids.size}</strong> <strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-zinc-400"> grupos para monitorear</span> <span className="text-neutral-400"> grupos para monitorear</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setIsPickerOpen(false)} 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 Cancelar
</button> </button>
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button" type="button"
onClick={handleSaveSelectedGroups} onClick={handleSaveSelectedGroups}
disabled={isLoadingLive} 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" /> <CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span> <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) { export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '30', 10); const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales // 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set( const groupsMap = new Map<string, string>();
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid) localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
);
// Si no se pide en vivo, retornar lo almacenado localmente let liveGroups: any[] = [];
if (!fetchLive) { try {
let posts = dataService.getSupplierPosts(); const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
}
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
});
}
// 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', method: 'POST',
headers: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}), body: JSON.stringify({}),
}); });
const allChats = await chatsRes.json();
const msgData = await msgRes.json(); if (Array.isArray(allChats)) {
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
// 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(); liveGroups.forEach((g: any) => {
if (Array.isArray(grpData)) { const jid = g.remoteJid || g.id || g.jid;
grpData.forEach((g: any) => { const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
const jid = g.id || g.jid; if (jid) groupsMap.set(jid, name);
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
}); });
} }
} catch (e) {} } 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,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 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 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[] = []; 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 || ''; 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
} }
if (imageUrls.length === 0) { 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 // Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName); const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp 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'; : 'Reciente';
const post: SupplierPost = { const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review', status: 'pending_review',
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
}; };
processedPosts.push(post); processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true, success: true,
isLive: true, isLive: true,
totalFound: processedPosts.length, 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) { } catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error); 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 { try {
const response = await fetch( 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: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', '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 merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid; const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre'; const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase()); 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 { return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`, id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid, jid,
name, name,
isMonitored: existing ? existing.isMonitored : false, isMonitored,
supplierId: existing?.supplierId, supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name, supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35, profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true, autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined), participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '', notes: existing?.notes || '',
}; };
}); });
// Asegurar que los locales que no vinieron en live sigan presentes // Asegurar que los locales sigan presentes
localGroups.forEach((lg) => { localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) { if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg); merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
totalLive: liveGroups.length, totalLive: merged.length,
groups: merged, groups: merged,
}); });
} }

View File

@ -23,6 +23,7 @@ import {
Edit3, Edit3,
Trash2, Trash2,
ArrowUpRight, ArrowUpRight,
Eye,
} from 'lucide-react'; } from 'lucide-react';
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types'; import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
import { formatCOP } from '@/lib/pricing-engine'; import { formatCOP } from '@/lib/pricing-engine';
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
const [editSizes, setEditSizes] = useState(''); const [editSizes, setEditSizes] = useState('');
// Target brand selector for publishing // 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 () => { const loadInitialData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const grpRes = await fetch('/api/whatsapp-groups'); // 1. Grupos
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
const grpData = await grpRes.json(); const grpData = await grpRes.json();
if (grpData.success && Array.isArray(grpData.groups)) { if (grpData.success && Array.isArray(grpData.groups)) {
setMonitoredGroups(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(); const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) { if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts); setPosts(feedData.posts);
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
loadInitialData(); 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 // Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => { const handleSyncLive = async () => {
setIsSyncingLive(true); setIsSyncingLive(true);
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}` ? `/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 res = await fetch(url);
const data = await res.json(); const data = await res.json();
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
} }
}; };
// Manejo de Selección
const handleToggleSelect = (id: string) => { const handleToggleSelect = (id: string) => {
setSelectedPostIds((prev) => { setSelectedPostIds((prev) => {
const next = new Set(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) => { const filteredPosts = posts.filter((p) => {
// Estado
if (tabStatus === 'pending' && p.status === 'published') return false; if (tabStatus === 'pending' && p.status === 'published') return false;
if (tabStatus === 'pending' && p.status === 'discarded') return false; if (tabStatus === 'pending' && p.status === 'discarded') return false;
if (tabStatus === 'published' && p.status !== 'published') return false; if (tabStatus === 'published' && p.status !== 'published') return false;
if (tabStatus === 'discarded' && p.status !== 'discarded') return false; if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
// Grupo
if (selectedGroupFilter !== 'all') { if (selectedGroupFilter !== 'all') {
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter; 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 className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div> <div>
<div className="flex items-center gap-2"> <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" /> <Radio className="w-5 h-5 animate-pulse" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Zapply</h1> <h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
<p className="text-xs text-slate-400"> <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. Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
</p> </p>
</div> </div>
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2.5 flex-wrap"> <div className="flex items-center gap-2.5 flex-wrap">
<Link <Link
href="/admin/grupos-whatsapp" 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> <span>Gestionar Grupos</span>
</Link> </Link>
<button <button
onClick={handleSyncLive} onClick={handleSyncLive}
disabled={isSyncingLive} 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' : ''}`} /> <RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span> <span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
{/* Toast feedback */} {/* Toast feedback */}
{actionMessage && ( {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"> <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> <span>{actionMessage}</span>
</div> </div>
<button <button
onClick={() => setActionMessage(null)} 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> </button>
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
)} )}
{/* Filter & Tabs Bar */} {/* 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 */} {/* 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 <button
onClick={() => setTabStatus('pending')} onClick={() => setTabStatus('pending')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'pending' tabStatus === 'pending'
? 'bg-emerald-600 text-white shadow' ? 'bg-purple-600 text-white shadow'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>📥 Pendientes</span> <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} {pendingCount}
</span> </span>
</button> </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 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'published' tabStatus === 'published'
? 'bg-emerald-600 text-white shadow' ? 'bg-emerald-600 text-white shadow'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>✅ Publicados en Tienda</span> <span>✅ Publicados en Tienda</span>
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
onClick={() => setTabStatus('discarded')} onClick={() => setTabStatus('discarded')}
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${ className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
tabStatus === 'discarded' tabStatus === 'discarded'
? 'bg-slate-800 text-white shadow' ? 'bg-neutral-800 text-white shadow'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
<span>🗑️ Descartados</span> <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} {discardedCount}
</span> </span>
</button> </button>
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
{/* Group Selector Filter */} {/* Group Selector Filter */}
<div className="flex items-center gap-2"> <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 <select
value={selectedGroupFilter} value={selectedGroupFilter}
onChange={(e) => setSelectedGroupFilter(e.target.value)} onChange={(e) => handleGroupFilterChange(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" 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> <option value="all">Todos los Grupos de WhatsApp</option>
{monitoredGroups.map((g) => ( {monitoredGroups.map((g) => (
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
{/* Multi-Selection Bulk Actions Bar */} {/* Multi-Selection Bulk Actions Bar */}
{selectedPostIds.size > 0 && tabStatus === 'pending' && ( {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"> <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 {selectedPostIds.size} seleccionados
</span> </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 Listos para transferir al catálogo de la tienda
</span> </span>
</div> </div>
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setSelectedPostIds(new Set())} onClick={() => setSelectedPostIds(new Set())}
className="text-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 Desmarcar
</button> </button>
<button <button
onClick={handleBulkPublish} 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" /> <Plus className="w-4 h-4" />
<span>Publicar Seleccionados ({selectedPostIds.size})</span> <span>Publicar Seleccionados ({selectedPostIds.size})</span>
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
{/* Posts List / Grid */} {/* Posts List / Grid */}
{filteredPosts.length === 0 ? ( {filteredPosts.length === 0 ? (
<div className="bg-slate-900/60 rounded-3xl p-12 text-center border border-slate-800 space-y-4"> <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-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto"> <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" /> <MessageSquare className="w-8 h-8" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3> <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. Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
</p> </p>
</div> </div>
<button <button
onClick={handleSyncLive} onClick={handleSyncLive}
disabled={isSyncingLive} 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' : ''}`} /> <RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Escanear Grupos Ahora</span> <span>Escanear Grupos Ahora</span>
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
return ( return (
<div <div
key={post.id} 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 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 : isPublished
? 'border-emerald-500/30 bg-emerald-950/10' ? 'border-emerald-500/30 bg-emerald-950/10'
: 'border-slate-800 hover:border-slate-700' : 'border-neutral-800 hover:border-neutral-700'
}`} }`}
> >
{/* Header info */} {/* 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"> <div className="flex items-center gap-2">
{tabStatus === 'pending' && ( {tabStatus === 'pending' && (
<button <button
onClick={() => handleToggleSelect(post.id)} 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 ? ( {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> </button>
)} )}
<div> <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'} {post.channelName || 'Grupo WhatsApp'}
</span> </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>
</div> </div>
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda <CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
</span> </span>
) : isDiscarded ? ( ) : 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 Descartado
</span> </span>
) : ( ) : (
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
{/* Sneaker Image & Raw text */} {/* Sneaker Image & Raw text */}
<div className="sm:col-span-5 space-y-2"> <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] ? ( {post.rawImages && post.rawImages[0] ? (
<img <img
src={post.rawImages[0]} 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" 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>
<div className="bg-slate-950 rounded-xl p-2.5 border border-slate-800/80"> {/* Raw WhatsApp Message snippet */}
<p className="text-[11px] text-slate-400 font-mono whitespace-pre-line line-clamp-3"> <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} {post.rawMessage}
</p> </p>
</div> </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="sm:col-span-7 space-y-3 flex flex-col justify-between">
<div className="space-y-2"> <div className="space-y-2">
<div> <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'} {extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
</span> </span>
<h3 className="text-base font-black text-white leading-tight"> <h3 className="text-base font-black text-white leading-tight">
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
</div> </div>
{/* Pricing breakdown */} {/* 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> <div>
<span className="text-[10px] text-slate-400 block font-bold">Costo Proveedor:</span> <span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
<span className="text-xs font-bold text-slate-300 font-mono"> <span className="text-xs font-bold text-neutral-300 font-mono">
{formatCOP(extracted?.supplierPrice || 90000)} {formatCOP(extracted?.supplierPrice || 90000)}
</span> </span>
</div> </div>
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
{/* Sizes badge pills */} {/* Sizes badge pills */}
<div> <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: Tallas Disponibles:
</span> </span>
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
extracted.sizes.map((s) => ( extracted.sizes.map((s) => (
<span <span
key={s} 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} {s}
</span> </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> </div>
</div> </div>
{/* Card Actions */} {/* 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 ? ( {!isPublished ? (
<> <>
<button <button
onClick={() => handlePublishPost(post)} 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" /> <Plus className="w-3.5 h-3.5" />
<span>Publicar en la App</span> <span>Publicar en la App</span>
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
<button <button
onClick={() => handleOpenEdit(post)} onClick={() => handleOpenEdit(post)}
title="Editar antes de publicar" 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" /> <Edit3 className="w-4 h-4" />
</button> </button>
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
<button <button
onClick={() => handleDiscardPost(post)} onClick={() => handleDiscardPost(post)}
title="Descartar" 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" /> <Trash2 className="w-4 h-4" />
</button> </button>
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
<Link <Link
href="/" href="/"
target="_blank" 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> <span>Ver en Tienda</span>
<ArrowUpRight className="w-3 h-3" /> <ArrowUpRight className="w-3 h-3" />
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
{/* Edit Modal Before Publishing */} {/* Edit Modal Before Publishing */}
{editingPost && ( {editingPost && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"> <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="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-slate-800 pb-3"> <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"> <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> <span>Personalizar antes de Publicar</span>
</h3> </h3>
<button <button
onClick={() => setEditingPost(null)} onClick={() => setEditingPost(null)}
className="text-slate-400 hover:text-white font-bold" className="text-neutral-400 hover:text-white font-bold"
> >
✕ ✕
</button> </button>
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
<div className="space-y-3.5 text-xs"> <div className="space-y-3.5 text-xs">
<div> <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 <input
type="text" type="text"
value={editName} value={editName}
onChange={(e) => setEditName(e.target.value)} 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>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <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 <input
type="text" type="text"
value={editBrand} value={editBrand}
onChange={(e) => setEditBrand(e.target.value)} 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> </div>
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
type="number" type="number"
value={editPrice} value={editPrice}
onChange={(e) => setEditPrice(Number(e.target.value))} 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> </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 <input
type="text" type="text"
value={editSizes} value={editSizes}
onChange={(e) => setEditSizes(e.target.value)} onChange={(e) => setEditSizes(e.target.value)}
placeholder="38, 39, 40, 41, 42, 43" 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> </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 <button
onClick={() => setEditingPost(null)} 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 Cancelar
</button> </button>
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
sizes: sizesArray, 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 Guardar y Publicar en la App
</button> </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 className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-black text-white flex items-center gap-2"> <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> <span>Monitoreo de Grupos de WhatsApp</span>
</h1> </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. 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> </p>
</div> </div>
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Big Search and Select Button */} {/* Big Search and Select Button */}
<button <button
onClick={handleOpenPicker} 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" /> <Search className="w-4 h-4" />
<span>Buscar y Seleccionar Grupos</span> <span>Buscar y Seleccionar Grupos</span>
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toast Notification */} {/* Toast Notification */}
{toast && ( {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> <span>{toast}</span>
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer"> <button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
✕ ✕
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
{/* Stats Bar */} {/* Stats Bar */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <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> <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> <p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
</div> </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" /> <Users className="w-5 h-5" />
</div> </div>
</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> <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> <p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
</div> </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" /> <Radio className="w-5 h-5" />
</div> </div>
</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> <div>
<p className="text-[11px] font-bold text-emerald-400">Evolution API (WhatsApp)</p> <p className="text-[11px] font-bold text-purple-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-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 className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
<span>AutomataAlmeira (En línea)</span> <span>AutomataAlmeira (En línea)</span>
</p> </p>
</div> </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" /> <Zap className="w-5 h-5" />
</div> </div>
</div> </div>
</div> </div>
{/* Filter and Search Toolbar */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre tus grupos activos..." placeholder="Buscar entre tus grupos activos..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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> </div>
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('all')} onClick={() => setFilterMode('all')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'all' filterMode === 'all'
? 'bg-slate-800 text-white border border-slate-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Todos ({groups.length}) Todos ({groups.length})
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('monitored')} onClick={() => setFilterMode('monitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'monitored' filterMode === 'monitored'
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Monitoreados ({monitoredCount}) Monitoreados ({monitoredCount})
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => setFilterMode('unmonitored')} onClick={() => setFilterMode('unmonitored')}
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${ className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
filterMode === 'unmonitored' filterMode === 'unmonitored'
? 'bg-slate-800 text-white border border-slate-700' ? 'bg-neutral-800 text-white border border-neutral-700'
: 'text-slate-400 hover:text-white' : 'text-neutral-400 hover:text-white'
}`} }`}
> >
Inactivos ({groups.length - monitoredCount}) Inactivos ({groups.length - monitoredCount})
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
{/* Active Monitored Groups List */} {/* Active Monitored Groups List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{isLoading ? ( {isLoading ? (
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2"> <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-emerald-400" /> <RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
<p>Cargando grupos de proveedores...</p> <p>Cargando grupos de proveedores...</p>
</div> </div>
) : filteredGroups.length === 0 ? ( ) : 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"> <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-slate-600" /> <MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
<p>No tienes grupos seleccionados para monitorear.</p> <p>No tienes grupos seleccionados para monitorear.</p>
<button <button
onClick={handleOpenPicker} 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 Buscar y Seleccionar Grupos desde WhatsApp
</button> </button>
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
key={group.id || group.jid} key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${ className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored 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-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-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80' : 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`} }`}
> >
{/* Header */} {/* Header */}
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${ 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"> <h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name} {group.name}
</h3> </h3>
</div> </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} {group.jid}
</p> </p>
</div> </div>
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleGroup(group)} 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 ${ 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 isMonitored
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black' ? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white' : 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
}`} }`}
> >
{isMonitored ? ( {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> <span>Monitoreando</span>
</> </>
) : ( ) : (
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
{/* Info Pills */} {/* Info Pills */}
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1"> <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"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span> <span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
<span className="font-bold text-white"> <span className="font-bold text-white">
+{group.profitMarginOverride || 35}% (Redondeo a $5.000) +{group.profitMarginOverride || 35}% (Redondeo a $5.000)
</span> </span>
</div> </div>
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800"> <div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span> <span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
<span className="font-bold text-emerald-300 flex items-center gap-1"> <span className="font-bold text-purple-300 flex items-center gap-1">
<Zap className="w-3 h-3" /> Auto-Publicar Directo <Zap className="w-3 h-3" /> Auto-Publicar Directo
</span> </span>
</div> </div>
</div> </div>
{group.notes && ( {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} ℹ️ {group.notes}
</p> </p>
)} )}
{/* Action footer */} {/* Action footer */}
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80"> <div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
<span className="text-[10px] text-slate-500 font-mono"> <span className="text-[10px] text-neutral-500 font-mono">
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'} {group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
</span> </span>
<button <button
onClick={() => handleScanGroup(group)} onClick={() => handleScanGroup(group)}
disabled={scanningGroupId === group.id} 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> <span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
</button> </button>
</div> </div>
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
{/* ========================================================= */} {/* ========================================================= */}
{isPickerOpen && ( {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="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 */} {/* 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="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" /> <MessageSquare className="w-5 h-5" />
</div> </div>
<div> <div>
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2> <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. Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
</p> </p>
</div> </div>
</div> </div>
<button <button
onClick={() => setIsPickerOpen(false)} 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" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
{/* Search and Action Toolbar in Modal */} {/* 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"> <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 <input
type="text" type="text"
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..." placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
value={pickerSearch} value={pickerSearch}
onChange={(e) => setPickerSearch(e.target.value)} 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>
<div className="flex items-center justify-between text-xs"> <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 Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => handleSelectAllFiltered(filteredPickerGroups)} 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 Seleccionar visibles
</button> </button>
<span className="text-slate-600">•</span> <span className="text-neutral-600">•</span>
<button <button
onClick={handleDeselectAll} 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 Deseleccionar todos
</button> </button>
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
{/* Groups Scrollable List */} {/* Groups Scrollable List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2"> <div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoadingLive ? ( {isLoadingLive ? (
<div className="py-16 text-center text-xs text-slate-400 space-y-3"> <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-emerald-400" /> <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="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> </div>
) : filteredPickerGroups.length === 0 ? ( ) : 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> <p>No se encontraron grupos con el término "{pickerSearch}".</p>
</div> </div>
) : ( ) : (
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
onClick={() => handleToggleSelectJid(group.jid)} onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${ className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected isSelected
? 'bg-emerald-950/30 border-emerald-500/50 shadow-md ring-1 ring-emerald-500/20' ? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-slate-950 border-slate-800 hover:border-slate-700' : '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="flex items-center space-x-3 flex-1 min-w-0">
<div <div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${ className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected isSelected
? 'bg-emerald-500 border-emerald-400 text-slate-950' ? 'bg-purple-600 border-purple-400 text-white'
: 'border-slate-700 bg-slate-900' : 'border-neutral-700 bg-neutral-900'
}`} }`}
> >
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />} {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"> <h4 className="font-bold text-white text-xs truncate">
{group.name} {group.name}
</h4> </h4>
<p className="text-[10px] font-mono text-slate-400 truncate"> <p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid} {group.jid}
</p> </p>
</div> </div>
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
Monitorear Monitorear
</span> </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 Ignorar
</span> </span>
)} )}
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
</div> </div>
{/* Modal Footer */} {/* 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"> <div className="text-xs">
<span className="text-slate-400">Seleccionados: </span> <span className="text-neutral-400">Seleccionados: </span>
<strong className="text-emerald-400 font-mono text-sm">{selectedJids.size}</strong> <strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
<span className="text-slate-400"> grupos para monitorear</span> <span className="text-neutral-400"> grupos para monitorear</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setIsPickerOpen(false)} 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 Cancelar
</button> </button>
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
type="button" type="button"
onClick={handleSaveSelectedGroups} onClick={handleSaveSelectedGroups}
disabled={isLoadingLive} 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" /> <CheckCircle2 className="w-4 h-4" />
<span>Guardar y Monitorear ({selectedJids.size})</span> <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) { export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') === 'true'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '30', 10); const limit = parseInt(searchParams.get('limit') || '50', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
// 1. Obtener grupos monitoreados locales // 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups(); const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
const monitoredJids = new Set( const groupsMap = new Map<string, string>();
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid) localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
);
// Si no se pide en vivo, retornar lo almacenado localmente let liveGroups: any[] = [];
if (!fetchLive) { try {
let posts = dataService.getSupplierPosts(); const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
if (filterJid) {
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
}
return NextResponse.json({
success: true,
posts,
monitoredCount: monitoredJids.size,
});
}
// 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', method: 'POST',
headers: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}), body: JSON.stringify({}),
}); });
const allChats = await chatsRes.json();
const msgData = await msgRes.json(); if (Array.isArray(allChats)) {
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); liveGroups = allChats.filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
// 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(); liveGroups.forEach((g: any) => {
if (Array.isArray(grpData)) { const jid = g.remoteJid || g.id || g.jid;
grpData.forEach((g: any) => { const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
const jid = g.id || g.jid; if (jid) groupsMap.set(jid, name);
const name = g.subject || g.name || 'Grupo WhatsApp';
groupsMap.set(jid, name);
}); });
} }
} catch (e) {} } 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,
isLive: true,
totalFound: 0,
posts: dataService.getSupplierPosts(),
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
});
}
// 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 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[] = []; 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 || ''; 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 = const text =
record.message?.conversation || record.message?.conversation ||
record.message?.extendedTextMessage?.text || record.message?.extendedTextMessage?.text ||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
} }
if (imageUrls.length === 0) { 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 // Parsear oferta de calzado con IA
const extracted = await parseSupplierMessage(text || groupName); const extracted = await parseSupplierMessage(text || groupName);
const timestamp = record.messageTimestamp 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'; : 'Reciente';
const post: SupplierPost = { const post: SupplierPost = {
@ -141,7 +175,7 @@ export async function GET(request: Request) {
status: 'pending_review', status: 'pending_review',
receivedAt: timestamp, receivedAt: timestamp,
supplierName: groupName, supplierName: groupName,
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000', supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
}; };
processedPosts.push(post); processedPosts.push(post);
@ -156,7 +190,8 @@ export async function GET(request: Request) {
success: true, success: true,
isLive: true, isLive: true,
totalFound: processedPosts.length, 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) { } catch (error: any) {
console.error('Error en /api/whatsapp-feed:', error); 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 { try {
const response = await fetch( 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: { headers: {
apikey: EVOLUTION_API_KEY, apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json', '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 merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.id || lg.jid; const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.subject || lg.name || 'Grupo Sin Nombre'; const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase()); 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 { return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`, id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid, jid,
name, name,
isMonitored: existing ? existing.isMonitored : false, isMonitored,
supplierId: existing?.supplierId, supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name, supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35, profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true, autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined), participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '', notes: existing?.notes || '',
}; };
}); });
// Asegurar que los locales que no vinieron en live sigan presentes // Asegurar que los locales sigan presentes
localGroups.forEach((lg) => { localGroups.forEach((lg) => {
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) { if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
merged.unshift(lg); merged.unshift(lg);
@ -64,7 +73,7 @@ export async function GET(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
isLive: true, isLive: true,
totalLive: liveGroups.length, totalLive: merged.length,
groups: merged, groups: merged,
}); });
} }