// Zaytoven Lead Dashboard — Token-Based Auth (No Cookies) // Updated: Premium Zaytoven Home view, mobile menu, role-based nav let currentView = 'leads'; let currentPage = 1; let totalPages = 1; let currentFilters = {}; let currentSort = { field: 'total_spent', order: 'desc' }; let userRole = 'admin'; // 'admin' or 'zaytoven' // ===== AUTH TOKEN HELPERS ===== function getToken() { let token = localStorage.getItem('zaytovenToken'); if (!token) { const hash = window.location.hash; if (hash && hash.includes('token=')) { token = hash.split('token=')[1].split('&')[0]; if (token) { localStorage.setItem('zaytovenToken', decodeURIComponent(token)); } } } return token; } function authUrl(url) { const token = getToken(); if (!token) return url; const sep = url.includes('?') ? '&' : '?'; return url + sep + 'token=' + encodeURIComponent(token); } function authFetch(url, options = {}) { return fetch(authUrl(url), options); } // ===== AUTH CHECK ===== async function checkAuth() { const token = getToken(); if (!token) { if (!window.location.pathname.includes('login')) { window.location.href = '/login.html'; } return; } try { const res = await authFetch('/api/auth/check?_cb=' + Date.now()); const data = await res.json(); if (!data.authenticated && !window.location.pathname.includes('login')) { localStorage.removeItem('zaytovenToken'); window.location.href = '/login.html'; } else { // Store role from auth response userRole = data.role || 'admin'; applyRoleBasedNav(); } } catch (err) { console.error('[Dashboard] Auth check error:', err); } } // ===== ROLE-BASED NAVIGATION ===== function applyRoleBasedNav() { // Hide/show sidebar nav items based on role document.querySelectorAll('.nav-item[data-role]').forEach(item => { const role = item.dataset.role; if (role === 'admin' && userRole !== 'admin') { item.style.display = 'none'; } else { item.style.display = ''; } }); // Update mobile role badge const badge = document.getElementById('mobileRoleBadge'); if (badge) { badge.textContent = userRole === 'zaytoven' ? 'Zaytoven' : 'Admin'; } // Hide mobile bottom tabs for admin-only views document.querySelectorAll('.mobile-tab').forEach(tab => { const view = tab.dataset.view; const isAdminOnly = ['failed', 'manychat', 'email-campaigns', 'import'].includes(view); if (isAdminOnly && userRole !== 'admin') { tab.style.display = 'none'; } else { tab.style.display = ''; } }); // If Zaytoven user, default to home view if (userRole === 'zaytoven') { // Only switch if we're on the default leads view and haven't loaded yet const urlParams = new URLSearchParams(window.location.search); if (!urlParams.get('view') && currentView === 'leads') { // Will be set in init } } } // ===== MOBILE MENU ===== const hamburgerBtn = document.getElementById('hamburgerBtn'); const sidebar = document.getElementById('sidebar'); const sidebarOverlay = document.getElementById('sidebarOverlay'); const menuClose = document.getElementById('menuClose'); function openSidebar() { sidebar.classList.add('open'); sidebarOverlay.classList.add('active'); hamburgerBtn?.classList.add('active'); document.body.style.overflow = 'hidden'; } function closeSidebar() { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('active'); hamburgerBtn?.classList.remove('active'); document.body.style.overflow = ''; } hamburgerBtn?.addEventListener('click', () => { if (sidebar.classList.contains('open')) { closeSidebar(); } else { openSidebar(); } }); menuClose?.addEventListener('click', closeSidebar); sidebarOverlay?.addEventListener('click', closeSidebar); // Close sidebar when clicking a nav link on mobile document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', () => { if (window.innerWidth <= 768) { closeSidebar(); } }); }); // ===== NAVIGATION ===== document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const view = link.dataset.view; switchView(view); }); }); function switchView(view) { currentView = view; // Update views document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); const targetView = document.getElementById(`${view}-view`); const targetLink = document.querySelector(`[data-view="${view}"]`); if (targetView) targetView.classList.add('active'); if (targetLink) targetLink.classList.add('active'); // Update mobile bottom tabs document.querySelectorAll('.mobile-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.view === view); }); // Close mobile sidebar closeSidebar(); // Load view data if (view === 'home') loadHome(); if (view === 'leads') loadLeads(); if (view === 'priority') loadPriorityQueue(); if (view === 'services') loadServices(); if (view === 'fulfillment') loadFulfillment(); if (view === 'failed') loadFailedTransactions(); if (view === 'manychat') loadManyChatFeed(); if (view === 'analytics') loadAnalytics(); if (view === 'trill') loadTrillTransactions(); if (view === 'brand-deals') loadBrandDeals(); if (view === 'import') { // Import view is static, nothing to load } // Scroll to top window.scrollTo(0, 0); } // ===== LOGOUT ===== document.getElementById('logoutBtn').addEventListener('click', async () => { localStorage.removeItem('zaytovenToken'); window.location.href = '/login.html'; localStorage.removeItem('zaytovenToken'); window.location.href = '/login.html'; }); // ===== HOME / ZAYTOVEN DASHBOARD ===== async function loadHome() { // Load leads data for stats try { const res = await authFetch(`/api/leads?limit=1000&_cb=${Date.now()}`); const data = await res.json(); const leads = data.leads || []; // Calculate revenue const totalRevenue = leads.reduce((sum, l) => sum + (l.total_spent || 0), 0); // Monthly revenue (this month) const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const monthRevenue = leads .filter(l => l.last_contact && new Date(l.last_contact) >= monthStart) .reduce((sum, l) => sum + (l.total_spent || 0), 0); // Weekly revenue (last 7 days) const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000); const weekRevenue = leads .filter(l => l.last_contact && new Date(l.last_contact) >= weekAgo) .reduce((sum, l) => sum + (l.total_spent || 0), 0); // Deals closed (paid status) const dealsClosed = leads.filter(l => l.status === 'paid').length; // Leads converted (paid + booked) const leadsConverted = leads.filter(l => l.status === 'paid' || l.status === 'booked').length; // New leads today const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const newLeadsToday = leads.filter(l => { const created = l.created_at ? new Date(l.created_at) : null; return created && created >= today; }).length; // Repeat customers const repeatCustomers = leads.filter(l => l.is_repeat_customer).length; // Update DOM document.getElementById('homeTotalRevenue').textContent = '$' + totalRevenue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); document.getElementById('homeMonthRevenue').textContent = '$' + monthRevenue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); document.getElementById('homeWeekRevenue').textContent = '$' + weekRevenue.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); document.getElementById('homeDealsClosed').textContent = dealsClosed; document.getElementById('homeLeadsConverted').textContent = leadsConverted; document.getElementById('homeNewLeadsToday').textContent = newLeadsToday; document.getElementById('homeRepeatCustomers').textContent = repeatCustomers; // Update trends const trendEl = document.getElementById('homeMonthTrend'); if (monthRevenue > 0) { const percent = totalRevenue > 0 ? Math.round((monthRevenue / totalRevenue) * 100) : 0; trendEl.textContent = `↑ ${percent}% of total`; trendEl.style.color = 'var(--success)'; } else { trendEl.textContent = '—'; } const weekTrendEl = document.getElementById('homeWeekTrend'); if (weekRevenue > 0) { weekTrendEl.textContent = '↑ Active week'; weekTrendEl.style.color = 'var(--success)'; } else { weekTrendEl.textContent = '—'; } // Load recent activity (last 10 paid/booked leads) const recentLeads = leads .filter(l => l.status === 'paid' || l.status === 'booked') .sort((a, b) => new Date(b.last_contact || b.created_at) - new Date(a.last_contact || a.created_at)) .slice(0, 10); const activityList = document.getElementById('homeActivityList'); if (recentLeads.length === 0) { activityList.innerHTML = '
No recent activity yet. Leads with payments will appear here.
'; } else { activityList.innerHTML = recentLeads.map(lead => `
${lead.status === 'paid' ? '💰' : '📋'}
${escapeHtml(lead.name || 'Unknown')} ${lead.is_repeat_customer ? 'Repeat' : ''}
@${escapeHtml(lead.ig_handle || 'N/A')} · ${lead.platform || 'N/A'} · ${formatDate(lead.last_contact || lead.created_at)}
$${(lead.total_spent || 0).toFixed(2)}
`).join(''); } } catch (err) { console.error('[Home] Error loading stats:', err); } } // ===== LEADS ===== async function loadLeads() { const params = new URLSearchParams(); params.set('limit', '20'); params.set('offset', ((currentPage - 1) * 20).toString()); if (currentFilters.tier) params.set('tier', currentFilters.tier); if (currentFilters.status) params.set('status', currentFilters.status); if (currentFilters.search) params.set('search', currentFilters.search); const res = await authFetch(`/api/leads?${params}&_cb=${Date.now()}`); const data = await res.json(); renderLeads(data.leads); totalPages = Math.ceil((data.total || data.count || data.leads.length) / 20); renderPagination(); updateStats(data.leads, data.total || data.count || data.leads.length, data.totalRevenue); } function renderLeads(leads) { const tbody = document.getElementById('leadsTableBody'); tbody.innerHTML = leads.map(lead => ` Tier ${lead.tier} ${escapeHtml(lead.name || 'Unknown')} ${lead.is_repeat_customer ? '💰 Repeat' : ''}
@${escapeHtml(lead.ig_handle || '')} ${lead.platform || 'instagram'} ${escapeHtml(lead.phone || 'N/A')} ${escapeHtml(lead.email || 'N/A')} ${lead.status} $${(lead.total_spent || 0).toFixed(2)} ${escapeHtml(lead.payment_method_detail || 'N/A')} ${escapeHtml(lead.package || 'N/A')} `).join(''); } function renderPagination() { const container = document.getElementById('pagination'); if (totalPages <= 1) { container.innerHTML = ''; return; } let pages = []; const maxVisible = 5; if (totalPages <= maxVisible) { for (let i = 1; i <= totalPages; i++) pages.push(i); } else { if (currentPage <= 3) { pages = [1, 2, 3, 4, '...', totalPages]; } else if (currentPage >= totalPages - 2) { pages = [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages]; } else { pages = [1, '...', currentPage - 1, currentPage, currentPage + 1, '...', totalPages]; } } container.innerHTML = `
${pages.map(p => { if (p === '...') return `...`; return ``; }).join('')}
Page ${currentPage} of ${totalPages}
`; } function changePage(page) { if (page < 1 || page > totalPages) return; currentPage = page; loadLeads(); window.scrollTo(0, 0); } function updateStats(leads, totalCount) { const priority = leads.filter(l => l.tier === 1).length; const repeat = leads.filter(l => l.is_repeat_customer).length; const revenue = leads.reduce((sum, l) => sum + (l.total_spent || 0), 0); document.getElementById('totalLeads').textContent = totalCount || leads.length; document.getElementById('priorityCount').textContent = priority; document.getElementById('repeatCount').textContent = repeat; document.getElementById('revenueTotal').textContent = '$' + revenue.toFixed(2); } // ===== FILTERS ===== document.getElementById('searchInput').addEventListener('input', debounce((e) => { currentFilters.search = e.target.value; currentPage = 1; loadLeads(); }, 300)); document.getElementById('tierFilter').addEventListener('change', (e) => { currentFilters.tier = e.target.value; currentPage = 1; loadLeads(); }); document.getElementById('statusFilter').addEventListener('change', (e) => { currentFilters.status = e.target.value; currentPage = 1; loadLeads(); }); document.getElementById('platformFilter').addEventListener('change', (e) => { currentFilters.platform = e.target.value; currentPage = 1; loadLeads(); }); document.getElementById('searchName').addEventListener('input', debounce((e) => { currentFilters.searchName = e.target.value; currentPage = 1; loadLeads(); }, 300)); document.getElementById('searchEmail').addEventListener('input', debounce((e) => { currentFilters.searchEmail = e.target.value; currentPage = 1; loadLeads(); }, 300)); document.getElementById('searchPhone').addEventListener('input', debounce((e) => { currentFilters.searchPhone = e.target.value; currentPage = 1; loadLeads(); }, 300)); document.getElementById('searchPackage').addEventListener('input', debounce((e) => { currentFilters.searchPackage = e.target.value; currentPage = 1; loadLeads(); }, 300)); // Sort headers document.querySelectorAll('.sort-header').forEach(th => { th.addEventListener('click', () => { const field = th.dataset.sort; if (currentSort.field === field) { currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc'; } else { currentSort.field = field; currentSort.order = 'desc'; } document.querySelectorAll('.sort-header').forEach(h => { h.textContent = h.textContent.replace(' ↓', '').replace(' ↑', '').replace(' ↕', '') + ' ↕'; }); th.textContent = th.textContent.replace(' ↕', '').replace(' ↓', '').replace(' ↑', '') + (currentSort.order === 'asc' ? ' ↑' : ' ↓'); currentPage = 1; loadLeads(); }); }); // ===== LEAD DETAIL MODAL ===== async function openLeadDetail(leadId) { const res = await authFetch(`/api/leads/${leadId}?_cb=${Date.now()}`); const lead = await res.json(); const content = document.getElementById('leadDetailContent'); content.innerHTML = `

