From fb38c42fe95372678a7e7058af568d2cdc5621f0 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Thu, 10 Sep 2026 14:16:45 +0200 Subject: [PATCH] feat(planner): add widget layout saving and scratchpad note functionality feat(notifications): implement real-time live sync for mail, Jira, and calendar updates fix(database): add new columns for widget layout and last check timestamps fix(schema): update users table schema to include new fields for widget layout and last checks feat(microsoft): enhance Microsoft integration with demo email data and inbox fetching feat(jira): add listener for live updates on Jira tickets --- api/planner.php | 34 ++ assets/js/notifications.js | 104 +++++- config/database.php | 3 + config/schema.sql | 3 + includes/header.php | 18 +- includes/integrations/microsoft.php | 476 +++++++++++++++++++++++++++- jira.php | 5 + 7 files changed, 636 insertions(+), 7 deletions(-) diff --git a/api/planner.php b/api/planner.php index d192fb6..61f5bc1 100644 --- a/api/planner.php +++ b/api/planner.php @@ -32,6 +32,40 @@ try { exit; } + // 1b. Widget layout opslaan + if ($action === 'save_widget_layout') { + $data = json_decode(file_get_contents('php://input'), true); + $layout = $data['layout'] ?? null; + + if (!is_array($layout)) { + echo json_encode(['success' => false, 'error' => 'Ongeldig layout formaat']); + exit; + } + + $stmt = $db->prepare("UPDATE users SET fociz_widget_layout = ? WHERE id = ?"); + $stmt->execute([json_encode($layout), $user['id']]); + + echo json_encode(['success' => true, 'message' => 'Widget indeling succesvol opgeslagen']); + exit; + } + + // 1c. Scratchpad snelle notities opslaan + if ($action === 'save_scratchpad') { + $data = json_decode(file_get_contents('php://input'), true); + $date = $data['date'] ?? $today; + $notes = $data['notes'] ?? ''; + + $stmt = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date, notes) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE notes = VALUES(notes) + "); + $stmt->execute([$user['id'], $date, $notes]); + + echo json_encode(['success' => true]); + exit; + } + // 2. Taak toevoegen if ($action === 'add_task') { $data = json_decode(file_get_contents('php://input'), true); diff --git a/assets/js/notifications.js b/assets/js/notifications.js index cc41147..96862b5 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -408,13 +408,113 @@ } }; + // 7b. Real-time Live Sync (Mail, Jira & Agenda) + let lastKnownMailId = localStorage.getItem('fociz_last_mail_id') || ''; + let lastKnownJiraKey = localStorage.getItem('fociz_last_jira_key') || ''; + let lastCalendarHash = ''; + let isLiveSyncRunning = false; + let isInitialLiveSync = true; + + async function checkLiveSync() { + if (isLiveSyncRunning) return; + isLiveSyncRunning = true; + try { + const url = `/api/live_sync.php?last_mail_id=${encodeURIComponent(lastKnownMailId)}&last_jira_key=${encodeURIComponent(lastKnownJiraKey)}&calendar_hash=${encodeURIComponent(lastCalendarHash)}`; + const res = await fetch(url, { cache: 'no-store' }); + if (res.status === 401) return; + if (res.ok) { + const data = await res.json(); + if (data && data.success) { + // Update mail badge in navigatie + const mailBadge = document.getElementById('navMailBadge'); + if (mailBadge && typeof data.mail?.unread_count !== 'undefined') { + if (data.mail.unread_count > 0) { + mailBadge.innerText = data.mail.unread_count; + mailBadge.classList.remove('hidden'); + } else { + mailBadge.classList.add('hidden'); + } + } + + // E-mail melding + if (!isInitialLiveSync && data.mail?.has_new && data.mail.new_mail) { + const m = data.mail.new_mail; + sendBrowserNotification( + `✉️ Nieuwe mail: ${m.from_name}`, + `${m.subject} • ${m.received_human || 'Zojuist'}`, + m.web_link || '/mail.php', + `fociz-mail-${m.id}` + ); + } + + // Jira ticket melding + if (!isInitialLiveSync && data.jira?.has_new && data.jira.new_issue) { + const j = data.jira.new_issue; + sendBrowserNotification( + `⚡ Nieuw Jira ticket: [${j.key}]`, + `${j.summary} (${j.priority})`, + j.url || '/jira.php', + `fociz-jira-${j.key}` + ); + } + + // Dispatch custom event voor actieve pagina's + if (data.mail?.has_new || (data.mail?.latest_id && data.mail.latest_id !== lastKnownMailId)) { + window.dispatchEvent(new CustomEvent('fociz:mail-updated', { detail: data.mail })); + } + if (data.jira?.has_new || (data.jira?.newest_key && data.jira.newest_key !== lastKnownJiraKey)) { + window.dispatchEvent(new CustomEvent('fociz:jira-updated', { detail: data.jira })); + } + if (data.calendar?.has_changes) { + window.dispatchEvent(new CustomEvent('fociz:calendar-updated', { detail: data.calendar })); + } + + if (data.mail?.latest_id) { + lastKnownMailId = data.mail.latest_id; + localStorage.setItem('fociz_last_mail_id', lastKnownMailId); + } + if (data.jira?.newest_key) { + lastKnownJiraKey = data.jira.newest_key; + localStorage.setItem('fociz_last_jira_key', lastKnownJiraKey); + } + if (data.calendar?.hash) { + lastCalendarHash = data.calendar.hash; + } + + isInitialLiveSync = false; + } + } + } catch(e) { + // Stil negeren bij netwerkvertraging + } finally { + isLiveSyncRunning = false; + } + } + // 10. Initialisatie document.addEventListener('DOMContentLoaded', () => { refreshEvents(); - // Polling interval elke 60 seconden + checkLiveSync(); + + // Polling interval elke 60 seconden voor agenda evaluatie setInterval(refreshEvents, 60000); - // Evaluatie interval elke 15 seconden + // Evaluatie interval elke 15 seconden voor agenda popups setInterval(evaluateEvents, 15000); + // Live sync interval elke 12 seconden voor instant mail, Jira & calendar updates + setInterval(checkLiveSync, 12000); + + window.addEventListener('focus', () => { + checkLiveSync(); + refreshEvents(); + }); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + checkLiveSync(); + refreshEvents(); + } + }); + updateDotStatus(); }); diff --git a/config/database.php b/config/database.php index 88ea16d..3c72702 100644 --- a/config/database.php +++ b/config/database.php @@ -148,6 +148,9 @@ function ensure_tables_exist(PDO $pdo): void { "ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme", "ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled", "ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url", + "ALTER TABLE users ADD COLUMN fociz_widget_layout TEXT NULL AFTER default_jira_filter_code", + "ALTER TABLE users ADD COLUMN last_mail_check_at DATETIME NULL AFTER fociz_widget_layout", + "ALTER TABLE users ADD COLUMN last_jira_check_at DATETIME NULL AFTER last_mail_check_at", "ALTER TABLE board_card_schedules ADD UNIQUE KEY uk_sched_card (card_id)", "CREATE TABLE IF NOT EXISTS `password_resets` (`id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(191) NOT NULL, `token` VARCHAR(64) NOT NULL UNIQUE, `expires_at` DATETIME NOT NULL, `used_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`email`), INDEX (`token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", "CREATE TABLE IF NOT EXISTS `jira_queue_filters` (`id` INT AUTO_INCREMENT PRIMARY KEY, `filter_code` VARCHAR(50) NOT NULL UNIQUE, `filter_name` VARCHAR(120) NOT NULL, `project_key` VARCHAR(50) NULL, `jql_clause` TEXT NOT NULL, `description` VARCHAR(255) NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `sort_order` INT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", diff --git a/config/schema.sql b/config/schema.sql index e855a8c..afd353e 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -15,6 +15,9 @@ CREATE TABLE IF NOT EXISTS `users` ( `notification_settings` TEXT NULL, `avatar_url` VARCHAR(255) NULL, `default_jira_filter_code` VARCHAR(50) NULL, + `fociz_widget_layout` TEXT NULL, + `last_mail_check_at` DATETIME NULL, + `last_jira_check_at` DATETIME NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/header.php b/includes/header.php index 499c30b..fc0a5e6 100644 --- a/includes/header.php +++ b/includes/header.php @@ -161,8 +161,14 @@ require_once __DIR__ . '/env_badge.php';