feat(notifications): add browser push notifications for calendar events with personal profile settings

This commit is contained in:
2026-09-08 20:43:38 +02:00
parent 8cb7bd8acd
commit ffb340b9ea
7 changed files with 810 additions and 2 deletions
+75
View File
@@ -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
*/
@@ -324,4 +342,61 @@ function verify_oauth_state(string $state): ?int {
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;
}
}
+3
View File
@@ -13,6 +13,9 @@
</div>
</footer>
<!-- Fociz Agenda Notificaties Engine -->
<script src="/assets/js/notifications.js"></script>
<script>
// Lucide Icons initialiseren
if (typeof lucide !== 'undefined') {
+142 -2
View File
@@ -95,8 +95,13 @@ require_once __DIR__ . '/env_badge.php';
</nav>
</div>
<!-- Right side: User & 2FA Status -->
<div class="flex items-center space-x-3">
<!-- Right side: User, Notifications & 2FA Status -->
<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">
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i>
<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>
<span>Koppelingen</span>
</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'): ?>
<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>
@@ -142,6 +151,137 @@ require_once __DIR__ . '/env_badge.php';
</div>
</div>
</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; ?>
<main class="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">