/** * BMC Hub Reminder Notifications System * * Frontend system for displaying reminder popups using Bootstrap 5 Toast * Polls API endpoint for pending reminders every 30 seconds */ class ReminderNotifications { constructor() { this.pollingInterval = 30000; // 30 seconds this.isPolling = false; this.userId = this._getUserId(); this.toastContainer = null; this.shownTtlMs = 10 * 60 * 1000; // 10 minutes this.shownCache = new Map(); this.priorityColors = { 'low': '#6c757d', 'normal': '#0f4c75', 'high': '#ffc107', 'urgent': '#dc3545' }; this.priorityLabels = { 'low': 'Lav', 'normal': 'Normal', 'high': 'HΓΈj', 'urgent': 'Kritisk' }; this.snoozeOptions = [ { label: '15 min', minutes: 15 }, { label: '30 min', minutes: 30 }, { label: '1 time', minutes: 60 }, { label: '4 timer', minutes: 240 }, { label: '1 dag', minutes: 1440 }, { label: 'Skjul...', minutes: 'custom' } ]; this._initToastContainer(); } /** * Initialize the toast container on page load */ _initToastContainer() { // Check if container already exists let container = document.getElementById('reminder-toast-container'); if (!container) { container = document.createElement('div'); container.id = 'reminder-toast-container'; container.setAttribute('aria-live', 'polite'); container.setAttribute('aria-atomic', 'true'); container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; width: 400px; max-width: 90%; `; document.body.appendChild(container); } this.toastContainer = container; } /** * Start polling for reminders */ startPolling() { if (this.isPolling) return; this.isPolling = true; console.log('πŸ”” Reminder polling started'); // Check immediately first this._checkReminders(); // Then poll at intervals this.pollingIntervalId = setInterval(() => { this._checkReminders(); }, this.pollingInterval); } /** * Stop polling for reminders */ stopPolling() { if (this.pollingIntervalId) { clearInterval(this.pollingIntervalId); } this.isPolling = false; console.log('πŸ›‘ Reminder polling stopped'); } /** * Fetch pending reminders from API */ async _checkReminders() { try { const response = await fetch(`/api/v1/reminders/pending/me?user_id=${this.userId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include' }); if (!response.ok) { if (response.status !== 401) { // Don't log 401 errors console.error(`Reminder check failed: ${response.status}`); } return; } const reminders = await response.json(); if (reminders && reminders.length > 0) { reminders.forEach(reminder => { if (this._shouldShowReminder(reminder)) { this.showReminder(reminder); this._markShown(reminder); } }); } } catch (error) { console.error('❌ Error checking reminders:', error); } } _shouldShowReminder(reminder) { if (!reminder || !reminder.id) return false; const now = Date.now(); // In-memory cache check const lastShown = this.shownCache.get(reminder.id); if (lastShown && (now - lastShown) < this.shownTtlMs) { return false; } // Cross-tab/localStorage check try { const stored = localStorage.getItem(`reminder_shown_${reminder.id}`); if (stored) { const ts = parseInt(stored, 10); if (!Number.isNaN(ts) && (now - ts) < this.shownTtlMs) { return false; } } } catch (e) { // ignore storage errors } return true; } _markShown(reminder) { if (!reminder || !reminder.id) return; const now = Date.now(); this.shownCache.set(reminder.id, now); try { localStorage.setItem(`reminder_shown_${reminder.id}`, String(now)); } catch (e) { // ignore storage errors } } /** * Display a single reminder as a toast */ showReminder(reminder) { const toastId = `reminder-${reminder.id}-${Date.now()}`; const priorityColor = this.priorityColors[reminder.priority] || this.priorityColors['normal']; const priorityLabel = this.priorityLabels[reminder.priority] || 'Info'; // Build snooze dropdown HTML const snoozeOptionsHtml = this.snoozeOptions .map(opt => ``) .join(''); const toastHTML = ` `; // Add toast to container const parser = new DOMParser(); const toastElement = parser.parseFromString(toastHTML, 'text/html').body.firstChild; this.toastContainer.appendChild(toastElement); // Initialize Bootstrap toast const bootstrapToast = new bootstrap.Toast(toastElement, { autohide: false, delay: 10000 }); bootstrapToast.show(); // Attach event listeners this._attachReminderEventListeners(toastElement, reminder.id, toastId); // Auto-remove after dismissal toastElement.addEventListener('hidden.bs.toast', () => { toastElement.remove(); }); } /** * Attach event listeners to reminder toast */ _attachReminderEventListeners(toastElement, reminderId, toastId) { // Snooze buttons toastElement.querySelectorAll('[data-snooze-minutes]').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); const minutes = btn.dataset.snoozeMinutes; if (minutes === 'custom') { // Show custom input toastElement.querySelector(`#custom-snooze-${toastId}`)?.focus(); } else { await this._snoozeReminder(reminderId, parseInt(minutes), toastElement); } }); }); // Custom snooze button toastElement.querySelectorAll('.btn-custom-snooze').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); const input = toastElement.querySelector(`#custom-snooze-${toastId}`); const minutes = parseInt(input.value); if (minutes >= 1 && minutes <= 1440) { await this._snoozeReminder(reminderId, minutes, toastElement); } else { alert('Indtast venligst mellem 1 og 1440 minutter'); } }); }); // Dismiss button toastElement.querySelectorAll('[data-dismiss-reminder]').forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); await this._dismissReminder(reminderId, toastElement); }); }); } /** * Snooze a reminder */ async _snoozeReminder(reminderId, minutes, toastElement) { try { const response = await fetch(`/api/v1/sag/reminders/${reminderId}/snooze`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include', body: JSON.stringify({ duration_minutes: minutes }) }); if (response.ok) { console.log(`βœ… Reminder ${reminderId} snoozed for ${minutes} minutes`); // Hide the toast const toast = bootstrap.Toast.getInstance(toastElement); if (toast) toast.hide(); } else { console.error(`Failed to snooze reminder: ${response.status}`); alert('Kunne ikke slumre reminder'); } } catch (error) { console.error('❌ Error snoozing reminder:', error); alert('Fejl ved slumre'); } } /** * Dismiss a reminder permanently */ async _dismissReminder(reminderId, toastElement) { try { const response = await fetch(`/api/v1/sag/reminders/${reminderId}/dismiss`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, credentials: 'include', body: JSON.stringify({ reason: 'Dismissed by user' }) }); if (response.ok) { console.log(`βœ… Reminder ${reminderId} dismissed`); // Hide the toast const toast = bootstrap.Toast.getInstance(toastElement); if (toast) toast.hide(); } else { console.error(`Failed to dismiss reminder: ${response.status}`); alert('Kunne ikke afvise reminder'); } } catch (error) { console.error('❌ Error dismissing reminder:', error); alert('Fejl ved afvisning'); } } /** * Extract user ID from auth token or page context */ _getUserId() { // Try to get from localStorage/sessionStorage if available const token = localStorage.getItem('access_token') || sessionStorage.getItem('access_token'); if (token) { try { // Decode JWT payload const payload = JSON.parse(atob(token.split('.')[1])); return payload.sub || payload.user_id; } catch (e) { console.warn('Could not decode token for user_id'); } } // Try to get from page meta tag const metaTag = document.querySelector('meta[name="user-id"]'); if (metaTag) { return metaTag.getAttribute('content'); } console.warn('⚠️ Could not determine user_id for reminders'); return null; } } // Global instance let reminderNotifications = null; /** * Initialize reminder system when DOM is ready */ function initReminderNotifications() { if (!reminderNotifications) { reminderNotifications = new ReminderNotifications(); // Only start polling if user is authenticated if (reminderNotifications.userId) { reminderNotifications.startPolling(); console.log('βœ… Reminder system initialized'); } else { console.warn('⚠️ Reminder system not initialized - user not authenticated'); } } } // Auto-init when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initReminderNotifications); } else { initReminderNotifications(); } // Handle page visibility (pause polling when tab not visible) document.addEventListener('visibilitychange', () => { if (reminderNotifications) { if (document.hidden) { reminderNotifications.stopPolling(); console.log('πŸ’€ Reminder polling paused (tab hidden)'); } else { reminderNotifications.startPolling(); console.log('πŸ‘€ Reminder polling resumed'); } } }); // Export for manual control if needed window.ReminderNotifications = ReminderNotifications;