/** * Popup UI - Extension popup interface * Shows upcoming meetings and settings */ import { StorageManager } from '../utils/storage.js'; import { DurationTracker } from '../utils/duration-tracker.js'; import { ReportGenerator } from '../utils/report-generator.js'; import { CalendarAPI } from '../utils/calendar-api.js'; import { AIInsights } from '../utils/ai-insights.js'; class PopupUI { constructor() { this.events = []; this.settings = null; this.currentFilter = 'all'; // Track active calendar filter } /** * Initialize the popup */ async init() { console.log('PingMeet: Popup initialized'); // Load data await this.loadSettings(); await this.loadEvents(); // Set up event listeners this.setupEventListeners(); this.setupAIEventListeners(); this.setupQuickCreateListeners(); // Update display this.renderEvents(); this.populateSettings(); await this.updateDurationStats(); await this.updateCalendarConnectionStatus(); await this.updateAIStatus(); // Auto-refresh every 30 seconds setInterval(() => { this.loadEvents(); this.updateDurationStats(); }, 30000); } /** * Update duration statistics */ async updateDurationStats() { try { const stats = await DurationTracker.getStatistics(); document.getElementById('todayDuration').textContent = stats.today.formatted; document.getElementById('weekDuration').textContent = stats.week.formatted; } catch (error) { console.error('PingMeet: Error updating duration stats', error); } } /** * Set up button event listeners * Helper to safely add event listener with null check */ safeAddEventListener(elementId, event, handler) { const element = document.getElementById(elementId); if (element) { element.addEventListener(event, handler); } else { console.warn(`PingMeet: Element '${elementId}' not found`); } } setupEventListeners() { this.safeAddEventListener('settingsBtn', 'click', () => this.showSettings()); this.safeAddEventListener('backBtn', 'click', () => this.showMain()); this.safeAddEventListener('saveBtn', 'click', () => this.saveSettings()); this.safeAddEventListener('weeklyReportBtn', 'click', () => this.viewWeeklyReport()); // Calendar filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { const filter = e.currentTarget.dataset.filter; this.setCalendarFilter(filter); }); }); // Calendar connection toggle buttons this.safeAddEventListener('googleToggleBtn', 'click', () => this.toggleGoogleSetup()); this.safeAddEventListener('outlookToggleBtn', 'click', () => this.toggleOutlookSetup()); // Google Calendar connection buttons - Simple mode this.safeAddEventListener('googleSimpleConnectBtn', 'click', () => this.handleGoogleSimpleConnect()); this.safeAddEventListener('googleSimpleDisconnectBtn', 'click', () => this.handleGoogleDisconnect()); // Google Calendar connection buttons - Advanced mode this.safeAddEventListener('googleConnectBtn', 'click', () => this.handleGoogleConnect()); this.safeAddEventListener('googleDisconnectBtn', 'click', () => this.handleGoogleDisconnect()); // Simple/Advanced mode toggles this.safeAddEventListener('showAdvancedGoogleBtn', 'click', () => this.showGoogleAdvancedMode()); this.safeAddEventListener('showSimpleGoogleBtn', 'click', () => this.showGoogleSimpleMode()); // Outlook Calendar buttons - Simple mode this.safeAddEventListener('outlookSimpleConnectBtn', 'click', () => this.handleOutlookSimpleConnect()); this.safeAddEventListener('outlookSimpleDisconnectBtn', 'click', () => this.handleOutlookDisconnect()); // Outlook Calendar buttons - Advanced mode this.safeAddEventListener('outlookConnectBtn', 'click', () => this.handleOutlookConnect()); this.safeAddEventListener('outlookDisconnectBtn', 'click', () => this.handleOutlookDisconnect()); this.safeAddEventListener('outlookOpenBtn', 'click', () => this.openOutlookCalendar()); // Outlook Simple/Advanced mode toggles this.safeAddEventListener('showAdvancedOutlookBtn', 'click', () => this.showOutlookAdvancedMode()); this.safeAddEventListener('showSimpleOutlookBtn', 'click', () => this.showOutlookSimpleMode()); // Footer help link const footerHelp = document.getElementById('footerHelp'); if (footerHelp) { footerHelp.addEventListener('click', (e) => { e.preventDefault(); this.showSettings(); // Scroll to help section after a brief delay setTimeout(() => { const helpSection = document.querySelector('.help-section'); if (helpSection) { helpSection.scrollIntoView({ behavior: 'smooth' }); } }, 100); }); } // Display actual extension ID in redirect URI this.displayRedirectUri(); // Event delegation for decline buttons (dynamically created) document.addEventListener('click', async (e) => { if (e.target.closest('.event-decline-btn')) { const btn = e.target.closest('.event-decline-btn'); const eventId = btn.dataset.eventId; const source = btn.dataset.source; await this.handleDeclineMeeting(eventId, source); } }); } /** * Display the actual redirect URI for OAuth setup */ displayRedirectUri() { const extensionId = chrome.runtime.id; const redirectUri = `https://${extensionId}.chromiumapp.org/`; const googleRedirectUriEl = document.getElementById('redirectUri'); if (googleRedirectUriEl) { googleRedirectUriEl.textContent = redirectUri; } const outlookRedirectUriEl = document.getElementById('outlookRedirectUri'); if (outlookRedirectUriEl) { outlookRedirectUriEl.textContent = redirectUri; } } /** * Toggle Google Calendar setup form visibility */ toggleGoogleSetup() { const setup = document.getElementById('googleSetup'); const toggleBtn = document.getElementById('googleToggleBtn'); setup.classList.toggle('hidden'); toggleBtn.classList.toggle('expanded'); } /** * Toggle Outlook Calendar setup form visibility */ toggleOutlookSetup() { const setup = document.getElementById('outlookSetup'); const toggleBtn = document.getElementById('outlookToggleBtn'); setup.classList.toggle('hidden'); toggleBtn.classList.toggle('expanded'); } /** * Open Outlook Calendar in new tab */ openOutlookCalendar() { chrome.tabs.create({ url: 'https://outlook.office.com/calendar', active: true }); } /** * Handle Outlook Calendar connect */ async handleOutlookConnect() { const btn = document.getElementById('outlookConnectBtn'); const clientIdInput = document.getElementById('outlookClientId'); const clientId = clientIdInput.value.trim(); // Validate Client ID if (!clientId) { alert('Please enter your Microsoft Application (client) ID'); clientIdInput.focus(); return; } // Basic GUID format validation const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!guidRegex.test(clientId)) { alert('Invalid Client ID format. It should be a GUID (e.g., xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)'); clientIdInput.focus(); return; } btn.disabled = true; btn.textContent = 'Connecting...'; try { const result = await CalendarAPI.connectOutlook(clientId); if (result.success) { await this.updateCalendarConnectionStatus(); // Fetch events from API await this.syncCalendarEvents(); } else { alert('Failed to connect: ' + result.error); } } catch (error) { console.error('PingMeet: Outlook connection error', error); alert('Connection error: ' + error.message); } finally { btn.disabled = false; await this.updateCalendarConnectionStatus(); } } /** * Handle Outlook Calendar disconnect */ async handleOutlookDisconnect() { // Get the correct disconnect button based on what's visible const advancedBtn = document.getElementById('outlookDisconnectBtn'); const simpleBtn = document.getElementById('outlookSimpleDisconnectBtn'); const btn = advancedBtn?.classList.contains('hidden') ? simpleBtn : advancedBtn; if (btn) { btn.disabled = true; btn.textContent = 'Disconnecting...'; } try { const result = await CalendarAPI.disconnectOutlook(); if (result.success) { await this.updateCalendarConnectionStatus(); await this.loadEvents(); } else { alert('Failed to disconnect: ' + result.error); } } catch (error) { console.error('PingMeet: Outlook disconnect error', error); alert('Disconnect error: ' + error.message); } finally { if (btn) { btn.disabled = false; } await this.updateCalendarConnectionStatus(); } } /** * Handle Outlook Calendar one-click connect (simple mode) */ async handleOutlookSimpleConnect() { const btn = document.getElementById('outlookSimpleConnectBtn'); btn.disabled = true; btn.innerHTML = ' Connecting...'; try { const result = await CalendarAPI.connectOutlookSimple(); if (result.success) { await this.updateCalendarConnectionStatus(); // Fetch events from API await this.syncCalendarEvents(); } else { // Check if needs advanced mode if (result.needsAdvanced) { alert('One-click Outlook connection is not configured yet. Please use Advanced mode to enter your Azure App credentials.\n\nOnce the developer configures the app, one-click will work for everyone.'); this.showOutlookAdvancedMode(); } else { alert('Failed to connect: ' + result.error); } } } catch (error) { console.error('PingMeet: Simple Outlook connection error', error); alert('Connection error: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = ` Connect with Microsoft`; await this.updateCalendarConnectionStatus(); } } /** * Show Outlook Advanced mode (custom Azure App) */ showOutlookAdvancedMode() { const simpleSection = document.getElementById('outlookSimpleSection'); const advancedSection = document.getElementById('outlookAdvancedSection'); if (simpleSection) simpleSection.classList.add('hidden'); if (advancedSection) advancedSection.classList.remove('hidden'); } /** * Show Outlook Simple mode (one-click connect) */ showOutlookSimpleMode() { const simpleSection = document.getElementById('outlookSimpleSection'); const advancedSection = document.getElementById('outlookAdvancedSection'); if (advancedSection) advancedSection.classList.add('hidden'); if (simpleSection) simpleSection.classList.remove('hidden'); } /** * Handle Google Calendar connect */ async handleGoogleConnect() { const btn = document.getElementById('googleConnectBtn'); const clientIdInput = document.getElementById('googleClientId'); const clientSecretInput = document.getElementById('googleClientSecret'); const clientId = clientIdInput.value.trim(); const clientSecret = clientSecretInput.value.trim(); // Validate Client ID if (!clientId) { alert('Please enter your Google OAuth Client ID'); clientIdInput.focus(); return; } if (!clientId.endsWith('.apps.googleusercontent.com')) { alert('Invalid Client ID. It should end with .apps.googleusercontent.com'); clientIdInput.focus(); return; } // Validate Client Secret if (!clientSecret) { alert('Please enter your Google OAuth Client Secret'); clientSecretInput.focus(); return; } btn.disabled = true; btn.textContent = 'Connecting...'; try { const result = await CalendarAPI.connectGoogle(clientId, clientSecret); if (result.success) { await this.updateCalendarConnectionStatus(); // Fetch events from API await this.syncCalendarEvents(); } else { alert('Failed to connect: ' + result.error); } } catch (error) { console.error('PingMeet: Calendar connection error', error); alert('Connection error: ' + error.message); } finally { btn.disabled = false; await this.updateCalendarConnectionStatus(); } } /** * Handle Google Calendar disconnect */ async handleGoogleDisconnect() { // Get the correct disconnect button based on what's visible const advancedBtn = document.getElementById('googleDisconnectBtn'); const simpleBtn = document.getElementById('googleSimpleDisconnectBtn'); const btn = advancedBtn?.classList.contains('hidden') ? simpleBtn : advancedBtn; if (btn) { btn.disabled = true; btn.textContent = 'Disconnecting...'; } try { const result = await CalendarAPI.disconnectGoogle(); if (result.success) { await this.updateCalendarConnectionStatus(); // Clear API-sourced events await this.loadEvents(); } else { alert('Failed to disconnect: ' + result.error); } } catch (error) { console.error('PingMeet: Calendar disconnect error', error); alert('Disconnect error: ' + error.message); } finally { if (btn) { btn.disabled = false; } await this.updateCalendarConnectionStatus(); } } /** * Handle Google Calendar one-click connect (simple mode) */ async handleGoogleSimpleConnect() { const btn = document.getElementById('googleSimpleConnectBtn'); btn.disabled = true; btn.innerHTML = ' Connecting...'; try { const result = await CalendarAPI.connectGoogleSimple(); if (result.success) { await this.updateCalendarConnectionStatus(); // Fetch events from API await this.syncCalendarEvents(); } else { // Check for specific error about client_id if (result.error.includes('bad client id') || result.error.includes('OAuth2 not granted')) { alert('One-click connection requires setup. Please use Advanced mode to enter your OAuth credentials.\n\nThis is a one-time setup that enables one-click authentication.'); this.showGoogleAdvancedMode(); } else { alert('Failed to connect: ' + result.error); } } } catch (error) { console.error('PingMeet: Simple Google connection error', error); if (error.message.includes('bad client id') || error.message.includes('OAuth2 not granted')) { alert('One-click connection requires setup. Please use Advanced mode to enter your OAuth credentials.\n\nThis is a one-time setup that enables one-click authentication.'); this.showGoogleAdvancedMode(); } else { alert('Connection error: ' + error.message); } } finally { btn.disabled = false; btn.innerHTML = ` Connect with Google`; await this.updateCalendarConnectionStatus(); } } /** * Show Google Advanced mode (custom OAuth credentials) */ showGoogleAdvancedMode() { const simpleSection = document.getElementById('googleSimpleSection'); const advancedSection = document.getElementById('googleAdvancedSection'); if (simpleSection) simpleSection.classList.add('hidden'); if (advancedSection) advancedSection.classList.remove('hidden'); } /** * Show Google Simple mode (one-click connect) */ showGoogleSimpleMode() { const simpleSection = document.getElementById('googleSimpleSection'); const advancedSection = document.getElementById('googleAdvancedSection'); if (advancedSection) advancedSection.classList.add('hidden'); if (simpleSection) simpleSection.classList.remove('hidden'); } /** * Handle declining a meeting */ async handleDeclineMeeting(eventId, source) { if (!confirm('Decline this meeting?')) { return; } try { let result; if (source.includes('google')) { result = await CalendarAPI.declineGoogleEvent(eventId); } else if (source.includes('outlook')) { result = await CalendarAPI.declineOutlookEvent(eventId); } else { alert('Cannot decline this event (unsupported source)'); return; } if (result.success) { // Refresh events to show updated status await this.syncCalendarEvents(); await this.loadEvents(); this.renderEvents(); } else { alert('Failed to decline meeting: ' + result.error); } } catch (error) { console.error('PingMeet: Error declining meeting', error); alert('Error declining meeting: ' + error.message); } } /** * Sync calendar events from connected APIs */ async syncCalendarEvents() { const status = await CalendarAPI.getConnectionStatus(); let allEvents = []; if (status.google) { const result = await CalendarAPI.fetchGoogleEvents(); if (result.success && result.events.length > 0) { allEvents = allEvents.concat(result.events); } } if (status.outlook) { const result = await CalendarAPI.fetchOutlookEvents(); if (result.success && result.events.length > 0) { allEvents = allEvents.concat(result.events); } } if (allEvents.length > 0) { // Send events to service worker await chrome.runtime.sendMessage({ type: 'CALENDAR_EVENTS', events: allEvents, source: 'api' }); await CalendarAPI.updateLastSync(); await this.loadEvents(); } } /** * Update calendar connection status UI */ async updateCalendarConnectionStatus() { const status = await CalendarAPI.getConnectionStatus(); const connection = await chrome.storage.local.get('calendarConnection'); const googleCredentials = await CalendarAPI.getCredentials('google'); const outlookCredentials = await CalendarAPI.getCredentials('outlook'); // Google connection const googleCard = document.getElementById('googleConnection'); const googleStatus = document.getElementById('googleStatus'); // Simple mode buttons const googleSimpleConnectBtn = document.getElementById('googleSimpleConnectBtn'); const googleSimpleDisconnectBtn = document.getElementById('googleSimpleDisconnectBtn'); // Advanced mode buttons const googleConnectBtn = document.getElementById('googleConnectBtn'); const googleDisconnectBtn = document.getElementById('googleDisconnectBtn'); const googleClientIdInput = document.getElementById('googleClientId'); const googleClientSecretInput = document.getElementById('googleClientSecret'); // Populate Google credentials fields if we have stored credentials if (googleCredentials?.clientId && googleClientIdInput) { googleClientIdInput.value = googleCredentials.clientId; } if (googleCredentials?.clientSecret && googleClientSecretInput) { googleClientSecretInput.value = googleCredentials.clientSecret; } // Check auth mode to determine which section to show const authMode = connection.calendarConnection?.google?.authMode; if (status.google) { const email = connection.calendarConnection?.google?.email || 'Connected'; googleCard.classList.add('connected'); googleStatus.textContent = email; googleStatus.classList.add('connected'); // Show disconnect button in the appropriate section if (authMode === 'simple') { // Show simple mode disconnect if (googleSimpleConnectBtn) googleSimpleConnectBtn.classList.add('hidden'); if (googleSimpleDisconnectBtn) { googleSimpleDisconnectBtn.classList.remove('hidden'); googleSimpleDisconnectBtn.textContent = 'Disconnect'; } // Hide advanced section buttons if (googleConnectBtn) googleConnectBtn.classList.add('hidden'); if (googleDisconnectBtn) googleDisconnectBtn.classList.add('hidden'); } else { // Show advanced mode disconnect if (googleConnectBtn) googleConnectBtn.classList.add('hidden'); if (googleDisconnectBtn) { googleDisconnectBtn.classList.remove('hidden'); googleDisconnectBtn.textContent = 'Disconnect'; } // Hide simple section buttons if (googleSimpleConnectBtn) googleSimpleConnectBtn.classList.add('hidden'); if (googleSimpleDisconnectBtn) googleSimpleDisconnectBtn.classList.add('hidden'); // Switch to advanced mode view this.showGoogleAdvancedMode(); } } else { googleCard.classList.remove('connected'); googleStatus.textContent = 'Not connected'; googleStatus.classList.remove('connected'); // Reset simple mode buttons if (googleSimpleConnectBtn) { googleSimpleConnectBtn.classList.remove('hidden'); googleSimpleConnectBtn.innerHTML = ` Connect with Google`; } if (googleSimpleDisconnectBtn) googleSimpleDisconnectBtn.classList.add('hidden'); // Reset advanced mode buttons if (googleConnectBtn) { googleConnectBtn.classList.remove('hidden'); googleConnectBtn.textContent = 'Connect'; } if (googleDisconnectBtn) googleDisconnectBtn.classList.add('hidden'); } // Outlook connection const outlookCard = document.getElementById('outlookConnection'); const outlookStatus = document.getElementById('outlookStatus'); // Simple mode buttons const outlookSimpleConnectBtn = document.getElementById('outlookSimpleConnectBtn'); const outlookSimpleDisconnectBtn = document.getElementById('outlookSimpleDisconnectBtn'); // Advanced mode buttons const outlookConnectBtn = document.getElementById('outlookConnectBtn'); const outlookDisconnectBtn = document.getElementById('outlookDisconnectBtn'); const outlookClientIdInput = document.getElementById('outlookClientId'); // Populate Outlook Client ID field if we have stored credentials if (outlookCredentials?.clientId && outlookClientIdInput) { outlookClientIdInput.value = outlookCredentials.clientId; } // Check auth mode to determine which section to show const outlookAuthMode = connection.calendarConnection?.outlook?.authMode; if (status.outlook) { const email = connection.calendarConnection?.outlook?.email || 'Connected'; outlookCard.classList.add('connected'); outlookStatus.textContent = email; outlookStatus.classList.add('connected'); // Show disconnect button in the appropriate section if (outlookAuthMode === 'simple') { // Show simple mode disconnect if (outlookSimpleConnectBtn) outlookSimpleConnectBtn.classList.add('hidden'); if (outlookSimpleDisconnectBtn) { outlookSimpleDisconnectBtn.classList.remove('hidden'); outlookSimpleDisconnectBtn.textContent = 'Disconnect'; } // Hide advanced section buttons if (outlookConnectBtn) outlookConnectBtn.classList.add('hidden'); if (outlookDisconnectBtn) outlookDisconnectBtn.classList.add('hidden'); } else { // Show advanced mode disconnect if (outlookConnectBtn) outlookConnectBtn.classList.add('hidden'); if (outlookDisconnectBtn) { outlookDisconnectBtn.classList.remove('hidden'); outlookDisconnectBtn.textContent = 'Disconnect'; } // Hide simple section buttons if (outlookSimpleConnectBtn) outlookSimpleConnectBtn.classList.add('hidden'); if (outlookSimpleDisconnectBtn) outlookSimpleDisconnectBtn.classList.add('hidden'); // Switch to advanced mode view this.showOutlookAdvancedMode(); } } else { outlookCard.classList.remove('connected'); outlookStatus.textContent = 'Not connected'; outlookStatus.classList.remove('connected'); // Reset simple mode buttons if (outlookSimpleConnectBtn) { outlookSimpleConnectBtn.classList.remove('hidden'); outlookSimpleConnectBtn.innerHTML = ` Connect with Microsoft`; } if (outlookSimpleDisconnectBtn) outlookSimpleDisconnectBtn.classList.add('hidden'); // Reset advanced mode buttons if (outlookConnectBtn) { outlookConnectBtn.classList.remove('hidden'); outlookConnectBtn.textContent = 'Connect'; } if (outlookDisconnectBtn) outlookDisconnectBtn.classList.add('hidden'); } // Show API warning if neither Google nor Outlook API is connected const apiWarning = document.getElementById('apiWarning'); if (apiWarning) { if (!status.google && !status.outlook) { apiWarning.classList.remove('hidden'); } else { apiWarning.classList.add('hidden'); } } // Update status text to show sync method and frequency const statusText = document.getElementById('statusText'); if (statusText) { if (status.google || status.outlook) { const connectedServices = []; if (status.google) connectedServices.push('Google'); if (status.outlook) connectedServices.push('Outlook'); statusText.textContent = `API Connected (${connectedServices.join(' & ')}) • Syncing every 2 min`; } else { statusText.textContent = 'Monitoring calendar tabs...'; } } } /** * View weekly report */ async viewWeeklyReport() { try { await ReportGenerator.openReport(); } catch (error) { console.error('PingMeet: Error generating report', error); alert('Error generating report. Please try again.'); } } /** * Load settings from storage */ async loadSettings() { this.settings = await StorageManager.getSettings(); } /** * Load events from storage */ async loadEvents() { this.events = await StorageManager.getEvents(); this.renderEvents(); } /** * Set calendar filter and re-render */ setCalendarFilter(filter) { this.currentFilter = filter; // Update active button document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); this.renderEvents(); } /** * Render events list */ renderEvents() { const eventsList = document.getElementById('eventsList'); if (!this.events || this.events.length === 0) { eventsList.innerHTML = '