feat(notifications): add browser push notifications for calendar events with personal profile settings
This commit is contained in:
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AJAX API voor Persoonlijke Agenda Notificaties
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../includes/integrations/microsoft.php';
|
||||||
|
|
||||||
|
$user = require_auth();
|
||||||
|
$db = get_db_connection();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Instellingen ophalen
|
||||||
|
if ($action === 'get_settings') {
|
||||||
|
$settings = get_user_notification_settings((int)$user['id']);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'settings' => $settings,
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Instellingen opslaan
|
||||||
|
if ($action === 'save_settings') {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
|
||||||
|
$saved = update_user_notification_settings((int)$user['id'], $input);
|
||||||
|
$settings = get_user_notification_settings((int)$user['id']);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => $saved,
|
||||||
|
'settings' => $settings,
|
||||||
|
'message' => $saved ? 'Notificatievoorkeuren succesvol opgeslagen.' : 'Fout bij opslaan.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Actuele agenda-afspraken van vandaag ophalen voor notificatie-evaluatie
|
||||||
|
if ($action === 'get_events') {
|
||||||
|
$userId = (int)$user['id'];
|
||||||
|
$settings = get_user_notification_settings($userId);
|
||||||
|
|
||||||
|
// Controleer of Outlook gekoppeld is
|
||||||
|
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
|
||||||
|
$stmtInt->execute([$userId]);
|
||||||
|
$outlookInt = $stmtInt->fetch();
|
||||||
|
|
||||||
|
$isLiveOutlook = !empty($outlookInt['access_token']) && empty($outlookInt['is_demo_mode']);
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
if ($isLiveOutlook) {
|
||||||
|
$events = microsoft_fetch_today_events($userId, $today);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als er geen live Outlook events zijn, haal ook taken met tijdblokken op uit Fociz dagplanning
|
||||||
|
$stmtTasks = $db->prepare("
|
||||||
|
SELECT t.id, t.title, t.time_block_start, t.time_block_end, t.source, t.external_url
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN daily_plans dp ON t.daily_plan_id = dp.id
|
||||||
|
WHERE t.user_id = ? AND (dp.plan_date = ? OR t.time_block_start IS NOT NULL) AND t.is_completed = 0
|
||||||
|
ORDER BY t.time_block_start ASC
|
||||||
|
");
|
||||||
|
$stmtTasks->execute([$userId, $today]);
|
||||||
|
$tasks = $stmtTasks->fetchAll();
|
||||||
|
|
||||||
|
foreach ($tasks as $t) {
|
||||||
|
if (!empty($t['time_block_start'])) {
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'task_' . $t['id'],
|
||||||
|
'subject' => $t['title'],
|
||||||
|
'start' => $t['time_block_start'],
|
||||||
|
'end' => $t['time_block_end'] ?: date('H:i', strtotime($t['time_block_start'] . ' +30 minutes')),
|
||||||
|
'start_raw' => "{$today}T{$t['time_block_start']}:00",
|
||||||
|
'location' => 'Fociz Dagplanning',
|
||||||
|
'meeting_url' => $t['external_url'] ?? '/planner.php',
|
||||||
|
'is_all_day' => false,
|
||||||
|
'source' => 'FOCIZ_TASK',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als er nog steeds geen afspraken zijn (en demo mode actief), lever sandbox items zodat er altijd getest kan worden
|
||||||
|
if (empty($events) && (!empty($outlookInt['is_demo_mode']) || !$isLiveOutlook)) {
|
||||||
|
$currentHour = (int)date('H');
|
||||||
|
$nextHour = str_pad((string)(($currentHour + 1) % 24), 2, '0', STR_PAD_LEFT);
|
||||||
|
$events = [
|
||||||
|
[
|
||||||
|
'id' => 'sandbox_meeting_1',
|
||||||
|
'subject' => 'Focus Sync & Dagstart',
|
||||||
|
'start' => "{$nextHour}:00",
|
||||||
|
'end' => "{$nextHour}:30",
|
||||||
|
'start_raw' => "{$today}T{$nextHour}:00:00",
|
||||||
|
'location' => 'Microsoft Teams',
|
||||||
|
'meeting_url' => 'https://teams.microsoft.com',
|
||||||
|
'is_all_day' => false,
|
||||||
|
'source' => 'SANDBOX',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'server_time' => date('H:i:s'),
|
||||||
|
'server_datetime' => date('Y-m-d H:i:s'),
|
||||||
|
'server_timestamp'=> time(),
|
||||||
|
'settings' => $settings,
|
||||||
|
'events' => $events,
|
||||||
|
'count' => count($events),
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['error' => 'Ongeldige actie']);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
/**
|
||||||
|
* Fociz Browser Push Notificaties Engine
|
||||||
|
* Monitort actuele agenda-afspraken en stuurt browser notificaties op basis van persoonlijk profiel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
// 1. Globale state
|
||||||
|
window.focizNotifSettings = {
|
||||||
|
enabled: 1,
|
||||||
|
lead_time_minutes: 5,
|
||||||
|
repeat_count: 2,
|
||||||
|
repeat_interval_minutes: 2,
|
||||||
|
sound_enabled: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedEvents = [];
|
||||||
|
let isFetchingEvents = false;
|
||||||
|
|
||||||
|
// 2. Service Worker Registratie
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js', { scope: '/' })
|
||||||
|
.then(reg => {
|
||||||
|
// Service worker geregistreerd
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// Stille fallback indien niet op HTTPS of lokaal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Web Audio API Chime Synthesizer
|
||||||
|
window.playNotificationChime = function () {
|
||||||
|
try {
|
||||||
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (!AudioContext) return;
|
||||||
|
const ctx = new AudioContext();
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
|
||||||
|
// Noot 1: 587.33 Hz (D5)
|
||||||
|
const osc1 = ctx.createOscillator();
|
||||||
|
const gain1 = ctx.createGain();
|
||||||
|
osc1.type = 'sine';
|
||||||
|
osc1.frequency.setValueAtTime(587.33, now);
|
||||||
|
gain1.gain.setValueAtTime(0.2, now);
|
||||||
|
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
|
||||||
|
osc1.connect(gain1);
|
||||||
|
gain1.connect(ctx.destination);
|
||||||
|
osc1.start(now);
|
||||||
|
osc1.stop(now + 0.35);
|
||||||
|
|
||||||
|
// Noot 2: 880 Hz (A5)
|
||||||
|
const osc2 = ctx.createOscillator();
|
||||||
|
const gain2 = ctx.createGain();
|
||||||
|
osc2.type = 'sine';
|
||||||
|
osc2.frequency.setValueAtTime(880.0, now + 0.12);
|
||||||
|
gain2.gain.setValueAtTime(0.25, now + 0.12);
|
||||||
|
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
|
||||||
|
osc2.connect(gain2);
|
||||||
|
gain2.connect(ctx.destination);
|
||||||
|
osc2.start(now + 0.12);
|
||||||
|
osc2.stop(now + 0.55);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Audio chime kon niet afspelen:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. In-App Toast Banner
|
||||||
|
function showInAppToast(title, body, meetingUrl) {
|
||||||
|
let container = document.getElementById('focizToastContainer');
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = 'focizToastContainer';
|
||||||
|
container.className = 'fixed bottom-5 right-5 z-[9999] flex flex-col space-y-3 max-w-sm pointer-events-none';
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'pointer-events-auto bg-slate-900/95 border border-blue-500/40 text-white p-4 rounded-2xl shadow-2xl backdrop-blur-md flex items-start space-x-3 transform translate-y-4 opacity-0 transition duration-300 ease-out';
|
||||||
|
|
||||||
|
const iconDiv = document.createElement('div');
|
||||||
|
iconDiv.className = 'w-9 h-9 rounded-xl bg-blue-500/20 text-blue-400 flex items-center justify-center flex-shrink-0 mt-0.5 border border-blue-500/30';
|
||||||
|
iconDiv.innerHTML = '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2" stroke-width="2"/><line x1="16" y1="2" x2="16" y2="6" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke-width="2"/></svg>';
|
||||||
|
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'flex-1 min-w-0';
|
||||||
|
|
||||||
|
const titleEl = document.createElement('div');
|
||||||
|
titleEl.className = 'text-xs font-bold text-white truncate';
|
||||||
|
titleEl.textContent = title;
|
||||||
|
|
||||||
|
const bodyEl = document.createElement('div');
|
||||||
|
bodyEl.className = 'text-[11px] text-slate-300 mt-0.5 leading-snug';
|
||||||
|
bodyEl.textContent = body;
|
||||||
|
|
||||||
|
contentDiv.appendChild(titleEl);
|
||||||
|
contentDiv.appendChild(bodyEl);
|
||||||
|
|
||||||
|
if (meetingUrl) {
|
||||||
|
const btn = document.createElement('a');
|
||||||
|
btn.href = meetingUrl;
|
||||||
|
btn.target = '_blank';
|
||||||
|
btn.className = 'inline-flex items-center space-x-1 mt-2 px-2.5 py-1 rounded-lg bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-bold transition';
|
||||||
|
btn.innerHTML = '<span>Direct Deelnemen</span> →';
|
||||||
|
contentDiv.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'text-slate-500 hover:text-white transition -mr-1 -mt-1 p-1';
|
||||||
|
closeBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>';
|
||||||
|
closeBtn.onclick = () => {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-2');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.appendChild(iconDiv);
|
||||||
|
toast.appendChild(contentDiv);
|
||||||
|
toast.appendChild(closeBtn);
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
// Animaties
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.classList.remove('opacity-0', 'translate-y-4');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatisch verdwijnen na 10 seconden
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentElement) {
|
||||||
|
toast.classList.add('opacity-0', 'translate-y-2');
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Browser Notificatie Verzenden
|
||||||
|
window.sendBrowserNotification = async function (title, body, meetingUrl, tag) {
|
||||||
|
if (!window.focizNotifSettings.enabled) return;
|
||||||
|
|
||||||
|
// Geluidssignaal
|
||||||
|
if (window.focizNotifSettings.sound_enabled) {
|
||||||
|
playNotificationChime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-app toast banner
|
||||||
|
showInAppToast(title, body, meetingUrl);
|
||||||
|
|
||||||
|
// HTML5 Browser Push Notificatie
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted') {
|
||||||
|
try {
|
||||||
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||||
|
const reg = await navigator.serviceWorker.ready;
|
||||||
|
if (reg && reg.showNotification) {
|
||||||
|
reg.showNotification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: '/favicon.ico',
|
||||||
|
badge: '/favicon.ico',
|
||||||
|
tag: tag || 'fociz-' + Date.now(),
|
||||||
|
data: { url: meetingUrl || '/planner.php' },
|
||||||
|
requireInteraction: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const n = new Notification(title, {
|
||||||
|
body: body,
|
||||||
|
icon: '/favicon.ico',
|
||||||
|
tag: tag || 'fociz-' + Date.now()
|
||||||
|
});
|
||||||
|
n.onclick = function () {
|
||||||
|
window.focus();
|
||||||
|
if (meetingUrl) {
|
||||||
|
window.open(meetingUrl, '_blank');
|
||||||
|
}
|
||||||
|
n.close();
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Desktop notificatie kon niet worden getoond:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6. Evaluatie van agenda-afspraken
|
||||||
|
function evaluateEvents() {
|
||||||
|
if (!window.focizNotifSettings.enabled) return;
|
||||||
|
if (!cachedEvents || cachedEvents.length === 0) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const leadMs = (parseInt(window.focizNotifSettings.lead_time_minutes) || 5) * 60 * 1000;
|
||||||
|
const repeatCount = Math.max(1, parseInt(window.focizNotifSettings.repeat_count) || 2);
|
||||||
|
const intervalMs = (parseInt(window.focizNotifSettings.repeat_interval_minutes) || 2) * 60 * 1000;
|
||||||
|
|
||||||
|
cachedEvents.forEach(evt => {
|
||||||
|
if (!evt.start_raw && !evt.start) return;
|
||||||
|
|
||||||
|
// Bepaal starttijd in milliseconds
|
||||||
|
let startMs = 0;
|
||||||
|
if (evt.start_raw) {
|
||||||
|
startMs = new Date(evt.start_raw).getTime();
|
||||||
|
} else if (evt.start) {
|
||||||
|
const parts = evt.start.split(':');
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(parseInt(parts[0], 10), parseInt(parts[1], 10), 0, 0);
|
||||||
|
startMs = today.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startMs || isNaN(startMs)) return;
|
||||||
|
|
||||||
|
// Check herhalingen
|
||||||
|
for (let i = 0; i < repeatCount; i++) {
|
||||||
|
// Eerste notificatie: start - leadMs
|
||||||
|
// Volgende notificaties: T_0 + i * intervalMs (tot maximaal starttijd)
|
||||||
|
let triggerMs = startMs - leadMs + (i * intervalMs);
|
||||||
|
if (triggerMs > startMs) {
|
||||||
|
triggerMs = startMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Venster: notificatie is nu actueel (tussen triggerMs en 4 minuten na de start)
|
||||||
|
const isDue = (now >= triggerMs) && (now <= startMs + (4 * 60 * 1000));
|
||||||
|
if (isDue) {
|
||||||
|
const storageKey = `fociz_notif_${evt.id}_rep_${i}`;
|
||||||
|
const alreadySent = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (!alreadySent) {
|
||||||
|
localStorage.setItem(storageKey, String(now));
|
||||||
|
|
||||||
|
const minutesLeft = Math.max(0, Math.round((startMs - now) / 60000));
|
||||||
|
let notifTitle = '';
|
||||||
|
let notifBody = '';
|
||||||
|
|
||||||
|
if (now >= startMs) {
|
||||||
|
notifTitle = `🚀 Nu gestart: ${evt.subject}`;
|
||||||
|
notifBody = `Afspraak is begonnen (${evt.start}) • ${evt.location || 'Teams'}`;
|
||||||
|
} else if (minutesLeft === 0) {
|
||||||
|
notifTitle = `⚡ Begint zo: ${evt.subject}`;
|
||||||
|
notifBody = `Start om ${evt.start} • ${evt.location || 'Teams'}`;
|
||||||
|
} else {
|
||||||
|
notifTitle = `📅 Over ${minutesLeft} min: ${evt.subject}`;
|
||||||
|
notifBody = `Aanvang ${evt.start} - ${evt.end} • ${evt.location || 'Teams'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendBrowserNotification(notifTitle, notifBody, evt.meeting_url, `fociz-event-${evt.id}-${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Data ophalen van backend
|
||||||
|
async function refreshEvents() {
|
||||||
|
if (isFetchingEvents) return;
|
||||||
|
isFetchingEvents = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/notifications.php?action=get_events', { cache: 'no-store' });
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.success) {
|
||||||
|
cachedEvents = data.events || [];
|
||||||
|
if (data.settings) {
|
||||||
|
window.focizNotifSettings = data.settings;
|
||||||
|
updateDotStatus();
|
||||||
|
}
|
||||||
|
evaluateEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Netwerkfout stil opvangen
|
||||||
|
} finally {
|
||||||
|
isFetchingEvents = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Dot indicator bijwerken op de bel in de header
|
||||||
|
function updateDotStatus() {
|
||||||
|
const dot = document.getElementById('focizNotificationDot');
|
||||||
|
if (!dot) return;
|
||||||
|
if ('Notification' in window && Notification.permission === 'granted' && window.focizNotifSettings.enabled) {
|
||||||
|
dot.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
dot.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Modal helpers
|
||||||
|
window.openNotificationModal = function () {
|
||||||
|
const modal = document.getElementById('focizNotificationModal');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
// Vul actuele waarden in formulier
|
||||||
|
const s = window.focizNotifSettings;
|
||||||
|
const enabledCheckbox = document.getElementById('notifEnabledInput');
|
||||||
|
const leadTimeSelect = document.getElementById('notifLeadTimeSelect');
|
||||||
|
const repeatCountSelect = document.getElementById('notifRepeatCountSelect');
|
||||||
|
const intervalSelect = document.getElementById('notifIntervalSelect');
|
||||||
|
const soundCheckbox = document.getElementById('notifSoundInput');
|
||||||
|
|
||||||
|
if (enabledCheckbox) enabledCheckbox.checked = !!parseInt(s.enabled);
|
||||||
|
if (leadTimeSelect) leadTimeSelect.value = String(s.lead_time_minutes);
|
||||||
|
if (repeatCountSelect) repeatCountSelect.value = String(s.repeat_count);
|
||||||
|
if (intervalSelect) intervalSelect.value = String(s.repeat_interval_minutes);
|
||||||
|
if (soundCheckbox) soundCheckbox.checked = !!parseInt(s.sound_enabled);
|
||||||
|
|
||||||
|
updatePermissionBanner();
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.classList.add('flex');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeNotificationModal = function () {
|
||||||
|
const modal = document.getElementById('focizNotificationModal');
|
||||||
|
if (!modal) return;
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.classList.remove('flex');
|
||||||
|
};
|
||||||
|
|
||||||
|
function updatePermissionBanner() {
|
||||||
|
const grantedDiv = document.getElementById('notifPermGranted');
|
||||||
|
const requestDiv = document.getElementById('notifPermRequest');
|
||||||
|
const deniedDiv = document.getElementById('notifPermDenied');
|
||||||
|
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
if (grantedDiv) grantedDiv.classList.add('hidden');
|
||||||
|
if (requestDiv) requestDiv.classList.add('hidden');
|
||||||
|
if (deniedDiv) deniedDiv.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const perm = Notification.permission;
|
||||||
|
if (perm === 'granted') {
|
||||||
|
if (grantedDiv) grantedDiv.classList.remove('hidden');
|
||||||
|
if (requestDiv) requestDiv.classList.add('hidden');
|
||||||
|
if (deniedDiv) deniedDiv.classList.add('hidden');
|
||||||
|
} else if (perm === 'denied') {
|
||||||
|
if (grantedDiv) grantedDiv.classList.add('hidden');
|
||||||
|
if (requestDiv) requestDiv.classList.add('hidden');
|
||||||
|
if (deniedDiv) deniedDiv.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
if (grantedDiv) grantedDiv.classList.add('hidden');
|
||||||
|
if (requestDiv) requestDiv.classList.remove('hidden');
|
||||||
|
if (deniedDiv) deniedDiv.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.focizRequestPermission = async function () {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
alert('Uw browser ondersteunt helaas geen HTML5 Web Notificaties.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await Notification.requestPermission();
|
||||||
|
updatePermissionBanner();
|
||||||
|
updateDotStatus();
|
||||||
|
if (res === 'granted') {
|
||||||
|
sendBrowserNotification('✅ Notificaties Ingeschakeld!', 'Fociz stuurt u nu pushberichten bij actuele afspraken.', null, 'fociz-test-perm');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.focizTestNotification = function () {
|
||||||
|
sendBrowserNotification('🔔 Test Notificatie Fociz', 'Dit is hoe uw agenda-herinnering eruit ziet. Het geluid en de melding werken perfect!', '/planner.php', 'fociz-test-manual');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveNotificationPreferences = async function (e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
const btn = document.getElementById('saveNotifBtn');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
enabled: document.getElementById('notifEnabledInput').checked ? 1 : 0,
|
||||||
|
lead_time_minutes: parseInt(document.getElementById('notifLeadTimeSelect').value, 10),
|
||||||
|
repeat_count: parseInt(document.getElementById('notifRepeatCountSelect').value, 10),
|
||||||
|
repeat_interval_minutes: parseInt(document.getElementById('notifIntervalSelect').value, 10),
|
||||||
|
sound_enabled: document.getElementById('notifSoundInput').checked ? 1 : 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/notifications.php?action=save_settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.success) {
|
||||||
|
window.focizNotifSettings = data.settings;
|
||||||
|
updateDotStatus();
|
||||||
|
const statusMsg = document.getElementById('notifSaveStatus');
|
||||||
|
if (statusMsg) {
|
||||||
|
statusMsg.textContent = 'Voorkeuren succesvol opgeslagen!';
|
||||||
|
statusMsg.classList.remove('hidden');
|
||||||
|
setTimeout(() => statusMsg.classList.add('hidden'), 3000);
|
||||||
|
}
|
||||||
|
setTimeout(() => closeNotificationModal(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Fout bij opslaan van voorkeuren: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 10. Initialisatie
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
refreshEvents();
|
||||||
|
// Polling interval elke 60 seconden
|
||||||
|
setInterval(refreshEvents, 60000);
|
||||||
|
// Evaluatie interval elke 15 seconden
|
||||||
|
setInterval(evaluateEvents, 15000);
|
||||||
|
updateDotStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
`backup_codes` TEXT NULL,
|
`backup_codes` TEXT NULL,
|
||||||
`remember_token` VARCHAR(64) NULL,
|
`remember_token` VARCHAR(64) NULL,
|
||||||
`remember_token_expires_at` DATETIME NULL,
|
`remember_token_expires_at` DATETIME NULL,
|
||||||
|
`notification_settings` TEXT NULL,
|
||||||
`avatar_url` VARCHAR(255) NULL,
|
`avatar_url` VARCHAR(255) NULL,
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
|||||||
@@ -80,6 +80,24 @@ function ensure_remember_token_columns(PDO $db): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garandeert dat de kolom voor notificatie-instellingen aanwezig is in MariaDB
|
||||||
|
*/
|
||||||
|
function ensure_notification_columns(PDO $db): void {
|
||||||
|
static $notifColumnsChecked = false;
|
||||||
|
if ($notifColumnsChecked) return;
|
||||||
|
$notifColumnsChecked = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $db->query("SHOW COLUMNS FROM `users` LIKE 'notification_settings'");
|
||||||
|
if (!$result || $result->rowCount() === 0) {
|
||||||
|
$db->exec("ALTER TABLE `users` ADD COLUMN `notification_settings` TEXT NULL AFTER `remember_token_expires_at`");
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
// Stil negeren indien al aanwezig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Genereert en bewaart een persistente 'Onthoud mij' token in MariaDB en plaatst een veilige cookie
|
* Genereert en bewaart een persistente 'Onthoud mij' token in MariaDB en plaatst een veilige cookie
|
||||||
*/
|
*/
|
||||||
@@ -324,4 +342,61 @@ function verify_oauth_state(string $state): ?int {
|
|||||||
return (int)$payload['uid'];
|
return (int)$payload['uid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haalt de persoonlijke notificatie-instellingen van een gebruiker op
|
||||||
|
*/
|
||||||
|
function get_user_notification_settings(int $userId): array {
|
||||||
|
$db = get_db_connection();
|
||||||
|
ensure_notification_columns($db);
|
||||||
|
|
||||||
|
$defaults = [
|
||||||
|
'enabled' => 1,
|
||||||
|
'lead_time_minutes' => 5, // 0, 2, 5, 10, 15, 30
|
||||||
|
'repeat_count' => 2, // 1, 2, 3, 5
|
||||||
|
'repeat_interval_minutes' => 2, // 1, 2, 3, 5
|
||||||
|
'sound_enabled' => 1, // 0 of 1
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("SELECT notification_settings FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$userId]);
|
||||||
|
$json = $stmt->fetchColumn();
|
||||||
|
if (!empty($json)) {
|
||||||
|
$saved = json_decode($json, true);
|
||||||
|
if (is_array($saved)) {
|
||||||
|
return array_merge($defaults, $saved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("get_user_notification_settings error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slaat de persoonlijke notificatie-instellingen van een gebruiker op
|
||||||
|
*/
|
||||||
|
function update_user_notification_settings(int $userId, array $settings): bool {
|
||||||
|
$db = get_db_connection();
|
||||||
|
ensure_notification_columns($db);
|
||||||
|
|
||||||
|
$current = get_user_notification_settings($userId);
|
||||||
|
$merged = [
|
||||||
|
'enabled' => !empty($settings['enabled']) ? 1 : 0,
|
||||||
|
'lead_time_minutes' => max(0, min(60, (int)($settings['lead_time_minutes'] ?? $current['lead_time_minutes']))),
|
||||||
|
'repeat_count' => max(1, min(10, (int)($settings['repeat_count'] ?? $current['repeat_count']))),
|
||||||
|
'repeat_interval_minutes' => max(1, min(30, (int)($settings['repeat_interval_minutes'] ?? $current['repeat_interval_minutes']))),
|
||||||
|
'sound_enabled' => !empty($settings['sound_enabled']) ? 1 : 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("UPDATE users SET notification_settings = ? WHERE id = ?");
|
||||||
|
return $stmt->execute([json_encode($merged), $userId]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("update_user_notification_settings error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Fociz Agenda Notificaties Engine -->
|
||||||
|
<script src="/assets/js/notifications.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Lucide Icons initialiseren
|
// Lucide Icons initialiseren
|
||||||
if (typeof lucide !== 'undefined') {
|
if (typeof lucide !== 'undefined') {
|
||||||
|
|||||||
+142
-2
@@ -95,8 +95,13 @@ require_once __DIR__ . '/env_badge.php';
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right side: User & 2FA Status -->
|
<!-- Right side: User, Notifications & 2FA Status -->
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<button type="button" onclick="openNotificationModal()" class="relative p-2 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition" title="Meldingen & Agenda Herinneringen">
|
||||||
|
<i data-lucide="bell" class="w-4 h-4"></i>
|
||||||
|
<span id="focizNotificationDot" class="hidden absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="hidden sm:flex items-center space-x-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-emerald-400 text-xs font-medium">
|
<div class="hidden sm:flex items-center space-x-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/25 text-emerald-400 text-xs font-medium">
|
||||||
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i>
|
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i>
|
||||||
<span>2FA Actief</span>
|
<span>2FA Actief</span>
|
||||||
@@ -133,6 +138,10 @@ require_once __DIR__ . '/env_badge.php';
|
|||||||
<i data-lucide="layers" class="w-4 h-4 mb-0.5"></i>
|
<i data-lucide="layers" class="w-4 h-4 mb-0.5"></i>
|
||||||
<span>Koppelingen</span>
|
<span>Koppelingen</span>
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" onclick="openNotificationModal()" class="flex flex-col items-center py-1 text-slate-400 hover:text-white">
|
||||||
|
<i data-lucide="bell" class="w-4 h-4 mb-0.5"></i>
|
||||||
|
<span>Meldingen</span>
|
||||||
|
</button>
|
||||||
<?php if ($currentUser['role'] === 'ADMIN'): ?>
|
<?php if ($currentUser['role'] === 'ADMIN'): ?>
|
||||||
<a href="/users.php" class="flex flex-col items-center py-1 <?= $currentPage === 'users.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>">
|
<a href="/users.php" class="flex flex-col items-center py-1 <?= $currentPage === 'users.php' ? 'text-emerald-400 font-bold' : 'text-slate-400' ?>">
|
||||||
<i data-lucide="users" class="w-4 h-4 mb-0.5"></i>
|
<i data-lucide="users" class="w-4 h-4 mb-0.5"></i>
|
||||||
@@ -142,6 +151,137 @@ require_once __DIR__ . '/env_badge.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Fociz Agenda Notificaties Modal -->
|
||||||
|
<div id="focizNotificationModal" class="hidden fixed inset-0 z-50 bg-black/80 backdrop-blur-sm items-center justify-center p-4">
|
||||||
|
<div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-md overflow-hidden shadow-2xl animate-in fade-in zoom-in-95 duration-200">
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="p-5 border-b border-slate-800/80 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<div class="w-9 h-9 rounded-xl bg-blue-500/10 border border-blue-500/30 text-blue-400 flex items-center justify-center font-bold">
|
||||||
|
<i data-lucide="bell-ring" class="w-4 h-4"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-bold text-white">Agenda Push Meldingen</h3>
|
||||||
|
<p class="text-[11px] text-slate-400">Persoonlijke waarschuwingen bij actuele afspraken</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="closeNotificationModal()" class="text-slate-500 hover:text-white p-1 rounded-lg transition">
|
||||||
|
<i data-lucide="x" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inhoud Formulier -->
|
||||||
|
<form onsubmit="saveNotificationPreferences(event)" class="p-5 space-y-4 text-xs">
|
||||||
|
<!-- Browser Permissie Banner -->
|
||||||
|
<div id="notifPermGranted" class="hidden p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-400 flex-shrink-0"></i>
|
||||||
|
<span class="text-[11px]">Browser pushberichten zijn actief</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="focizTestNotification()" class="px-2 py-1 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 font-semibold text-[10px] transition">
|
||||||
|
Test Melding
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notifPermRequest" class="p-3 rounded-xl bg-blue-500/10 border border-blue-500/30 text-blue-200 flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i data-lucide="bell" class="w-4 h-4 text-blue-400 flex-shrink-0"></i>
|
||||||
|
<span class="text-[11px]">Toestemming browser vereist</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="focizRequestPermission()" class="px-2.5 py-1 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-bold text-[10px] transition shadow">
|
||||||
|
Inschakelen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notifPermDenied" class="hidden p-3 rounded-xl bg-red-500/10 border border-red-500/30 text-red-300 text-[11px] leading-relaxed">
|
||||||
|
<i data-lucide="alert-triangle" class="w-4 h-4 text-red-400 inline mr-1 -mt-0.5"></i>
|
||||||
|
Browser meldingen zijn geblokkeerd. Klik op het slotje links in de browser adresbalk om meldingen toe te staan.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Meldingen Inschakelen -->
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-xl bg-slate-950/60 border border-slate-800">
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-white block">Meldingen Actief</span>
|
||||||
|
<span class="text-[10px] text-slate-400">Automatisch waarschuwen bij naderende afspraken</span>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="notifEnabledInput" class="sr-only peer">
|
||||||
|
<div class="w-9 h-5 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vooraankondigingstijd -->
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-300 mb-1">
|
||||||
|
Hoeveel tijd van tevoren waarschuwen?
|
||||||
|
</label>
|
||||||
|
<select id="notifLeadTimeSelect" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white focus:outline-none focus:border-blue-500">
|
||||||
|
<option value="0">0 minuten (Precies op aanvangstijd)</option>
|
||||||
|
<option value="2">2 minuten van tevoren</option>
|
||||||
|
<option value="5" selected>5 minuten van tevoren (Aanbevolen)</option>
|
||||||
|
<option value="10">10 minuten van tevoren</option>
|
||||||
|
<option value="15">15 minuten van tevoren</option>
|
||||||
|
<option value="30">30 minuten van tevoren</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aantal Herhalingen & Interval Grid -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-300 mb-1">
|
||||||
|
Aantal herhalingen
|
||||||
|
</label>
|
||||||
|
<select id="notifRepeatCountSelect" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white focus:outline-none focus:border-blue-500">
|
||||||
|
<option value="1">1x (Alleen 1e melding)</option>
|
||||||
|
<option value="2" selected>2x (Aanbevolen)</option>
|
||||||
|
<option value="3">3x</option>
|
||||||
|
<option value="5">5x (Intensief)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block font-semibold text-slate-300 mb-1">
|
||||||
|
Herhalingsinterval
|
||||||
|
</label>
|
||||||
|
<select id="notifIntervalSelect" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-3 py-2 text-white focus:outline-none focus:border-blue-500">
|
||||||
|
<option value="1">Elke 1 minuut</option>
|
||||||
|
<option value="2" selected>Elke 2 minuten</option>
|
||||||
|
<option value="3">Elke 3 minuten</option>
|
||||||
|
<option value="5">Elke 5 minuten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Geluidssignaal Toggle -->
|
||||||
|
<div class="flex items-center justify-between p-3 rounded-xl bg-slate-950/60 border border-slate-800">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<i data-lucide="volume-2" class="w-4 h-4 text-blue-400"></i>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-white block">Geluidssignaal</span>
|
||||||
|
<span class="text-[10px] text-slate-400">Zachte bel/chime afspelen bij melding</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="notifSoundInput" checked class="sr-only peer">
|
||||||
|
<div class="w-9 h-5 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="notifSaveStatus" class="hidden text-emerald-400 text-center font-semibold text-[11px] py-1"></div>
|
||||||
|
|
||||||
|
<!-- Knoppen -->
|
||||||
|
<div class="pt-2 flex items-center space-x-2">
|
||||||
|
<button type="button" onclick="closeNotificationModal()" class="flex-1 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 font-semibold transition">
|
||||||
|
Annuleren
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="saveNotifBtn" class="flex-1 py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-bold transition shadow-md shadow-blue-500/20">
|
||||||
|
Voorkeuren Opslaan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<main class="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">
|
<main class="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Fociz Service Worker
|
||||||
|
* Beheert desktop push notificaties en click events voor agenda-afspraken.
|
||||||
|
*/
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Behandel klik op een notificatie
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close();
|
||||||
|
|
||||||
|
const targetUrl = (event.notification.data && event.notification.data.url)
|
||||||
|
? event.notification.data.url
|
||||||
|
: '/planner.php';
|
||||||
|
|
||||||
|
event.waitUntil(
|
||||||
|
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
|
||||||
|
// Als er al een Fociz tabblad open staat, breng die naar voren
|
||||||
|
for (let i = 0; i < windowClients.length; i++) {
|
||||||
|
const client = windowClients[i];
|
||||||
|
if ('focus' in client) {
|
||||||
|
if (targetUrl.startsWith('http') && !targetUrl.includes(self.location.host)) {
|
||||||
|
// Externe meeting link (zoals Teams): open in nieuw tabblad
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(targetUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.navigate(targetUrl);
|
||||||
|
return client.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Geen tabblad open: open een nieuw venster
|
||||||
|
if (clients.openWindow) {
|
||||||
|
return clients.openWindow(targetUrl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user