feat: visualizacion completa de mensajes en vivo por grupos de whatsapp con imagenes e IA
This commit is contained in:
parent
e89c65c426
commit
ac82c9aaae
@ -3,3 +3,4 @@
|
||||
- **Autonomía Total**: Ejecuta todas las acciones, ediciones de archivos, comandos de terminal, pruebas y diagnósticos de forma proactiva e inmediata sin solicitar confirmación previa ni esperar aprobaciones intermedias.
|
||||
- **Flujo Completo**: Cuando se solicite un cambio o se reporte un problema, investiga, modifica el código, compila, prueba y entrega la solución terminada.
|
||||
- **Sin Bloqueos**: No te detengas a pedir permisos antes de ejecutar herramientas, llamadas a bases de datos o scripts de verificación.
|
||||
- **Despliegue y Envío de APK Continuo**: Tras cualquier cambio o ajuste en el código, SIEMPRE compilar el APK Android, desplegar en el servidor VPS de producción y enviar automáticamente el archivo APK actualizado al WhatsApp de Carlos (+57 316 745 5877) sin esperar confirmación.
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
Edit3,
|
||||
Trash2,
|
||||
ArrowUpRight,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
@ -50,17 +51,19 @@ export default function SupplierFeedPage() {
|
||||
// Target brand selector for publishing
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||
|
||||
// Cargar grupos monitoreados y posts iniciales
|
||||
// Cargar grupos monitoreados y posts iniciales en vivo
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const grpRes = await fetch('/api/whatsapp-groups');
|
||||
// 1. Grupos
|
||||
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
|
||||
const grpData = await grpRes.json();
|
||||
if (grpData.success && Array.isArray(grpData.groups)) {
|
||||
setMonitoredGroups(grpData.groups);
|
||||
}
|
||||
|
||||
const feedRes = await fetch('/api/whatsapp-feed');
|
||||
// 2. Posts en vivo desde los grupos de WhatsApp
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Cambiar filtro de grupo y recargar mensajes
|
||||
const handleGroupFilterChange = async (groupJid: string) => {
|
||||
setSelectedGroupFilter(groupJid);
|
||||
setIsSyncingLive(true);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(
|
||||
`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del grupo.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setActionMessage('❌ Error cargando mensajes del grupo.');
|
||||
} finally {
|
||||
setIsSyncingLive(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizar mensajes en vivo desde Evolution API
|
||||
const handleSyncLive = async () => {
|
||||
setIsSyncingLive(true);
|
||||
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=40';
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Manejo de Selección
|
||||
const handleToggleSelect = (id: string) => {
|
||||
setSelectedPostIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Filtrado de posts por estado y grupo
|
||||
const filteredPosts = posts.filter((p) => {
|
||||
// Estado
|
||||
if (tabStatus === 'pending' && p.status === 'published') return false;
|
||||
if (tabStatus === 'pending' && p.status === 'discarded') return false;
|
||||
if (tabStatus === 'published' && p.status !== 'published') return false;
|
||||
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
|
||||
|
||||
// Grupo
|
||||
if (selectedGroupFilter !== 'all') {
|
||||
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
|
||||
}
|
||||
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-cyan-500/20 text-cyan-400 flex items-center justify-center border border-cyan-500/30 shadow-[0_0_15px_rgba(6,182,212,0.3)]">
|
||||
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
|
||||
<Radio className="w-5 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Almeira Store</h1>
|
||||
<p className="text-xs text-slate-400">
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
|
||||
</p>
|
||||
</div>
|
||||
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2.5 flex-wrap">
|
||||
<Link
|
||||
href="/admin/grupos-whatsapp"
|
||||
className="bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
className="bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 text-neutral-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 text-cyan-400" />
|
||||
<MessageSquare className="w-4 h-4 text-purple-400" />
|
||||
<span>Gestionar Grupos</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
|
||||
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Toast feedback */}
|
||||
{actionMessage && (
|
||||
<div className="bg-cyan-950/80 border border-cyan-500/50 text-cyan-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||||
<Sparkles className="w-4 h-4 text-purple-400" />
|
||||
<span>{actionMessage}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActionMessage(null)}
|
||||
className="text-white hover:text-cyan-300 font-bold px-2 cursor-pointer"
|
||||
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
|
||||
)}
|
||||
|
||||
{/* Filter & Tabs Bar */}
|
||||
<div className="bg-slate-900/90 border border-slate-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
{/* Status Tabs */}
|
||||
<div className="flex items-center gap-1 bg-slate-950 p-1 rounded-2xl border border-slate-800 text-xs font-bold">
|
||||
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
|
||||
<button
|
||||
onClick={() => setTabStatus('pending')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'pending'
|
||||
? 'bg-cyan-600 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-purple-600 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>📥 Pendientes</span>
|
||||
<span className="bg-cyan-950 text-cyan-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{pendingCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'published'
|
||||
? 'bg-emerald-600 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>✅ Publicados en Tienda</span>
|
||||
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
|
||||
onClick={() => setTabStatus('discarded')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'discarded'
|
||||
? 'bg-slate-800 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>🗑️ Descartados</span>
|
||||
<span className="bg-slate-900 text-slate-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{discardedCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Group Selector Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-cyan-400 shrink-0" />
|
||||
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
|
||||
<select
|
||||
value={selectedGroupFilter}
|
||||
onChange={(e) => setSelectedGroupFilter(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
onChange={(e) => handleGroupFilterChange(e.target.value)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
>
|
||||
<option value="all">Todos los Grupos de WhatsApp</option>
|
||||
{monitoredGroups.map((g) => (
|
||||
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Multi-Selection Bulk Actions Bar */}
|
||||
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
|
||||
<div className="bg-gradient-to-r from-cyan-950/90 to-slate-900 border border-cyan-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="bg-cyan-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
{selectedPostIds.size} seleccionados
|
||||
</span>
|
||||
<span className="text-xs text-slate-300 font-semibold hidden sm:inline">
|
||||
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
|
||||
Listos para transferir al catálogo de la tienda
|
||||
</span>
|
||||
</div>
|
||||
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedPostIds(new Set())}
|
||||
className="text-slate-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
>
|
||||
Desmarcar
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleBulkPublish}
|
||||
className="bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
|
||||
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Posts List / Grid */}
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="bg-slate-900/60 rounded-3xl p-12 text-center border border-slate-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 flex items-center justify-center mx-auto">
|
||||
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
|
||||
<MessageSquare className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
|
||||
<p className="text-xs text-slate-400 max-w-md mx-auto">
|
||||
<p className="text-xs text-neutral-400 max-w-md mx-auto">
|
||||
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>Escanear Grupos Ahora</span>
|
||||
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className={`bg-slate-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
isSelected
|
||||
? 'border-cyan-500 shadow-[0_0_20px_rgba(6,182,212,0.2)]'
|
||||
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
|
||||
: isPublished
|
||||
? 'border-emerald-500/30 bg-emerald-950/10'
|
||||
: 'border-slate-800 hover:border-slate-700'
|
||||
: 'border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{/* Header info */}
|
||||
<div className="flex items-center justify-between gap-2 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{tabStatus === 'pending' && (
|
||||
<button
|
||||
onClick={() => handleToggleSelect(post.id)}
|
||||
className="text-cyan-400 hover:text-cyan-300 cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 cursor-pointer"
|
||||
>
|
||||
{isSelected ? (
|
||||
<CheckSquare className="w-5 h-5 fill-cyan-600 text-white" />
|
||||
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
|
||||
) : (
|
||||
<Square className="w-5 h-5 text-slate-600" />
|
||||
<Square className="w-5 h-5 text-neutral-600" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<span className="bg-cyan-950 text-cyan-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-cyan-500/30 inline-block">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
|
||||
{post.channelName || 'Grupo WhatsApp'}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
|
||||
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
|
||||
</span>
|
||||
) : isDiscarded ? (
|
||||
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
Descartado
|
||||
</span>
|
||||
) : (
|
||||
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
|
||||
{/* Sneaker Image & Raw text */}
|
||||
<div className="sm:col-span-5 space-y-2">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-slate-950 border border-slate-800 flex items-center justify-center group">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
|
||||
{post.rawImages && post.rawImages[0] ? (
|
||||
<img
|
||||
src={post.rawImages[0]}
|
||||
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-all"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-slate-600 text-xs">Sin foto</div>
|
||||
<div className="text-neutral-600 text-xs">Sin foto</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950 rounded-xl p-2.5 border border-slate-800/80">
|
||||
<p className="text-[11px] text-slate-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{/* Raw WhatsApp Message snippet */}
|
||||
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
|
||||
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{post.rawMessage}
|
||||
</p>
|
||||
</div>
|
||||
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-[10px] font-black uppercase text-cyan-400 tracking-wider">
|
||||
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
|
||||
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
|
||||
</span>
|
||||
<h3 className="text-base font-black text-white leading-tight">
|
||||
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
|
||||
</div>
|
||||
|
||||
{/* Pricing breakdown */}
|
||||
<div className="bg-slate-950 p-3 rounded-2xl border border-slate-800 grid grid-cols-2 gap-2">
|
||||
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-slate-300 font-mono">
|
||||
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-neutral-300 font-mono">
|
||||
{formatCOP(extracted?.supplierPrice || 90000)}
|
||||
</span>
|
||||
</div>
|
||||
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Sizes badge pills */}
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase block mb-1">
|
||||
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
|
||||
Tallas Disponibles:
|
||||
</span>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
|
||||
extracted.sizes.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="bg-slate-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-slate-800"
|
||||
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-slate-500">37 a 43</span>
|
||||
<span className="text-xs text-neutral-500">37 a 43</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="pt-2 border-t border-slate-800 flex items-center gap-2">
|
||||
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
|
||||
{!isPublished ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePublishPost(post)}
|
||||
className="flex-1 bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-cyan-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Publicar en la App</span>
|
||||
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleOpenEdit(post)}
|
||||
title="Editar antes de publicar"
|
||||
className="bg-slate-800 hover:bg-slate-700 text-slate-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleDiscardPost(post)}
|
||||
title="Descartar"
|
||||
className="bg-slate-800 hover:bg-red-950/40 text-slate-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
|
||||
<Link
|
||||
href="/"
|
||||
target="_blank"
|
||||
className="text-cyan-400 hover:underline font-bold flex items-center gap-1"
|
||||
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
|
||||
>
|
||||
<span>Ver en Tienda</span>
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
|
||||
{/* Edit Modal Before Publishing */}
|
||||
{editingPost && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-slate-900 border border-cyan-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
|
||||
<h3 className="font-black text-white text-base flex items-center gap-2">
|
||||
<Edit3 className="w-5 h-5 text-cyan-400" />
|
||||
<Edit3 className="w-5 h-5 text-purple-400" />
|
||||
<span>Personalizar antes de Publicar</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="text-slate-400 hover:text-white font-bold"
|
||||
className="text-neutral-400 hover:text-white font-bold"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
|
||||
|
||||
<div className="space-y-3.5 text-xs">
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-cyan-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Marca:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editBrand}
|
||||
onChange={(e) => setEditBrand(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
|
||||
type="number"
|
||||
value={editPrice}
|
||||
onChange={(e) => setEditPrice(Number(e.target.value))}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editSizes}
|
||||
onChange={(e) => setEditSizes(e.target.value)}
|
||||
placeholder="38, 39, 40, 41, 42, 43"
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-cyan-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
|
||||
sizes: sizesArray,
|
||||
});
|
||||
}}
|
||||
className="flex-1 bg-gradient-to-r from-cyan-600 to-blue-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-cyan-600/30 cursor-pointer"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
|
||||
>
|
||||
Guardar y Publicar en la App
|
||||
</button>
|
||||
|
||||
@ -70,7 +70,6 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
data.groups.forEach((g: MonitoredWhatsAppGroup) => {
|
||||
if (g.isMonitored) initialSelected.add(g.jid);
|
||||
});
|
||||
// Also add local groups
|
||||
groups.forEach((g) => {
|
||||
if (g.isMonitored) initialSelected.add(g.jid);
|
||||
});
|
||||
@ -198,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<Radio className="w-6 h-6 text-cyan-400 animate-pulse" />
|
||||
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
|
||||
<span>Monitoreo de Grupos de WhatsApp</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
|
||||
</p>
|
||||
</div>
|
||||
@ -209,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Big Search and Select Button */}
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-cyan-500/25 active:scale-95 transition-all cursor-pointer"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
<span>Buscar y Seleccionar Grupos</span>
|
||||
@ -218,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toast && (
|
||||
<div className="bg-emerald-950/90 border border-emerald-500 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<span>{toast}</span>
|
||||
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
|
||||
✕
|
||||
@ -228,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-slate-300">
|
||||
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
|
||||
<Users className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-slate-900 to-emerald-950/40 border border-emerald-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-emerald-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<Radio className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-cyan-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-slate-300 mt-1 flex items-center gap-1.5">
|
||||
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
|
||||
<span>AutomataAlmeira (En línea)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-cyan-500/10 text-cyan-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter and Search Toolbar */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre tus grupos activos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-cyan-400"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -280,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'all'
|
||||
? 'bg-slate-800 text-white border border-slate-700'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({groups.length})
|
||||
@ -290,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('monitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'monitored'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Monitoreados ({monitoredCount})
|
||||
@ -300,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('unmonitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'unmonitored'
|
||||
? 'bg-slate-800 text-white border border-slate-700'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Inactivos ({groups.length - monitoredCount})
|
||||
@ -312,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Active Monitored Groups List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-cyan-400" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
|
||||
<p>Cargando grupos de proveedores...</p>
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-3 bg-slate-900 border border-slate-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-slate-600" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
|
||||
<p>No tienes grupos seleccionados para monitorear.</p>
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
>
|
||||
Buscar y Seleccionar Grupos desde WhatsApp
|
||||
</button>
|
||||
@ -336,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
key={group.id || group.jid}
|
||||
className={`p-5 rounded-3xl border transition-all space-y-4 ${
|
||||
isMonitored
|
||||
? 'bg-gradient-to-br from-slate-900 to-slate-900/90 border-emerald-500/40 shadow-lg shadow-emerald-950/20 ring-1 ring-emerald-500/20'
|
||||
: 'bg-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80'
|
||||
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
@ -346,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
||||
isMonitored ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'
|
||||
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
|
||||
{group.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-slate-400 pl-4 mt-0.5 truncate">
|
||||
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -363,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleGroup(group)}
|
||||
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
|
||||
isMonitored
|
||||
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black'
|
||||
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white'
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
|
||||
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{isMonitored ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-slate-950 text-emerald-500" />
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
|
||||
<span>Monitoreando</span>
|
||||
</>
|
||||
) : (
|
||||
@ -383,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Info Pills */}
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
|
||||
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
|
||||
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span>
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
|
||||
<span className="font-bold text-white">
|
||||
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
|
||||
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-emerald-300 flex items-center gap-1">
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-purple-300 flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" /> Auto-Publicar Directo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{group.notes && (
|
||||
<p className="text-[11px] text-slate-400 bg-slate-950/40 p-2 rounded-lg border border-slate-800/60">
|
||||
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
|
||||
ℹ️ {group.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80">
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
|
||||
<span className="text-[10px] text-neutral-500 font-mono">
|
||||
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => handleScanGroup(group)}
|
||||
disabled={scanningGroupId === group.id}
|
||||
className="text-xs font-bold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<Sparkles className={`w-3.5 h-3.5 text-cyan-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -430,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* ========================================================= */}
|
||||
{isPickerOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
{/* Modal Header */}
|
||||
<div className="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950">
|
||||
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="w-10 h-10 rounded-2xl bg-cyan-500/20 text-cyan-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
<p className="text-xs text-neutral-400">
|
||||
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search and Action Toolbar in Modal */}
|
||||
<div className="p-4 border-b border-slate-800 bg-slate-900/50 space-y-3">
|
||||
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
|
||||
value={pickerSearch}
|
||||
onChange={(e) => setPickerSearch(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-cyan-400 font-semibold"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-slate-400 font-semibold">
|
||||
<span className="text-neutral-400 font-semibold">
|
||||
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
|
||||
className="text-cyan-400 hover:text-cyan-300 font-bold hover:underline cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Seleccionar visibles
|
||||
</button>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-neutral-600">•</span>
|
||||
<button
|
||||
onClick={handleDeselectAll}
|
||||
className="text-slate-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Deseleccionar todos
|
||||
</button>
|
||||
@ -490,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Groups Scrollable List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{isLoadingLive ? (
|
||||
<div className="py-16 text-center text-xs text-slate-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-cyan-400" />
|
||||
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
|
||||
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
|
||||
<p className="text-[11px] text-slate-500">Conectando con WhatsApp...</p>
|
||||
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
|
||||
</div>
|
||||
) : filteredPickerGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-xs text-slate-400 space-y-2">
|
||||
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -509,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleSelectJid(group.jid)}
|
||||
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
|
||||
isSelected
|
||||
? 'bg-cyan-950/30 border-cyan-500/50 shadow-md ring-1 ring-cyan-500/20'
|
||||
: 'bg-slate-950 border-slate-800 hover:border-slate-700'
|
||||
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div
|
||||
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
|
||||
isSelected
|
||||
? 'bg-cyan-500 border-cyan-400 text-slate-950'
|
||||
: 'border-slate-700 bg-slate-900'
|
||||
? 'bg-purple-600 border-purple-400 text-white'
|
||||
: 'border-neutral-700 bg-neutral-900'
|
||||
}`}
|
||||
>
|
||||
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
|
||||
@ -528,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<h4 className="font-bold text-white text-xs truncate">
|
||||
{group.name}
|
||||
</h4>
|
||||
<p className="text-[10px] font-mono text-slate-400 truncate">
|
||||
<p className="text-[10px] font-mono text-neutral-400 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -540,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
Monitorear
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
Ignorar
|
||||
</span>
|
||||
)}
|
||||
@ -552,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-slate-800 bg-slate-950 flex items-center justify-between">
|
||||
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
|
||||
<div className="text-xs">
|
||||
<span className="text-slate-400">Seleccionados: </span>
|
||||
<strong className="text-cyan-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-slate-400"> grupos para monitorear</span>
|
||||
<span className="text-neutral-400">Seleccionados: </span>
|
||||
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-neutral-400"> grupos para monitorear</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer transition-all"
|
||||
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -571,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
type="button"
|
||||
onClick={handleSaveSelectedGroups}
|
||||
disabled={isLoadingLive}
|
||||
className="px-5 py-2.5 rounded-xl bg-cyan-600 hover:bg-cyan-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-cyan-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>Guardar y Monitorear ({selectedJids.size})</span>
|
||||
|
||||
@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') === 'true';
|
||||
const limit = parseInt(searchParams.get('limit') || '30', 10);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
|
||||
// 1. Obtener grupos monitoreados locales
|
||||
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const monitoredJids = new Set(
|
||||
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
|
||||
);
|
||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const groupsMap = new Map<string, string>();
|
||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si no se pide en vivo, retornar lo almacenado localmente
|
||||
if (!fetchLive) {
|
||||
let posts = dataService.getSupplierPosts();
|
||||
if (filterJid) {
|
||||
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
|
||||
let liveGroups: any[] = [];
|
||||
try {
|
||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const allChats = await chatsRes.json();
|
||||
if (Array.isArray(allChats)) {
|
||||
liveGroups = allChats.filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
liveGroups.forEach((g: any) => {
|
||||
const jid = g.remoteJid || g.id || g.jid;
|
||||
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
|
||||
if (jid) groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||
}
|
||||
|
||||
// 2. Determinar JIDs objetivo a consultar
|
||||
let targetJids: string[] = [];
|
||||
if (filterJid && filterJid !== 'all') {
|
||||
targetJids = [filterJid];
|
||||
} else {
|
||||
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
|
||||
if (monitored.length > 0) {
|
||||
targetJids = monitored;
|
||||
} else if (liveGroups.length > 0) {
|
||||
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
|
||||
} else {
|
||||
targetJids = Array.from(groupsMap.keys());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJids.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
posts,
|
||||
monitoredCount: monitoredJids.size,
|
||||
isLive: true,
|
||||
totalFound: 0,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
|
||||
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
|
||||
const msgRes = await fetch(msgUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
|
||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const msgData = await msgRes.json();
|
||||
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
|
||||
// 3. Mapear grupos para resolver nombres
|
||||
const groupsMap = new Map<string, string>();
|
||||
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si faltan nombres, intentar traerlos de Evolution
|
||||
if (groupsMap.size === 0) {
|
||||
try {
|
||||
const grpRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
{ headers: { apikey: EVOLUTION_API_KEY } }
|
||||
);
|
||||
const grpData = await grpRes.json();
|
||||
if (Array.isArray(grpData)) {
|
||||
grpData.forEach((g: any) => {
|
||||
const jid = g.id || g.jid;
|
||||
const name = g.subject || g.name || 'Grupo WhatsApp';
|
||||
groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
const allRecords: any[] = [];
|
||||
for (const res of results) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allRecords.push(...res.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
// Filtrar solo grupos de WhatsApp (@g.us)
|
||||
if (!remoteJid.includes('@g.us')) continue;
|
||||
|
||||
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
|
||||
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
|
||||
// Si el grupo no está monitoreado, omitir
|
||||
continue;
|
||||
}
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
|
||||
imageUrls.push(
|
||||
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear oferta de calzado con IA
|
||||
const extracted = await parseSupplierMessage(text || groupName);
|
||||
|
||||
const timestamp = record.messageTimestamp
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: 'Reciente';
|
||||
|
||||
const post: SupplierPost = {
|
||||
@ -141,7 +175,7 @@ export async function GET(request: Request) {
|
||||
status: 'pending_review',
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
@ -156,7 +190,8 @@ export async function GET(request: Request) {
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: processedPosts.length,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error en /api/whatsapp-feed:', error);
|
||||
|
||||
@ -20,41 +20,50 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Consultar grupos en vivo desde Evolution API
|
||||
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const liveGroups = await response.json();
|
||||
const allChats = await response.json();
|
||||
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
|
||||
if (Array.isArray(liveGroups)) {
|
||||
if (liveGroups.length > 0) {
|
||||
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
|
||||
const jid = lg.id || lg.jid;
|
||||
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
|
||||
const jid = lg.remoteJid || lg.id || lg.jid;
|
||||
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
|
||||
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
|
||||
|
||||
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
|
||||
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
|
||||
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
|
||||
|
||||
return {
|
||||
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
|
||||
jid,
|
||||
name,
|
||||
isMonitored: existing ? existing.isMonitored : false,
|
||||
isMonitored,
|
||||
supplierId: existing?.supplierId,
|
||||
supplierName: existing?.supplierName || name,
|
||||
profitMarginOverride: existing?.profitMarginOverride || 35,
|
||||
autoPublish: existing?.autoPublish ?? true,
|
||||
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
|
||||
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
|
||||
notes: existing?.notes || '',
|
||||
};
|
||||
});
|
||||
|
||||
// Asegurar que los locales que no vinieron en live sigan presentes
|
||||
// Asegurar que los locales sigan presentes
|
||||
localGroups.forEach((lg) => {
|
||||
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
|
||||
merged.unshift(lg);
|
||||
@ -64,7 +73,7 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalLive: liveGroups.length,
|
||||
totalLive: merged.length,
|
||||
groups: merged,
|
||||
});
|
||||
}
|
||||
|
||||
@ -51,19 +51,19 @@ export default function SupplierFeedPage() {
|
||||
// Target brand selector for publishing
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||
|
||||
// Cargar grupos monitoreados y posts iniciales
|
||||
// Cargar grupos monitoreados y posts iniciales en vivo
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// 1. Grupos
|
||||
const grpRes = await fetch('/api/whatsapp-groups');
|
||||
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
|
||||
const grpData = await grpRes.json();
|
||||
if (grpData.success && Array.isArray(grpData.groups)) {
|
||||
setMonitoredGroups(grpData.groups);
|
||||
}
|
||||
|
||||
// 2. Posts almacenados
|
||||
const feedRes = await fetch('/api/whatsapp-feed');
|
||||
// 2. Posts en vivo desde los grupos de WhatsApp
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -79,6 +79,32 @@ export default function SupplierFeedPage() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Cambiar filtro de grupo y recargar mensajes
|
||||
const handleGroupFilterChange = async (groupJid: string) => {
|
||||
setSelectedGroupFilter(groupJid);
|
||||
setIsSyncingLive(true);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(
|
||||
`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del grupo.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setActionMessage('❌ Error cargando mensajes del grupo.');
|
||||
} finally {
|
||||
setIsSyncingLive(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizar mensajes en vivo desde Evolution API
|
||||
const handleSyncLive = async () => {
|
||||
setIsSyncingLive(true);
|
||||
@ -87,7 +113,7 @@ export default function SupplierFeedPage() {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=40';
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
@ -341,7 +367,7 @@ export default function SupplierFeedPage() {
|
||||
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
|
||||
<select
|
||||
value={selectedGroupFilter}
|
||||
onChange={(e) => setSelectedGroupFilter(e.target.value)}
|
||||
onChange={(e) => handleGroupFilterChange(e.target.value)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
>
|
||||
<option value="all">Todos los Grupos de WhatsApp</option>
|
||||
|
||||
@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') === 'true';
|
||||
const limit = parseInt(searchParams.get('limit') || '30', 10);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
|
||||
// 1. Obtener grupos monitoreados locales
|
||||
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const monitoredJids = new Set(
|
||||
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
|
||||
);
|
||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const groupsMap = new Map<string, string>();
|
||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si no se pide en vivo, retornar lo almacenado localmente
|
||||
if (!fetchLive) {
|
||||
let posts = dataService.getSupplierPosts();
|
||||
if (filterJid) {
|
||||
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
|
||||
let liveGroups: any[] = [];
|
||||
try {
|
||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const allChats = await chatsRes.json();
|
||||
if (Array.isArray(allChats)) {
|
||||
liveGroups = allChats.filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
liveGroups.forEach((g: any) => {
|
||||
const jid = g.remoteJid || g.id || g.jid;
|
||||
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
|
||||
if (jid) groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||
}
|
||||
|
||||
// 2. Determinar JIDs objetivo a consultar
|
||||
let targetJids: string[] = [];
|
||||
if (filterJid && filterJid !== 'all') {
|
||||
targetJids = [filterJid];
|
||||
} else {
|
||||
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
|
||||
if (monitored.length > 0) {
|
||||
targetJids = monitored;
|
||||
} else if (liveGroups.length > 0) {
|
||||
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
|
||||
} else {
|
||||
targetJids = Array.from(groupsMap.keys());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJids.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
posts,
|
||||
monitoredCount: monitoredJids.size,
|
||||
isLive: true,
|
||||
totalFound: 0,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
|
||||
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
|
||||
const msgRes = await fetch(msgUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
|
||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const msgData = await msgRes.json();
|
||||
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
|
||||
// 3. Mapear grupos para resolver nombres
|
||||
const groupsMap = new Map<string, string>();
|
||||
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si faltan nombres, intentar traerlos de Evolution
|
||||
if (groupsMap.size === 0) {
|
||||
try {
|
||||
const grpRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
{ headers: { apikey: EVOLUTION_API_KEY } }
|
||||
);
|
||||
const grpData = await grpRes.json();
|
||||
if (Array.isArray(grpData)) {
|
||||
grpData.forEach((g: any) => {
|
||||
const jid = g.id || g.jid;
|
||||
const name = g.subject || g.name || 'Grupo WhatsApp';
|
||||
groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
const allRecords: any[] = [];
|
||||
for (const res of results) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allRecords.push(...res.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
// Filtrar solo grupos de WhatsApp (@g.us)
|
||||
if (!remoteJid.includes('@g.us')) continue;
|
||||
|
||||
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
|
||||
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
|
||||
// Si el grupo no está monitoreado, omitir
|
||||
continue;
|
||||
}
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
|
||||
imageUrls.push(
|
||||
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear oferta de calzado con IA
|
||||
const extracted = await parseSupplierMessage(text || groupName);
|
||||
|
||||
const timestamp = record.messageTimestamp
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: 'Reciente';
|
||||
|
||||
const post: SupplierPost = {
|
||||
@ -141,7 +175,7 @@ export async function GET(request: Request) {
|
||||
status: 'pending_review',
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
@ -156,7 +190,8 @@ export async function GET(request: Request) {
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: processedPosts.length,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error en /api/whatsapp-feed:', error);
|
||||
|
||||
@ -20,41 +20,50 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Consultar grupos en vivo desde Evolution API
|
||||
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const liveGroups = await response.json();
|
||||
const allChats = await response.json();
|
||||
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
|
||||
if (Array.isArray(liveGroups)) {
|
||||
if (liveGroups.length > 0) {
|
||||
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
|
||||
const jid = lg.id || lg.jid;
|
||||
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
|
||||
const jid = lg.remoteJid || lg.id || lg.jid;
|
||||
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
|
||||
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
|
||||
|
||||
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
|
||||
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
|
||||
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
|
||||
|
||||
return {
|
||||
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
|
||||
jid,
|
||||
name,
|
||||
isMonitored: existing ? existing.isMonitored : false,
|
||||
isMonitored,
|
||||
supplierId: existing?.supplierId,
|
||||
supplierName: existing?.supplierName || name,
|
||||
profitMarginOverride: existing?.profitMarginOverride || 35,
|
||||
autoPublish: existing?.autoPublish ?? true,
|
||||
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
|
||||
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
|
||||
notes: existing?.notes || '',
|
||||
};
|
||||
});
|
||||
|
||||
// Asegurar que los locales que no vinieron en live sigan presentes
|
||||
// Asegurar que los locales sigan presentes
|
||||
localGroups.forEach((lg) => {
|
||||
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
|
||||
merged.unshift(lg);
|
||||
@ -64,7 +73,7 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalLive: liveGroups.length,
|
||||
totalLive: merged.length,
|
||||
groups: merged,
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
Edit3,
|
||||
Trash2,
|
||||
ArrowUpRight,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
|
||||
const [editSizes, setEditSizes] = useState('');
|
||||
|
||||
// Target brand selector for publishing
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('sneaky');
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||
|
||||
// Cargar grupos monitoreados y posts iniciales
|
||||
// Cargar grupos monitoreados y posts iniciales en vivo
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const grpRes = await fetch('/api/whatsapp-groups');
|
||||
// 1. Grupos
|
||||
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
|
||||
const grpData = await grpRes.json();
|
||||
if (grpData.success && Array.isArray(grpData.groups)) {
|
||||
setMonitoredGroups(grpData.groups);
|
||||
}
|
||||
|
||||
const feedRes = await fetch('/api/whatsapp-feed');
|
||||
// 2. Posts en vivo desde los grupos de WhatsApp
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Cambiar filtro de grupo y recargar mensajes
|
||||
const handleGroupFilterChange = async (groupJid: string) => {
|
||||
setSelectedGroupFilter(groupJid);
|
||||
setIsSyncingLive(true);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(
|
||||
`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del grupo.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setActionMessage('❌ Error cargando mensajes del grupo.');
|
||||
} finally {
|
||||
setIsSyncingLive(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizar mensajes en vivo desde Evolution API
|
||||
const handleSyncLive = async () => {
|
||||
setIsSyncingLive(true);
|
||||
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=40';
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Manejo de Selección
|
||||
const handleToggleSelect = (id: string) => {
|
||||
setSelectedPostIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Filtrado de posts por estado y grupo
|
||||
const filteredPosts = posts.filter((p) => {
|
||||
// Estado
|
||||
if (tabStatus === 'pending' && p.status === 'published') return false;
|
||||
if (tabStatus === 'pending' && p.status === 'discarded') return false;
|
||||
if (tabStatus === 'published' && p.status !== 'published') return false;
|
||||
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
|
||||
|
||||
// Grupo
|
||||
if (selectedGroupFilter !== 'all') {
|
||||
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
|
||||
}
|
||||
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-orange-500/20 text-orange-400 flex items-center justify-center border border-orange-500/30 shadow-[0_0_15px_rgba(249,115,22,0.3)]">
|
||||
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
|
||||
<Radio className="w-5 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Sneaky</h1>
|
||||
<p className="text-xs text-zinc-400">
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
|
||||
</p>
|
||||
</div>
|
||||
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2.5 flex-wrap">
|
||||
<Link
|
||||
href="/admin/grupos-whatsapp"
|
||||
className="bg-zinc-900 hover:bg-zinc-800 border border-zinc-700 text-zinc-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
className="bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 text-neutral-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 text-orange-400" />
|
||||
<MessageSquare className="w-4 h-4 text-purple-400" />
|
||||
<span>Gestionar Grupos</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-orange-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
|
||||
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Toast feedback */}
|
||||
{actionMessage && (
|
||||
<div className="bg-orange-950/80 border border-orange-500/50 text-orange-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-orange-400" />
|
||||
<Sparkles className="w-4 h-4 text-purple-400" />
|
||||
<span>{actionMessage}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActionMessage(null)}
|
||||
className="text-white hover:text-orange-300 font-bold px-2 cursor-pointer"
|
||||
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
|
||||
)}
|
||||
|
||||
{/* Filter & Tabs Bar */}
|
||||
<div className="bg-zinc-900/90 border border-zinc-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
{/* Status Tabs */}
|
||||
<div className="flex items-center gap-1 bg-zinc-950 p-1 rounded-2xl border border-zinc-800 text-xs font-bold">
|
||||
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
|
||||
<button
|
||||
onClick={() => setTabStatus('pending')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'pending'
|
||||
? 'bg-orange-600 text-white shadow'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-purple-600 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>📥 Pendientes</span>
|
||||
<span className="bg-orange-950 text-orange-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{pendingCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'published'
|
||||
? 'bg-emerald-600 text-white shadow'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>✅ Publicados en Tienda</span>
|
||||
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
|
||||
onClick={() => setTabStatus('discarded')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'discarded'
|
||||
? 'bg-zinc-800 text-white shadow'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>🗑️ Descartados</span>
|
||||
<span className="bg-zinc-900 text-zinc-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{discardedCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Group Selector Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-orange-400 shrink-0" />
|
||||
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
|
||||
<select
|
||||
value={selectedGroupFilter}
|
||||
onChange={(e) => setSelectedGroupFilter(e.target.value)}
|
||||
className="bg-zinc-950 border border-zinc-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
onChange={(e) => handleGroupFilterChange(e.target.value)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
>
|
||||
<option value="all">Todos los Grupos de WhatsApp</option>
|
||||
{monitoredGroups.map((g) => (
|
||||
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Multi-Selection Bulk Actions Bar */}
|
||||
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
|
||||
<div className="bg-gradient-to-r from-orange-950/90 to-zinc-900 border border-orange-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="bg-orange-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
{selectedPostIds.size} seleccionados
|
||||
</span>
|
||||
<span className="text-xs text-zinc-300 font-semibold hidden sm:inline">
|
||||
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
|
||||
Listos para transferir al catálogo de la tienda
|
||||
</span>
|
||||
</div>
|
||||
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedPostIds(new Set())}
|
||||
className="text-zinc-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
>
|
||||
Desmarcar
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleBulkPublish}
|
||||
className="bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-orange-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
|
||||
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Posts List / Grid */}
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="bg-zinc-900/60 rounded-3xl p-12 text-center border border-zinc-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 text-orange-400 flex items-center justify-center mx-auto">
|
||||
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
|
||||
<MessageSquare className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
|
||||
<p className="text-xs text-zinc-400 max-w-md mx-auto">
|
||||
<p className="text-xs text-neutral-400 max-w-md mx-auto">
|
||||
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-orange-600 hover:bg-orange-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>Escanear Grupos Ahora</span>
|
||||
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className={`bg-zinc-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
isSelected
|
||||
? 'border-orange-500 shadow-[0_0_20px_rgba(249,115,22,0.2)]'
|
||||
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
|
||||
: isPublished
|
||||
? 'border-emerald-500/30 bg-emerald-950/10'
|
||||
: 'border-zinc-800 hover:border-zinc-700'
|
||||
: 'border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{/* Header info */}
|
||||
<div className="flex items-center justify-between gap-2 border-b border-zinc-800 pb-3">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{tabStatus === 'pending' && (
|
||||
<button
|
||||
onClick={() => handleToggleSelect(post.id)}
|
||||
className="text-orange-400 hover:text-orange-300 cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 cursor-pointer"
|
||||
>
|
||||
{isSelected ? (
|
||||
<CheckSquare className="w-5 h-5 fill-orange-600 text-white" />
|
||||
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
|
||||
) : (
|
||||
<Square className="w-5 h-5 text-zinc-600" />
|
||||
<Square className="w-5 h-5 text-neutral-600" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<span className="bg-orange-950 text-orange-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-orange-500/30 inline-block">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
|
||||
{post.channelName || 'Grupo WhatsApp'}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
|
||||
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
|
||||
</span>
|
||||
) : isDiscarded ? (
|
||||
<span className="bg-zinc-800 text-zinc-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
Descartado
|
||||
</span>
|
||||
) : (
|
||||
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
|
||||
{/* Sneaker Image & Raw text */}
|
||||
<div className="sm:col-span-5 space-y-2">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-zinc-950 border border-zinc-800 flex items-center justify-center group">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
|
||||
{post.rawImages && post.rawImages[0] ? (
|
||||
<img
|
||||
src={post.rawImages[0]}
|
||||
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-all"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-zinc-600 text-xs">Sin foto</div>
|
||||
<div className="text-neutral-600 text-xs">Sin foto</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950 rounded-xl p-2.5 border border-zinc-800/80">
|
||||
<p className="text-[11px] text-zinc-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{/* Raw WhatsApp Message snippet */}
|
||||
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
|
||||
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{post.rawMessage}
|
||||
</p>
|
||||
</div>
|
||||
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-[10px] font-black uppercase text-orange-400 tracking-wider">
|
||||
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
|
||||
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
|
||||
</span>
|
||||
<h3 className="text-base font-black text-white leading-tight">
|
||||
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
|
||||
</div>
|
||||
|
||||
{/* Pricing breakdown */}
|
||||
<div className="bg-zinc-950 p-3 rounded-2xl border border-zinc-800 grid grid-cols-2 gap-2">
|
||||
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<span className="text-[10px] text-zinc-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-zinc-300 font-mono">
|
||||
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-neutral-300 font-mono">
|
||||
{formatCOP(extracted?.supplierPrice || 90000)}
|
||||
</span>
|
||||
</div>
|
||||
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Sizes badge pills */}
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase block mb-1">
|
||||
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
|
||||
Tallas Disponibles:
|
||||
</span>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
|
||||
extracted.sizes.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="bg-zinc-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-zinc-800"
|
||||
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-zinc-500">37 a 43</span>
|
||||
<span className="text-xs text-neutral-500">37 a 43</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="pt-2 border-t border-zinc-800 flex items-center gap-2">
|
||||
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
|
||||
{!isPublished ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePublishPost(post)}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-orange-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Publicar en la App</span>
|
||||
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleOpenEdit(post)}
|
||||
title="Editar antes de publicar"
|
||||
className="bg-zinc-800 hover:bg-zinc-700 text-zinc-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleDiscardPost(post)}
|
||||
title="Descartar"
|
||||
className="bg-zinc-800 hover:bg-red-950/40 text-zinc-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
|
||||
<Link
|
||||
href="/"
|
||||
target="_blank"
|
||||
className="text-orange-400 hover:underline font-bold flex items-center gap-1"
|
||||
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
|
||||
>
|
||||
<span>Ver en Tienda</span>
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
|
||||
{/* Edit Modal Before Publishing */}
|
||||
{editingPost && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-zinc-900 border border-orange-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-zinc-800 pb-3">
|
||||
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
|
||||
<h3 className="font-black text-white text-base flex items-center gap-2">
|
||||
<Edit3 className="w-5 h-5 text-orange-400" />
|
||||
<Edit3 className="w-5 h-5 text-purple-400" />
|
||||
<span>Personalizar antes de Publicar</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="text-zinc-400 hover:text-white font-bold"
|
||||
className="text-neutral-400 hover:text-white font-bold"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
|
||||
|
||||
<div className="space-y-3.5 text-xs">
|
||||
<div>
|
||||
<label className="font-bold text-zinc-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-orange-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="font-bold text-zinc-300 block mb-1">Marca:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editBrand}
|
||||
onChange={(e) => setEditBrand(e.target.value)}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
|
||||
type="number"
|
||||
value={editPrice}
|
||||
onChange={(e) => setEditPrice(Number(e.target.value))}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-bold text-zinc-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editSizes}
|
||||
onChange={(e) => setEditSizes(e.target.value)}
|
||||
placeholder="38, 39, 40, 41, 42, 43"
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-orange-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-zinc-800">
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="flex-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
|
||||
sizes: sizesArray,
|
||||
});
|
||||
}}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-orange-500 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-orange-600/30 cursor-pointer"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
|
||||
>
|
||||
Guardar y Publicar en la App
|
||||
</button>
|
||||
|
||||
@ -197,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<Radio className="w-6 h-6 text-orange-500 animate-pulse" />
|
||||
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
|
||||
<span>Monitoreo de Grupos de WhatsApp</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400 mt-1">
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
|
||||
</p>
|
||||
</div>
|
||||
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Big Search and Select Button */}
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-400 hover:to-amber-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-orange-500/25 active:scale-95 transition-all cursor-pointer"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
<span>Buscar y Seleccionar Grupos</span>
|
||||
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toast && (
|
||||
<div className="bg-orange-950/90 border border-orange-500 text-orange-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<span>{toast}</span>
|
||||
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
|
||||
✕
|
||||
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-zinc-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-zinc-800 flex items-center justify-center text-zinc-300">
|
||||
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
|
||||
<Users className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-zinc-900 to-orange-950/40 border border-orange-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-orange-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-orange-500/20 text-orange-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<Radio className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-orange-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-zinc-300 mt-1 flex items-center gap-1.5">
|
||||
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
|
||||
<span>AutomataAlmeira (En línea)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-orange-500/10 text-orange-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter and Search Toolbar */}
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre tus grupos activos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-orange-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'all'
|
||||
? 'bg-zinc-800 text-white border border-zinc-700'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({groups.length})
|
||||
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('monitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'monitored'
|
||||
? 'bg-orange-500/20 text-orange-300 border border-orange-500/30'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Monitoreados ({monitoredCount})
|
||||
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('unmonitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'unmonitored'
|
||||
? 'bg-zinc-800 text-white border border-zinc-700'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Inactivos ({groups.length - monitoredCount})
|
||||
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Active Monitored Groups List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-zinc-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-orange-400" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
|
||||
<p>Cargando grupos de proveedores...</p>
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-zinc-400 space-y-3 bg-zinc-900 border border-zinc-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-zinc-600" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
|
||||
<p>No tienes grupos seleccionados para monitorear.</p>
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-orange-500 hover:bg-orange-400 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
>
|
||||
Buscar y Seleccionar Grupos desde WhatsApp
|
||||
</button>
|
||||
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
key={group.id || group.jid}
|
||||
className={`p-5 rounded-3xl border transition-all space-y-4 ${
|
||||
isMonitored
|
||||
? 'bg-gradient-to-br from-zinc-900 to-zinc-900/90 border-orange-500/40 shadow-lg shadow-orange-950/20 ring-1 ring-orange-500/20'
|
||||
: 'bg-zinc-900/60 border-zinc-800 hover:border-zinc-700 opacity-80'
|
||||
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
||||
isMonitored ? 'bg-orange-400 animate-pulse' : 'bg-zinc-600'
|
||||
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
|
||||
{group.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-zinc-400 pl-4 mt-0.5 truncate">
|
||||
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleGroup(group)}
|
||||
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
|
||||
isMonitored
|
||||
? 'bg-orange-500 text-zinc-950 border-orange-400 shadow-md font-black'
|
||||
: 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-white'
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
|
||||
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{isMonitored ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-zinc-950 text-orange-500" />
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
|
||||
<span>Monitoreando</span>
|
||||
</>
|
||||
) : (
|
||||
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Info Pills */}
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
|
||||
<div className="bg-zinc-950/80 p-2.5 rounded-xl border border-zinc-800">
|
||||
<span className="text-zinc-400 block text-[10px]">Margen Aplicado</span>
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
|
||||
<span className="font-bold text-white">
|
||||
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-950/80 p-2.5 rounded-xl border border-zinc-800">
|
||||
<span className="text-zinc-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-orange-300 flex items-center gap-1">
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-purple-300 flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" /> Auto-Publicar Directo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{group.notes && (
|
||||
<p className="text-[11px] text-zinc-400 bg-zinc-950/40 p-2 rounded-lg border border-zinc-800/60">
|
||||
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
|
||||
ℹ️ {group.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-zinc-800/80">
|
||||
<span className="text-[10px] text-zinc-500 font-mono">
|
||||
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
|
||||
<span className="text-[10px] text-neutral-500 font-mono">
|
||||
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => handleScanGroup(group)}
|
||||
disabled={scanningGroupId === group.id}
|
||||
className="text-xs font-bold text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<Sparkles className={`w-3.5 h-3.5 text-orange-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* ========================================================= */}
|
||||
{isPickerOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
|
||||
<div className="bg-zinc-900 border border-zinc-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
{/* Modal Header */}
|
||||
<div className="p-5 border-b border-zinc-800 flex items-center justify-between bg-zinc-950">
|
||||
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="w-10 h-10 rounded-2xl bg-orange-500/20 text-orange-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
|
||||
<p className="text-xs text-zinc-400">
|
||||
<p className="text-xs text-neutral-400">
|
||||
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="w-8 h-8 rounded-full bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search and Action Toolbar in Modal */}
|
||||
<div className="p-4 border-b border-zinc-800 bg-zinc-900/50 space-y-3">
|
||||
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-zinc-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
|
||||
value={pickerSearch}
|
||||
onChange={(e) => setPickerSearch(e.target.value)}
|
||||
className="w-full bg-zinc-950 border border-zinc-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-orange-500 font-semibold"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-zinc-400 font-semibold">
|
||||
<span className="text-neutral-400 font-semibold">
|
||||
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
|
||||
className="text-orange-400 hover:text-orange-300 font-bold hover:underline cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Seleccionar visibles
|
||||
</button>
|
||||
<span className="text-zinc-600">•</span>
|
||||
<span className="text-neutral-600">•</span>
|
||||
<button
|
||||
onClick={handleDeselectAll}
|
||||
className="text-zinc-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Deseleccionar todos
|
||||
</button>
|
||||
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Groups Scrollable List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{isLoadingLive ? (
|
||||
<div className="py-16 text-center text-xs text-zinc-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-orange-400" />
|
||||
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
|
||||
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
|
||||
<p className="text-[11px] text-zinc-500">Conectando con WhatsApp...</p>
|
||||
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
|
||||
</div>
|
||||
) : filteredPickerGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-xs text-zinc-400 space-y-2">
|
||||
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleSelectJid(group.jid)}
|
||||
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
|
||||
isSelected
|
||||
? 'bg-orange-950/30 border-orange-500/50 shadow-md ring-1 ring-orange-500/20'
|
||||
: 'bg-zinc-950 border-zinc-800 hover:border-zinc-700'
|
||||
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div
|
||||
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
|
||||
isSelected
|
||||
? 'bg-orange-500 border-orange-400 text-zinc-950'
|
||||
: 'border-zinc-700 bg-zinc-900'
|
||||
? 'bg-purple-600 border-purple-400 text-white'
|
||||
: 'border-neutral-700 bg-neutral-900'
|
||||
}`}
|
||||
>
|
||||
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
|
||||
@ -527,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<h4 className="font-bold text-white text-xs truncate">
|
||||
{group.name}
|
||||
</h4>
|
||||
<p className="text-[10px] font-mono text-zinc-400 truncate">
|
||||
<p className="text-[10px] font-mono text-neutral-400 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
Monitorear
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-zinc-800 text-zinc-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
Ignorar
|
||||
</span>
|
||||
)}
|
||||
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-zinc-800 bg-zinc-950 flex items-center justify-between">
|
||||
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
|
||||
<div className="text-xs">
|
||||
<span className="text-zinc-400">Seleccionados: </span>
|
||||
<strong className="text-orange-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-zinc-400"> grupos para monitorear</span>
|
||||
<span className="text-neutral-400">Seleccionados: </span>
|
||||
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-neutral-400"> grupos para monitorear</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="px-4 py-2.5 rounded-xl bg-zinc-800 hover:bg-zinc-700 text-zinc-300 font-bold text-xs cursor-pointer transition-all"
|
||||
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
type="button"
|
||||
onClick={handleSaveSelectedGroups}
|
||||
disabled={isLoadingLive}
|
||||
className="px-5 py-2.5 rounded-xl bg-orange-500 hover:bg-orange-400 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-orange-500/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>Guardar y Monitorear ({selectedJids.size})</span>
|
||||
|
||||
@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') === 'true';
|
||||
const limit = parseInt(searchParams.get('limit') || '30', 10);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
|
||||
// 1. Obtener grupos monitoreados locales
|
||||
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const monitoredJids = new Set(
|
||||
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
|
||||
);
|
||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const groupsMap = new Map<string, string>();
|
||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si no se pide en vivo, retornar lo almacenado localmente
|
||||
if (!fetchLive) {
|
||||
let posts = dataService.getSupplierPosts();
|
||||
if (filterJid) {
|
||||
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
|
||||
let liveGroups: any[] = [];
|
||||
try {
|
||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const allChats = await chatsRes.json();
|
||||
if (Array.isArray(allChats)) {
|
||||
liveGroups = allChats.filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
liveGroups.forEach((g: any) => {
|
||||
const jid = g.remoteJid || g.id || g.jid;
|
||||
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
|
||||
if (jid) groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||
}
|
||||
|
||||
// 2. Determinar JIDs objetivo a consultar
|
||||
let targetJids: string[] = [];
|
||||
if (filterJid && filterJid !== 'all') {
|
||||
targetJids = [filterJid];
|
||||
} else {
|
||||
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
|
||||
if (monitored.length > 0) {
|
||||
targetJids = monitored;
|
||||
} else if (liveGroups.length > 0) {
|
||||
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
|
||||
} else {
|
||||
targetJids = Array.from(groupsMap.keys());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJids.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
posts,
|
||||
monitoredCount: monitoredJids.size,
|
||||
isLive: true,
|
||||
totalFound: 0,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
|
||||
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
|
||||
const msgRes = await fetch(msgUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
|
||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const msgData = await msgRes.json();
|
||||
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
|
||||
// 3. Mapear grupos para resolver nombres
|
||||
const groupsMap = new Map<string, string>();
|
||||
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si faltan nombres, intentar traerlos de Evolution
|
||||
if (groupsMap.size === 0) {
|
||||
try {
|
||||
const grpRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
{ headers: { apikey: EVOLUTION_API_KEY } }
|
||||
);
|
||||
const grpData = await grpRes.json();
|
||||
if (Array.isArray(grpData)) {
|
||||
grpData.forEach((g: any) => {
|
||||
const jid = g.id || g.jid;
|
||||
const name = g.subject || g.name || 'Grupo WhatsApp';
|
||||
groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
const allRecords: any[] = [];
|
||||
for (const res of results) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allRecords.push(...res.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
// Filtrar solo grupos de WhatsApp (@g.us)
|
||||
if (!remoteJid.includes('@g.us')) continue;
|
||||
|
||||
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
|
||||
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
|
||||
// Si el grupo no está monitoreado, omitir
|
||||
continue;
|
||||
}
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
|
||||
imageUrls.push(
|
||||
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear oferta de calzado con IA
|
||||
const extracted = await parseSupplierMessage(text || groupName);
|
||||
|
||||
const timestamp = record.messageTimestamp
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: 'Reciente';
|
||||
|
||||
const post: SupplierPost = {
|
||||
@ -141,7 +175,7 @@ export async function GET(request: Request) {
|
||||
status: 'pending_review',
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
@ -156,7 +190,8 @@ export async function GET(request: Request) {
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: processedPosts.length,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error en /api/whatsapp-feed:', error);
|
||||
|
||||
@ -20,41 +20,50 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Consultar grupos en vivo desde Evolution API
|
||||
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const liveGroups = await response.json();
|
||||
const allChats = await response.json();
|
||||
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
|
||||
if (Array.isArray(liveGroups)) {
|
||||
if (liveGroups.length > 0) {
|
||||
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
|
||||
const jid = lg.id || lg.jid;
|
||||
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
|
||||
const jid = lg.remoteJid || lg.id || lg.jid;
|
||||
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
|
||||
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
|
||||
|
||||
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
|
||||
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
|
||||
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
|
||||
|
||||
return {
|
||||
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
|
||||
jid,
|
||||
name,
|
||||
isMonitored: existing ? existing.isMonitored : false,
|
||||
isMonitored,
|
||||
supplierId: existing?.supplierId,
|
||||
supplierName: existing?.supplierName || name,
|
||||
profitMarginOverride: existing?.profitMarginOverride || 35,
|
||||
autoPublish: existing?.autoPublish ?? true,
|
||||
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
|
||||
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
|
||||
notes: existing?.notes || '',
|
||||
};
|
||||
});
|
||||
|
||||
// Asegurar que los locales que no vinieron en live sigan presentes
|
||||
// Asegurar que los locales sigan presentes
|
||||
localGroups.forEach((lg) => {
|
||||
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
|
||||
merged.unshift(lg);
|
||||
@ -64,7 +73,7 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalLive: liveGroups.length,
|
||||
totalLive: merged.length,
|
||||
groups: merged,
|
||||
});
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
Edit3,
|
||||
Trash2,
|
||||
ArrowUpRight,
|
||||
Eye,
|
||||
} from 'lucide-react';
|
||||
import { SupplierPost, Product, MonitoredWhatsAppGroup } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
@ -48,19 +49,21 @@ export default function SupplierFeedPage() {
|
||||
const [editSizes, setEditSizes] = useState('');
|
||||
|
||||
// Target brand selector for publishing
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('zapply');
|
||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||
|
||||
// Cargar grupos monitoreados y posts iniciales
|
||||
// Cargar grupos monitoreados y posts iniciales en vivo
|
||||
const loadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const grpRes = await fetch('/api/whatsapp-groups');
|
||||
// 1. Grupos
|
||||
const grpRes = await fetch('/api/whatsapp-groups?fetchLive=true');
|
||||
const grpData = await grpRes.json();
|
||||
if (grpData.success && Array.isArray(grpData.groups)) {
|
||||
setMonitoredGroups(grpData.groups);
|
||||
}
|
||||
|
||||
const feedRes = await fetch('/api/whatsapp-feed');
|
||||
// 2. Posts en vivo desde los grupos de WhatsApp
|
||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=50');
|
||||
const feedData = await feedRes.json();
|
||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||
setPosts(feedData.posts);
|
||||
@ -76,6 +79,32 @@ export default function SupplierFeedPage() {
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Cambiar filtro de grupo y recargar mensajes
|
||||
const handleGroupFilterChange = async (groupJid: string) => {
|
||||
setSelectedGroupFilter(groupJid);
|
||||
setIsSyncingLive(true);
|
||||
setActionMessage(null);
|
||||
try {
|
||||
const url =
|
||||
groupJid !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.posts)) {
|
||||
setPosts(data.posts);
|
||||
setActionMessage(
|
||||
`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del grupo.`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setActionMessage('❌ Error cargando mensajes del grupo.');
|
||||
} finally {
|
||||
setIsSyncingLive(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizar mensajes en vivo desde Evolution API
|
||||
const handleSyncLive = async () => {
|
||||
setIsSyncingLive(true);
|
||||
@ -84,7 +113,7 @@ export default function SupplierFeedPage() {
|
||||
const url =
|
||||
selectedGroupFilter !== 'all'
|
||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=40';
|
||||
: '/api/whatsapp-feed?fetchLive=true&limit=50';
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
@ -191,6 +220,7 @@ export default function SupplierFeedPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Manejo de Selección
|
||||
const handleToggleSelect = (id: string) => {
|
||||
setSelectedPostIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@ -200,12 +230,15 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Filtrado de posts por estado y grupo
|
||||
const filteredPosts = posts.filter((p) => {
|
||||
// Estado
|
||||
if (tabStatus === 'pending' && p.status === 'published') return false;
|
||||
if (tabStatus === 'pending' && p.status === 'discarded') return false;
|
||||
if (tabStatus === 'published' && p.status !== 'published') return false;
|
||||
if (tabStatus === 'discarded' && p.status !== 'discarded') return false;
|
||||
|
||||
// Grupo
|
||||
if (selectedGroupFilter !== 'all') {
|
||||
return (p as any).channelJid === selectedGroupFilter || p.channelName === selectedGroupFilter;
|
||||
}
|
||||
@ -233,12 +266,12 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center border border-emerald-500/30 shadow-[0_0_15px_rgba(16,185,129,0.3)]">
|
||||
<div className="w-9 h-9 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center border border-purple-500/30 shadow-[0_0_15px_rgba(168,85,247,0.3)]">
|
||||
<Radio className="w-5 h-5 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp • Zapply</h1>
|
||||
<p className="text-xs text-slate-400">
|
||||
<h1 className="text-2xl font-black text-white">Feed de Mensajes de WhatsApp</h1>
|
||||
<p className="text-xs text-neutral-400">
|
||||
Visualiza los mensajes de tus grupos de proveedores y selecciona los calzados que deseas publicar en la app móvil.
|
||||
</p>
|
||||
</div>
|
||||
@ -249,16 +282,16 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2.5 flex-wrap">
|
||||
<Link
|
||||
href="/admin/grupos-whatsapp"
|
||||
className="bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
className="bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 text-neutral-300 font-bold text-xs px-3.5 py-2.5 rounded-xl flex items-center gap-1.5 transition-all"
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 text-emerald-400" />
|
||||
<MessageSquare className="w-4 h-4 text-purple-400" />
|
||||
<span>Gestionar Grupos</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
|
||||
@ -268,14 +301,14 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Toast feedback */}
|
||||
{actionMessage && (
|
||||
<div className="bg-emerald-950/80 border border-emerald-500/50 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="bg-purple-950/80 border border-purple-500/50 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between animate-in fade-in shadow-xl">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-emerald-400" />
|
||||
<Sparkles className="w-4 h-4 text-purple-400" />
|
||||
<span>{actionMessage}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActionMessage(null)}
|
||||
className="text-white hover:text-emerald-300 font-bold px-2 cursor-pointer"
|
||||
className="text-white hover:text-purple-300 font-bold px-2 cursor-pointer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -283,19 +316,19 @@ export default function SupplierFeedPage() {
|
||||
)}
|
||||
|
||||
{/* Filter & Tabs Bar */}
|
||||
<div className="bg-slate-900/90 border border-slate-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
<div className="bg-neutral-900/90 border border-neutral-800 rounded-3xl p-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4 backdrop-blur-xl">
|
||||
{/* Status Tabs */}
|
||||
<div className="flex items-center gap-1 bg-slate-950 p-1 rounded-2xl border border-slate-800 text-xs font-bold">
|
||||
<div className="flex items-center gap-1 bg-neutral-950 p-1 rounded-2xl border border-neutral-800 text-xs font-bold">
|
||||
<button
|
||||
onClick={() => setTabStatus('pending')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'pending'
|
||||
? 'bg-emerald-600 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-purple-600 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>📥 Pendientes</span>
|
||||
<span className="bg-emerald-950 text-emerald-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{pendingCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -305,7 +338,7 @@ export default function SupplierFeedPage() {
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'published'
|
||||
? 'bg-emerald-600 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>✅ Publicados en Tienda</span>
|
||||
@ -318,12 +351,12 @@ export default function SupplierFeedPage() {
|
||||
onClick={() => setTabStatus('discarded')}
|
||||
className={`px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
tabStatus === 'discarded'
|
||||
? 'bg-slate-800 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white shadow'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>🗑️ Descartados</span>
|
||||
<span className="bg-slate-900 text-slate-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
<span className="bg-neutral-900 text-neutral-400 text-[10px] px-1.5 py-0.5 rounded-full font-mono">
|
||||
{discardedCount}
|
||||
</span>
|
||||
</button>
|
||||
@ -331,11 +364,11 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Group Selector Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
<Filter className="w-4 h-4 text-purple-400 shrink-0" />
|
||||
<select
|
||||
value={selectedGroupFilter}
|
||||
onChange={(e) => setSelectedGroupFilter(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
onChange={(e) => handleGroupFilterChange(e.target.value)}
|
||||
className="bg-neutral-950 border border-neutral-800 text-white text-xs font-semibold rounded-xl px-3 py-2 outline-hidden cursor-pointer"
|
||||
>
|
||||
<option value="all">Todos los Grupos de WhatsApp</option>
|
||||
{monitoredGroups.map((g) => (
|
||||
@ -349,12 +382,12 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Multi-Selection Bulk Actions Bar */}
|
||||
{selectedPostIds.size > 0 && tabStatus === 'pending' && (
|
||||
<div className="bg-gradient-to-r from-emerald-950/90 to-slate-900 border border-emerald-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="bg-gradient-to-r from-purple-950/90 to-neutral-900 border border-purple-500/40 rounded-2xl p-3.5 flex items-center justify-between animate-in fade-in shadow-2xl">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="bg-emerald-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
<span className="bg-purple-600 text-white text-xs font-black px-2.5 py-1 rounded-lg">
|
||||
{selectedPostIds.size} seleccionados
|
||||
</span>
|
||||
<span className="text-xs text-slate-300 font-semibold hidden sm:inline">
|
||||
<span className="text-xs text-neutral-300 font-semibold hidden sm:inline">
|
||||
Listos para transferir al catálogo de la tienda
|
||||
</span>
|
||||
</div>
|
||||
@ -362,14 +395,14 @@ export default function SupplierFeedPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedPostIds(new Set())}
|
||||
className="text-slate-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white text-xs font-bold px-3 py-1.5 rounded-xl cursor-pointer"
|
||||
>
|
||||
Desmarcar
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleBulkPublish}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:opacity-95 text-white text-xs font-black px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
|
||||
@ -380,20 +413,20 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Posts List / Grid */}
|
||||
{filteredPosts.length === 0 ? (
|
||||
<div className="bg-slate-900/60 rounded-3xl p-12 text-center border border-slate-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center justify-center mx-auto">
|
||||
<div className="bg-neutral-900/60 rounded-3xl p-12 text-center border border-neutral-800 space-y-4">
|
||||
<div className="w-16 h-16 rounded-2xl bg-purple-500/10 border border-purple-500/20 text-purple-400 flex items-center justify-center mx-auto">
|
||||
<MessageSquare className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-black text-white text-base">No hay mensajes en esta bandeja</h3>
|
||||
<p className="text-xs text-slate-400 max-w-md mx-auto">
|
||||
<p className="text-xs text-neutral-400 max-w-md mx-auto">
|
||||
Haz clic en <strong>"Sincronizar WhatsApp en Vivo"</strong> para extraer automáticamente las ofertas recientes de tus grupos seleccionados.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSyncLive}
|
||||
disabled={isSyncingLive}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2.5 rounded-xl inline-flex items-center gap-2 cursor-pointer shadow-md"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} />
|
||||
<span>Escanear Grupos Ahora</span>
|
||||
@ -410,35 +443,35 @@ export default function SupplierFeedPage() {
|
||||
return (
|
||||
<div
|
||||
key={post.id}
|
||||
className={`bg-slate-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
className={`bg-neutral-900 border rounded-3xl p-5 space-y-4 transition-all ${
|
||||
isSelected
|
||||
? 'border-emerald-500 shadow-[0_0_20px_rgba(16,185,129,0.2)]'
|
||||
? 'border-purple-500 shadow-[0_0_20px_rgba(168,85,247,0.2)]'
|
||||
: isPublished
|
||||
? 'border-emerald-500/30 bg-emerald-950/10'
|
||||
: 'border-slate-800 hover:border-slate-700'
|
||||
: 'border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{/* Header info */}
|
||||
<div className="flex items-center justify-between gap-2 border-b border-slate-800 pb-3">
|
||||
<div className="flex items-center justify-between gap-2 border-b border-neutral-800 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{tabStatus === 'pending' && (
|
||||
<button
|
||||
onClick={() => handleToggleSelect(post.id)}
|
||||
className="text-emerald-400 hover:text-emerald-300 cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 cursor-pointer"
|
||||
>
|
||||
{isSelected ? (
|
||||
<CheckSquare className="w-5 h-5 fill-emerald-600 text-white" />
|
||||
<CheckSquare className="w-5 h-5 fill-purple-600 text-white" />
|
||||
) : (
|
||||
<Square className="w-5 h-5 text-slate-600" />
|
||||
<Square className="w-5 h-5 text-neutral-600" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<span className="bg-emerald-950 text-emerald-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-emerald-500/30 inline-block">
|
||||
<span className="bg-purple-950 text-purple-300 text-[10px] font-black px-2 py-0.5 rounded-full border border-purple-500/30 inline-block">
|
||||
{post.channelName || 'Grupo WhatsApp'}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
<span className="text-[10px] text-neutral-500 ml-2 font-mono">{post.receivedAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -448,7 +481,7 @@ export default function SupplierFeedPage() {
|
||||
<CheckCircle2 className="w-3 h-3" /> Publicado en Tienda
|
||||
</span>
|
||||
) : isDiscarded ? (
|
||||
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2.5 py-0.5 rounded-full">
|
||||
Descartado
|
||||
</span>
|
||||
) : (
|
||||
@ -463,7 +496,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4">
|
||||
{/* Sneaker Image & Raw text */}
|
||||
<div className="sm:col-span-5 space-y-2">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-slate-950 border border-slate-800 flex items-center justify-center group">
|
||||
<div className="relative aspect-square w-full rounded-2xl overflow-hidden bg-neutral-950 border border-neutral-800 flex items-center justify-center group">
|
||||
{post.rawImages && post.rawImages[0] ? (
|
||||
<img
|
||||
src={post.rawImages[0]}
|
||||
@ -471,12 +504,13 @@ export default function SupplierFeedPage() {
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-all"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-slate-600 text-xs">Sin foto</div>
|
||||
<div className="text-neutral-600 text-xs">Sin foto</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950 rounded-xl p-2.5 border border-slate-800/80">
|
||||
<p className="text-[11px] text-slate-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{/* Raw WhatsApp Message snippet */}
|
||||
<div className="bg-neutral-950 rounded-xl p-2.5 border border-neutral-800/80">
|
||||
<p className="text-[11px] text-neutral-400 font-mono whitespace-pre-line line-clamp-3">
|
||||
{post.rawMessage}
|
||||
</p>
|
||||
</div>
|
||||
@ -486,7 +520,7 @@ export default function SupplierFeedPage() {
|
||||
<div className="sm:col-span-7 space-y-3 flex flex-col justify-between">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-[10px] font-black uppercase text-emerald-400 tracking-wider">
|
||||
<span className="text-[10px] font-black uppercase text-purple-400 tracking-wider">
|
||||
{extracted?.brand || 'Nike'} • {extracted?.category || 'Sneakers'}
|
||||
</span>
|
||||
<h3 className="text-base font-black text-white leading-tight">
|
||||
@ -495,10 +529,10 @@ export default function SupplierFeedPage() {
|
||||
</div>
|
||||
|
||||
{/* Pricing breakdown */}
|
||||
<div className="bg-slate-950 p-3 rounded-2xl border border-slate-800 grid grid-cols-2 gap-2">
|
||||
<div className="bg-neutral-950 p-3 rounded-2xl border border-neutral-800 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<span className="text-[10px] text-slate-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-slate-300 font-mono">
|
||||
<span className="text-[10px] text-neutral-400 block font-bold">Costo Proveedor:</span>
|
||||
<span className="text-xs font-bold text-neutral-300 font-mono">
|
||||
{formatCOP(extracted?.supplierPrice || 90000)}
|
||||
</span>
|
||||
</div>
|
||||
@ -512,7 +546,7 @@ export default function SupplierFeedPage() {
|
||||
|
||||
{/* Sizes badge pills */}
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase block mb-1">
|
||||
<span className="text-[10px] font-bold text-neutral-400 uppercase block mb-1">
|
||||
Tallas Disponibles:
|
||||
</span>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
@ -520,25 +554,25 @@ export default function SupplierFeedPage() {
|
||||
extracted.sizes.map((s) => (
|
||||
<span
|
||||
key={s}
|
||||
className="bg-slate-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-slate-800"
|
||||
className="bg-neutral-950 text-white font-bold text-[11px] px-2 py-0.5 rounded-lg border border-neutral-800"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-slate-500">37 a 43</span>
|
||||
<span className="text-xs text-neutral-500">37 a 43</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="pt-2 border-t border-slate-800 flex items-center gap-2">
|
||||
<div className="pt-2 border-t border-neutral-800 flex items-center gap-2">
|
||||
{!isPublished ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handlePublishPost(post)}
|
||||
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-emerald-600/30 cursor-pointer transition-all active:scale-95"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-2.5 px-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md shadow-purple-600/30 cursor-pointer transition-all active:scale-95"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Publicar en la App</span>
|
||||
@ -547,7 +581,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleOpenEdit(post)}
|
||||
title="Editar antes de publicar"
|
||||
className="bg-slate-800 hover:bg-slate-700 text-slate-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-neutral-300 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -555,7 +589,7 @@ export default function SupplierFeedPage() {
|
||||
<button
|
||||
onClick={() => handleDiscardPost(post)}
|
||||
title="Descartar"
|
||||
className="bg-slate-800 hover:bg-red-950/40 text-slate-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
className="bg-neutral-800 hover:bg-red-950/40 text-neutral-400 hover:text-red-400 p-2.5 rounded-xl cursor-pointer transition-all"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
@ -568,7 +602,7 @@ export default function SupplierFeedPage() {
|
||||
<Link
|
||||
href="/"
|
||||
target="_blank"
|
||||
className="text-emerald-400 hover:underline font-bold flex items-center gap-1"
|
||||
className="text-purple-400 hover:underline font-bold flex items-center gap-1"
|
||||
>
|
||||
<span>Ver en Tienda</span>
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
@ -587,15 +621,15 @@ export default function SupplierFeedPage() {
|
||||
{/* Edit Modal Before Publishing */}
|
||||
{editingPost && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div className="bg-slate-900 border border-emerald-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl animate-in zoom-in-95">
|
||||
<div className="flex items-center justify-between border-b border-neutral-800 pb-3">
|
||||
<h3 className="font-black text-white text-base flex items-center gap-2">
|
||||
<Edit3 className="w-5 h-5 text-emerald-400" />
|
||||
<Edit3 className="w-5 h-5 text-purple-400" />
|
||||
<span>Personalizar antes de Publicar</span>
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="text-slate-400 hover:text-white font-bold"
|
||||
className="text-neutral-400 hover:text-white font-bold"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@ -603,23 +637,23 @@ export default function SupplierFeedPage() {
|
||||
|
||||
<div className="space-y-3.5 text-xs">
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Nombre Comercial del Modelo:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white font-bold outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Marca:</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Marca:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editBrand}
|
||||
onChange={(e) => setEditBrand(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -629,27 +663,27 @@ export default function SupplierFeedPage() {
|
||||
type="number"
|
||||
value={editPrice}
|
||||
onChange={(e) => setEditPrice(Number(e.target.value))}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-emerald-300 font-black font-mono outline-hidden focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-bold text-slate-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<label className="font-bold text-neutral-300 block mb-1">Tallas (separadas por coma):</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editSizes}
|
||||
onChange={(e) => setEditSizes(e.target.value)}
|
||||
placeholder="38, 39, 40, 41, 42, 43"
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-emerald-500"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-slate-800">
|
||||
<div className="flex items-center gap-2.5 pt-3 border-t border-neutral-800">
|
||||
<button
|
||||
onClick={() => setEditingPost(null)}
|
||||
className="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
className="flex-1 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-3 rounded-xl cursor-pointer"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -667,7 +701,7 @@ export default function SupplierFeedPage() {
|
||||
sizes: sizesArray,
|
||||
});
|
||||
}}
|
||||
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-500 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-emerald-600/30 cursor-pointer"
|
||||
className="flex-1 bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs py-3 rounded-xl shadow-lg shadow-purple-600/30 cursor-pointer"
|
||||
>
|
||||
Guardar y Publicar en la App
|
||||
</button>
|
||||
|
||||
@ -197,10 +197,10 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<Radio className="w-6 h-6 text-emerald-400 animate-pulse" />
|
||||
<Radio className="w-6 h-6 text-purple-400 animate-pulse" />
|
||||
<span>Monitoreo de Grupos de WhatsApp</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Busca y selecciona los grupos de proveedores que el bot de <strong>Evolution API & Gemini</strong> debe escuchar 24/7 para auto-publicar zapatillas.
|
||||
</p>
|
||||
</div>
|
||||
@ -208,7 +208,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Big Search and Select Button */}
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-500 hover:from-emerald-500 hover:to-teal-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/25 active:scale-95 transition-all cursor-pointer"
|
||||
className="bg-gradient-to-r from-purple-600 to-fuchsia-500 hover:from-purple-500 hover:to-fuchsia-400 text-white font-black text-xs px-5 py-3 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-purple-600/25 active:scale-95 transition-all cursor-pointer"
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
<span>Buscar y Seleccionar Grupos</span>
|
||||
@ -217,7 +217,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toast && (
|
||||
<div className="bg-emerald-950/90 border border-emerald-500 text-emerald-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<div className="bg-purple-950/90 border border-purple-500 text-purple-200 px-4 py-3 rounded-2xl text-xs flex items-center justify-between shadow-xl animate-in fade-in">
|
||||
<span>{toast}</span>
|
||||
<button onClick={() => setToast(null)} className="font-bold text-white ml-3 cursor-pointer">
|
||||
✕
|
||||
@ -227,50 +227,50 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Stats Bar */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-slate-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-[11px] font-bold text-neutral-400">Grupos en tu WhatsApp</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{groups.length}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-slate-300">
|
||||
<div className="w-10 h-10 rounded-xl bg-neutral-800 flex items-center justify-center text-neutral-300">
|
||||
<Users className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-slate-900 to-emerald-950/40 border border-emerald-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-gradient-to-br from-neutral-900 to-purple-950/40 border border-purple-500/30 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-emerald-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-[11px] font-bold text-purple-400">Grupos Monitoreados (Activos)</p>
|
||||
<p className="text-xl font-black text-white mt-0.5">{monitoredCount}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<Radio className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[11px] font-bold text-emerald-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-slate-300 mt-1 flex items-center gap-1.5">
|
||||
<p className="text-[11px] font-bold text-purple-400">Evolution API (WhatsApp)</p>
|
||||
<p className="text-xs font-mono font-bold text-neutral-300 mt-1 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
|
||||
<span>AutomataAlmeira (En línea)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-xl bg-emerald-500/10 text-emerald-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/10 text-purple-400 flex items-center justify-center">
|
||||
<Zap className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter and Search Toolbar */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-2xl p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||
<div className="relative w-full sm:w-80">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre tus grupos activos..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-emerald-400"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3 py-2 text-xs text-white outline-hidden focus:border-purple-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -279,8 +279,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('all')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'all'
|
||||
? 'bg-slate-800 text-white border border-slate-700'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Todos ({groups.length})
|
||||
@ -289,8 +289,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('monitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'monitored'
|
||||
? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Monitoreados ({monitoredCount})
|
||||
@ -299,8 +299,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => setFilterMode('unmonitored')}
|
||||
className={`text-xs font-bold px-3 py-1.5 rounded-xl transition-all cursor-pointer ${
|
||||
filterMode === 'unmonitored'
|
||||
? 'bg-slate-800 text-white border border-slate-700'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
? 'bg-neutral-800 text-white border border-neutral-700'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Inactivos ({groups.length - monitoredCount})
|
||||
@ -311,17 +311,17 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Active Monitored Groups List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-emerald-400" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto text-purple-400" />
|
||||
<p>Cargando grupos de proveedores...</p>
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<div className="col-span-full py-12 text-center text-xs text-slate-400 space-y-3 bg-slate-900 border border-slate-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-slate-600" />
|
||||
<div className="col-span-full py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900 border border-neutral-800 rounded-3xl">
|
||||
<MessageSquare className="w-8 h-8 mx-auto text-neutral-600" />
|
||||
<p>No tienes grupos seleccionados para monitorear.</p>
|
||||
<button
|
||||
onClick={handleOpenPicker}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-xl shadow-lg cursor-pointer"
|
||||
>
|
||||
Buscar y Seleccionar Grupos desde WhatsApp
|
||||
</button>
|
||||
@ -335,8 +335,8 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
key={group.id || group.jid}
|
||||
className={`p-5 rounded-3xl border transition-all space-y-4 ${
|
||||
isMonitored
|
||||
? 'bg-gradient-to-br from-slate-900 to-slate-900/90 border-emerald-500/40 shadow-lg shadow-emerald-950/20 ring-1 ring-emerald-500/20'
|
||||
: 'bg-slate-900/60 border-slate-800 hover:border-slate-700 opacity-80'
|
||||
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
@ -345,14 +345,14 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
||||
isMonitored ? 'bg-emerald-400 animate-pulse' : 'bg-slate-600'
|
||||
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
|
||||
{group.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[11px] font-mono text-slate-400 pl-4 mt-0.5 truncate">
|
||||
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -362,13 +362,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleGroup(group)}
|
||||
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
|
||||
isMonitored
|
||||
? 'bg-emerald-500 text-slate-950 border-emerald-400 shadow-md font-black'
|
||||
: 'bg-slate-800 text-slate-400 border-slate-700 hover:text-white'
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
|
||||
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{isMonitored ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-slate-950 text-emerald-500" />
|
||||
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
|
||||
<span>Monitoreando</span>
|
||||
</>
|
||||
) : (
|
||||
@ -382,39 +382,39 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
|
||||
{/* Info Pills */}
|
||||
<div className="grid grid-cols-2 gap-2 text-[11px] pt-1">
|
||||
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
|
||||
<span className="text-slate-400 block text-[10px]">Margen Aplicado</span>
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Margen Aplicado</span>
|
||||
<span className="font-bold text-white">
|
||||
+{group.profitMarginOverride || 35}% (Redondeo a $5.000)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-950/80 p-2.5 rounded-xl border border-slate-800">
|
||||
<span className="text-slate-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-emerald-300 flex items-center gap-1">
|
||||
<div className="bg-neutral-950/80 p-2.5 rounded-xl border border-neutral-800">
|
||||
<span className="text-neutral-400 block text-[10px]">Modo de Ingreso</span>
|
||||
<span className="font-bold text-purple-300 flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" /> Auto-Publicar Directo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{group.notes && (
|
||||
<p className="text-[11px] text-slate-400 bg-slate-950/40 p-2 rounded-lg border border-slate-800/60">
|
||||
<p className="text-[11px] text-neutral-400 bg-neutral-950/40 p-2 rounded-lg border border-neutral-800/60">
|
||||
ℹ️ {group.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action footer */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-800/80">
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
<div className="flex items-center justify-between pt-2 border-t border-neutral-800/80">
|
||||
<span className="text-[10px] text-neutral-500 font-mono">
|
||||
{group.participantsCount ? `👥 ${group.participantsCount} miembros` : 'Canal Mayorista'}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => handleScanGroup(group)}
|
||||
disabled={scanningGroupId === group.id}
|
||||
className="text-xs font-bold text-slate-300 hover:text-white bg-slate-800 hover:bg-slate-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
className="text-xs font-bold text-neutral-300 hover:text-white bg-neutral-800 hover:bg-neutral-700 px-3 py-1.5 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<Sparkles className={`w-3.5 h-3.5 text-emerald-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<Sparkles className={`w-3.5 h-3.5 text-purple-400 ${scanningGroupId === group.id ? 'animate-spin' : ''}`} />
|
||||
<span>{scanningGroupId === group.id ? 'Extrayendo...' : 'Escanear Mensajes'}</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -429,56 +429,56 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* ========================================================= */}
|
||||
{isPickerOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xs flex items-center justify-center p-4 animate-in fade-in duration-200">
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl max-w-2xl w-full max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
{/* Modal Header */}
|
||||
<div className="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950">
|
||||
<div className="p-5 border-b border-neutral-800 flex items-center justify-between bg-neutral-950">
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<div className="w-10 h-10 rounded-2xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
||||
<div className="w-10 h-10 rounded-2xl bg-purple-500/20 text-purple-400 flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-black text-white">Seleccionar Grupos de WhatsApp</h2>
|
||||
<p className="text-xs text-slate-400">
|
||||
<p className="text-xs text-neutral-400">
|
||||
Marca los grupos que deseas monitorear con IA para auto-publicar calzado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
className="w-8 h-8 rounded-full bg-neutral-800 hover:bg-neutral-700 text-neutral-400 hover:text-white flex items-center justify-center transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search and Action Toolbar in Modal */}
|
||||
<div className="p-4 border-b border-slate-800 bg-slate-900/50 space-y-3">
|
||||
<div className="p-4 border-b border-neutral-800 bg-neutral-900/50 space-y-3">
|
||||
<div className="relative">
|
||||
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<Search className="w-4 h-4 text-neutral-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar entre todos tus grupos de WhatsApp (ej: Jovic, Mío, Bodega)..."
|
||||
value={pickerSearch}
|
||||
onChange={(e) => setPickerSearch(e.target.value)}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-emerald-400 font-semibold"
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-white outline-hidden focus:border-purple-400 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-slate-400 font-semibold">
|
||||
<span className="text-neutral-400 font-semibold">
|
||||
Mostrando <strong className="text-white">{filteredPickerGroups.length}</strong> grupos encontrados
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleSelectAllFiltered(filteredPickerGroups)}
|
||||
className="text-emerald-400 hover:text-emerald-300 font-bold hover:underline cursor-pointer"
|
||||
className="text-purple-400 hover:text-purple-300 font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Seleccionar visibles
|
||||
</button>
|
||||
<span className="text-slate-600">•</span>
|
||||
<span className="text-neutral-600">•</span>
|
||||
<button
|
||||
onClick={handleDeselectAll}
|
||||
className="text-slate-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
className="text-neutral-400 hover:text-white font-bold hover:underline cursor-pointer"
|
||||
>
|
||||
Deseleccionar todos
|
||||
</button>
|
||||
@ -489,13 +489,13 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
{/* Groups Scrollable List */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
||||
{isLoadingLive ? (
|
||||
<div className="py-16 text-center text-xs text-slate-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-emerald-400" />
|
||||
<div className="py-16 text-center text-xs text-neutral-400 space-y-3">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mx-auto text-purple-400" />
|
||||
<p className="font-bold text-white">Consultando grupos en vivo desde Evolution API...</p>
|
||||
<p className="text-[11px] text-slate-500">Conectando con WhatsApp...</p>
|
||||
<p className="text-[11px] text-neutral-500">Conectando con WhatsApp...</p>
|
||||
</div>
|
||||
) : filteredPickerGroups.length === 0 ? (
|
||||
<div className="py-12 text-center text-xs text-slate-400 space-y-2">
|
||||
<div className="py-12 text-center text-xs text-neutral-400 space-y-2">
|
||||
<p>No se encontraron grupos con el término "{pickerSearch}".</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -508,16 +508,16 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
onClick={() => handleToggleSelectJid(group.jid)}
|
||||
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
|
||||
isSelected
|
||||
? 'bg-emerald-950/30 border-emerald-500/50 shadow-md ring-1 ring-emerald-500/20'
|
||||
: 'bg-slate-950 border-slate-800 hover:border-slate-700'
|
||||
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
|
||||
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
||||
<div
|
||||
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
|
||||
isSelected
|
||||
? 'bg-emerald-500 border-emerald-400 text-slate-950'
|
||||
: 'border-slate-700 bg-slate-900'
|
||||
? 'bg-purple-600 border-purple-400 text-white'
|
||||
: 'border-neutral-700 bg-neutral-900'
|
||||
}`}
|
||||
>
|
||||
{isSelected && <CheckSquare className="w-3.5 h-3.5 fill-current" />}
|
||||
@ -527,7 +527,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
<h4 className="font-bold text-white text-xs truncate">
|
||||
{group.name}
|
||||
</h4>
|
||||
<p className="text-[10px] font-mono text-slate-400 truncate">
|
||||
<p className="text-[10px] font-mono text-neutral-400 truncate">
|
||||
{group.jid}
|
||||
</p>
|
||||
</div>
|
||||
@ -539,7 +539,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
Monitorear
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-slate-800 text-slate-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
<span className="bg-neutral-800 text-neutral-400 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||||
Ignorar
|
||||
</span>
|
||||
)}
|
||||
@ -551,18 +551,18 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 border-t border-slate-800 bg-slate-950 flex items-center justify-between">
|
||||
<div className="p-4 border-t border-neutral-800 bg-neutral-950 flex items-center justify-between">
|
||||
<div className="text-xs">
|
||||
<span className="text-slate-400">Seleccionados: </span>
|
||||
<strong className="text-emerald-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-slate-400"> grupos para monitorear</span>
|
||||
<span className="text-neutral-400">Seleccionados: </span>
|
||||
<strong className="text-purple-400 font-mono text-sm">{selectedJids.size}</strong>
|
||||
<span className="text-neutral-400"> grupos para monitorear</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPickerOpen(false)}
|
||||
className="px-4 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold text-xs cursor-pointer transition-all"
|
||||
className="px-4 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs cursor-pointer transition-all"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
@ -570,7 +570,7 @@ export default function AdminWhatsAppGroupsPage() {
|
||||
type="button"
|
||||
onClick={handleSaveSelectedGroups}
|
||||
disabled={isLoadingLive}
|
||||
className="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-emerald-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
className="px-5 py-2.5 rounded-xl bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs flex items-center space-x-1.5 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span>Guardar y Monitorear ({selectedJids.size})</span>
|
||||
|
||||
@ -11,78 +11,105 @@ const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || 'Automata
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fetchLive = searchParams.get('fetchLive') === 'true';
|
||||
const limit = parseInt(searchParams.get('limit') || '30', 10);
|
||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||
const filterJid = searchParams.get('groupJid');
|
||||
|
||||
// 1. Obtener grupos monitoreados locales
|
||||
const monitoredGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const monitoredJids = new Set(
|
||||
monitoredGroups.filter((g) => g.isMonitored).map((g) => g.jid)
|
||||
);
|
||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||
const groupsMap = new Map<string, string>();
|
||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si no se pide en vivo, retornar lo almacenado localmente
|
||||
if (!fetchLive) {
|
||||
let posts = dataService.getSupplierPosts();
|
||||
if (filterJid) {
|
||||
posts = posts.filter((p) => (p as any).channelJid === filterJid || p.channelName?.includes(filterJid));
|
||||
let liveGroups: any[] = [];
|
||||
try {
|
||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const allChats = await chatsRes.json();
|
||||
if (Array.isArray(allChats)) {
|
||||
liveGroups = allChats.filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
liveGroups.forEach((g: any) => {
|
||||
const jid = g.remoteJid || g.id || g.jid;
|
||||
const name = g.name || g.pushName || g.subject || 'Grupo WhatsApp';
|
||||
if (jid) groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||
}
|
||||
|
||||
// 2. Determinar JIDs objetivo a consultar
|
||||
let targetJids: string[] = [];
|
||||
if (filterJid && filterJid !== 'all') {
|
||||
targetJids = [filterJid];
|
||||
} else {
|
||||
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
|
||||
if (monitored.length > 0) {
|
||||
targetJids = monitored;
|
||||
} else if (liveGroups.length > 0) {
|
||||
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
|
||||
} else {
|
||||
targetJids = Array.from(groupsMap.keys());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJids.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
posts,
|
||||
monitoredCount: monitoredJids.size,
|
||||
isLive: true,
|
||||
totalFound: 0,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Consultar Evolution API para obtener los mensajes recientes de los grupos
|
||||
const msgUrl = `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`;
|
||||
const msgRes = await fetch(msgUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(filterJid ? { where: { key: { remoteJid: filterJid } } } : {}),
|
||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||
try {
|
||||
const msgRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
|
||||
}
|
||||
);
|
||||
const msgData = await msgRes.json();
|
||||
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const msgData = await msgRes.json();
|
||||
const records = msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
|
||||
|
||||
// 3. Mapear grupos para resolver nombres
|
||||
const groupsMap = new Map<string, string>();
|
||||
monitoredGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||
|
||||
// Si faltan nombres, intentar traerlos de Evolution
|
||||
if (groupsMap.size === 0) {
|
||||
try {
|
||||
const grpRes = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
{ headers: { apikey: EVOLUTION_API_KEY } }
|
||||
);
|
||||
const grpData = await grpRes.json();
|
||||
if (Array.isArray(grpData)) {
|
||||
grpData.forEach((g: any) => {
|
||||
const jid = g.id || g.jid;
|
||||
const name = g.subject || g.name || 'Grupo WhatsApp';
|
||||
groupsMap.set(jid, name);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const results = await Promise.allSettled(groupFetchPromises);
|
||||
const allRecords: any[] = [];
|
||||
for (const res of results) {
|
||||
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
|
||||
allRecords.push(...res.value);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Ordenar cronológicamente (más recientes primero)
|
||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||
|
||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||
const processedPosts: SupplierPost[] = [];
|
||||
|
||||
for (const record of records) {
|
||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
||||
const sliceRecords = allRecords.slice(0, 60);
|
||||
|
||||
for (const record of sliceRecords) {
|
||||
const remoteJid = record.key?.remoteJid || '';
|
||||
// Filtrar solo grupos de WhatsApp (@g.us)
|
||||
if (!remoteJid.includes('@g.us')) continue;
|
||||
|
||||
// Si hay grupos monitoreados configurados y no se pasó un filtro específico, respetar monitoreo
|
||||
if (monitoredJids.size > 0 && !filterJid && !monitoredJids.has(remoteJid)) {
|
||||
// Si el grupo no está monitoreado, omitir
|
||||
continue;
|
||||
}
|
||||
|
||||
const text =
|
||||
record.message?.conversation ||
|
||||
record.message?.extendedTextMessage?.text ||
|
||||
@ -121,14 +148,21 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
if (imageUrls.length === 0) {
|
||||
imageUrls.push('https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80');
|
||||
imageUrls.push(
|
||||
'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear oferta de calzado con IA
|
||||
const extracted = await parseSupplierMessage(text || groupName);
|
||||
|
||||
const timestamp = record.messageTimestamp
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
? new Date(record.messageTimestamp * 1000).toLocaleString('es-CO', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
})
|
||||
: 'Reciente';
|
||||
|
||||
const post: SupplierPost = {
|
||||
@ -141,7 +175,7 @@ export async function GET(request: Request) {
|
||||
status: 'pending_review',
|
||||
receivedAt: timestamp,
|
||||
supplierName: groupName,
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573000000000',
|
||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||
};
|
||||
|
||||
processedPosts.push(post);
|
||||
@ -156,7 +190,8 @@ export async function GET(request: Request) {
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalFound: processedPosts.length,
|
||||
posts: dataService.getSupplierPosts(),
|
||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error en /api/whatsapp-feed:', error);
|
||||
|
||||
@ -20,41 +20,50 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// Consultar grupos en vivo desde Evolution API
|
||||
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
|
||||
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
const liveGroups = await response.json();
|
||||
const allChats = await response.json();
|
||||
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
|
||||
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
|
||||
);
|
||||
|
||||
if (Array.isArray(liveGroups)) {
|
||||
if (liveGroups.length > 0) {
|
||||
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
|
||||
const jid = lg.id || lg.jid;
|
||||
const name = lg.subject || lg.name || 'Grupo Sin Nombre';
|
||||
const jid = lg.remoteJid || lg.id || lg.jid;
|
||||
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
|
||||
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
|
||||
|
||||
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
|
||||
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
|
||||
const isMonitored = existing ? existing.isMonitored : isKnownSupplierGroup;
|
||||
|
||||
return {
|
||||
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
|
||||
jid,
|
||||
name,
|
||||
isMonitored: existing ? existing.isMonitored : false,
|
||||
isMonitored,
|
||||
supplierId: existing?.supplierId,
|
||||
supplierName: existing?.supplierName || name,
|
||||
profitMarginOverride: existing?.profitMarginOverride || 35,
|
||||
autoPublish: existing?.autoPublish ?? true,
|
||||
participantsCount: lg.size || (lg.participants ? lg.participants.length : undefined),
|
||||
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
|
||||
notes: existing?.notes || '',
|
||||
};
|
||||
});
|
||||
|
||||
// Asegurar que los locales que no vinieron en live sigan presentes
|
||||
// Asegurar que los locales sigan presentes
|
||||
localGroups.forEach((lg) => {
|
||||
if (!merged.some((m) => m.jid === lg.jid || m.id === lg.id)) {
|
||||
merged.unshift(lg);
|
||||
@ -64,7 +73,7 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
isLive: true,
|
||||
totalLive: liveGroups.length,
|
||||
totalLive: merged.length,
|
||||
groups: merged,
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user