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