Root cause: MongoDB ObjectId objects were being inserted into data-id attributes as '[object Object]' instead of their string representation. Fix: Explicitly call String() on sub._id when creating data attributes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
293 lines
8.6 KiB
JavaScript
293 lines
8.6 KiB
JavaScript
/**
|
|
* Newsletter Management - Admin Interface
|
|
*/
|
|
|
|
let currentPage = 1;
|
|
const perPage = 50;
|
|
let currentFilters = {
|
|
status: 'active',
|
|
verified: 'all'
|
|
};
|
|
|
|
/**
|
|
* Initialize page
|
|
*/
|
|
async function init() {
|
|
// Event listeners (navbar handles admin name and logout now)
|
|
document.getElementById('refresh-btn').addEventListener('click', () => loadAll());
|
|
document.getElementById('export-btn').addEventListener('click', exportSubscribers);
|
|
document.getElementById('filter-status').addEventListener('change', handleFilterChange);
|
|
document.getElementById('filter-verified').addEventListener('change', handleFilterChange);
|
|
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
|
|
document.getElementById('next-page').addEventListener('click', () => changePage(1));
|
|
|
|
// Load data
|
|
await loadAll();
|
|
}
|
|
|
|
/**
|
|
* Load all data
|
|
*/
|
|
async function loadAll() {
|
|
await Promise.all([
|
|
loadStats(),
|
|
loadSubscribers()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Load statistics
|
|
*/
|
|
async function loadStats() {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch('/api/newsletter/admin/stats', {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load stats');
|
|
|
|
const data = await response.json();
|
|
const stats = data.stats;
|
|
|
|
document.getElementById('stat-total').textContent = stats.total || 0;
|
|
document.getElementById('stat-active').textContent = stats.active || 0;
|
|
document.getElementById('stat-verified').textContent = stats.verified || 0;
|
|
document.getElementById('stat-recent').textContent = stats.recent_30_days || 0;
|
|
} catch (error) {
|
|
console.error('Error loading stats:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load subscribers list
|
|
*/
|
|
async function loadSubscribers() {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const skip = (currentPage - 1) * perPage;
|
|
|
|
const params = new URLSearchParams({
|
|
limit: perPage,
|
|
skip,
|
|
active: currentFilters.status === 'all' ? null : currentFilters.status === 'active',
|
|
verified: currentFilters.verified === 'all' ? null : currentFilters.verified === 'verified'
|
|
});
|
|
|
|
// Remove null values
|
|
for (const [key, value] of [...params.entries()]) {
|
|
if (value === 'null' || value === null) {
|
|
params.delete(key);
|
|
}
|
|
}
|
|
|
|
const response = await fetch(`/api/newsletter/admin/subscriptions?${params}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to load subscribers');
|
|
|
|
const data = await response.json();
|
|
renderSubscribers(data.subscriptions);
|
|
updatePagination(data.pagination);
|
|
} catch (error) {
|
|
console.error('Error loading subscribers:', error);
|
|
document.getElementById('subscribers-table').innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-8 text-center text-red-600">
|
|
Error loading subscribers. Please refresh the page.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render subscribers table
|
|
*/
|
|
function renderSubscribers(subscriptions) {
|
|
const tbody = document.getElementById('subscribers-table');
|
|
|
|
if (!subscriptions || subscriptions.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-8 text-center text-gray-500">
|
|
No subscribers found
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = subscriptions.map(sub => `
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
${escapeHtml(sub.email)}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
${escapeHtml(sub.name) || '-'}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
${escapeHtml(sub.source) || 'unknown'}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
${sub.active
|
|
? `<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
|
|
${sub.verified ? '<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>' : ''}
|
|
${sub.verified ? 'Active ✓' : 'Active'}
|
|
</span>`
|
|
: '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">Inactive</span>'
|
|
}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
${formatDate(sub.subscribed_at)}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button class="view-details-btn text-blue-600 hover:text-blue-900 mr-3" data-id="${String(sub._id)}">View</button>
|
|
<button class="delete-subscriber-btn text-red-600 hover:text-red-900" data-id="${String(sub._id)}" data-email="${escapeHtml(sub.email)}">Delete</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
// Add event listeners to buttons
|
|
tbody.querySelectorAll('.view-details-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const id = this.getAttribute('data-id');
|
|
viewDetails(id);
|
|
});
|
|
});
|
|
|
|
tbody.querySelectorAll('.delete-subscriber-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const id = this.getAttribute('data-id');
|
|
const email = this.getAttribute('data-email');
|
|
deleteSubscriber(id, email);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update pagination UI
|
|
*/
|
|
function updatePagination(pagination) {
|
|
document.getElementById('showing-from').textContent = pagination.skip + 1;
|
|
document.getElementById('showing-to').textContent = Math.min(pagination.skip + pagination.limit, pagination.total);
|
|
document.getElementById('total-count').textContent = pagination.total;
|
|
|
|
document.getElementById('prev-page').disabled = currentPage === 1;
|
|
document.getElementById('next-page').disabled = !pagination.has_more;
|
|
}
|
|
|
|
/**
|
|
* Handle filter change
|
|
*/
|
|
function handleFilterChange() {
|
|
currentFilters.status = document.getElementById('filter-status').value;
|
|
currentFilters.verified = document.getElementById('filter-verified').value;
|
|
currentPage = 1;
|
|
loadSubscribers();
|
|
}
|
|
|
|
/**
|
|
* Change page
|
|
*/
|
|
function changePage(direction) {
|
|
currentPage += direction;
|
|
loadSubscribers();
|
|
}
|
|
|
|
/**
|
|
* View subscriber details
|
|
*/
|
|
async function viewDetails(id) {
|
|
alert(`Subscriber details for ID: ${id}\n(Full implementation would show a modal with complete subscriber information)`);
|
|
}
|
|
|
|
/**
|
|
* Delete subscriber
|
|
*/
|
|
async function deleteSubscriber(id, email) {
|
|
if (!confirm(`Are you sure you want to delete subscription for ${email}?\n\nThis action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const response = await fetch(`/api/newsletter/admin/subscriptions/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete subscriber');
|
|
|
|
alert('Subscriber deleted successfully');
|
|
await loadAll();
|
|
} catch (error) {
|
|
console.error('Error deleting subscriber:', error);
|
|
alert('Failed to delete subscriber. Please try again.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export subscribers as CSV
|
|
*/
|
|
async function exportSubscribers() {
|
|
try {
|
|
const token = localStorage.getItem('admin_token');
|
|
const active = currentFilters.status === 'all' ? 'all' : 'true';
|
|
|
|
const response = await fetch(`/api/newsletter/admin/export?active=${active}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to export subscribers');
|
|
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `newsletter-subscribers-${Date.now()}.csv`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
} catch (error) {
|
|
console.error('Error exporting subscribers:', error);
|
|
alert('Failed to export subscribers. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Logout handled by navbar component
|
|
|
|
/**
|
|
* Format date
|
|
*/
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Escape HTML
|
|
*/
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Initialize on page load
|
|
document.addEventListener('DOMContentLoaded', init);
|