${escapeHtml(lead.name || 'Unknown')}

@${escapeHtml(lead.ig_handle || 'N/A')} · ${lead.platform} ${lead.is_repeat_customer ? '💰 Repeat Customer' : ''}

Tier ${lead.tier} ${lead.status}
${formatNumber(lead.follower_count)}
Followers
${formatNumber(lead.spotify_monthly)}
Spotify Monthly
$${(lead.total_spent || 0).toFixed(2)}
Total Spent

Contact Info

Email: ${lead.email ? `${lead.email}` : 'N/A'}

Phone: ${lead.phone ? `${lead.phone}` : 'N/A'}

Stage Name: ${escapeHtml(lead.stage_name || 'N/A')}

Apple Music: ${lead.apple_music_id || 'N/A'}

First Contact: ${formatDate(lead.first_contact)}

Last Contact: ${formatDate(lead.last_contact)}

${lead.priority_flag ? `

🚨 Priority Flag: ${escapeHtml(lead.priority_flag)}

` : ''}

Sales Info

Package: ${escapeHtml(lead.package || 'N/A')}

Closer: ${escapeHtml(lead.closer || 'N/A')}

Setter: ${escapeHtml(lead.setter || 'N/A')}

Payment Method: ${escapeHtml(lead.payment_method_detail || 'N/A')}

