Files
gallery/dashboard.html
T
2026-05-30 00:31:21 -06:00

1050 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Panel de Campañas</title>
<!-- Tailwind CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Supabase JS -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
</head>
<body class="bg-zinc-950 text-white min-h-screen">
<!-- ========================= -->
<!-- LOGIN SCREEN -->
<!-- ========================= -->
<div id="loginScreen" class="flex items-center justify-center min-h-screen p-6">
<div class="w-full max-w-md bg-zinc-900 border border-zinc-800 rounded-2xl shadow-lg p-6">
<h1 class="text-2xl font-bold mb-2">🏠 Panel de Campañas</h1>
<p class="text-zinc-400 text-sm mb-6">
Inicia sesión para gestionar campañas, sesiones y archivos.
</p>
<div class="space-y-4">
<input id="loginEmail" type="email" placeholder="Correo"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
<input id="loginPassword" type="password" placeholder="Contraseña"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
<button id="loginBtn"
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
Entrar
</button>
<div id="loginError" class="text-red-400 text-sm"></div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- DASHBOARD -->
<!-- ========================= -->
<div id="dashboard" class="hidden min-h-screen">
<!-- Top Bar -->
<div class="flex items-center justify-between p-4 border-b border-zinc-800 bg-zinc-950 sticky top-0 z-50">
<div>
<h2 class="font-bold text-lg">Panel</h2>
<p id="userEmail" class="text-xs text-zinc-400"></p>
</div>
<button id="logoutBtn"
class="bg-zinc-800 hover:bg-zinc-700 px-4 py-2 rounded-xl text-sm font-semibold">
Cerrar Sesión
</button>
</div>
<!-- Tabs -->
<div class="p-4">
<div class="grid grid-cols-3 gap-2 bg-zinc-900 border border-zinc-800 rounded-2xl p-2">
<button class="tabBtn bg-indigo-600 rounded-xl py-2 text-sm font-semibold" data-tab="campaignsTab">
Campañas
</button>
<button class="tabBtn bg-zinc-800 rounded-xl py-2 text-sm font-semibold" data-tab="sessionsTab">
Sesiones
</button>
<button class="tabBtn bg-zinc-800 rounded-xl py-2 text-sm font-semibold" data-tab="mediaTab">
Archivos
</button>
</div>
</div>
<!-- ========================= -->
<!-- CAMPAIGNS TAB -->
<!-- ========================= -->
<div id="campaignsTab" class="tabContent p-4 space-y-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<h3 class="font-bold text-lg mb-3">Crear Campaña</h3>
<div class="space-y-3">
<input id="newCampaignName" placeholder="Nombre de campaña"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none"/>
<textarea id="newCampaignNotes" placeholder="Instrucciones para IA (opcional)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none"></textarea>
<button id="createCampaignBtn"
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
Crear Campaña
</button>
<div id="campaignCreateMsg" class="text-sm text-zinc-400"></div>
</div>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-bold text-lg">Lista de Campañas</h3>
<button id="toggleInactiveBtn" class="text-xs bg-zinc-800 hover:bg-zinc-700 px-3 py-1.5 rounded-lg border border-zinc-700 transition">
Mostrar Inactivas
</button>
</div>
<div id="campaignList" class="space-y-3"></div>
</div>
</div>
<!-- ========================= -->
<!-- SESSIONS TAB -->
<!-- ========================= -->
<div id="sessionsTab" class="tabContent hidden p-4 space-y-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<h3 class="font-bold text-lg mb-3">Sesiones</h3>
<select id="sessionsCampaignFilter"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3">
<option value="">Todas las campañas</option>
</select>
<button id="refreshSessionsBtn"
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold">
Actualizar Sesiones
</button>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-bold text-lg">Lista de Sesiones</h3>
</div>
<input id="sessionSearch" placeholder="Buscar por teléfono, nombre o resumen..."
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500 mb-3"/>
<div id="sessionList" class="space-y-3"></div>
</div>
</div>
<!-- ========================= -->
<!-- MEDIA TAB -->
<!-- ========================= -->
<div id="mediaTab" class="tabContent hidden p-4 space-y-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<h3 class="font-bold text-lg mb-3">Subir Archivo</h3>
<select id="mediaCampaignSelect"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3">
<option value="">Seleccionar campaña</option>
</select>
<input id="mediaCategory" placeholder="Categoría (ej. cocina, frente, baño)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-3 focus:ring-2 focus:ring-indigo-500"/>
<input id="mediaType" placeholder="Tipo (ej. foto, video)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-4 focus:ring-2 focus:ring-indigo-500"/>
<div class="mb-2 text-xs font-semibold text-zinc-400 uppercase tracking-wider">Fuente de archivo</div>
<input id="mediaFile" type="file" accept="image/*,video/*"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
<div class="py-2 flex items-center justify-center">
<div class="h-px bg-zinc-800 w-full"></div>
<span class="px-3 text-xs text-zinc-500 font-bold mb-1">O</span>
<div class="h-px bg-zinc-800 w-full"></div>
</div>
<input id="mediaUrl" type="url" placeholder="Pegar URL remota (ej. Google Drive, Imgur)"
class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none mb-5 focus:ring-2 focus:ring-indigo-500"/>
<button id="uploadMediaBtn"
class="w-full bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold text-white">
Guardar Archivo
</button>
<div id="uploadMsg" class="text-sm text-zinc-400 mt-2"></div>
</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl p-4">
<h3 class="font-bold text-lg mb-3">Lista de Archivos</h3>
<div id="mediaList" class="columns-2 md:columns-3 lg:columns-4 gap-3 space-y-3"></div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- EDIT CAMPAIGN MODAL -->
<!-- ========================= -->
<div id="editCampaignModal" class="hidden fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl max-w-lg w-full p-6 shadow-2xl">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Editar Campaña</h2>
<button id="closeModalBtn" class="text-zinc-500 hover:text-white transition text-2xl">&times;</button>
</div>
<div class="space-y-4">
<input type="hidden" id="editCampaignId" />
<div>
<label class="block text-xs font-semibold text-zinc-400 mb-1">Nombre</label>
<input id="editCampaignName" class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500"/>
</div>
<div>
<label class="block text-xs font-semibold text-zinc-400 mb-1">Palabras clave (separadas por coma)</label>
<input id="editCampaignKeywords" class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none focus:ring-2 focus:ring-indigo-500" placeholder="ej. casa, piscina, jardín"/>
</div>
<div>
<label class="block text-xs font-semibold text-zinc-400 mb-1">Instrucciones para IA</label>
<textarea id="editCampaignPrompt" class="w-full bg-zinc-950 border border-zinc-800 rounded-xl p-3 outline-none h-24 focus:ring-2 focus:ring-indigo-500" placeholder="Instrucciones para IA..."></textarea>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="editCampaignWelcomePhotos" class="w-4 h-4 rounded border-zinc-800 bg-zinc-950 text-indigo-600 focus:ring-indigo-500"/>
<label for="editCampaignWelcomePhotos" class="text-sm font-semibold text-zinc-300">Requerir Fotos de Bienvenida</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="editCampaignActive" class="w-4 h-4 rounded border-zinc-800 bg-zinc-950 text-indigo-600 focus:ring-indigo-500"/>
<label for="editCampaignActive" class="text-sm font-semibold text-zinc-300">Campaña Activa</label>
</div>
<div class="mt-6 flex gap-3">
<button id="saveCampaignBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-500 transition rounded-xl p-3 font-semibold text-white">Guardar Cambios</button>
<button id="cancelModalBtn" class="flex-1 bg-zinc-800 hover:bg-zinc-700 transition rounded-xl p-3 font-semibold text-white">Cancelar</button>
</div>
<div id="editModalMsg" class="text-sm text-center"></div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- DELETE MEDIA MODAL -->
<!-- ========================= -->
<div id="deleteMediaModal" class="hidden fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl max-w-sm w-full p-6 shadow-2xl text-center">
<h2 class="text-xl font-bold mb-3">¿Eliminar Archivo?</h2>
<p class="text-sm text-zinc-400 mb-6">¿Estás seguro de que quieres eliminar esta foto? Esta acción no se puede deshacer.</p>
<input type="hidden" id="deleteMediaId" />
<input type="hidden" id="deleteMediaUrl" />
<div class="flex gap-3">
<button id="confirmDeleteMediaBtn" class="flex-1 bg-red-600 hover:bg-red-500 transition rounded-xl p-3 font-semibold text-white">Eliminar</button>
<button id="cancelDeleteMediaBtn" class="flex-1 bg-zinc-800 hover:bg-zinc-700 transition rounded-xl p-3 font-semibold text-white">Cancelar</button>
</div>
<div id="deleteMediaMsg" class="text-sm mt-3"></div>
</div>
</div>
<!-- ========================= -->
<!-- BLOCK SESSION MODAL -->
<!-- ========================= -->
<div id="blockSessionModal" class="hidden fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-2xl max-w-sm w-full p-6 shadow-2xl text-center">
<h2 class="text-xl font-bold mb-3">¿Bloquear Sesión?</h2>
<p class="text-sm text-zinc-400 mb-6">¿Estás seguro de que quieres bloquear esta sesión? El número de teléfono será bloqueado.</p>
<input type="hidden" id="blockSessionId" />
<input type="hidden" id="blockSessionPhone" />
<div class="flex gap-3">
<button id="confirmBlockSessionBtn" class="flex-1 bg-red-600 hover:bg-red-500 transition rounded-xl p-3 font-semibold text-white">Bloquear</button>
<button id="cancelBlockSessionBtn" class="flex-1 bg-zinc-800 hover:bg-zinc-700 transition rounded-xl p-3 font-semibold text-white">Cancelar</button>
</div>
<div id="blockSessionMsg" class="text-sm mt-3"></div>
</div>
</div>
<script>
// =============================
// CONFIG
// =============================
const SUPABASE_URL = "https://rehavit.beroth.moe/supabase";
const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc2NTU4OTc2LCJleHAiOjE5MzQyMzg5NzZ9.eFvvIQUstht8TxNffqAcqfmgS7W2JMZsDxkV41XBPOA";
// This bucket must exist in Supabase Storage
const STORAGE_BUCKET = "campaign-media";
const supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
let allCampaigns = [];
let showInactive = false;
let campaignsWithMedia = new Set();
// =============================
// UI HELPERS
// =============================
function show(el) { el.classList.remove("hidden"); }
function hide(el) { el.classList.add("hidden"); }
function setMessage(el, msg, isError=false) {
el.innerText = msg;
el.className = isError ? "text-sm text-red-400" : "text-sm text-zinc-400";
}
function formatDate(d) {
try {
return new Date(d).toLocaleString();
} catch {
return d;
}
}
// =============================
// AUTH
// =============================
async function login() {
const email = document.getElementById("loginEmail").value.trim();
const password = document.getElementById("loginPassword").value.trim();
const loginError = document.getElementById("loginError");
loginError.innerText = "";
const { data, error } = await supabaseClient.auth.signInWithPassword({
email,
password
});
if (error) {
loginError.innerText = error.message;
return;
}
await refreshUI();
}
async function logout() {
await supabaseClient.auth.signOut();
await refreshUI();
}
async function refreshUI() {
const { data: { session } } = await supabaseClient.auth.getSession();
const loginScreen = document.getElementById("loginScreen");
const dashboard = document.getElementById("dashboard");
if (!session) {
show(loginScreen);
hide(dashboard);
return;
}
hide(loginScreen);
show(dashboard);
document.getElementById("userEmail").innerText = session.user.email;
await loadCampaigns();
await loadSessions();
await loadMedia();
}
// =============================
// TABS
// =============================
function setupTabs() {
const tabButtons = document.querySelectorAll(".tabBtn");
const tabContents = document.querySelectorAll(".tabContent");
tabButtons.forEach(btn => {
btn.addEventListener("click", () => {
tabButtons.forEach(b => b.classList.remove("bg-indigo-600"));
tabButtons.forEach(b => b.classList.add("bg-zinc-800"));
btn.classList.remove("bg-zinc-800");
btn.classList.add("bg-indigo-600");
tabContents.forEach(tab => hide(tab));
show(document.getElementById(btn.dataset.tab));
});
});
}
// =============================
// CAMPAIGNS CRUD
// =============================
async function loadCampaigns() {
const { data, error } = await supabaseClient
.from("campaigns")
.select("*")
.order("created_at", { ascending: false });
if (error) {
document.getElementById("campaignList").innerHTML = `<div class="text-red-400 text-sm">Error loading campaigns: ${error.message}</div>`;
return;
}
const { data: mediaData } = await supabaseClient
.from("media")
.select("campaign_id");
campaignsWithMedia = new Set(mediaData?.map(m => m.campaign_id) || []);
allCampaigns = data || [];
// Populate filters
const sessionsFilter = document.getElementById("sessionsCampaignFilter");
const mediaSelect = document.getElementById("mediaCampaignSelect");
sessionsFilter.innerHTML = `<option value="">All campaigns</option>`;
mediaSelect.innerHTML = `<option value="">Select campaign</option>`;
allCampaigns.forEach(c => {
const opt1 = document.createElement("option");
opt1.value = c.id;
opt1.innerText = c.name;
sessionsFilter.appendChild(opt1);
const opt2 = document.createElement("option");
opt2.value = c.id;
opt2.innerText = c.name;
mediaSelect.appendChild(opt2);
});
renderCampaigns();
await loadMedia();
}
function renderCampaigns() {
const list = document.getElementById("campaignList");
list.innerHTML = "";
const filtered = showInactive ? allCampaigns : allCampaigns.filter(c => c.active !== false);
filtered.forEach(c => {
const card = document.createElement("div");
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl p-4";
const statusColor = c.active === false ? "bg-red-500/20 text-red-500" : "bg-green-500/20 text-green-500";
const statusText = c.active === false ? "Inactive" : "Active";
const keywordsStr = Array.isArray(c.keywords) ? c.keywords.join(", ") : "";
card.innerHTML = `
<div class="flex justify-between items-start gap-3">
<div class="flex-1">
<div class="flex items-center gap-2">
<div class="font-bold text-lg">${c.name}</div>
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${statusColor}">${statusText}</span>
</div>
${c.send_welcome_photos ? `<div class="text-xs text-indigo-400 font-semibold mt-1">📸 Requires Welcome Photos</div>` : ''}
${keywordsStr ? `<div class="text-xs text-zinc-400 mt-2"><span class="font-semibold text-zinc-300">Keywords:</span> ${keywordsStr}</div>` : ''}
${c.promt ? `<div class="text-xs text-zinc-400 mt-1 line-clamp-2"><span class="font-semibold text-zinc-300">Prompt:</span> ${c.promt}</div>` : ''}
<div class="text-xs text-zinc-600 mt-3">Created: ${formatDate(c.created_at)} • ID: ${c.id.split('-')[0]}...</div>
</div>
<div class="flex flex-col gap-2 shrink-0">
${campaignsWithMedia.has(c.id) ? `
<a href="/gallery?campaign_id=${c.id}" target="_blank"
class="bg-indigo-600 hover:bg-indigo-500 text-center text-xs px-3 py-2 rounded-xl transition text-white font-semibold">
Gallery
</a>` : ''}
<button class="editBtn bg-zinc-800 hover:bg-zinc-700 text-xs px-3 py-2 rounded-xl transition"
data-id="${c.id}">
Edit
</button>
<button class="deleteBtn bg-red-600/20 text-red-500 hover:bg-red-600/40 text-xs px-3 py-2 rounded-xl transition"
data-id="${c.id}">
Delete
</button>
</div>
</div>
`;
list.appendChild(card);
});
document.querySelectorAll(".deleteBtn").forEach(btn => {
btn.addEventListener("click", async () => {
const id = btn.dataset.id;
if (!confirm("Delete this campaign?")) return;
await deleteCampaign(id);
});
});
document.querySelectorAll(".editBtn").forEach(btn => {
btn.addEventListener("click", () => {
const id = btn.dataset.id;
openEditModal(id);
});
});
}
function openEditModal(id) {
const c = allCampaigns.find(camp => camp.id === id);
if (!c) return;
document.getElementById("editCampaignId").value = c.id;
document.getElementById("editCampaignName").value = c.name || "";
document.getElementById("editCampaignKeywords").value = Array.isArray(c.keywords) ? c.keywords.join(", ") : "";
document.getElementById("editCampaignPrompt").value = c.promt || "";
document.getElementById("editCampaignWelcomePhotos").checked = c.send_welcome_photos === true;
document.getElementById("editCampaignActive").checked = c.active !== false;
setMessage(document.getElementById("editModalMsg"), "");
show(document.getElementById("editCampaignModal"));
}
function closeEditModal() {
hide(document.getElementById("editCampaignModal"));
}
async function saveEditedCampaign() {
const id = document.getElementById("editCampaignId").value;
const name = document.getElementById("editCampaignName").value.trim();
const keywordsRaw = document.getElementById("editCampaignKeywords").value.trim();
const promt = document.getElementById("editCampaignPrompt").value.trim();
const send_welcome_photos = document.getElementById("editCampaignWelcomePhotos").checked;
const active = document.getElementById("editCampaignActive").checked;
const msg = document.getElementById("editModalMsg");
if (!name) {
setMessage(msg, "Name is required.", true);
return;
}
// Convert comma-separated string to array
const keywords = keywordsRaw ? keywordsRaw.split(",").map(k => k.trim()).filter(k => k.length > 0) : [];
setMessage(msg, "Saving...");
const { error } = await supabaseClient
.from("campaigns")
.update({ name, keywords, promt, send_welcome_photos, active })
.eq("id", id);
if (error) {
setMessage(msg, "Error: " + error.message, true);
return;
}
setMessage(msg, "Saved!", false);
closeEditModal();
await loadCampaigns();
}
async function createCampaign() {
const name = document.getElementById("newCampaignName").value.trim();
const promt = document.getElementById("newCampaignNotes").value.trim();
const msg = document.getElementById("campaignCreateMsg");
if (!name) {
setMessage(msg, "Campaign name is required.", true);
return;
}
const { error } = await supabaseClient
.from("campaigns")
.insert([{ name, promt, active: true }]);
if (error) {
setMessage(msg, "Error: " + error.message, true);
return;
}
document.getElementById("newCampaignName").value = "";
document.getElementById("newCampaignNotes").value = "";
setMessage(msg, "Campaign created!");
await loadCampaigns();
}
async function deleteCampaign(id) {
const { error } = await supabaseClient
.from("campaigns")
.delete()
.eq("id", id);
if (error) {
alert("Error deleting campaign: " + error.message);
return;
}
await loadCampaigns();
}
// =============================
// SESSIONS VIEWER
// sessions table must exist
// columns: id, campaign_id, created_at, ip, user_agent, etc
// =============================
let allSessions = [];
async function loadSessions() {
const campaignFilter = document.getElementById("sessionsCampaignFilter").value;
let query = supabaseClient
.from("sessions")
.select("*")
.order("created_at", { ascending: false })
.limit(100);
if (campaignFilter) {
query = query.eq("campaign_id", campaignFilter);
}
const { data, error } = await query;
if (error) {
document.getElementById("sessionList").innerHTML = `<div class="text-red-400 text-sm">Error loading sessions: ${error.message}</div>`;
return;
}
allSessions = data || [];
renderSessions();
}
function renderSessions() {
const searchTerm = document.getElementById("sessionSearch").value.toLowerCase();
const list = document.getElementById("sessionList");
list.innerHTML = "";
let filtered = allSessions;
if (searchTerm) {
filtered = allSessions.filter(s =>
(s.phone && s.phone.toLowerCase().includes(searchTerm)) ||
(s.name && s.name.toLowerCase().includes(searchTerm)) ||
(s.summary && s.summary.toLowerCase().includes(searchTerm))
);
}
if (filtered.length === 0) {
list.innerHTML = `<div class="text-zinc-400 text-sm">${allSessions.length === 0 ? "No sessions found." : "No sessions match the search."}</div>`;
return;
}
filtered.forEach(s => {
const card = document.createElement("div");
card.className = "bg-zinc-950 border border-zinc-800 rounded-2xl p-4";
const statusClass = s.finished ? "bg-green-500/20 text-green-500" : "bg-amber-500/20 text-amber-500";
const statusText = s.finished ? "Finished" : "Active";
const blockedBadge = s.block ? `<span class="text-[10px] px-2 py-0.5 rounded-full font-bold uppercase bg-red-500/20 text-red-500">Blocked</span>` : '';
card.innerHTML = `
<div class="flex justify-between items-start">
<div class="w-full">
<div class="flex justify-between items-center w-full">
<div class="text-sm font-semibold text-zinc-200">
${s.name ? `${s.name} <span class="text-zinc-500 font-normal text-xs ml-1">(${s.phone || 'unknown'})</span>` : `Phone: ${s.phone || "unknown"}`}
</div>
<div class="flex items-center gap-2">
<div class="text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${statusClass}">
${statusText}
</div>
${blockedBadge}
<div class="relative">
<button class="sessionMenuBtn bg-zinc-800 hover:bg-zinc-700 p-1.5 rounded-lg transition" data-id="${s.id}" data-phone="${s.phone}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</button>
<div class="sessionMenu hidden absolute right-0 top-full mt-1 bg-zinc-900 border border-zinc-800 rounded-xl shadow-xl z-10 min-w-[140px] overflow-hidden">
<button class="blockSessionItem w-full text-left px-4 py-2.5 text-sm hover:bg-zinc-800 transition text-red-400" data-id="${s.id}" data-phone="${s.phone}">
${s.block ? 'Desbloquear' : 'Bloquear'}
</button>
<button class="deleteSessionItem w-full text-left px-4 py-2.5 text-sm hover:bg-zinc-800 transition text-red-400 border-t border-zinc-800" data-id="${s.id}" data-phone="${s.phone}">
Eliminar
</button>
</div>
</div>
</div>
</div>
<div class="text-xs text-zinc-400 mt-2"><span class="font-semibold text-zinc-300">Summary:</span> ${s.summary || "No summary yet."}</div>
<div class="text-xs text-zinc-500 mt-3">Campaign: ${s.campaign_id?.split('-')[0] || "N/A"}...</div>
<div class="flex gap-4 mt-1">
<div class="text-xs text-zinc-500">Started: ${formatDate(s.created_at)}</div>
<div class="text-xs text-zinc-500">Last Interaction: ${s.last_interaction ? formatDate(s.last_interaction) : "None"}</div>
</div>
<div class="text-xs text-zinc-600 mt-2">Session ID: ${s.id}</div>
</div>
</div>
`;
list.appendChild(card);
});
list.querySelectorAll(".sessionMenuBtn").forEach(btn => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const menu = btn.nextElementSibling;
const wasHidden = menu.classList.contains("hidden");
document.querySelectorAll(".sessionMenu").forEach(m => m.classList.add("hidden"));
if (wasHidden) menu.classList.remove("hidden");
});
});
list.querySelectorAll(".deleteSessionItem").forEach(btn => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const phone = btn.dataset.phone;
const id = btn.dataset.id;
if (id && phone && phone !== 'unknown') {
deleteSession(id, phone);
}
document.querySelectorAll(".sessionMenu").forEach(m => m.classList.add("hidden"));
});
});
list.querySelectorAll(".blockSessionItem").forEach(btn => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const phone = btn.dataset.phone;
const id = btn.dataset.id;
if (id && phone && phone !== 'unknown') {
if (btn.textContent.trim() === 'Desbloquear') {
unblockSession(id, phone);
} else {
openBlockModal(id, phone);
}
}
document.querySelectorAll(".sessionMenu").forEach(m => m.classList.add("hidden"));
});
});
}
document.addEventListener("click", () => {
document.querySelectorAll(".sessionMenu").forEach(m => m.classList.add("hidden"));
});
document.getElementById("sessionSearch").addEventListener("input", renderSessions);
async function deleteSession(id, phone) {
if (!confirm(`Delete session for ${phone} and all its chat history?`)) return;
const { error: historyError } = await supabaseClient
.from('n8n_chat_histories')
.delete()
.eq('session_id', id);
if (historyError) {
alert('Error deleting chat history: ' + historyError.message);
return;
}
const { error: sessionError } = await supabaseClient
.from('sessions')
.delete()
.eq('id', id);
if (sessionError) {
alert('Error deleting session: ' + sessionError.message);
return;
}
await loadSessions();
}
function openBlockModal(id, phone) {
document.getElementById("blockSessionId").value = id;
document.getElementById("blockSessionPhone").value = phone;
setMessage(document.getElementById("blockSessionMsg"), "");
show(document.getElementById("blockSessionModal"));
}
function closeBlockModal() {
hide(document.getElementById("blockSessionModal"));
}
async function confirmBlockSession() {
const id = document.getElementById("blockSessionId").value;
const phone = document.getElementById("blockSessionPhone").value;
const msg = document.getElementById("blockSessionMsg");
setMessage(msg, "Bloqueando...");
const { error } = await supabaseClient
.from("sessions")
.update({ block: true })
.eq("phone", phone);
if (error) {
setMessage(msg, "Error: " + error.message, true);
return;
}
setMessage(msg, "Sesión bloqueada!", false);
closeBlockModal();
await loadSessions();
}
async function unblockSession(id, phone) {
if (!confirm(`Desbloquear sesión para ${phone}?`)) return;
const { error } = await supabaseClient
.from("sessions")
.update({ block: false })
.eq("phone", phone);
if (error) {
alert("Error: " + error.message);
return;
}
await loadSessions();
}
// =============================
// MEDIA UPLOAD
// media table must exist
// columns: id, campaign_id, url, category, type, created_at
// =============================
async function uploadMedia() {
const campaignId = document.getElementById("mediaCampaignSelect").value;
const category = document.getElementById("mediaCategory").value.trim();
const type = document.getElementById("mediaType").value.trim();
const fileInput = document.getElementById("mediaFile");
const urlInput = document.getElementById("mediaUrl").value.trim();
const msg = document.getElementById("uploadMsg");
if (!campaignId) {
setMessage(msg, "Select a campaign first.", true);
return;
}
const hasFile = fileInput.files && fileInput.files.length > 0;
const hasUrl = !!urlInput;
if (!hasFile && !hasUrl) {
setMessage(msg, "Please select a file OR paste a URL.", true);
return;
}
if (hasFile && hasUrl) {
setMessage(msg, "Please select EITHER a file OR a URL, not both.", true);
return;
}
let finalUrl = "";
if (hasUrl) {
// Validate URL
try {
new URL(urlInput);
finalUrl = urlInput;
} catch (_) {
setMessage(msg, "Please provide a valid URL (include http/https).", true);
return;
}
} else {
const file = fileInput.files[0];
setMessage(msg, "Uploading to bucket...");
const ext = file.name.split(".").pop();
const filePath = `${campaignId}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
const { error: uploadError } = await supabaseClient
.storage
.from(STORAGE_BUCKET)
.upload(filePath, file, { cacheControl: "3600", upsert: false });
if (uploadError) {
setMessage(msg, "Upload error: " + uploadError.message, true);
return;
}
const { data: publicUrlData } = supabaseClient
.storage
.from(STORAGE_BUCKET)
.getPublicUrl(filePath);
finalUrl = publicUrlData.publicUrl;
}
setMessage(msg, "Saving in database...");
// Insert into media table
const { error: insertError } = await supabaseClient
.from("media")
.insert([{
campaign_id: campaignId,
url: finalUrl,
category,
type
}]);
if (insertError) {
setMessage(msg, "DB insert error: " + insertError.message, true);
return;
}
fileInput.value = "";
document.getElementById("mediaUrl").value = "";
document.getElementById("mediaCategory").value = "";
document.getElementById("mediaType").value = "";
setMessage(msg, "Media saved successfully!");
campaignsWithMedia.add(campaignId);
renderCampaigns();
await loadMedia();
}
async function loadMedia() {
const { data, error } = await supabaseClient
.from("media")
.select("*")
.order("created_at", { ascending: false })
.limit(100);
const list = document.getElementById("mediaList");
list.innerHTML = "";
if (error) {
list.innerHTML = `<div class="text-red-400 text-sm">Error loading media: ${error.message}</div>`;
return;
}
if (!data || data.length === 0) {
list.innerHTML = `<div class="text-zinc-400 text-sm">No media uploaded yet.</div>`;
return;
}
// Filter media from active campaigns
const activeCampaignIds = new Set(allCampaigns.filter(c => c.active !== false).map(c => c.id));
const filteredMedia = data.filter(m => activeCampaignIds.has(m.campaign_id));
if (filteredMedia.length === 0) {
list.innerHTML = `<div class="text-zinc-400 text-sm">No media from active campaigns found.</div>`;
return;
}
filteredMedia.forEach(m => {
const campaign = allCampaigns.find(c => c.id === m.campaign_id);
const campaignName = campaign ? campaign.name : "Unknown Campaign";
const card = document.createElement("div");
card.className = "bg-zinc-950 border border-zinc-800 rounded-xl overflow-hidden break-inside-avoid mb-3";
card.innerHTML = `
<div class="relative group">
<img src="${m.url}" class="w-full h-auto block" loading="lazy" />
<button class="deleteMediaBtn absolute top-2 right-2 bg-black/60 hover:bg-black/90 text-white rounded-lg p-1.5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity" data-id="${m.id}" data-url="${m.url}" title="Delete media">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="p-2.5">
<div class="text-[10px] font-bold text-indigo-400 uppercase tracking-tight mb-0.5">${m.category || "photo"}${m.type || "unknown"}</div>
<div class="text-xs font-semibold text-zinc-200 truncate">${campaignName}</div>
<div class="text-[10px] text-zinc-500 mt-1">${formatDate(m.created_at)}</div>
</div>
`;
list.appendChild(card);
});
document.querySelectorAll(".deleteMediaBtn").forEach(btn => {
btn.addEventListener("click", () => {
openDeleteMediaModal(btn.dataset.id, btn.dataset.url);
});
});
}
function openDeleteMediaModal(id, url) {
document.getElementById("deleteMediaId").value = id;
document.getElementById("deleteMediaUrl").value = url;
setMessage(document.getElementById("deleteMediaMsg"), "");
show(document.getElementById("deleteMediaModal"));
}
function closeDeleteMediaModal() {
hide(document.getElementById("deleteMediaModal"));
}
async function confirmDeleteMedia() {
const id = document.getElementById("deleteMediaId").value;
const url = document.getElementById("deleteMediaUrl").value;
const msg = document.getElementById("deleteMediaMsg");
setMessage(msg, "Deleting...");
const { error: dbError } = await supabaseClient
.from("media")
.delete()
.eq("id", id);
if (dbError) {
setMessage(msg, "Error: " + dbError.message, true);
return;
}
if (url.includes(STORAGE_BUCKET)) {
try {
const parts = url.split(STORAGE_BUCKET + "/");
if (parts.length >= 2) {
const filePath = parts[1];
await supabaseClient.storage.from(STORAGE_BUCKET).remove([filePath]);
}
} catch (e) {
console.error("Failed to delete from storage", e);
}
}
setMessage(msg, "Deleted successfully!");
closeDeleteMediaModal();
await loadMedia();
}
// =============================
// INIT
// =============================
document.getElementById("loginBtn").addEventListener("click", login);
document.getElementById("logoutBtn").addEventListener("click", logout);
document.getElementById("createCampaignBtn").addEventListener("click", createCampaign);
document.getElementById("toggleInactiveBtn").addEventListener("click", (e) => {
showInactive = !showInactive;
e.target.innerText = showInactive ? "Hide Inactive" : "Show Inactive";
renderCampaigns();
});
document.getElementById("closeModalBtn").addEventListener("click", closeEditModal);
document.getElementById("cancelModalBtn").addEventListener("click", closeEditModal);
document.getElementById("saveCampaignBtn").addEventListener("click", saveEditedCampaign);
document.getElementById("refreshSessionsBtn").addEventListener("click", loadSessions);
document.getElementById("sessionsCampaignFilter").addEventListener("change", loadSessions);
document.getElementById("uploadMediaBtn").addEventListener("click", uploadMedia);
document.getElementById("cancelDeleteMediaBtn").addEventListener("click", closeDeleteMediaModal);
document.getElementById("confirmDeleteMediaBtn").addEventListener("click", confirmDeleteMedia);
document.getElementById("cancelBlockSessionBtn").addEventListener("click", closeBlockModal);
document.getElementById("confirmBlockSessionBtn").addEventListener("click", confirmBlockSession);
setupTabs();
// Keep session state updated
supabaseClient.auth.onAuthStateChange(() => {
refreshUI();
});
refreshUI();
</script>
</body>
</html>