// 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 = `
${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;
});
});
}