Total Spent: $${(lead.total_spent || 0).toFixed(2)}

Repeat Customer: ${lead.is_repeat_customer ? 'Yes 💰' : 'No'}

Notes

Update Status

${lead.transactions && lead.transactions.length > 0 ? `

Transaction History

${lead.transactions.map(t => `
${t.service_type} ${t.status}
$${t.amount.toFixed(2)}
${t.payment_method} · ${formatDate(t.date)}
`).join('')}
` : ''} ${lead.conversations && lead.conversations.length > 0 ? `

Conversation History (${lead.conversations.length} messages)

${lead.conversations.map(c => `
${c.direction === 'inbound' ? '📥 From Lead' : '📤 To Lead'} · ${formatDate(c.timestamp)}
${escapeHtml(c.message_content)}
`).join('')}
` : '

No conversation history yet.

'} `; document.getElementById('leadModal').classList.add('active'); } async function saveNotes(leadId) { const notes = document.getElementById('leadNotes').value; const res = await authFetch(`/api/leads/${leadId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notes }) }); if (res.ok) { showToast('Notes saved'); } } async function updateStatus(leadId) { const status = document.getElementById('statusUpdate').value; const res = await authFetch(`/api/leads/${leadId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (res.ok) { showToast('Status updated'); loadLeads(); } } // ===== ADD LEAD ===== document.getElementById('addLeadBtn').addEventListener('click', () => { document.getElementById('addLeadModal').classList.add('active'); }); document.getElementById('addLeadForm').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData); if (data.follower_count) data.follower_count = parseInt(data.follower_count); if (data.spotify_monthly) data.spotify_monthly = parseInt(data.spotify_monthly); if (data.tier) data.tier = parseInt(data.tier); const res = await authFetch('/api/leads?_cb=' + Date.now(), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (res.ok) { document.getElementById('addLeadModal').classList.remove('active'); e.target.reset(); loadLeads(); showToast('Lead added'); } }); // ===== PRIORITY QUEUE ===== async function loadPriorityQueue() { const res = await authFetch('/api/leads?tier=1&limit=50&_cb=' + Date.now()); const data = await res.json(); const container = document.getElementById('priorityCards'); if (!data.leads || data.leads.length === 0) { container.innerHTML = '

No priority leads in the queue. Check back soon.

'; return; } container.innerHTML = data.leads.map(lead => `

${escapeHtml(lead.name || 'Unknown')} @${escapeHtml(lead.ig_handle || '')}

${escapeHtml(lead.priority_flag || 'Brand/Company Account')}
👥 ${formatNumber(lead.follower_count)} 🎵 ${formatNumber(lead.spotify_monthly)} 💰 $${(lead.total_spent || 0).toFixed(2)}
`).join(''); } async function markContacted(leadId) { const res = await authFetch(`/api/leads/${leadId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'contacted' }) }); if (res.ok) { showToast('Marked as contacted'); loadPriorityQueue(); } } // ===== ANALYTICS ===== async function loadAnalytics() { const res = await authFetch('/api/analytics/overview?_cb=' + Date.now()); const data = await res.json(); document.getElementById('analyticsGrid').innerHTML = `

