tractatus/public/js/admin/newsletter-management.js
TheFlow c0bc35f2de fix(newsletter): convert ObjectId to string in DELETE button data attributes
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>
2025-10-24 20:24:54 +13:00

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);