listmonk sync

This commit is contained in:
2025-08-15 11:14:38 -06:00
parent e36610ceed
commit 6ac45f7adf
20 changed files with 3416 additions and 6 deletions

View File

@@ -51,6 +51,10 @@
<span class="nav-icon">🗄️</span>
<span class="nav-text">NocoDB Links</span>
</a>
<a href="#listmonk-links">
<span class="nav-icon">📧</span>
<span class="nav-text">Listmonk Links</span>
</a>
<a href="#start-location" class="active">
<span class="nav-icon">📍</span>
<span class="nav-text">Start Location</span>
@@ -75,6 +79,10 @@
<span class="nav-icon">📊</span>
<span class="nav-text">Convert Data</span>
</a>
<a href="#listmonk">
<span class="nav-icon">📧</span>
<span class="nav-text">Email Lists</span>
</a>
</nav>
<div class="sidebar-footer">
<div id="mobile-admin-info" class="mobile-admin-info mobile-only"></div>
@@ -207,6 +215,86 @@
</div>
</section>
<!-- Listmonk Links Section -->
<section id="listmonk-links" class="admin-section" style="display: none;">
<h2>Listmonk Email Management Links</h2>
<p>Quick access to all Listmonk email management interfaces for campaign communications.</p>
<div class="nocodb-info">
<div class="info-box">
<h4>📧 About Listmonk</h4>
<p>
Listmonk is the email marketing platform that powers campaign communications. Use these links to directly access and manage your email lists, campaigns, and subscribers through the Listmonk interface.
</p>
<p>
The Map application automatically syncs data to Listmonk email lists based on support levels and sign requests.<br>
<a href="https://listmonk.app/docs/" target="_blank" rel="noopener" class="btn btn-link">Listmonk Documentation</a>
<a href="https://github.com/knadh/listmonk" target="_blank" rel="noopener" class="btn btn-link">GitHub Repository</a>
</p>
</div>
</div>
<div class="nocodb-links-container">
<div class="nocodb-cards">
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>🏠 Dashboard</h3>
<span class="nocodb-card-badge">Main</span>
</div>
<p>Main Listmonk dashboard with overview and analytics</p>
<a href="#" id="admin-listmonk-admin-link" class="btn btn-primary" target="_blank">
Open Dashboard
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>📋 Email Lists</h3>
<span class="nocodb-card-badge">Lists</span>
</div>
<p>Manage email lists and subscriber segments</p>
<a href="#" id="admin-listmonk-lists-link" class="btn btn-secondary" target="_blank">
Open Lists
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>📧 Campaigns</h3>
<span class="nocodb-card-badge">Email</span>
</div>
<p>Create and manage email campaigns</p>
<a href="#" id="admin-listmonk-campaigns-link" class="btn btn-secondary" target="_blank">
Open Campaigns
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>👤 Subscribers</h3>
<span class="nocodb-card-badge">People</span>
</div>
<p>View and manage email subscribers</p>
<a href="#" id="admin-listmonk-subscribers-link" class="btn btn-secondary" target="_blank">
Open Subscribers
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>⚙️ Settings</h3>
<span class="nocodb-card-badge">Config</span>
</div>
<p>Configure Listmonk settings and SMTP</p>
<a href="#" id="admin-listmonk-settings-link" class="btn btn-secondary" target="_blank">
Open Settings
</a>
</div>
</div>
</div>
</section>
<!-- Start Location Section -->
<section id="start-location" class="admin-section">
<h2>Map Start Location</h2>
@@ -976,6 +1064,113 @@
</div>
</div>
</section>
<!-- Email Lists (Listmonk) Section -->
<section id="listmonk" class="admin-section" style="display: none;">
<h2>Email List Management (Listmonk)</h2>
<p>Manage email list synchronization with Listmonk for campaign communications.</p>
<div class="listmonk-container">
<div class="listmonk-stats">
<h3>Sync Status</h3>
<div id="sync-status-display">
<div class="status-row">
<span>Connection:</span>
<span id="connection-status">Checking...</span>
</div>
<div class="status-row">
<span>Auto-sync:</span>
<span id="autosync-status">Checking...</span>
</div>
<div class="status-row">
<span>Last Error:</span>
<span id="last-error">None</span>
</div>
</div>
</div>
<div class="listmonk-actions">
<h3>Sync Actions</h3>
<p>Force synchronization of all data to Listmonk email lists:</p>
<div class="sync-buttons">
<button id="sync-locations-btn" class="btn btn-primary">
<span>📍</span> Sync All Locations
</button>
<button id="sync-users-btn" class="btn btn-primary">
<span>👤</span> Sync All Users
</button>
<button id="sync-all-btn" class="btn btn-success">
<span>🔄</span> Sync Everything
</button>
<button id="refresh-status-btn" class="btn btn-secondary">
<span>🔍</span> Refresh Status
</button>
<button id="test-connection-btn" class="btn btn-warning">
<span>🔗</span> Test Connection
</button>
<button id="reinitialize-lists-btn" class="btn btn-info">
<span>🛠️</span> Reinitialize Lists
</button>
</div>
</div>
</div>
<div id="sync-progress" class="sync-progress" style="display: none;">
<h3>Sync Progress</h3>
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-bar-fill" id="sync-progress-bar"></div>
</div>
</div>
<div id="sync-results"></div>
</div>
<div class="listmonk-info">
<h3>About Email List Sync</h3>
<div class="info-grid">
<div class="info-section">
<h4>Automatic Lists Created:</h4>
<ul>
<li><strong>Map Locations - All:</strong> All contacts with email addresses</li>
<li><strong>Map Users - All:</strong> All system users</li>
<li><strong>Support Level 1-4:</strong> Segmented by support level</li>
<li><strong>Has Campaign Sign:</strong> Contacts with signs</li>
<li><strong>Wants Campaign Sign:</strong> Contacts wanting signs (from notes)</li>
</ul>
</div>
<div class="info-section">
<h4>Real-time Sync:</h4>
<ul>
<li>New locations automatically sync to appropriate lists</li>
<li>Updates are reflected immediately in email lists</li>
<li>Deletions remove contacts from all lists</li>
<li>Sync failures are logged and shown in status</li>
</ul>
</div>
<div class="info-section">
<h4>Contact Data Synced:</h4>
<ul>
<li>Email, name, phone, address</li>
<li>Support level, sign status, notes</li>
<li>Geographic coordinates</li>
<li>Source tracking (map_location/map_user)</li>
</ul>
</div>
<div class="info-section">
<h4>Troubleshooting:</h4>
<ul>
<li>Check Listmonk service is running</li>
<li>Verify credentials in environment variables</li>
<li>Use "Test Connection" to verify setup</li>
<li>Check logs for detailed error information</li>
</ul>
</div>
</div>
</div>
</section>
</div>
</div>
@@ -1053,6 +1248,10 @@
<!-- Admin Cuts JavaScript -->
<script src="js/admin-cuts.js"></script>
<!-- Listmonk Status and Admin -->
<script src="js/listmonk-status.js"></script>
<script src="js/listmonk-admin.js"></script>
<!-- Data Convert JavaScript -->
<!-- Admin JavaScript -->
<script src="js/admin.js"></script>