Total Leads

${formatNumber(data.totalLeads)}

Recent (7 Days)

${formatNumber(data.recentLeads)}

Total Revenue

$${parseFloat(data.totalRevenue || 0).toFixed(2)}

Repeat Customers

${formatNumber(data.repeatCustomers)}

By Tier

${data.byTier.map(t => `
Tier ${t.tier} ${t.count}
`).join('')}

By Status

${data.byStatus.map(s => `
${s.status} ${s.count}
`).join('')}
`; } // ===== CSV IMPORT ===== document.getElementById('importCsvBtn').addEventListener('click', async () => { const fileInput = document.getElementById('csvFile'); const resultDiv = document.getElementById('csvImportResult'); const source = document.getElementById('csvSource').value; if (!fileInput.files || !fileInput.files[0]) { resultDiv.className = 'error'; resultDiv.textContent = 'Select a CSV file first.'; return; } const file = fileInput.files[0]; const text = await file.text(); const lines = text.trim().split('\n'); if (lines.length < 2) { resultDiv.className = 'error'; resultDiv.textContent = 'CSV is empty or has no data rows.'; return; } const headers = parseCSVLine(lines[0]); const leads = []; for (let i = 1; i < lines.length; i++) { const values = parseCSVLine(lines[i]); const row = {}; headers.forEach((h, idx) => { row[h.trim().toLowerCase()] = values[idx]?.trim() || ''; }); const lead = { email: row.email || row['e-mail'] || '', name: row.name || row['full name'] || '', ig_handle: row.ig_handle || row.handle || row.username || '', follower_count: parseInt(row.follower_count || row.followers || '0') || 0, platform: row.platform || source === 'beatstars' ? 'beatstars' : 'email', package: row.package || row.product || row['service type'] || '', amount: parseFloat(row.amount || row.price || '0') || 0, is_repeat: source === 'sales' || parseFloat(row.amount || '0') > 0, message: row.package || row.notes || '' }; if (lead.email || lead.ig_handle) { leads.push(lead); } } resultDiv.className = ''; resultDiv.textContent = `Parsed ${leads.length} leads. Uploading...`; const res = await authFetch('/api/import/csv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ leads, source }) }); const data = await res.json(); resultDiv.className = data.imported > 0 ? 'success' : 'error'; resultDiv.innerHTML = ` Imported: ${data.imported}
Duplicates skipped: ${data.duplicates}
Errors: ${data.errors?.length || 0} `; loadLeads(); }); function parseCSVLine(line) { const result = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; } // ===== JSON IMPORT ===== document.getElementById('importBtn').addEventListener('click', async () => { const jsonText = document.getElementById('importJson').value; const resultDiv = document.getElementById('importResult'); if (!jsonText.trim()) { resultDiv.className = 'error'; resultDiv.textContent = 'Paste some JSON first.'; return; } try { const conversations = JSON.parse(jsonText); const res = await authFetch('/api/import/instagram', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversations }) }); const data = await res.json(); resultDiv.className = 'success'; resultDiv.innerHTML = `Imported ${data.imported} of ${data.total} conversations.
${data.errors?.length > 0 ? 'Errors: ' + data.errors.join(', ') : ''}`; loadLeads(); } catch (err) { resultDiv.className = 'error'; resultDiv.textContent = 'Invalid JSON: ' + err.message; } }); // ===== MODAL CLOSE ===== document.querySelectorAll('.close-btn').forEach(btn => { btn.addEventListener('click', () => { btn.closest('.modal').classList.remove('active'); }); }); window.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { e.target.classList.remove('active'); } }); // ===== UTILS ===== function formatNumber(num) { if (!num || num === 0) return 'N/A'; if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; if (num >= 1000) return (num / 1000).toFixed(1) + 'K'; return num.toString(); } function formatDate(dateStr) { if (!dateStr) return 'N/A'; const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function debounce(fn, ms) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), ms); }; } function showToast(message) { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: var(--accent); color: var(--bg-dark); padding: 12px 20px; border-radius: 8px; font-weight: 700; z-index: 1000; animation: slideIn 0.3s ease; box-shadow: 0 4px 16px var(--accent-glow); `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } // ===== SERVICES ===== async function loadServices() { const res = await authFetch(`/api/services?_cb=${Date.now()}`); const data = await res.json(); renderServices(data.services); } function renderServices(services) { const tbody = document.getElementById('servicesTableBody'); if (!tbody) return; tbody.innerHTML = services.map(s => ` ${escapeHtml(s.name)}
${escapeHtml(s.description || '')} ${escapeHtml(s.category)} $${s.advertised_price?.toFixed(2) || '0.00'} $${s.minimum_price?.toFixed(2) || '0.00'} $${s.maximum_price?.toFixed(2) || '0.00'} ${escapeHtml(s.flexibility_note || 'N/A')} ${s.status} `).join(''); } async function editService(id) { const res = await authFetch(`/api/services/${id}?_cb=${Date.now()}`); const s = await res.json(); document.getElementById('serviceId').value = s.id; document.getElementById('serviceName').value = s.name; document.getElementById('serviceCategory').value = s.category; document.getElementById('serviceDescription').value = s.description || ''; document.getElementById('serviceAdvertised').value = s.advertised_price; document.getElementById('serviceMinimum').value = s.minimum_price; document.getElementById('serviceMaximum').value = s.maximum_price; document.getElementById('serviceFlexibility').value = s.flexibility_note || ''; document.getElementById('serviceFulfillment').value = s.fulfillment_requirements || ''; document.getElementById('serviceSortOrder').value = s.sort_order || 0; document.getElementById('serviceStatus').value = s.status; document.getElementById('serviceModalTitle').textContent = 'Edit Service'; document.getElementById('serviceModal').style.display = 'block'; } async function deleteService(id) { if (!confirm('Delete this service?')) return; const res = await authFetch(`/api/services/${id}`, { method: 'DELETE' }); if (res.ok) { loadServices(); } } function closeServiceModal() { document.getElementById('serviceModal').style.display = 'none'; document.getElementById('serviceForm').reset(); document.getElementById('serviceId').value = ''; document.getElementById('serviceModalTitle').textContent = 'Add Service'; } document.getElementById('addServiceBtn')?.addEventListener('click', () => { document.getElementById('serviceModal').style.display = 'block'; }); document.getElementById('serviceForm')?.addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('serviceId').value; const payload = { name: document.getElementById('serviceName').value, category: document.getElementById('serviceCategory').value, description: document.getElementById('serviceDescription').value, advertised_price: parseFloat(document.getElementById('serviceAdvertised').value), minimum_price: parseFloat(document.getElementById('serviceMinimum').value), maximum_price: parseFloat(document.getElementById('serviceMaximum').value), flexibility_note: document.getElementById('serviceFlexibility').value, fulfillment_requirements: document.getElementById('serviceFulfillment').value, sort_order: parseInt(document.getElementById('serviceSortOrder').value) || 0, status: document.getElementById('serviceStatus').value }; const url = id ? `/api/services/${id}` : '/api/services'; const method = id ? 'PUT' : 'POST'; const res = await authFetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { closeServiceModal(); loadServices(); } }); // ===== FULFILLMENT ===== async function loadFulfillment() { const res = await authFetch(`/api/service-bookings?_cb=${Date.now()}`); const data = await res.json(); renderFulfillment(data.bookings); } function renderFulfillment(bookings) { const tbody = document.getElementById('fulfillmentTableBody'); if (!tbody) return; const statusFilter = document.getElementById('fulfillmentStatusFilter')?.value; const serviceFilter = document.getElementById('fulfillmentServiceFilter')?.value; let filtered = bookings; if (statusFilter) filtered = filtered.filter(b => b.status === statusFilter); if (serviceFilter) filtered = filtered.filter(b => b.service_id === parseInt(serviceFilter)); const active = filtered.filter(b => b.status !== 'completed' && b.status !== 'cancelled'); const previous = filtered.filter(b => b.status === 'completed' || b.status === 'cancelled'); let html = ''; if (active.length > 0) { html += `ACTIVE BOOKINGS`; html += active.map(b => ` ${escapeHtml(b.service_name || 'N/A')}
${escapeHtml(b.category || '')} ${escapeHtml(b.lead_name || 'N/A')}
${escapeHtml(b.lead_email || '')} $${b.agreed_price?.toFixed(2) || '0.00'} $${b.deposit_amount?.toFixed(2) || '0.00'} ${b.deposit_paid ? '✅' : '⏳'} ${b.status} ${b.fulfillment_status} `).join(''); } if (previous.length > 0) { html += `PREVIOUS FULFILLMENTS`; html += previous.map(b => ` ${escapeHtml(b.service_name || 'N/A')}
${escapeHtml(b.category || '')} ${escapeHtml(b.lead_name || 'N/A')}
${escapeHtml(b.lead_email || '')} $${b.agreed_price?.toFixed(2) || '0.00'} $${b.deposit_amount?.toFixed(2) || '0.00'} ${b.deposit_paid ? '✅' : '⏳'} ${b.status} ${b.fulfillment_status} `).join(''); } if (active.length === 0 && previous.length === 0) { html = 'No bookings found'; } tbody.innerHTML = html; } // ===== FAILED TRANSACTIONS ===== async function loadFailedTransactions() { const res = await authFetch(`/api/failed-transactions?_cb=${Date.now()}`); const data = await res.json(); renderFailedTransactions(data.transactions); } function renderFailedTransactions(transactions) { const tbody = document.getElementById('failedTableBody'); if (!tbody) return; tbody.innerHTML = transactions.map(t => ` ${escapeHtml(t.lead_name || 'N/A')}
${escapeHtml(t.lead_email || '')} ${escapeHtml(t.service_name || 'N/A')} $${t.amount?.toFixed(2) || '0.00'} ${escapeHtml(t.payment_method || 'N/A')} ${escapeHtml(t.failure_reason || 'Unknown')} ${formatDate(t.attempted_at)} `).join(''); } async function contactFailedLead(leadId) { if (!leadId) return alert('No lead ID'); openLeadDetail(leadId); } // ===== MANYCHAT INTEGRATION ===== async function loadManyChatFeed() { const res = await authFetch(`/api/manychat/feed?_cb=${Date.now()}`); const data = await res.json(); renderManyChatFeed(data.subscribers || []); } function renderManyChatFeed(subscribers) { const container = document.getElementById('manychatFeedBody'); if (!container) return; if (!subscribers.length) { container.innerHTML = '
No recent Instagram activity
'; return; } container.innerHTML = subscribers.map(sub => `
${(sub.name || 'U').charAt(0).toUpperCase()}
${escapeHtml(sub.name || 'Unknown')} ${formatDate(sub.last_interaction)}

