feat: seleccion masiva de pagina activa y exclusion/descarte inteligente de grupos inactivos o abandonados

This commit is contained in:
AI Bot 2026-09-03 19:27:33 -05:00
parent eb4f1affd4
commit c7abbd2d54
8 changed files with 377 additions and 83 deletions

View File

@ -78,6 +78,10 @@ def deploy():
cd {REMOTE_DIR}
docker compose -f docker-compose.prod.yml down --remove-orphans || true
DOCKER_BUILDKIT=1 docker compose -f docker-compose.prod.yml up -d --build
sleep 3
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/DripZone.apk || true
docker exec dripzone-production chmod 644 /app/public/DripZone.apk /app/DripZone.apk || true
docker ps | grep dripzone
"""

View File

@ -20,6 +20,9 @@ import {
Filter,
CheckSquare,
Square,
MinusSquare,
AlertCircle,
AlertTriangle,
Edit3,
Trash2,
ArrowUpRight,
@ -383,14 +386,6 @@ export default function SupplierFeedPage() {
});
};
const handleSelectAllCurrentPage = () => {
const next = new Set(selectedPostIds);
paginatedPosts.forEach((p) => {
if (p.status !== 'published') next.add(p.id);
});
setSelectedPostIds(next);
};
const handleDeselectAll = () => {
setSelectedPostIds(new Set());
};
@ -550,6 +545,88 @@ export default function SupplierFeedPage() {
const publishedCount = posts.filter((p) => p.status === 'published').length;
const discardedCount = posts.filter((p) => p.status === 'discarded').length;
const selectedGroupObj = useMemo(() => {
return monitoredGroups.find((g) => g.jid === selectedGroupFilter);
}, [monitoredGroups, selectedGroupFilter]);
// Selección de página y global
const selectableOnPage = useMemo(() => {
return paginatedPosts.filter((p) => p.status !== 'published' && p.status !== 'discarded');
}, [paginatedPosts]);
const isAllPageSelected = useMemo(() => {
if (selectableOnPage.length === 0) return false;
return selectableOnPage.every((p) => selectedPostIds.has(p.id));
}, [selectableOnPage, selectedPostIds]);
const somePageSelected = useMemo(() => {
return selectableOnPage.some((p) => selectedPostIds.has(p.id));
}, [selectableOnPage, selectedPostIds]);
const handleToggleSelectPage = () => {
setSelectedPostIds((prev) => {
const next = new Set(prev);
if (isAllPageSelected) {
selectableOnPage.forEach((p) => next.delete(p.id));
} else {
selectableOnPage.forEach((p) => next.add(p.id));
}
return next;
});
};
const handleSelectAllFiltered = () => {
const selectableFiltered = filteredPosts.filter((p) => p.status !== 'published' && p.status !== 'discarded');
setSelectedPostIds((prev) => {
const next = new Set(prev);
selectableFiltered.forEach((p) => next.add(p.id));
return next;
});
};
const handleDiscardGroupMessages = async (groupJid: string, groupName: string) => {
const confirmMsg = `¿Deseas descartar y archivar TODOS los mensajes del grupo "${groupName}"? No volverán a aparecer en pendientes.`;
if (!window.confirm(confirmMsg)) return;
setIsLoading(true);
try {
const res = await fetch('/api/whatsapp-feed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'discard-group',
groupJid,
groupName,
}),
});
const data = await res.json();
if (data.success) {
setPosts((prev) =>
prev.map((p) =>
(groupJid && p.channelJid === groupJid) || (groupName && p.channelName === groupName)
? { ...p, status: 'discarded' }
: p
)
);
setSelectedPostIds((prev) => {
const next = new Set(prev);
posts.forEach((p) => {
if ((groupJid && p.channelJid === groupJid) || (groupName && p.channelName === groupName)) {
next.delete(p.id);
}
});
return next;
});
setActionMessage(`🗑️ Se han descartado ${data.discardedCount ?? 'todos los'} mensajes de "${groupName}".`);
}
} catch (e) {
console.error(e);
setActionMessage('❌ Error al descartar mensajes del grupo.');
} finally {
setIsLoading(false);
}
};
const handleOpenEdit = (post: SupplierPost) => {
setEditingPost(post);
const prodName = post.extractedProduct?.name || 'Artículo Urbano';
@ -711,11 +788,14 @@ export default function SupplierFeedPage() {
className="bg-neutral-950 border border-neutral-800 text-neutral-300 text-xs rounded-xl px-3 py-2 outline-hidden focus:border-purple-500 max-w-[220px] truncate"
>
<option value="all">Todos los Grupos ({monitoredGroups.length})</option>
{monitoredGroups.map((g) => (
<option key={g.id || g.jid} value={g.jid}>
{g.name}
</option>
))}
{monitoredGroups.map((g) => {
const isInactive = g.hasLeft || g.isStillMember === false;
return (
<option key={g.id || g.jid} value={g.jid}>
{isInactive ? `⚠️ ${g.name} (Saliste del grupo)` : g.name}
</option>
);
})}
</select>
</div>
</div>
@ -752,38 +832,134 @@ export default function SupplierFeedPage() {
})}
</div>
{/* Bulk Action Bar */}
{/* Aviso Inteligente si el usuario seleccionó un grupo del que ya salió */}
{selectedGroupObj && (selectedGroupObj.hasLeft || selectedGroupObj.isStillMember === false) && (
<div className="bg-amber-950/40 border border-amber-500/50 rounded-2xl p-4 flex items-center justify-between gap-4 text-xs animate-in slide-in-from-top-2 backdrop-blur-md">
<div className="flex items-center gap-3 text-amber-200">
<AlertTriangle className="w-5 h-5 text-amber-400 shrink-0" />
<div>
<p className="font-bold text-sm text-amber-300">Ya no perteneces a este grupo en WhatsApp</p>
<p className="text-neutral-300 text-[11px] mt-0.5">
Saliste de "{selectedGroupObj.name}". Sus mensajes ya no se actualizarán y puedes descartarlos todos con un solo clic para limpiar tu lista.
</p>
</div>
</div>
<button
type="button"
onClick={() => handleDiscardGroupMessages(selectedGroupObj.jid, selectedGroupObj.name)}
disabled={isLoading}
className="shrink-0 bg-rose-600 hover:bg-rose-500 text-white font-bold px-4 py-2 rounded-xl cursor-pointer transition-all shadow-md shadow-rose-950/40 flex items-center gap-1.5"
>
<Trash2 className="w-4 h-4" />
<span>Descartar Todo este Grupo</span>
</button>
</div>
)}
{/* Barra de Selección de Página y Acciones Rápidas */}
<div className="bg-neutral-900/80 border border-neutral-800 rounded-2xl p-3 flex items-center justify-between gap-3 flex-wrap backdrop-blur-md shadow-md">
<div className="flex items-center gap-2.5 flex-wrap">
{/* Botón Seleccionar Página Actual */}
<button
type="button"
onClick={handleToggleSelectPage}
disabled={selectableOnPage.length === 0}
className={`flex items-center gap-2 text-xs font-bold px-3.5 py-2 rounded-xl border transition-all cursor-pointer ${
isAllPageSelected
? 'bg-purple-600 text-white border-purple-400 shadow-md shadow-purple-900/30 scale-102'
: somePageSelected
? 'bg-purple-950/80 text-purple-200 border-purple-500/50'
: 'bg-neutral-950 text-neutral-300 border-neutral-800 hover:border-neutral-700 hover:text-white'
}`}
title="Selecciona o deselecciona todos los mensajes pendientes de la página activa"
>
{isAllPageSelected ? (
<CheckSquare className="w-4 h-4 text-white" />
) : somePageSelected ? (
<MinusSquare className="w-4 h-4 text-purple-400" />
) : (
<Square className="w-4 h-4 text-neutral-400" />
)}
<span>
{isAllPageSelected
? `Deseleccionar Página Actual (${selectableOnPage.length})`
: `Seleccionar Página Actual (${selectableOnPage.length})`}
</span>
</button>
{/* Botón Seleccionar Todo el Filtro */}
{filteredPosts.length > selectableOnPage.length && (
<button
type="button"
onClick={handleSelectAllFiltered}
className="text-xs font-bold text-neutral-400 hover:text-purple-300 px-3 py-2 rounded-xl border border-neutral-800 hover:border-purple-500/50 bg-neutral-950 transition-all cursor-pointer flex items-center gap-1.5"
title="Seleccionar todos los mensajes pendientes bajo los filtros actuales"
>
<CheckSquare className="w-3.5 h-3.5 text-purple-400" />
<span>
Seleccionar Todo el Filtro (
{filteredPosts.filter((p) => p.status !== 'published' && p.status !== 'discarded').length}
)
</span>
</button>
)}
{selectedPostIds.size > 0 && (
<span className="text-xs text-purple-300 font-bold bg-purple-950/70 px-3 py-1.5 rounded-xl border border-purple-800/50 shadow-xs">
{selectedPostIds.size} {selectedPostIds.size === 1 ? 'seleccionado' : 'seleccionados'} en total
</span>
)}
</div>
{/* Botón para descartar todo el grupo seleccionado */}
{selectedGroupFilter !== 'all' && selectedGroupObj && (
<button
type="button"
onClick={() => handleDiscardGroupMessages(selectedGroupObj.jid, selectedGroupObj.name)}
disabled={isLoading}
className="text-xs text-rose-400 hover:text-white hover:bg-rose-950/80 px-3.5 py-2 rounded-xl border border-rose-900/60 bg-neutral-950 transition-all cursor-pointer flex items-center gap-1.5 ml-auto shadow-xs"
>
<Trash2 className="w-3.5 h-3.5" />
<span>Descartar Todo "{selectedGroupObj.name.slice(0, 20)}..."</span>
</button>
)}
</div>
{/* Bulk Action Bar (Visible cuando hay mensajes seleccionados) */}
{selectedPostIds.size > 0 && (
<div className="bg-purple-900/40 border border-purple-500/50 rounded-2xl p-4 flex items-center justify-between gap-4 animate-in slide-in-from-top-2">
<div className="bg-purple-900/50 border border-purple-500/60 rounded-2xl p-4 flex items-center justify-between gap-4 animate-in slide-in-from-top-2 shadow-xl shadow-purple-950/40">
<div className="flex items-center gap-3">
<CheckSquare className="w-5 h-5 text-purple-400" />
<span className="text-xs text-purple-200 font-bold">
<span className="text-xs text-purple-100 font-bold">
{selectedPostIds.size} {selectedPostIds.size === 1 ? 'mensaje seleccionado' : 'mensajes seleccionados'}
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setSelectedPostIds(new Set())}
className="text-xs text-neutral-400 hover:text-white px-3 py-1.5 cursor-pointer"
className="text-xs text-neutral-300 hover:text-white px-3 py-2 cursor-pointer bg-neutral-950/70 rounded-xl border border-neutral-800 hover:border-neutral-700 transition-all"
>
Deseleccionar
Deseleccionar Todo
</button>
<button
type="button"
onClick={handleBulkDiscard}
disabled={isLoading}
className="bg-neutral-800 hover:bg-rose-950/80 border border-neutral-700 hover:border-rose-800 text-neutral-300 hover:text-rose-200 font-bold text-xs px-3.5 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer transition-all"
className="bg-neutral-900 hover:bg-rose-950/90 border border-neutral-700 hover:border-rose-700 text-neutral-200 hover:text-rose-100 font-bold text-xs px-4 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer transition-all shadow"
>
<Trash2 className="w-4 h-4 text-rose-400" />
<span>Descartar Seleccionados</span>
<span>Descartar Seleccionados ({selectedPostIds.size})</span>
</button>
<button
type="button"
onClick={handleBulkPublish}
disabled={isLoading}
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-900/40 cursor-pointer"
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2 rounded-xl flex items-center gap-1.5 shadow-lg shadow-purple-900/40 cursor-pointer transition-all"
>
<ShoppingBag className="w-4 h-4" />
<span>Publicar Seleccionados en Tienda</span>
<span>Publicar Seleccionados ({selectedPostIds.size})</span>
</button>
</div>
</div>

View File

@ -91,23 +91,32 @@ export async function GET(request: Request) {
console.warn('Error resolviendo chats de WhatsApp:', e);
}
// 3. Determinar JIDs objetivo a consultar
// 3. Determinar JIDs objetivo a consultar (excluyendo grupos de los que salimos o inactivos)
const leftJids = new Set(
localGroups.filter((g) => g.hasLeft || g.isStillMember === false || !g.isMonitored).map((g) => g.jid)
);
let targetJids: string[] = [];
if (filterJid && filterJid !== 'all') {
targetJids = [filterJid];
} else {
const monitored = localGroups.filter((g) => g.isMonitored).map((g) => g.jid);
const monitored = localGroups.filter((g) => g.isMonitored && !g.hasLeft && g.isStillMember !== false).map((g) => g.jid);
if (monitored.length > 0) {
targetJids = monitored;
} else if (liveGroups.length > 0) {
targetJids = liveGroups.map((g: any) => g.remoteJid || g.id);
targetJids = liveGroups
.map((g: any) => g.remoteJid || g.id)
.filter((jid: string) => !leftJids.has(jid));
} else {
targetJids = Array.from(groupsMap.keys());
targetJids = Array.from(groupsMap.keys()).filter((jid: string) => !leftJids.has(jid));
}
}
if (!fetchLive || targetJids.length === 0) {
const filteredSaved = savedPosts.filter((p) => {
// Si el mensaje es de un grupo del que salimos y estamos en pendientes, no mostrarlo
if (p.channelJid && leftJids.has(p.channelJid) && statusFilter === 'pending') return false;
if (statusFilter === 'pending') return p.status === 'pending_review';
if (statusFilter === 'published') return p.status === 'published' || p.status === 'published_store';
if (statusFilter === 'discarded') return p.status === 'discarded';
@ -484,9 +493,15 @@ export async function POST(request: Request) {
}
if (action === 'bulk-discard' && Array.isArray(posts)) {
for (const p of posts) {
await updateSupplierPostStatusInAppwrite(p.id, 'discarded');
dataService.updateSupplierPostStatus(p.id, 'discarded');
const BATCH_SIZE = 10;
for (let i = 0; i < posts.length; i += BATCH_SIZE) {
const batch = posts.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(async (p) => {
await updateSupplierPostStatusInAppwrite(p.id, 'discarded');
dataService.updateSupplierPostStatus(p.id, 'discarded');
})
);
}
return NextResponse.json({
success: true,
@ -494,6 +509,37 @@ export async function POST(request: Request) {
});
}
if (action === 'discard-group' && (body.groupJid || body.groupName)) {
const { groupJid, groupName } = body;
const allSaved = await getSupplierPostsFromAppwrite();
const matchingPosts = allSaved.filter(
(p: SupplierPost) =>
(groupJid && p.channelJid === groupJid) ||
(groupName && (p.channelName === groupName || p.supplierName === groupName))
);
const BATCH_SIZE = 10;
for (let i = 0; i < matchingPosts.length; i += BATCH_SIZE) {
const batch = matchingPosts.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(async (p: SupplierPost) => {
await updateSupplierPostStatusInAppwrite(p.id, 'discarded');
dataService.updateSupplierPostStatus(p.id, 'discarded');
})
);
}
if (groupJid) {
dataService.toggleGroupMonitoring(groupJid, false);
}
return NextResponse.json({
success: true,
message: `¡Se han descartado y archivado ${matchingPosts.length} mensajes del grupo!`,
discardedCount: matchingPosts.length,
});
}
if (action === 'market-research' && post) {
const img = post.rawImages && post.rawImages[0];
const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice);

View File

@ -40,28 +40,57 @@ export async function GET(request: Request) {
);
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
const merged: MonitoredWhatsAppGroup[] = await Promise.all(
liveGroups.map(async (lg: any) => {
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
let isStillMember = true;
let participants = undefined;
// 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;
try {
const infoRes = await fetch(
`${EVOLUTION_API_URL}/group/findGroupInfos/${EVOLUTION_INSTANCE_NAME}?groupJid=${jid}`,
{
headers: { apikey: EVOLUTION_API_KEY },
signal: AbortSignal.timeout(3500),
}
);
if (infoRes.status === 404 || infoRes.status === 401 || infoRes.status === 403) {
isStillMember = false;
} else if (infoRes.ok) {
const infoData = await infoRes.json();
if (Array.isArray(infoData.participants)) {
participants = infoData.participants.length;
}
}
} catch {
// Si falla por timeout, mantener estado previo
}
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
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 && isStillMember)
: (isKnownSupplierGroup && isStillMember);
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: isStillMember ? isMonitored : false,
isStillMember,
hasLeft: !isStillMember,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: participants ?? lg.unreadCount,
notes: isStillMember ? (existing?.notes || '') : 'Saliste de este grupo en WhatsApp',
};
})
);
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
@ -70,6 +99,9 @@ export async function GET(request: Request) {
}
});
// Guardar estado actualizado en dataService
dataService.saveMonitoredGroups(merged);
return NextResponse.json({
success: true,
isLive: true,
@ -95,9 +127,9 @@ export async function POST(request: Request) {
try {
const body = await request.json();
if (body.action === 'toggle') {
if (body.action === 'toggle' || body.action === 'deactivate-group') {
const { idOrJid, isMonitored } = body;
const updated = dataService.toggleGroupMonitoring(idOrJid, isMonitored);
const updated = dataService.toggleGroupMonitoring(idOrJid, isMonitored !== undefined ? isMonitored : false);
return NextResponse.json({ success: true, group: updated });
}

View File

@ -224,6 +224,8 @@ export interface MonitoredWhatsAppGroup {
autoPublish: boolean;
participantsCount?: number;
lastMessageAt?: string;
isStillMember?: boolean;
hasLeft?: boolean;
notes?: string;
}

View File

@ -37,18 +37,18 @@ def send_request(endpoint, payload):
def send_update():
message_text = (
"🚀 *DripZone Sneakers - APK Oficial Actualizado*\n\n"
"✨ *Acceso Completo a Miles de Mensajes & Paginación Avanzada:*\n"
"📊 *Eliminación del Tope de 60:* Se eliminó la restricción que limitaba la vista a 60 mensajes. "
"Ahora la plataforma escanea en paralelo todas las páginas del historial de los grupos de WhatsApp, "
"cargando cientos o miles de mensajes sin recortes.\n"
"📖 *Paginación Dinámica:* Puedes seleccionar ver 12, 24, 48, 96 o 150 productos por página, "
"con contador total exacto y selector para saltar a cualquier página instantáneamente.\n"
"⚡ *Escaneo Todo el Historial:* Botón especial para explorar a profundidad (+1,000 mensajes) en segundos.\n"
"🆔 *Control de Duplicados & Sanitización:* Se omiten automáticamente los ya publicados y descartados, "
"con privacidad total comercial (sin nombres de grupos ni costos).\n"
"✨ *Identificación y Categorización Inteligente Multi-Producto:*\n"
"👟 *Zapatillas & Sneakers:* Detección de siluetas, modelos urbanos y deportivos.\n"
"🩴 *Chanclas & Sandalias:* Clasificación precisa de chanclas, slides, crocs y pantuflas con textos de confort.\n"
"👜 *Bolsos & Carteras:* Reconocimiento de bolsos de mano, tote bags y carteras con copys persuasivos de lujo.\n"
"🎒 *Maletines & Morrales:* Detección de maletines para portátil, mochilas y tulas deportivas.\n"
"👝 *Carrieles & Bandoleras:* Identificación de carrieles tradicionales, bandoleras, riñoneras, pecheras y crossbody.\n"
"🧢 *Gorras & Accesorios:* Categorización de gorras, caps, sombreros, billeteras y correas.\n"
"🏷️ *Filtro Rápido en Feed Administrativo:* Nueva barra de categorías con conteo en tiempo real y selector en el modal de edición.\n"
"🛍️ *Catálogo de Clientes Adaptado:* Los clientes ahora pueden filtrar por categoría directamente en la tienda.\n"
"🌐 *VPS Desplegado:* Contenedor en producción activo en `https://dripzone.almeira.com.co`.\n\n"
f"📲 *Descarga directa del APK:* {APK_URL}\n\n"
"Descarga e instala el archivo para disfrutar de la experiencia completa."
"Instala o actualiza el archivo en tu dispositivo Android para disfrutar de estas nuevas funciones."
)
print(f"📤 [1/2] Enviando mensaje de texto a WhatsApp (+{TARGET_PHONE})...")

View File

@ -40,28 +40,57 @@ export async function GET(request: Request) {
);
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = liveGroups.map((lg: any) => {
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
const merged: MonitoredWhatsAppGroup[] = await Promise.all(
liveGroups.map(async (lg: any) => {
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
let isStillMember = true;
let participants = undefined;
// 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;
try {
const infoRes = await fetch(
`${EVOLUTION_API_URL}/group/findGroupInfos/${EVOLUTION_INSTANCE_NAME}?groupJid=${jid}`,
{
headers: { apikey: EVOLUTION_API_KEY },
signal: AbortSignal.timeout(3500),
}
);
if (infoRes.status === 404 || infoRes.status === 401 || infoRes.status === 403) {
isStillMember = false;
} else if (infoRes.ok) {
const infoData = await infoRes.json();
if (Array.isArray(infoData.participants)) {
participants = infoData.participants.length;
}
}
} catch {
// Si falla por timeout, mantener estado previo
}
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: lg.unreadCount !== undefined ? lg.unreadCount : undefined,
notes: existing?.notes || '',
};
});
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 && isStillMember)
: (isKnownSupplierGroup && isStillMember);
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
jid,
name,
isMonitored: isStillMember ? isMonitored : false,
isStillMember,
hasLeft: !isStillMember,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
participantsCount: participants ?? lg.unreadCount,
notes: isStillMember ? (existing?.notes || '') : 'Saliste de este grupo en WhatsApp',
};
})
);
// Asegurar que los locales sigan presentes
localGroups.forEach((lg) => {
@ -70,6 +99,9 @@ export async function GET(request: Request) {
}
});
// Guardar estado actualizado en dataService
dataService.saveMonitoredGroups(merged);
return NextResponse.json({
success: true,
isLive: true,
@ -95,9 +127,9 @@ export async function POST(request: Request) {
try {
const body = await request.json();
if (body.action === 'toggle') {
if (body.action === 'toggle' || body.action === 'deactivate-group') {
const { idOrJid, isMonitored } = body;
const updated = dataService.toggleGroupMonitoring(idOrJid, isMonitored);
const updated = dataService.toggleGroupMonitoring(idOrJid, isMonitored !== undefined ? isMonitored : false);
return NextResponse.json({ success: true, group: updated });
}

View File

@ -209,5 +209,7 @@ export interface MonitoredWhatsAppGroup {
autoPublish: boolean;
participantsCount?: number;
lastMessageAt?: string;
isStillMember?: boolean;
hasLeft?: boolean;
notes?: string;
}