View File

@@ -0,0 +1,353 @@
/* Listmonk Status Indicator */
.listmonk-status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
cursor: help;
margin-left: 8px;
white-space: nowrap;
}
.listmonk-status-indicator.connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.listmonk-status-indicator.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
animation: pulse-error 2s infinite;
}
.listmonk-status-indicator.disabled {
background-color: #e2e3e5;
color: #6c757d;
border: 1px solid #d6d8db;
}
.listmonk-status-indicator.checking {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
@keyframes pulse-error {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.status-icon {
font-size: 14px;
line-height: 1;
}
.status-text {
font-size: 12px;
font-weight: 600;
}
/* Sync Notifications */
.sync-error-notification,
.sync-success-notification {
position: fixed;
top: 80px;
right: 20px;
max-width: 420px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideInRight 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.sync-error-notification {
border: 2px solid #dc3545;
}
.sync-success-notification {
border: 2px solid #28a745;
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-radius: 6px 6px 0 0;
font-size: 14px;
}
.sync-error-notification .notification-header {
background: #dc3545;
color: white;
}
.sync-success-notification .notification-header {
background: #28a745;
color: white;
}
.notification-body {
padding: 16px;
}
.notification-body p {
margin: 8px 0;
color: #333;
font-size: 14px;
line-height: 1.4;
}
.notification-body p:first-child {
margin-top: 0;
}
.notification-body p:last-child {
margin-bottom: 0;
}
.notification-body code {
display: block;
padding: 10px 12px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
color: #dc3545;
font-size: 12px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
margin: 10px 0;
word-break: break-all;
line-height: 1.3;
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
line-height: 1;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.close-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* Admin Panel Listmonk Section */
.listmonk-section {
margin-top: 30px;
}
.listmonk-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.listmonk-stats,
.listmonk-actions {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.listmonk-stats h3,
.listmonk-actions h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 16px;
font-weight: 600;
}
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.status-row:last-child {
border-bottom: none;
}
.status-row span:first-child {
font-weight: 500;
color: #666;
}
.status-row span:last-child {
font-weight: 600;
}
.status-connected {
color: #28a745;
}
.status-error {
color: #dc3545;
}
.status-disabled {
color: #6c757d;
}
.sync-buttons {
display: flex;
flex-direction: column;
gap: 10px;
}
.sync-buttons .btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
text-align: left;
}
.sync-buttons .btn span:first-child {
font-size: 16px;
}
.sync-progress {
background: white;
padding: 20px;
border-radius: 8px;
border: 1px solid #dee2e6;
margin-top: 20px;
}
.progress-bar-container {
margin: 15px 0;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #28a745, #20c997);
border-radius: 10px;
transition: width 0.3s ease;
width: 0%;
}
#sync-results {
margin-top: 15px;
}
.sync-result {
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 10px;
font-size: 14px;
}
.sync-result.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.sync-result.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.sync-result.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
/* Mobile responsive */
@media (max-width: 768px) {
.listmonk-status-indicator {
font-size: 12px;
padding: 4px 8px;
gap: 4px;
}
.status-text {
display: none; /* Hide text on mobile */
}
.sync-error-notification,
.sync-success-notification {
left: 10px;
right: 10px;
max-width: none;
top: 60px;
}
.listmonk-container {
grid-template-columns: 1fr;
gap: 15px;
}
.sync-buttons {
gap: 8px;
}
.sync-buttons .btn {
padding: 12px 16px;
}
}
@media (max-width: 480px) {
.listmonk-status-indicator {
padding: 3px 6px;
}
.status-icon {
font-size: 12px;
}
.notification-body {
padding: 12px;
}
.notification-body code {
font-size: 11px;
padding: 8px 10px;
}
}

View File

@@ -18,3 +18,4 @@
@import url("modules/apartment-marker.css");
@import url("modules/temp-user.css");
@import url("modules/cuts.css");
@import url("modules/listmonk.css");

View File

@@ -444,6 +444,9 @@
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<!-- Listmonk Status -->
<script src="js/listmonk-status.js"></script>
<!-- Application JavaScript -->
<script type="module" src="js/main.js"></script>
</body>

View File

@@ -56,6 +56,7 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
loadWalkSheetConfig();
initializeNocodbLinks();
initializeListmonkLinks();
}, 100);
// Check if URL has a hash to show specific section
@@ -1998,6 +1999,88 @@ function setAdminNocodbLink(elementId, url) {
}
}
// Initialize Listmonk links in admin panel
async function initializeListmonkLinks() {
console.log('Starting Listmonk links initialization...');
try {
// Since we're in the admin panel, the user is already verified as admin
// by the requireAdmin middleware. Let's get the URLs from the server directly.
console.log('Fetching Listmonk URLs for admin panel...');
const configResponse = await fetch('/api/admin/listmonk-urls');
if (!configResponse.ok) {
throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`);
}
const config = await configResponse.json();
console.log('Listmonk URLs received:', config);
if (config.success && config.listmonkUrls) {
console.log('Setting up Listmonk links with URLs:', config.listmonkUrls);
// Set up admin dashboard Listmonk links
setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl);
setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl);
setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl);
setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl);
setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl);
console.log('Listmonk links initialized in admin panel');
} else {
console.warn('No Listmonk URLs found in admin config response');
// Hide the Listmonk section if no URLs are available
const listmonkSection = document.getElementById('listmonk-links');
const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]');
if (listmonkSection) {
listmonkSection.style.display = 'none';
console.log('Hidden Listmonk section');
}
if (listmonkNav) {
listmonkNav.style.display = 'none';
console.log('Hidden Listmonk nav link');
}
}
} catch (error) {
console.error('Error initializing Listmonk links in admin panel:', error);
// Hide the Listmonk section on error
const listmonkSection = document.getElementById('listmonk-links');
const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]');
if (listmonkSection) {
listmonkSection.style.display = 'none';
console.log('Hidden Listmonk section due to error');
}
if (listmonkNav) {
listmonkNav.style.display = 'none';
console.log('Hidden Listmonk nav link due to error');
}
}
}
// Helper function to set admin Listmonk link href
function setAdminListmonkLink(elementId, url) {
console.log(`Setting up Listmonk link: ${elementId} = ${url}`);
const element = document.getElementById(elementId);
if (element && url) {
element.href = url;
element.style.display = 'inline-flex';
// Remove any disabled state
element.classList.remove('btn-disabled');
element.removeAttribute('disabled');
console.log(` Successfully set up ${elementId}`);
} else if (element) {
element.style.display = 'none';
// Add disabled state if no URL
element.classList.add('btn-disabled');
element.setAttribute('disabled', 'disabled');
element.href = '#';
console.log(` Disabled ${elementId} - no URL provided`);
} else {
console.error(` Element not found: ${elementId}`);
}
}
// Shift User Management Functions
let currentShiftData = null;
let allUsers = [];

View File

@@ -0,0 +1,570 @@
/**
* Admin Listmonk Management Functions
* Handles admin interface for email list synchronization
*/
// Global variables for admin Listmonk functionality
let syncInProgress = false;
let syncProgressInterval = null;
/**
* Initialize Listmonk admin section
*/
async function initListmonkAdmin() {
await refreshListmonkStatus();
await loadListmonkStats();
}
/**
* Refresh the Listmonk sync status display
*/
async function refreshListmonkStatus() {
console.log('🔄 Refreshing Listmonk status...');
try {
const response = await fetch('/api/listmonk/status', {
credentials: 'include'
});
console.log('📡 Status response:', response.status, response.statusText);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const status = await response.json();
console.log('📊 Status data:', status);
updateStatusDisplay(status);
// Update global status if available
if (window.listmonkStatus) {
window.listmonkStatus.currentStatus = status;
}
} catch (error) {
console.error('Failed to refresh Listmonk status:', error);
updateStatusDisplay({
enabled: false,
connected: false,
lastError: `Status check failed: ${error.message}`
});
}
}
/**
* Update the status display in the admin panel
*/
function updateStatusDisplay(status) {
console.log('🎨 Updating status display with:', status);
const connectionStatus = document.getElementById('connection-status');
const autosyncStatus = document.getElementById('autosync-status');
const lastError = document.getElementById('last-error');
console.log('🔍 Status elements found:', {
connectionStatus: !!connectionStatus,
autosyncStatus: !!autosyncStatus,
lastError: !!lastError
});
if (connectionStatus) {
if (status.enabled && status.connected) {
connectionStatus.innerHTML = '<span class="status-connected">✅ Connected</span>';
} else if (status.enabled && !status.connected) {
connectionStatus.innerHTML = '<span class="status-error">❌ Failed</span>';
} else {
connectionStatus.innerHTML = '<span class="status-disabled">⭕ Disabled</span>';
}
console.log('✅ Connection status updated:', connectionStatus.innerHTML);
}
if (autosyncStatus) {
if (status.enabled) {
autosyncStatus.innerHTML = '<span class="status-connected">✅ Enabled</span>';
} else {
autosyncStatus.innerHTML = '<span class="status-disabled">⭕ Disabled</span>';
}
console.log('✅ Auto-sync status updated:', autosyncStatus.innerHTML);
}
if (lastError) {
if (status.lastError) {
lastError.innerHTML = `<span class="status-error">${escapeHtml(status.lastError)}</span>`;
} else {
lastError.innerHTML = '<span class="status-connected">None</span>';
}
console.log('✅ Last error updated:', lastError.innerHTML);
}
}
/**
* Load and display Listmonk list statistics
*/
async function loadListmonkStats() {
try {
const response = await fetch('/api/listmonk/stats', {
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log('📊 Stats API response:', data); // Debug log
if (data.success && data.stats) {
// Ensure stats is an array
const statsArray = Array.isArray(data.stats) ? data.stats : [];
console.log('📊 Stats array:', statsArray); // Debug log
displayListStats(statsArray);
} else {
console.warn('No Listmonk stats available:', data.error || 'Unknown error');
displayListStats([]);
}
} catch (error) {
console.error('Failed to load Listmonk stats:', error);
}
}
/**
* Display list statistics in the admin panel
*/
function displayListStats(stats) {
const statusContent = document.getElementById('sync-status-display');
if (!statusContent) return;
console.log('📊 displayListStats called with:', stats, 'Type:', typeof stats); // Debug log
// Ensure stats is an array
const statsArray = Array.isArray(stats) ? stats : [];
console.log('📊 Stats array after conversion:', statsArray, 'Length:', statsArray.length); // Debug log
// Add stats section if it doesn't exist
let statsSection = document.getElementById('listmonk-stats-section');
if (!statsSection) {
statsSection = document.createElement('div');
statsSection.id = 'listmonk-stats-section';
statsSection.innerHTML = '<h4>Email Lists</h4>';
statusContent.appendChild(statsSection);
}
// Clear existing stats
const existingStats = statsSection.querySelector('.stats-list');
if (existingStats) {
existingStats.remove();
}
// Create stats display
const statsList = document.createElement('div');
statsList.className = 'stats-list';
if (statsArray.length === 0) {
statsList.innerHTML = '<p class="status-disabled">No email lists found</p>';
} else {
statsArray.forEach(list => {
const statRow = document.createElement('div');
statRow.className = 'status-row';
statRow.innerHTML = `
<span title="${escapeHtml(list.description || '')}">${escapeHtml(list.name)}</span>
<span class="status-connected">${list.subscriberCount} subscribers</span>
`;
statsList.appendChild(statRow);
});
}
statsSection.appendChild(statsList);
}
/**
* Sync data to Listmonk
* @param {string} type - 'locations', 'users', or 'all'
*/
async function syncToListmonk(type) {
if (syncInProgress) {
showNotification('Sync already in progress', 'warning');
return;
}
syncInProgress = true;
const progressSection = document.getElementById('sync-progress');
const resultsDiv = document.getElementById('sync-results');
const progressBar = document.getElementById('sync-progress-bar');
// Show progress section
if (progressSection) {
progressSection.style.display = 'block';
}
// Reset progress
if (progressBar) {
progressBar.style.width = '0%';
}
if (resultsDiv) {
resultsDiv.innerHTML = '<div class="sync-result info">Starting sync...</div>';
}
// Disable sync buttons
const buttons = document.querySelectorAll('.sync-buttons .btn');
buttons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.6';
});
try {
const response = await fetch(`/api/listmonk/sync/${type}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
// Animate progress to 100%
if (progressBar) {
progressBar.style.width = '100%';
}
// Display results
displaySyncResults(result, resultsDiv);
// Refresh stats after successful sync
setTimeout(() => {
loadListmonkStats();
}, 1000);
} catch (error) {
console.error('Sync failed:', error);
if (resultsDiv) {
resultsDiv.innerHTML = `
<div class="sync-result error">
<strong>Sync Failed:</strong> ${escapeHtml(error.message)}
</div>
`;
}
showNotification('Sync failed: ' + error.message, 'error');
} finally {
syncInProgress = false;
// Re-enable sync buttons
buttons.forEach(btn => {
btn.disabled = false;
btn.style.opacity = '1';
});
}
}
/**
* Display sync results in the admin panel
*/
function displaySyncResults(result, resultsDiv) {
if (!resultsDiv) return;
let html = '';
if (result.success) {
html += `<div class="sync-result success">
<strong>✅ ${escapeHtml(result.message)}</strong>
</div>`;
// Show detailed results for different sync types
if (result.results) {
if (result.results.locations) {
html += formatSyncResults('Locations', result.results.locations);
}
if (result.results.users) {
html += formatSyncResults('Users', result.results.users);
}
// For single type syncs
if (result.results.total !== undefined) {
html += formatSyncResults('Items', result.results);
}
}
showNotification('Sync completed successfully!', 'success');
} else {
html += `<div class="sync-result error">
<strong>❌ Sync Failed:</strong> ${escapeHtml(result.error)}
</div>`;
showNotification('Sync failed', 'error');
}
resultsDiv.innerHTML = html;
}
/**
* Format sync results for display
*/
function formatSyncResults(type, results) {
let html = `<div class="sync-result info">
<strong>${type}:</strong>
${results.success} succeeded, ${results.failed} failed
(${results.total} total)
</div>`;
// Show errors if any
if (results.errors && results.errors.length > 0) {
html += `<div class="sync-result error">
<strong>Errors:</strong>
<ul style="margin: 5px 0 0 20px; font-size: 12px;">`;
// Show only first 5 errors to avoid overwhelming the UI
const errorsToShow = results.errors.slice(0, 5);
errorsToShow.forEach(error => {
html += `<li>${escapeHtml(error.email || error.id)}: ${escapeHtml(error.error)}</li>`;
});
if (results.errors.length > 5) {
html += `<li><em>... and ${results.errors.length - 5} more errors</em></li>`;
}
html += `</ul></div>`;
}
return html;
}
/**
* Test Listmonk connection
*/
async function testListmonkConnection() {
try {
const response = await fetch('/api/listmonk/test-connection', {
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success && result.connected) {
showNotification('✅ Listmonk connection successful!', 'success');
} else {
showNotification('❌ Connection failed: ' + (result.message || 'Unknown error'), 'error');
}
// Refresh status after test
setTimeout(refreshListmonkStatus, 1000);
} catch (error) {
console.error('Connection test failed:', error);
showNotification('❌ Connection test failed: ' + error.message, 'error');
}
}
/**
* Reinitialize Listmonk lists
*/
async function reinitializeListmonk() {
if (!confirm('This will recreate all email lists. Are you sure?')) {
return;
}
try {
const response = await fetch('/api/listmonk/reinitialize', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success) {
showNotification('✅ Lists reinitialized successfully!', 'success');
setTimeout(() => {
refreshListmonkStatus();
loadListmonkStats();
}, 1000);
} else {
showNotification('❌ Failed to reinitialize: ' + result.message, 'error');
}
} catch (error) {
console.error('Reinitialize failed:', error);
showNotification('❌ Reinitialize failed: ' + error.message, 'error');
}
}
/**
* Utility function to escape HTML
*/
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Fallback notification function if showNotification is not available
*/
function showNotification(message, type = 'info') {
// Check if a global showNotification function exists
if (typeof window.showNotification === 'function' && window.showNotification !== showNotification) {
window.showNotification(message, type);
} else {
// Simple fallback - log to console and show alert for errors
console.log(`[${type.toUpperCase()}] ${message}`);
if (type === 'error') {
console.error(message);
alert(`ERROR: ${message}`);
} else if (type === 'warning') {
console.warn(message);
alert(`WARNING: ${message}`);
} else if (type === 'success') {
console.info(`SUCCESS: ${message}`);
}
}
}
// Auto-initialize when admin page loads
function initializeListmonkEventListeners() {
console.log('🔧 Initializing Listmonk event listeners...');
// Sync Locations button
const syncLocationsBtn = document.getElementById('sync-locations-btn');
if (syncLocationsBtn) {
syncLocationsBtn.addEventListener('click', () => {
console.log('📍 Sync Locations clicked');
syncToListmonk('locations');
});
console.log('✅ Sync Locations button listener added');
} else {
console.warn('❌ Sync Locations button not found');
}
// Sync Users button
const syncUsersBtn = document.getElementById('sync-users-btn');
if (syncUsersBtn) {
syncUsersBtn.addEventListener('click', () => {
console.log('👤 Sync Users clicked');
syncToListmonk('users');
});
console.log('✅ Sync Users button listener added');
} else {
console.warn('❌ Sync Users button not found');
}
// Sync All button
const syncAllBtn = document.getElementById('sync-all-btn');
if (syncAllBtn) {
syncAllBtn.addEventListener('click', () => {
console.log('🔄 Sync All clicked');
syncToListmonk('all');
});
console.log('✅ Sync All button listener added');
} else {
console.warn('❌ Sync All button not found');
}
// Refresh Status button
const refreshStatusBtn = document.getElementById('refresh-status-btn');
if (refreshStatusBtn) {
refreshStatusBtn.addEventListener('click', () => {
console.log('🔍 Refresh Status clicked');
refreshListmonkStatus();
});
console.log('✅ Refresh Status button listener added');
} else {
console.warn('❌ Refresh Status button not found');
}
// Test Connection button
const testConnectionBtn = document.getElementById('test-connection-btn');
if (testConnectionBtn) {
testConnectionBtn.addEventListener('click', () => {
console.log('🔗 Test Connection clicked');
testListmonkConnection();
});
console.log('✅ Test Connection button listener added');
} else {
console.warn('❌ Test Connection button not found');
}
// Reinitialize Lists button
const reinitializeListsBtn = document.getElementById('reinitialize-lists-btn');
if (reinitializeListsBtn) {
reinitializeListsBtn.addEventListener('click', () => {
console.log('🛠️ Reinitialize Lists clicked');
reinitializeListmonk();
});
console.log('✅ Reinitialize Lists button listener added');
} else {
console.warn('❌ Reinitialize Lists button not found');
}
}
function attemptInitialization(attempt = 1) {
console.log(`🔄 Attempting Listmonk initialization (attempt ${attempt})`);
// Try to initialize admin and event listeners directly
try {
initListmonkAdmin();
initializeListmonkEventListeners();
console.log('✅ Listmonk initialization completed successfully');
return true;
} catch (error) {
console.warn(`⚠️ Attempt ${attempt} failed:`, error.message);
if (attempt < 5) {
console.log(`⏱️ Retrying in ${attempt * 500}ms...`);
setTimeout(() => attemptInitialization(attempt + 1), attempt * 500);
return false;
} else {
console.error('❌ All initialization attempts failed');
// Still try to initialize event listeners as a fallback
try {
initializeListmonkEventListeners();
console.log('✅ Event listeners initialized as fallback');
} catch (fallbackError) {
console.error('❌ Even fallback initialization failed:', fallbackError);
}
return false;
}
}
}
// Also try a more direct approach - just initialize when the page is ready
function directInitialization() {
console.log('🚀 Direct Listmonk initialization...');
try {
initListmonkAdmin();
initializeListmonkEventListeners();
console.log('✅ Direct initialization successful');
} catch (error) {
console.error('Direct initialization failed:', error);
// Try just the event listeners
try {
initializeListmonkEventListeners();
console.log('✅ Event listeners initialized directly');
} catch (listenerError) {
console.error('❌ Even direct event listener initialization failed:', listenerError);
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
attemptInitialization();
// Also try direct initialization after a delay
setTimeout(directInitialization, 2000);
});
} else {
attemptInitialization();
// Also try direct initialization after a delay
setTimeout(directInitialization, 1000);
}
// Export functions for global use
window.syncToListmonk = syncToListmonk;
window.refreshListmonkStatus = refreshListmonkStatus;
window.testListmonkConnection = testListmonkConnection;
window.reinitializeListmonk = reinitializeListmonk;

View File

@@ -0,0 +1,225 @@
/**
* Listmonk Status Manager
* Handles real-time status monitoring and user notifications for email list sync
*/
class ListmonkStatus {
constructor() {
this.statusElement = null;
this.checkInterval = null;
this.lastErrorShown = null;
this.currentStatus = {
enabled: false,
connected: false,
lastError: null
};
this.init();
}
init() {
// Only initialize if user is authenticated
if (window.currentUser) {
this.createStatusIndicator();
this.startStatusCheck();
}
}
createStatusIndicator() {
// Create status indicator element
const indicator = document.createElement('div');
indicator.id = 'listmonk-status';
indicator.className = 'listmonk-status-indicator checking';
indicator.innerHTML = `
<span class="status-icon">⏳</span>
<span class="status-text">Email Sync</span>
`;
// Find a good place to put it - try header actions first
let container = document.querySelector('.header-actions');
if (!container) {
// Fallback to header nav
container = document.querySelector('.header-nav');
}
if (!container) {
// Fallback to header itself
container = document.querySelector('header');
}
if (container) {
container.appendChild(indicator);
this.statusElement = indicator;
}
}
async checkStatus() {
try {
const response = await fetch('/api/listmonk/status', {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const status = await response.json();
this.updateIndicator(status);
} catch (error) {
console.error('Failed to check Listmonk status:', error);
this.updateIndicator({
enabled: false,
connected: false,
lastError: error.message
});
}
}
updateIndicator(status) {
if (!this.statusElement) return;
const icon = this.statusElement.querySelector('.status-icon');
const text = this.statusElement.querySelector('.status-text');
// Update current status
const wasConnected = this.currentStatus.connected;
this.currentStatus = status;
if (!status.enabled) {
this.statusElement.className = 'listmonk-status-indicator disabled';
icon.innerHTML = '⭕';
text.textContent = 'Sync Off';
this.statusElement.title = 'Email list synchronization is disabled';
} else if (status.connected) {
this.statusElement.className = 'listmonk-status-indicator connected';
icon.innerHTML = '✅';
text.textContent = 'Sync On';
this.statusElement.title = 'Email lists are synchronizing automatically';
// If we just reconnected, show a brief success message
if (!wasConnected && this.lastErrorShown) {
this.showReconnectedNotification();
}
} else {
this.statusElement.className = 'listmonk-status-indicator error';
icon.innerHTML = '❌';
text.textContent = 'Sync Error';
this.statusElement.title = status.lastError || 'Email list sync failed - check configuration';
// Show popup warning if this is a new error or first time seeing this error
const errorKey = status.lastError + (status.lastErrorTime || '');
if ((!this.lastErrorShown || this.lastErrorShown !== errorKey) && status.lastError) {
this.showSyncError(status.lastError);
this.lastErrorShown = errorKey;
}
}
}
showSyncError(error) {
// Don't spam notifications
if (document.querySelector('.sync-error-notification')) {
return;
}
// Create and show error notification
const notification = document.createElement('div');
notification.className = 'sync-error-notification';
notification.innerHTML = `
<div class="notification-header">
<strong>⚠️ Email Sync Error</strong>
<button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
<div class="notification-body">
<p>The email list synchronization is not working:</p>
<code>${this.escapeHtml(error || 'Connection failed')}</code>
<p>New contacts will be saved locally but won't sync to email lists until this is resolved.</p>
<p>Please contact your administrator if this persists.</p>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 15 seconds
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 15000);
}
showReconnectedNotification() {
// Don't show if there's already a notification
if (document.querySelector('.sync-success-notification')) {
return;
}
const notification = document.createElement('div');
notification.className = 'sync-success-notification';
notification.innerHTML = `
<div class="notification-header">
<strong>✅ Email Sync Restored</strong>
<button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
<div class="notification-body">
<p>Email list synchronization is working again. New contacts will now sync automatically.</p>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 5000);
}
startStatusCheck() {
// Check immediately
this.checkStatus();
// Then check every 30 seconds
this.checkInterval = setInterval(() => {
this.checkStatus();
}, 30000);
}
stopStatusCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
// Utility method for HTML escaping
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Manual refresh method for admin panel
async refresh() {
if (this.statusElement) {
this.statusElement.className = 'listmonk-status-indicator checking';
this.statusElement.querySelector('.status-icon').innerHTML = '⏳';
}
await this.checkStatus();
}
// Get current status (for admin panel)
getCurrentStatus() {
return { ...this.currentStatus };
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Only create if we don't already have one
if (!window.listmonkStatus) {
window.listmonkStatus = new ListmonkStatus();
}
});
// Export for use in other modules
window.ListmonkStatus = ListmonkStatus;