feat: seleccion masiva de pagina activa y exclusion/descarte inteligente de grupos inactivos o abandonados
This commit is contained in:
parent
eb4f1affd4
commit
c7abbd2d54
@ -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
|
||||
"""
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -224,6 +224,8 @@ export interface MonitoredWhatsAppGroup {
|
||||
autoPublish: boolean;
|
||||
participantsCount?: number;
|
||||
lastMessageAt?: string;
|
||||
isStillMember?: boolean;
|
||||
hasLeft?: boolean;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -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})...")
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@ -209,5 +209,7 @@ export interface MonitoredWhatsAppGroup {
|
||||
autoPublish: boolean;
|
||||
participantsCount?: number;
|
||||
lastMessageAt?: string;
|
||||
isStillMember?: boolean;
|
||||
hasLeft?: boolean;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user