@${escapeHtml(sub.ig_handle || 'unknown')}

${escapeHtml(sub.last_message || 'No recent message')}

${sub.phone ? `📞 ${sub.phone}` : ''} ${sub.email ? `✉️ ${sub.email}` : ''} ${sub.platform || 'Instagram'}
`).join(''); } async function createLeadFromDM(subscriberId, name, igHandle, phone, email) { const res = await authFetch('/api/leads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name || 'Instagram DM', ig_handle: igHandle, phone: phone, email: email, platform: 'instagram', status: 'new', notes: `From ManyChat Instagram DM (ID: ${subscriberId})` }) }); if (res.ok) { alert('Lead created from Instagram DM!'); loadLeads(); } else { alert('Failed to create lead'); } } async function syncManyChatSubscribers() { const res = await authFetch('/api/manychat/sync?_cb=' + Date.now()); const data = await res.json(); alert(`Synced ${data.synced || 0} subscribers from ManyChat`); loadManyChatFeed(); } // ===== INIT ===== (async function init() { // If token is in URL, store it and clean URL const urlParams = new URLSearchParams(window.location.search); const urlToken = urlParams.get('token'); if (urlToken) { localStorage.setItem('zaytovenToken', urlToken); const cleanUrl = window.location.pathname + window.location.hash; window.history.replaceState({}, document.title, cleanUrl); } await checkAuth(); // If Zaytoven user, default to home view if (userRole === 'zaytoven') { currentView = 'home'; // Update active states document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); document.querySelectorAll('.mobile-tab').forEach(t => t.classList.remove('active')); document.getElementById('home-view')?.classList.add('active'); document.querySelector('[data-view="home"]')?.classList.add('active'); document.querySelector('.mobile-tab[data-view="home"]')?.classList.add('active'); loadHome(); } else { loadLeads(); } })(); // Refresh stats periodically setInterval(() => { if (currentView === 'leads') loadLeads(); if (currentView === 'home') loadHome(); }, 30000); // ===== TRILL TRANSACTION DASHBOARD ===== (insert before // ===== INIT =====) // ===== BRAND DEALS & BOOKINGS ===== async function loadBrandDeals() { const type = document.getElementById('dealTypeFilter')?.value || ''; const status = document.getElementById('dealStatusFilter')?.value || ''; let url = '/api/brand-deals?_cb=' + Date.now(); if (type) url += '&type=' + encodeURIComponent(type); if (status) url += '&status=' + encodeURIComponent(status); const res = await authFetch(url); const data = await res.json(); renderBrandDeals(data.deals || []); updateBrandDealStats(data.deals || []); } function renderBrandDeals(deals) { const tbody = document.getElementById('brandDealsTableBody'); if (!tbody) return; if (!deals.length) { tbody.innerHTML = 'No brand deals or bookings yet. Add your first deal above.'; return; } tbody.innerHTML = deals.map(deal => ` ${escapeHtml(deal.name)} ${deal.type.replace('_', ' ')} ${escapeHtml(deal.brand_name || 'N/A')} $${(deal.deal_value || 0).toFixed(2)} ${deal.status} ${formatDate(deal.event_date)} ${escapeHtml(deal.venue_location || 'N/A')} ${deal.video_link ? `Video` : ''} ${deal.ad_link ? `Ad` : ''} `).join(''); } function updateBrandDealStats(deals) { const total = deals.length; const brandDeals = deals.filter(d => d.type === 'brand_deal').length; const shows = deals.filter(d => d.type === 'show' || d.type === 'booking' || d.type === 'appearance').length; const revenue = deals.reduce((sum, d) => sum + (d.deal_value || 0), 0); document.getElementById('totalDeals').textContent = total; document.getElementById('brandDealsCount').textContent = brandDeals; document.getElementById('showsCount').textContent = shows; document.getElementById('dealsRevenue').textContent = '$' + revenue.toFixed(2); } async function deleteBrandDeal(id) { if (!confirm('Delete this deal?')) return; const res = await authFetch(`/api/brand-deals/${id}`, { method: 'DELETE' }); if (res.ok) { showToast('Deal deleted'); loadBrandDeals(); } } async function loadTrillTransactions() { const status = document.getElementById('trillStatusFilter')?.value || ''; let url = '/api/trill-transactions?limit=500&platform=instagram&_cb=' + Date.now(); if (status) url += '&status=' + encodeURIComponent(status); const res = await authFetch(url); const data = await res.json(); let leads = data.leads || []; // Update revenue totals from API if (data.totals) { document.getElementById('trillTotalRevenue').textContent = '$' + (data.totals.estimated_revenue || '0.00'); document.getElementById('trillPaymentMentions').textContent = data.totals.payment_mentions || '0'; } renderTrillTransactions(leads); } function renderTrillTransactions(leads) { const tbody = document.getElementById('trillTableBody'); if (!tbody) return; // Update stats document.getElementById('trillTotalCount').textContent = leads.length; document.getElementById('trillIGCount').textContent = leads.filter(l => l.platform === 'instagram').length; document.getElementById('trillGVCount').textContent = leads.filter(l => l.platform === 'google_voice').length; document.getElementById('trillPendingCount').textContent = leads.filter(l => !l.trill_status || l.trill_status === 'pending').length; if (!leads.length) { tbody.innerHTML = 'No trill transactions found. Click Rescan All Leads to scan.'; return; } tbody.innerHTML = leads.map(lead => { // Extract year from date const dateStr = lead.trill_detected_at || lead.created_at || lead.last_contact || ''; const year = dateStr ? new Date(dateStr).getFullYear() : 'N/A'; // Extract payment keywords from trill_notes let paymentKeywords = ''; if (lead.trill_notes) { const keywords = ['zelle', 'cash app', 'cashapp', 'venmo', 'paypal', 'apple pay', 'google pay', 'western union', 'moneygram', 'bank transfer', 'wire transfer', 'deposit', 'payment', 'paid', 'invoice', 'send money', 'transfer']; const found = keywords.filter(kw => lead.trill_notes.toLowerCase().includes(kw)); paymentKeywords = found.map(kw => kw.toUpperCase()).join(', '); } // Extract conversation text from notes (first 200 chars) let conversation = lead.trill_notes || lead.notes || ''; // Extract message text from notes const msgMatch = conversation.match(/Message:\n(.+?)(?:\n\nTRILL|\n\nSERVICES|$)/s); if (msgMatch) { conversation = msgMatch[1].trim(); } conversation = conversation.substring(0, 200); // Source emoji const sourceEmoji = { 'instagram': '📩', 'google_voice': '📞', 'muzic_money': '🎵', 'volume_control': '🔊', 'affiliates': '🔗', 'email': '📧' }[lead.platform] || '📋'; return ` ${escapeHtml(lead.name || 'Unknown')}
@${escapeHtml(lead.ig_handle || 'N/A')} ${escapeHtml(lead.phone || 'N/A')}
${escapeHtml(lead.email || 'N/A')} ${sourceEmoji}
${escapeHtml(lead.platform || 'unknown')} ${year} ${escapeHtml(paymentKeywords || 'N/A')} ${escapeHtml(conversation)}${conversation.length >= 200 ? '...' : ''} ${lead.trill_status === 'pending' ? '🔓 OPEN JOB' : ''} ${lead.trill_status === 'complete' ? '✅ COMPLETE' : ''} ${lead.trill_status === 'disputed' ? '⚠️ DISPUTED' : ''} ${lead.trill_status === 'transitioned' ? '↪️ TRANSITIONED' : ''} ${(!lead.trill_status || lead.trill_status === 'pending') ? '🔓 OPEN JOB' : ''} `; }).join(''); } async function markTrillTransitioned(leadId) { const res = await authFetch(`/api/leads/${leadId}/trill-status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'transitioned', notes: 'Marked as transitioned by admin' }) }); if (res.ok) { showToast('Marked as transitioned'); loadTrillTransactions(); } } async function runRetroactiveDetection() { const btn = document.getElementById('retroDetectBtn'); if (btn) btn.disabled = true; const res = await authFetch('/api/trill/detect-all', { method: 'POST' }); const data = await res.json(); if (data.success) { showToast(`Scanned ${data.scanned} leads, found ${data.found} trill transactions`); loadTrillTransactions(); } else { showToast('Error: ' + (data.error || 'Unknown')); } if (btn) btn.disabled = false; } // Google Voice import handler async function importGoogleVoice() { const fileInput = document.getElementById('gvFileInput'); const resultDiv = document.getElementById('gvImportResult'); if (!fileInput?.files?.[0]) { resultDiv.textContent = 'Select a ZIP file first.'; return; } const formData = new FormData(); formData.append('takeout', fileInput.files[0]); resultDiv.textContent = 'Uploading and parsing...'; const res = await authFetch('/api/import/google-voice', { method: 'POST', body: formData }); const data = await res.json(); resultDiv.innerHTML = ` Import complete:
Conversations parsed: ${data.conversations_parsed}
Leads created: ${data.leads_created}
Leads updated: ${data.leads_updated}
Messages imported: ${data.messages_imported}
Trill leads found: ${data.trill_leads}
Errors: ${data.errors?.length || 0} `; loadTrillTransactions(); } // ManyChat sync-all handler async function syncManyChatAll() { const res = await authFetch('/api/manychat/sync-all?_cb=' + Date.now()); const data = await res.json(); showToast(`Synced ${data.total || 0} subscribers: ${data.created} created, ${data.updated} updated, ${data.trill_found} trill found`); loadTrillTransactions(); } // ===== Trill view filters ===== if (document.getElementById('trillStatusFilter')) { document.getElementById('trillStatusFilter').addEventListener('change', () => { loadTrillTransactions(); }); } if (document.getElementById('retroDetectBtn')) { document.getElementById('retroDetectBtn').addEventListener('click', runRetroactiveDetection); } if (document.getElementById('gvImportBtn')) { document.getElementById('gvImportBtn').addEventListener('click', importGoogleVoice); } // ===== EMAIL CAMPAIGN FUNCTIONS ===== async function loadEmailTemplates() { const res = await authFetch('/api/email/templates'); const data = await res.json(); const select = document.getElementById('ecTemplate'); if (!select) return; select.innerHTML = ''; (data.templates || []).forEach(t => { const opt = document.createElement('option'); opt.value = t.name; opt.textContent = `${t.name} (${t.category})`; select.appendChild(opt); }); } async function loadTemplateIntoBuilder() { const name = document.getElementById('ecTemplate')?.value; if (!name) return; const res = await authFetch(`/api/email/template/${encodeURIComponent(name)}`); const data = await res.json(); if (data.html_content) document.getElementById('ecHtml').value = data.html_content; if (data.text_content) document.getElementById('ecText').value = data.text_content; if (data.subject) document.getElementById('ecSubject').value = data.subject; } async function previewCampaign() { const segment = document.getElementById('ecSegment')?.value || 'all'; const html = document.getElementById('ecHtml')?.value || ''; const subject = document.getElementById('ecSubject')?.value || 'Preview'; const res = await authFetch(`/api/email/segment-preview?segment=${encodeURIComponent(segment)}`); const data = await res.json(); const previewArea = document.getElementById('ecPreviewArea'); const previewCount = document.getElementById('ecPreviewCount'); if (previewCount) previewCount.textContent = data.count || 0; if (previewArea) { const sample = data.preview?.[0] || { name: 'John Doe', email: 'john@example.com', ig_handle: '@johndoe' }; let rendered = html .replace(/\{\{name\}\}/g, sample.name || 'Friend') .replace(/\{\{email\}\}/g, sample.email || '') .replace(/\{\{ig_handle\}\}/g, sample.ig_handle || ''); previewArea.innerHTML = `
Subject: ${escapeHtml(subject)}
` + rendered; } } async function validateCampaign() { const segment = document.getElementById('ecSegment')?.value || 'all'; const res = await authFetch('/api/email/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ segment }) }); const data = await res.json(); const resultDiv = document.getElementById('ecValidationResult'); if (resultDiv) { resultDiv.innerHTML = `
Validation Results:
Total: ${data.total} | Valid: ${data.valid} | Invalid: ${data.invalid} | No Email: ${data.noEmail}
${data.sample?.length > 0 ? `Sample: ${data.sample.map(s => s.email).join(', ')}` : ''}
`; } } async function sendCampaign() { const name = document.getElementById('ecName')?.value || document.getElementById('ecSubject')?.value || 'Campaign'; const subject = document.getElementById('ecSubject')?.value; const html = document.getElementById('ecHtml')?.value; const text = document.getElementById('ecText')?.value; const segment = document.getElementById('ecSegment')?.value || 'all'; const fromEmail = document.getElementById('ecFromEmail')?.value; const fromName = document.getElementById('ecFromName')?.value; if (!subject || !html) { showToast('Subject and HTML content are required'); return; } if (!confirm(`Send campaign "${name}" to ${segment} segment?`)) return; const res = await authFetch('/api/email/campaign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, subject, htmlContent: html, textContent: text, targetSegment: segment, fromEmail, fromName }) }); const data = await res.json(); if (data.success) { showToast(`Campaign sent! ${data.sent} sent, ${data.failed} failed`); loadCampaignHistory(); } else { showToast('Error: ' + (data.error || 'Failed to send')); } } async function saveAsTemplate() { const name = prompt('Template name:'); if (!name) return; const subject = document.getElementById('ecSubject')?.value; const html = document.getElementById('ecHtml')?.value; const text = document.getElementById('ecText')?.value; if (!subject || !html) { showToast('Subject and HTML content required'); return; } const res = await authFetch('/api/email/template', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, subject, htmlContent: html, textContent: text, category: 'custom' }) }); const data = await res.json(); if (data.success) { showToast('Template saved!'); loadEmailTemplates(); } else { showToast('Error: ' + (data.error || 'Failed to save')); } } async function loadCampaignHistory() { const res = await authFetch('/api/email/campaigns'); const data = await res.json(); const tbody = document.getElementById('ecHistoryTable'); if (!tbody) return; tbody.innerHTML = (data.campaigns || []).map(c => ` ${escapeHtml(c.name)} ${escapeHtml(c.subject)} ${escapeHtml(c.target_segment)} ${escapeHtml(c.status)} ${c.sent_count || 0} ${c.open_count || 0} ${c.click_count || 0} ${c.bounce_count || 0} ${c.created_at ? new Date(c.created_at).toLocaleDateString() : 'N/A'} `).join('') || 'No campaigns yet'; } // Auto-load email data when switching to email campaigns view const originalSwitchView = switchView; switchView = function(view) { originalSwitchView(view); if (view === 'email-campaigns') { loadEmailTemplates(); loadCampaignHistory(); } }; // Load email templates on init if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { loadEmailTemplates(); }); } else { loadEmailTemplates(); } if (document.getElementById('ecSegment')) { document.getElementById('ecSegment').addEventListener('change', () => { const segment = document.getElementById('ecSegment').value; authFetch(`/api/email/segment-preview?segment=${encodeURIComponent(segment)}`) .then(r => r.json()) .then(data => { const count = document.getElementById('ecPreviewCount'); if (count) count.textContent = data.count || 0; }); }); }