Files
Fociz/profile.php
T
2026-09-11 22:20:29 +02:00

1551 lines
99 KiB
PHP

<?php
/**
* Fociz Persoonlijk Profiel & Koppelingen Portaal (Voor alle gebruikers: Beheerders & Collega's)
*/
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php';
// Iedere ingelogde gebruiker heeft toegang tot zijn/haar eigen profiel
$user = require_auth();
$userId = (int)$user['id'];
$db = get_db_connection();
$message = null;
$messageType = 'success';
if (!empty($_GET['success'])) {
$message = $_GET['success'];
$messageType = 'success';
} elseif (!empty($_GET['error'])) {
$message = $_GET['error'];
$messageType = 'error';
}
// POST Verwerking VÓÓR headers zodat redirects en mutaties soepel verlopen
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!verify_csrf_token($_POST['csrf_token'] ?? null)) {
$message = "Beveiligingsfout: CSRF-token ongeldig of verlopen. Ververs de pagina en probeer opnieuw.";
$messageType = 'error';
} else {
$action = $_POST['action'] ?? '';
// 1. Outlook koppelen via OAuth
if ($action === 'connect_outlook') {
$appConfig = microsoft_get_app_config();
if (!$appConfig['is_configured']) {
$message = ($user['role'] === 'ADMIN')
? "Configureer eerst de Application (Client) ID en het Client Secret in het Systeembeheer."
: "Microsoft 365 is nog niet ingesteld door de beheerder van uw organisatie. Vraag uw beheerder om de koppeling te activeren.";
$messageType = 'error';
} else {
header("Location: /auth/oauth_microsoft.php");
exit;
}
}
// 2. Outlook ontkoppelen (alleen persoonlijke tokens van deze gebruiker wissen)
elseif ($action === 'disconnect_outlook') {
microsoft_disconnect($userId);
header('Location: /profile.php?success=' . urlencode('Uw Microsoft Outlook agenda is succesvol ontkoppeld.'));
exit;
}
// 2b. Outlook synchronisatievoorkeuren opslaan
elseif ($action === 'save_outlook_settings') {
$syncInterval = (int)($_POST['sync_interval_seconds'] ?? 30);
$stmtExisting = $db->prepare("SELECT settings FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$settings = json_decode($existing['settings'] ?? '{}', true) ?: [];
$settings['sync_interval_seconds'] = $syncInterval;
$stmtUp = $db->prepare("UPDATE integrations SET settings = ? WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtUp->execute([json_encode($settings), $userId]);
$message = "Uw Microsoft Outlook synchronisatievoorkeur is succesvol opgeslagen.";
$messageType = 'success';
}
// 3. Jira Opslaan & Testen
elseif ($action === 'save_jira') {
$companyDomain = jira_get_company_domain();
$rawDomain = trim($_POST['jira_domain'] ?? '');
$jiraDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : $companyDomain;
$jiraEmail = trim($_POST['jira_email'] ?? '');
$jiraToken = trim($_POST['jira_api_token'] ?? '');
// Haal bestaande instellingen op
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtExisting->execute([$userId]);
$existing = $stmtExisting->fetch();
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
if (($jiraToken === '' || $jiraToken === '••••••••••••••••') && !empty($existingSettings['jira_api_token']) && $existingSettings['jira_api_token'] !== '••••••••••••••••') {
$jiraToken = fociz_decrypt($existingSettings['jira_api_token']);
}
$accountName = $existingSettings['account_name'] ?? '';
$accountId = $existingSettings['account_id'] ?? '';
if (!empty($jiraDomain) && !empty($jiraEmail) && !empty($jiraToken)) {
$testRes = jira_test_connection($jiraDomain, $jiraEmail, $jiraToken);
if (!$testRes['success']) {
$message = "Jira Verbindingsfout: " . $testRes['error'];
$messageType = 'error';
} else {
$accountName = $testRes['user']['displayName'] ?? 'Jira Gebruiker';
$accountId = $testRes['user']['accountId'] ?? '';
$message = "Verbinding met Jira geslaagd! Ingelogd als: {$accountName}";
$messageType = 'success';
}
} else {
$message = "Jira instellingen opgeslagen.";
$messageType = 'success';
}
$newSettings = [
'jira_domain' => $jiraDomain,
'jira_email' => $jiraEmail,
'jira_api_token' => fociz_encrypt($jiraToken),
'account_name' => $accountName,
'account_id' => $accountId,
];
$stmt = $db->prepare("
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
VALUES (?, 'JIRA', 1, 0, ?, NOW())
ON DUPLICATE KEY UPDATE
is_demo_mode = 0,
settings = VALUES(settings),
last_sync_at = NOW()
");
$stmt->execute([$userId, json_encode($newSettings)]);
unset($_SESSION["jira_issues_user_{$userId}"]);
}
// 4. Jira ontkoppelen
elseif ($action === 'disconnect_jira') {
jira_disconnect($userId);
header('Location: /profile.php?success=' . urlencode('Atlassian Jira is succesvol ontkoppeld.'));
exit;
}
// 4b. Persoonlijke Google Gemini / AI instellingen opslaan
elseif ($action === 'save_personal_ai') {
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
$aiProvider = trim($_POST['ai_provider'] ?? 'gemini');
$aiModel = trim($_POST['ai_model'] ?? 'gemini-1.5-pro');
$apiKeyIn = trim($_POST['ai_api_key'] ?? '');
// Haal eventueel bestaande integratie op
$stmtEx = $db->prepare("SELECT id, access_token, settings FROM integrations WHERE user_id = ? AND provider IN ('GEMINI', 'AI') LIMIT 1");
$stmtEx->execute([$userId]);
$existing = $stmtEx->fetch();
$tokenToSave = $existing['access_token'] ?? null;
if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') {
$tokenToSave = fociz_encrypt($apiKeyIn);
}
$settingsJson = json_encode([
'provider' => $aiProvider,
'model' => $aiModel,
]);
if ($existing) {
$stmtUp = $db->prepare("UPDATE integrations SET provider = 'GEMINI', is_enabled = ?, access_token = ?, settings = ?, last_sync_at = NOW() WHERE id = ?");
$stmtUp->execute([$aiEnabled, $tokenToSave, $settingsJson, $existing['id']]);
} else {
$stmtIn = $db->prepare("INSERT INTO integrations (user_id, provider, is_enabled, access_token, settings, last_sync_at) VALUES (?, 'GEMINI', ?, ?, ?, NOW())");
$stmtIn->execute([$userId, $aiEnabled, $tokenToSave, $settingsJson]);
}
$message = "Uw persoonlijke AI instellingen zijn succesvol opgeslagen.";
$messageType = 'success';
}
// 4c. Persoonlijke AI sleutel ontkoppelen / verwijderen
elseif ($action === 'disconnect_personal_ai') {
$stmtDel = $db->prepare("DELETE FROM integrations WHERE user_id = ? AND provider IN ('GEMINI', 'AI')");
$stmtDel->execute([$userId]);
$message = "Uw persoonlijke AI sleutel is ontkoppeld. Fociz gebruikt nu de centrale instellingen van de organisatie.";
$messageType = 'success';
}
// 5. Notificatievoorkeuren opslaan
elseif ($action === 'save_notifications') {
$settings = [
'enabled' => isset($_POST['notif_enabled']) ? 1 : 0,
'lead_time_minutes' => (int)($_POST['lead_time_minutes'] ?? 5),
'repeat_count' => (int)($_POST['repeat_count'] ?? 2),
'repeat_interval_minutes' => (int)($_POST['repeat_interval_minutes'] ?? 2),
'sound_enabled' => isset($_POST['sound_enabled']) ? 1 : 0,
];
update_user_notification_settings($userId, $settings);
$message = "Uw persoonlijke notificatievoorkeuren zijn succesvol opgeslagen.";
$messageType = 'success';
}
// 6. Profielnaam bijwerken
elseif ($action === 'update_profile') {
$name = trim($_POST['name'] ?? '');
if (strlen($name) < 2) {
$message = "Voer een geldige naam in van minimaal 2 tekens.";
$messageType = 'error';
} else {
$stmt = $db->prepare("UPDATE users SET name = ? WHERE id = ?");
$stmt->execute([$name, $userId]);
$_SESSION['user_name'] = $name;
$user['name'] = $name;
$message = "Uw profielnaam is succesvol bijgewerkt.";
$messageType = 'success';
}
}
// 7. Wachtwoord wijzigen
elseif ($action === 'change_password') {
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$stmtU = $db->prepare("SELECT password_hash FROM users WHERE id = ?");
$stmtU->execute([$userId]);
$storedHash = $stmtU->fetchColumn();
if (!password_verify($currentPassword, $storedHash)) {
$message = "Uw huidige wachtwoord is niet correct ingevoerd.";
$messageType = 'error';
} elseif (($pwErr = fociz_validate_password($newPassword)) !== null) {
$message = $pwErr;
$messageType = 'error';
} elseif ($newPassword !== $confirmPassword) {
$message = "Het nieuwe wachtwoord en de bevestiging komen niet overeen.";
$messageType = 'error';
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
$stmtUp = $db->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$stmtUp->execute([$newHash, $userId]);
// SOC 2 CC6.1 / ISO 27001 A.9.4.2: Trek overige sessies in bij wachtwoordwijziging
fociz_revoke_other_sessions($userId, session_id());
fociz_log_audit($userId, 'PASSWORD_CHANGED', 'user', $userId, ['method' => 'profile'], 'SUCCESS', $user['email']);
$message = "Uw wachtwoord is succesvol gewijzigd. Alle overige sessies op andere apparaten zijn uit veiligheidsoverwegingen beëindigd.";
$messageType = 'success';
}
}
// 7b. Alle overige sessies op afstand beëindigen (SOC 2 / ISO 27001)
elseif ($action === 'revoke_other_sessions') {
$count = fociz_revoke_other_sessions($userId, session_id());
$message = "Succesvol {$count} andere actieve sessie(s) op afstand beëindigd.";
$messageType = 'success';
}
// 8. Standaard avatar kiezen uit bibliotheek
elseif ($action === 'select_preset_avatar') {
$preset = trim($_POST['avatar_preset'] ?? '');
if (preg_match('/^\/assets\/avatars\/avatar-(0[1-9]|1[0-9]|2[0-4])\.svg$/', $preset)) {
$stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
$stmtUp->execute([$preset, $userId]);
$_SESSION['user_avatar_url'] = $preset;
$user['avatar_url'] = $preset;
$message = "Uw avatar is succesvol bijgewerkt!";
$messageType = 'success';
} else {
$message = "Ongeldige avatar geselecteerd.";
$messageType = 'error';
}
}
// 9. Eigen profielfoto uploaden
elseif ($action === 'upload_custom_avatar') {
if (!isset($_FILES['avatar_file']) || $_FILES['avatar_file']['error'] !== UPLOAD_ERR_OK) {
$message = "Selecteer een geldige afbeelding om te uploaden.";
$messageType = 'error';
} else {
$file = $_FILES['avatar_file'];
$maxSize = 4 * 1024 * 1024; // 4MB
if ($file['size'] > $maxSize) {
$message = "De afbeelding is te groot (maximaal 4MB toegestaan).";
$messageType = 'error';
} else {
$processed = fociz_process_avatar_upload($file, $userId);
if (!$processed['success']) {
$message = $processed['error'] ?? "Fout bij verwerken van de afbeelding.";
$messageType = 'error';
} else {
$avatarUri = $processed['data_uri'];
$stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
$stmtUp->execute([$avatarUri, $userId]);
$_SESSION['user_avatar_url'] = $avatarUri;
$user['avatar_url'] = $avatarUri;
$message = "Uw profielfoto is succesvol geüpload en permanent bewaard!";
$messageType = 'success';
}
}
}
}
// 10. Avatar verwijderen
elseif ($action === 'remove_avatar') {
$stmtUp = $db->prepare("UPDATE users SET avatar_url = NULL WHERE id = ?");
$stmtUp->execute([$userId]);
unset($_SESSION['user_avatar_url']);
$user['avatar_url'] = null;
$message = "Uw avatar is teruggezet naar de standaard weergave met uw initialen.";
$messageType = 'success';
}
}
}
// Actuele gegevens ophalen
$stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$userId]);
$userProfile = $stmtUser->fetch();
if ($userProfile) {
$userProfile['avatar_url'] = fociz_get_valid_avatar_url($userProfile['avatar_url'] ?? null, $userId);
}
// Outlook status ophalen
$stmtOutlook = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
$stmtOutlook->execute([$userId]);
$outlookRow = $stmtOutlook->fetch();
$isOutlookConnected = !empty($outlookRow['access_token']) && empty($outlookRow['is_demo_mode']);
$outlookSettings = json_decode($outlookRow['settings'] ?? '{}', true) ?: [];
// Jira status ophalen
$stmtJira = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
$stmtJira->execute([$userId]);
$jiraRow = $stmtJira->fetch();
$jiraSettings = json_decode($jiraRow['settings'] ?? '{}', true) ?: [];
$isJiraConnected = !empty($jiraSettings['jira_api_token']);
$companyJiraDomain = jira_get_company_domain();
$activeJiraDomain = !empty($jiraSettings['jira_domain']) ? $jiraSettings['jira_domain'] : $companyJiraDomain;
// Centrale Azure App configuratie status
$appConfig = microsoft_get_app_config();
// Notificatie-instellingen
$notifSettings = get_user_notification_settings($userId);
// AI Co-Pilot & Google Gemini instellingen voor gebruiker
$userAiSettings = get_ai_settings($userId);
$stmtAiInt = $db->prepare("SELECT id, access_token, is_enabled, settings FROM integrations WHERE user_id = ? AND provider IN ('GEMINI', 'AI') LIMIT 1");
$stmtAiInt->execute([$userId]);
$userAiIntRow = $stmtAiInt->fetch();
$hasPersonalAiKey = !empty($userAiIntRow['access_token']);
$userAiRowSettings = json_decode($userAiIntRow['settings'] ?? '{}', true) ?: [];
// Thema ophalen
$userTheme = get_user_theme($userId);
// Actieve apparaten & sessies ophalen (SOC 2 / ISO 27001)
$activeSessions = fociz_get_user_active_sessions($userId, session_id());
$pageTitle = 'Mijn Profiel & Koppelingen — Fociz';
require_once __DIR__ . '/includes/header.php';
?>
<div class="space-y-8 max-w-5xl mx-auto pb-12">
<!-- Profiel Header Card -->
<div class="relative overflow-hidden fociz-card p-6 sm:p-8">
<div class="absolute -top-20 -right-20 w-72 h-72 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="relative z-10 flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div class="flex items-center space-x-4 sm:space-x-5">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 shadow-sm flex-shrink-0 overflow-hidden">
<?php if (!empty($userProfile['avatar_url'])): ?>
<img src="<?= htmlspecialchars($userProfile['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-2xl object-cover bg-white dark:bg-[#0c0f17]" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-full h-full rounded-2xl bg-white dark:bg-[#0c0f17] flex items-center justify-center font-bold text-2xl sm:text-3xl text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-full h-full rounded-2xl bg-white dark:bg-[#0c0f17] flex items-center justify-center font-bold text-2xl sm:text-3xl text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
</div>
<?php endif; ?>
</div>
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 dark:text-white tracking-tight">
<?= htmlspecialchars($userProfile['name']) ?>
</h1>
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-300 text-xs font-semibold flex items-center space-x-1">
<i data-lucide="shield" class="w-3 h-3"></i>
<span>Beheerder</span>
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 text-xs font-semibold flex items-center space-x-1">
<i data-lucide="user" class="w-3 h-3"></i>
<span>Collega</span>
</span>
<?php endif; ?>
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-700 dark:text-teal-400 text-xs font-semibold flex items-center space-x-1">
<i data-lucide="shield-check" class="w-3 h-3"></i>
<span>2FA Beveiligd</span>
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 flex items-center space-x-2">
<span><?= htmlspecialchars($userProfile['email']) ?></span>
<span>•</span>
<span>Lid sinds <?= date('d-m-Y', strtotime($userProfile['created_at'])) ?></span>
</p>
</div>
</div>
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<div class="flex-shrink-0">
<a href="/admin.php" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 border border-slate-200 dark:border-slate-700 text-xs font-semibold transition shadow-xs">
<i data-lucide="shield-alert" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<span>Systeembeheer Openen</span>
</a>
</div>
<?php endif; ?>
</div>
</div>
<!-- Meldingen Banner -->
<?php if ($message): ?>
<div class="p-4 rounded-xl flex items-start space-x-3 text-sm animate-in fade-in duration-200 <?= $messageType === 'success' ? 'bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-800 dark:text-teal-200' : 'bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 text-rose-800 dark:text-rose-200' ?>">
<i data-lucide="<?= $messageType === 'success' ? 'check-circle-2' : 'alert-triangle' ?>" class="w-5 h-5 flex-shrink-0 mt-0.5 <?= $messageType === 'success' ? 'text-teal-600 dark:text-teal-400' : 'text-rose-600 dark:text-rose-400' ?>"></i>
<div class="flex-1 font-medium"><?= htmlspecialchars($message) ?></div>
</div>
<?php endif; ?>
<!-- Sectie 0: Weergave & Thema Instellingen (Dark & Light mode) -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
<i data-lucide="palette" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Weergave & Thema</h2>
<span id="themeStatusBadge" class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 text-indigo-700 dark:text-indigo-400 text-[10px] font-semibold">
Actief: <?= ucfirst($userTheme) ?>
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">Kies uw gewenste uiterlijk: rustig donker grafiet, kraakhelder licht of synchroniseer met uw apparaat.</p>
</div>
</div>
</div>
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Optie 1: Donker (Dark) -->
<button type="button" onclick="selectThemePreference('dark')" id="themeBtn-dark" class="theme-card relative text-left p-4 rounded-xl border-2 transition duration-150 flex flex-col justify-between group hover:shadow-md cursor-pointer bg-[#0c0f17] border-slate-800 text-white">
<div class="w-full h-24 rounded-lg bg-[#111622] p-2.5 border border-slate-800/80 mb-3 flex flex-col justify-between overflow-hidden shadow-inner">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-rose-400/80"></span>
<span class="w-2 h-2 rounded-full bg-amber-400/80"></span>
<span class="w-2 h-2 rounded-full bg-teal-400/80"></span>
</div>
<span class="text-[9px] font-mono text-slate-500">fociz.dark</span>
</div>
<div class="space-y-1.5">
<div class="h-2 w-3/4 bg-indigo-500/60 rounded"></div>
<div class="h-2 w-1/2 bg-slate-800 rounded"></div>
</div>
<div class="flex space-x-1">
<div class="h-3 w-8 bg-slate-800 rounded"></div>
<div class="h-3 w-10 bg-indigo-500/20 rounded"></div>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-white flex items-center space-x-1.5">
<i data-lucide="moon" class="w-4 h-4 text-indigo-400"></i>
<span>Donkere Modus</span>
</span>
<span id="themeCheck-dark" class="theme-check hidden w-5 h-5 rounded-full bg-indigo-600 text-white items-center justify-center shadow-xs">
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
</span>
</div>
<p class="text-[11px] text-slate-400 mt-1 leading-relaxed">
Diepe neutrale contrasten, rustig voor de ogen in de avond of donkere werkplek.
</p>
</div>
</button>
<!-- Optie 2: Licht (Light) -->
<button type="button" onclick="selectThemePreference('light')" id="themeBtn-light" class="theme-card relative text-left p-4 rounded-xl border-2 transition duration-150 flex flex-col justify-between group hover:shadow-md cursor-pointer bg-[#f8fafc] border-slate-200 text-slate-900">
<div class="w-full h-24 rounded-lg bg-white p-2.5 border border-slate-200/90 mb-3 flex flex-col justify-between overflow-hidden shadow-xs">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-1.5">
<span class="w-2 h-2 rounded-full bg-rose-400/80"></span>
<span class="w-2 h-2 rounded-full bg-amber-400/80"></span>
<span class="w-2 h-2 rounded-full bg-teal-400/80"></span>
</div>
<span class="text-[9px] font-mono text-slate-400">fociz.light</span>
</div>
<div class="space-y-1.5">
<div class="h-2 w-3/4 bg-indigo-600/50 rounded"></div>
<div class="h-2 w-1/2 bg-slate-200 rounded"></div>
</div>
<div class="flex space-x-1">
<div class="h-3 w-8 bg-slate-100 rounded"></div>
<div class="h-3 w-10 bg-indigo-50 rounded"></div>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-slate-900 flex items-center space-x-1.5">
<i data-lucide="sun" class="w-4 h-4 text-amber-500"></i>
<span>Lichte Modus</span>
</span>
<span id="themeCheck-light" class="theme-check hidden w-5 h-5 rounded-full bg-indigo-600 text-white items-center justify-center shadow-xs">
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
</span>
</div>
<p class="text-[11px] text-slate-500 mt-1 leading-relaxed">
Kraakhelder, fris off-white en professioneel voor overdag en felle kantoorverlichting.
</p>
</div>
</button>
<!-- Optie 3: Systeem (Auto) -->
<button type="button" onclick="selectThemePreference('system')" id="themeBtn-system" class="theme-card relative text-left p-4 rounded-xl border-2 transition duration-150 flex flex-col justify-between group hover:shadow-md cursor-pointer bg-slate-50 dark:bg-[#111622] border-slate-200 dark:border-slate-800">
<div class="w-full h-24 rounded-lg border border-slate-200 dark:border-slate-800 mb-3 flex overflow-hidden shadow-xs">
<!-- Linker helft licht -->
<div class="w-1/2 bg-[#f8fafc] p-2 flex flex-col justify-between border-r border-slate-200">
<div class="flex space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-slate-300"></span>
<span class="w-1.5 h-1.5 rounded-full bg-slate-300"></span>
</div>
<div class="h-2 w-3/4 bg-indigo-600/40 rounded"></div>
<div class="h-2 w-1/2 bg-slate-200 rounded"></div>
</div>
<!-- Rechter helft donker -->
<div class="w-1/2 bg-[#0c0f17] p-2 flex flex-col justify-between">
<div class="flex justify-end space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-slate-700"></span>
<span class="w-1.5 h-1.5 rounded-full bg-slate-700"></span>
</div>
<div class="h-2 w-3/4 bg-indigo-500/50 rounded"></div>
<div class="h-2 w-1/2 bg-slate-800 rounded"></div>
</div>
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-slate-900 dark:text-white flex items-center space-x-1.5">
<i data-lucide="laptop" class="w-4 h-4 text-sky-500"></i>
<span>Systeem Volgen</span>
</span>
<span id="themeCheck-system" class="theme-check hidden w-5 h-5 rounded-full bg-indigo-600 text-white items-center justify-center shadow-xs">
<i data-lucide="check" class="w-3.5 h-3.5 stroke-[3]"></i>
</span>
</div>
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-1 leading-relaxed">
Schakelt automatisch mee met uw Windows, macOS of browser voorkeur.
</p>
</div>
</button>
</div>
</div>
<!-- Sectie 0b: Profielfoto & Avatar Beheer -->
<div class="fociz-card p-6 sm:p-7 space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-tr from-indigo-500 to-pink-500 p-0.5 shadow-xs flex-shrink-0 flex items-center justify-center text-white">
<i data-lucide="smile" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Profielfoto & Avatar</h2>
<span class="px-2.5 py-0.5 rounded-full <?= !empty($userProfile['avatar_url']) ? 'bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-700 dark:text-teal-400' : 'bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400' ?> text-[10px] font-semibold">
<?= !empty($userProfile['avatar_url']) ? 'Aangepaste Avatar Actief' : 'Standaard Initiaal-Badge' ?>
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">Kies een mini-avatar uit de 24 stijlvolle karakters of upload een eigen afbeelding.</p>
</div>
</div>
<?php if (!empty($userProfile['avatar_url'])): ?>
<form method="POST" action="/profile.php" class="self-start sm:self-auto">
<input type="hidden" name="action" value="remove_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" onclick="return confirm('Avatar herstellen naar standaard initiaal-badge?')" class="inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-xl text-xs font-semibold text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800/50 hover:bg-rose-100 dark:hover:bg-rose-900/50 transition cursor-pointer">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
<span>Standaard Herstellen</span>
</button>
</form>
<?php endif; ?>
</div>
<!-- Avatar Selector Grid & Upload -->
<div class="mt-6 grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<!-- Linker kolom: 24 mini karakters in grid (8 cols) -->
<div class="lg:col-span-8 space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="sparkles" class="w-3.5 h-3.5 text-indigo-500"></i>
<span>Kies een Karakter (24 Stijlen)</span>
</h3>
<!-- Categorie Filters -->
<div class="flex items-center space-x-1 bg-slate-100 dark:bg-[#0c0f17] p-1 rounded-xl border border-slate-200 dark:border-slate-800 text-[11px]" id="avatarFilterBar">
<button type="button" onclick="filterAvatars('all')" class="avatar-filter-btn active px-2.5 py-1 rounded-lg font-medium transition cursor-pointer" data-cat="all">Alles</button>
<button type="button" onclick="filterAvatars('pro')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="pro">Pro</button>
<button type="button" onclick="filterAvatars('creative')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="creative">Creatief</button>
<button type="button" onclick="filterAvatars('tech')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="tech">Tech</button>
<button type="button" onclick="filterAvatars('heroes')" class="avatar-filter-btn px-2.5 py-1 rounded-lg font-medium transition text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white cursor-pointer" data-cat="heroes">Heroes</button>
</div>
</div>
<div class="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2.5 p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200 dark:border-slate-800" id="avatarGrid">
<?php
$avatarPresets = [
['file' => 'avatar-01.svg', 'cat' => 'pro', 'name' => 'Alex (Business)'],
['file' => 'avatar-02.svg', 'cat' => 'pro', 'name' => 'Sophie (Corporate)'],
['file' => 'avatar-03.svg', 'cat' => 'pro', 'name' => 'Liam (Consultant)'],
['file' => 'avatar-04.svg', 'cat' => 'pro', 'name' => 'Emma (Manager)'],
['file' => 'avatar-05.svg', 'cat' => 'pro', 'name' => 'Daan (Director)'],
['file' => 'avatar-06.svg', 'cat' => 'pro', 'name' => 'Noa (Executive)'],
['file' => 'avatar-07.svg', 'cat' => 'creative', 'name' => 'Mila (Designer)'],
['file' => 'avatar-08.svg', 'cat' => 'creative', 'name' => 'Lucas (Creator)'],
['file' => 'avatar-09.svg', 'cat' => 'creative', 'name' => 'Zara (Artist)'],
['file' => 'avatar-10.svg', 'cat' => 'creative', 'name' => 'Finn (Illustrator)'],
['file' => 'avatar-11.svg', 'cat' => 'creative', 'name' => 'Lotte (Writer)'],
['file' => 'avatar-12.svg', 'cat' => 'creative', 'name' => 'Sem (Architect)'],
['file' => 'avatar-13.svg', 'cat' => 'tech', 'name' => 'Bram (Developer)'],
['file' => 'avatar-14.svg', 'cat' => 'tech', 'name' => 'Julia (Engineer)'],
['file' => 'avatar-15.svg', 'cat' => 'tech', 'name' => 'Jesse (DevOps)'],
['file' => 'avatar-16.svg', 'cat' => 'tech', 'name' => 'Tess (Data Scientist)'],
['file' => 'avatar-17.svg', 'cat' => 'tech', 'name' => 'Milan (Cyber Sec)'],
['file' => 'avatar-18.svg', 'cat' => 'tech', 'name' => 'Liv (AI Engineer)'],
['file' => 'avatar-19.svg', 'cat' => 'heroes', 'name' => 'Max (Ninja)'],
['file' => 'avatar-20.svg', 'cat' => 'heroes', 'name' => 'Sara (Astronaut)'],
['file' => 'avatar-21.svg', 'cat' => 'heroes', 'name' => 'Robin (Pilot)'],
['file' => 'avatar-22.svg', 'cat' => 'heroes', 'name' => 'Eva (Explorer)'],
['file' => 'avatar-23.svg', 'cat' => 'heroes', 'name' => 'Leo (Cosmic)'],
['file' => 'avatar-24.svg', 'cat' => 'heroes', 'name' => 'Nova (Cyberpunk)'],
];
foreach ($avatarPresets as $av):
$avatarPath = "/assets/avatars/{$av['file']}";
$isSelected = ($userProfile['avatar_url'] === $avatarPath);
?>
<form method="POST" action="/profile.php" class="relative group avatar-item" data-category="<?= $av['cat'] ?>">
<input type="hidden" name="action" value="select_preset_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<input type="hidden" name="avatar_preset" value="<?= $avatarPath ?>">
<button type="submit" class="w-full aspect-square rounded-2xl p-1 transition-all duration-200 transform hover:scale-110 active:scale-95 hover:shadow-lg hover:shadow-indigo-500/25 focus:outline-none cursor-pointer <?= $isSelected ? 'ring-2 ring-indigo-600 dark:ring-indigo-400 bg-indigo-50 dark:bg-indigo-950/40 shadow-sm' : 'hover:bg-white dark:hover:bg-slate-800 border border-transparent' ?>" title="<?= htmlspecialchars($av['name']) ?>">
<img src="<?= $avatarPath ?>" alt="<?= htmlspecialchars($av['name']) ?>" class="w-full h-full rounded-xl object-contain pointer-events-none drop-shadow-xs">
<?php if ($isSelected): ?>
<span class="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-indigo-600 text-white flex items-center justify-center shadow-xs border-2 border-white dark:border-[#0c0f17]">
<i data-lucide="check" class="w-3 h-3 stroke-[3]"></i>
</span>
<?php endif; ?>
</button>
</form>
<?php endforeach; ?>
</div>
</div>
<!-- Rechter kolom: Eigen Afbeelding Uploaden (4 cols) -->
<div class="lg:col-span-4 space-y-3">
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
<i data-lucide="upload-cloud" class="w-3.5 h-3.5 text-sky-500"></i>
<span>Eigen Foto Uploaden</span>
</h3>
<form method="POST" action="/profile.php" enctype="multipart/form-data" class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-4">
<input type="hidden" name="action" value="upload_custom_avatar">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="text-center p-5 border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-xl hover:border-indigo-500 dark:hover:border-indigo-400 transition bg-white dark:bg-[#111622]">
<div class="w-10 h-10 mx-auto mb-2 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
<i data-lucide="image" class="w-5 h-5"></i>
</div>
<label for="customAvatarFile" class="block text-xs font-semibold text-indigo-600 dark:text-indigo-400 hover:underline cursor-pointer mb-1">
Klik om bestand te kiezen
</label>
<p class="text-[10px] text-slate-500 dark:text-slate-400">JPG, PNG, WebP of GIF (max. 4MB)</p>
<input type="file" id="customAvatarFile" name="avatar_file" accept="image/png, image/jpeg, image/webp, image/gif" required class="hidden" onchange="previewUploadAvatar(this)">
<div id="uploadFileName" class="mt-2 text-[11px] font-mono text-slate-700 dark:text-slate-300 truncate hidden"></div>
</div>
<button type="submit" id="uploadAvatarBtn" class="w-full py-2.5 px-4 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer">
<i data-lucide="upload" class="w-4 h-4"></i>
<span>Foto Uploaden &amp; Opslaan</span>
</button>
</form>
</div>
</div>
</div>
<!-- Sectie 1: Mijn Microsoft 365 Outlook Agenda -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 flex items-center justify-center text-sky-600 dark:text-sky-400 font-bold shadow-xs">
<i data-lucide="calendar" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Mijn Microsoft 365 (Agenda & E-mail)</h2>
<?php if ($isOutlookConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-500/15 border border-teal-200 dark:border-teal-500/30 text-teal-700 dark:text-teal-400 text-[10px] font-semibold flex items-center space-x-1">
<span class="w-1.5 h-1.5 rounded-full bg-teal-500 animate-pulse"></span>
<span>Live Verbonden</span>
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-[10px] font-semibold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">Synchroniseer uw agenda-afspraken en zakelijke Outlook e-mails met Fociz</p>
</div>
</div>
<?php if ($isOutlookConnected): ?>
<div class="flex items-center space-x-2">
<a href="/auth/oauth_microsoft.php?reconsent=1" class="px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700 text-xs font-medium transition" title="Machtigingen bijwerken bij Microsoft (inclusief Mail.ReadWrite)">
Machtigingen Bijwerken
</a>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u uw Microsoft Outlook agenda wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_outlook">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer">
Ontkoppelen
</button>
</form>
</div>
<?php endif; ?>
</div>
<div class="mt-6">
<?php if ($isOutlookConnected): ?>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 space-y-1.5">
<span class="text-[10px] uppercase tracking-wider font-semibold text-slate-500 dark:text-slate-400 block">Gekoppeld Microsoft Account</span>
<span class="text-sm font-bold text-slate-900 dark:text-white block truncate"><?= htmlspecialchars($outlookSettings['account_name'] ?? 'Microsoft Gebruiker') ?></span>
<span class="text-xs text-indigo-600 dark:text-indigo-400 block truncate"><?= htmlspecialchars($outlookSettings['account_email'] ?? $userProfile['email']) ?></span>
</div>
<div class="p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 space-y-1.5">
<span class="text-[10px] uppercase tracking-wider font-semibold text-slate-500 dark:text-slate-400 block">Microsoft Teams Koppeling</span>
<span class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
<i data-lucide="video" class="w-4 h-4 text-sky-500"></i>
<span>Automatische Meeting Links</span>
</span>
<span class="text-xs text-slate-500 dark:text-slate-400 block">Vergaderlinks in uw agenda worden direct klikbaar in uw dagplanning</span>
</div>
</div>
<form method="POST" class="mt-4 p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<input type="hidden" name="action" value="save_outlook_settings">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="space-y-1">
<label for="sync_interval_seconds" class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-sky-500"></i>
<span>Automatische Agenda Synchronisatie</span>
</label>
<p class="text-[11px] text-slate-500 dark:text-slate-400 leading-relaxed">
Bepaalt hoe vaak het dagplanningscherm in de achtergrond controleert op gewijzigde of verwijderde Outlook afspraken.
</p>
</div>
<div class="flex items-center space-x-2">
<?php $currentInterval = (int)($outlookSettings['sync_interval_seconds'] ?? 30); ?>
<select id="sync_interval_seconds" name="sync_interval_seconds" class="px-3 py-2 rounded-xl border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white text-xs focus:ring-2 focus:ring-sky-500 focus:outline-hidden">
<option value="15" <?= $currentInterval === 15 ? 'selected' : '' ?>>Elke 15 seconden (Real-time)</option>
<option value="30" <?= $currentInterval === 30 ? 'selected' : '' ?>>Elke 30 seconden (Aanbevolen)</option>
<option value="60" <?= $currentInterval === 60 ? 'selected' : '' ?>>Elke 60 seconden (1 minuut)</option>
<option value="120" <?= $currentInterval === 120 ? 'selected' : '' ?>>Elke 2 minuten</option>
<option value="0" <?= $currentInterval === 0 ? 'selected' : '' ?>>Handmatig (en bij vensterfocus)</option>
</select>
<button type="submit" class="px-4 py-2 rounded-xl bg-sky-600 hover:bg-sky-500 text-white font-semibold text-xs transition shadow-2xs whitespace-nowrap cursor-pointer">
Opslaan
</button>
</div>
</form>
<div class="mt-4 p-3.5 rounded-xl bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/20 text-teal-800 dark:text-teal-300 text-xs flex items-center justify-between">
<div class="flex items-center space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-teal-600 dark:text-teal-400 flex-shrink-0"></i>
<span>Uw agenda synchroniseert live. Afspraken van vandaag verschijnen direct in uw <a href="/planner.php" class="underline font-semibold text-slate-900 dark:text-white">Dagplanning</a>.</span>
</div>
<a href="/planner.php" class="px-3 py-1 rounded-lg bg-teal-500/20 hover:bg-teal-500/30 text-teal-800 dark:text-teal-200 font-semibold text-xs transition flex-shrink-0">
Naar Dagplanning &rarr;
</a>
</div>
<?php else: ?>
<div class="p-6 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800 text-center space-y-4">
<div class="w-14 h-14 rounded-2xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center mx-auto">
<i data-lucide="calendar-plus" class="w-7 h-7"></i>
</div>
<div class="max-w-md mx-auto space-y-1">
<h3 class="text-base font-bold text-slate-900 dark:text-white">Verbind uw Microsoft Outlook Agenda</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 leading-relaxed">
Krijg uw dagelijkse afspraken automatisch te zien in Fociz en ontvang herinneringen vóórdat uw vergaderingen beginnen.
</p>
</div>
<?php if ($appConfig['is_configured']): ?>
<div>
<form method="POST">
<input type="hidden" name="action" value="connect_outlook">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="inline-flex items-center space-x-2 px-6 py-3 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs transition shadow-xs cursor-pointer">
<i data-lucide="log-in" class="w-4 h-4"></i>
<span>Koppel Mijn Outlook Agenda</span>
</button>
</form>
<span class="text-[11px] text-slate-500 block mt-2">U wordt veilig doorgestuurd naar Microsoft om in te loggen</span>
</div>
<?php else: ?>
<div class="p-3.5 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/30 text-amber-800 dark:text-amber-300 text-xs max-w-lg mx-auto">
<i data-lucide="alert-circle" class="w-4 h-4 text-amber-500 inline mr-1 -mt-0.5"></i>
Microsoft 365 koppeling is nog niet geconfigureerd door de beheerder.
<?php if ($userProfile['role'] === 'ADMIN'): ?>
<a href="/admin.php" class="underline font-bold text-slate-900 dark:text-white ml-1">Stel Azure in via Systeembeheer &rarr;</a>
<?php else: ?>
Vraag uw beheerder om dit in te schakelen.
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Sectie 2: Mijn Atlassian Jira Koppeling -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
<i data-lucide="check-square" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Mijn Atlassian Jira Koppeling</h2>
<?php if ($isJiraConnected): ?>
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-500/15 border border-teal-200 dark:border-teal-500/30 text-teal-700 dark:text-teal-400 text-[10px] font-semibold">
Verbonden
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-[10px] font-semibold">
Niet Gekoppeld
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">Importeer uw toegewezen Jira issues direct naar uw dagelijkse takenlijst</p>
</div>
</div>
<?php if ($isJiraConnected): ?>
<form method="POST" onsubmit="return confirm('Weet u zeker dat u Jira wilt ontkoppelen?');">
<input type="hidden" name="action" value="disconnect_jira">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-500/10 dark:hover:bg-rose-500/20 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20 text-xs font-medium transition cursor-pointer">
Jira Ontkoppelen
</button>
</form>
<?php endif; ?>
</div>
<form method="POST" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_jira">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Jira Cloud Domein
</label>
<input type="text" name="jira_domain" value="<?= htmlspecialchars($activeJiraDomain) ?>" placeholder="https://uwbedrijf.atlassian.net" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 font-mono">
<span class="text-[10px] text-slate-500 mt-1 block">Standaard ingevuld door beheerder indien beschikbaar</span>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Uw Jira E-mailadres
</label>
<input type="email" name="jira_email" value="<?= htmlspecialchars($jiraSettings['jira_email'] ?? $userProfile['email']) ?>" placeholder="naam@bedrijf.nl" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
</div>
<div>
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-semibold text-slate-700 dark:text-slate-300">
Persoonlijk Atlassian API Token
</label>
<a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noopener noreferrer" class="text-[10px] text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1">
<span>API Token Aanmaken</span>
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</div>
<div class="relative">
<input type="password" id="jiraApiTokenInput" name="jira_api_token" value="<?= !empty($jiraSettings['jira_api_token']) ? '••••••••••••••••' : '' ?>" placeholder="Plak hier uw Atlassian API token..." class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500">
<button type="button" onclick="toggleJiraToken()" id="toggleJiraTokenBtn" title="Toon / verberg token" class="absolute inset-y-0 right-0 pr-3 flex items-center text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 cursor-pointer">
<i data-lucide="eye" class="w-4 h-4" id="eyeJiraIcon"></i>
</button>
</div>
</div>
<div class="pt-2 flex items-center justify-between">
<?php if ($isJiraConnected && !empty($jiraSettings['account_name'])): ?>
<span class="text-xs text-teal-600 dark:text-teal-400 flex items-center space-x-1.5">
<i data-lucide="check" class="w-4 h-4"></i>
<span>Verbonden als: <strong><?= htmlspecialchars($jiraSettings['account_name']) ?></strong></span>
</span>
<?php else: ?>
<span class="text-[11px] text-slate-500 dark:text-slate-400">Verbinding wordt automatisch gevalideerd bij het opslaan</span>
<?php endif; ?>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold transition shadow-xs cursor-pointer">
Jira Opslaan & Testen
</button>
</div>
</form>
</div>
<!-- Sectie 2b: Mijn AI Co-Pilot & Google Gemini Pro -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
<i data-lucide="sparkles" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">AI Co-Pilot & Google Gemini Pro</h2>
<?php if ($hasPersonalAiKey): ?>
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-500/15 border border-indigo-200 dark:border-indigo-500/30 text-indigo-700 dark:text-indigo-400 text-[10px] font-semibold">
Eigen Sleutel
</span>
<?php elseif (!empty($userAiSettings['has_key'])): ?>
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-500/15 border border-teal-200 dark:border-teal-500/30 text-teal-700 dark:text-teal-400 text-[10px] font-semibold">
Organisatiesleutel Actief
</span>
<?php else: ?>
<span class="px-2.5 py-0.5 rounded-full bg-amber-50 dark:bg-amber-500/15 border border-amber-200 dark:border-amber-500/30 text-amber-700 dark:text-amber-400 text-[10px] font-semibold">
Geen Sleutel
</span>
<?php endif; ?>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Koppel uw eigen Google Gemini Pro account of gebruik de centrale organisatieconfiguratie</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="px-2.5 py-1 rounded-full bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800/60 text-xs font-bold flex items-center space-x-1">
<i data-lucide="shield-check" class="w-3.5 h-3.5"></i>
<span>Zero-Data Leakage Actief</span>
</span>
</div>
</div>
<!-- Privacy Shield Banner -->
<div class="mt-6 p-3.5 rounded-2xl bg-emerald-50/70 dark:bg-emerald-950/20 border border-emerald-200/70 dark:border-emerald-800/40 flex items-start space-x-3">
<div class="w-7 h-7 rounded-xl bg-emerald-100 dark:bg-emerald-900/40 text-emerald-700 dark:text-emerald-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<i data-lucide="shield-check" class="w-4 h-4"></i>
</div>
<div class="text-xs">
<strong class="text-emerald-900 dark:text-emerald-200 font-bold block">100% Zero-Data Leakage Garantie</strong>
<p class="text-emerald-800/80 dark:text-emerald-300/80 text-[11px] mt-0.5">
Uw gevoelige gegevens uit <strong>Jira tickets</strong>, <strong>Outlook e-mails</strong> en <strong>Microsoft Teams</strong> worden strikt afgeschermd en <strong>NOOIT</strong> naar Google Gemini of OpenAI verstuurd.
</p>
</div>
</div>
<form method="POST" action="/profile.php" class="mt-6 space-y-4">
<input type="hidden" name="action" value="save_personal_ai">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="p-3.5 rounded-2xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800">
<label class="flex items-center space-x-2.5 text-xs text-slate-900 dark:text-white cursor-pointer select-none">
<input type="checkbox" name="ai_enabled" value="1" <?= (!isset($userAiIntRow['is_enabled']) || !empty($userAiIntRow['is_enabled'])) ? 'checked' : '' ?> class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
<span class="font-semibold">AI Co-Pilot assistentie inschakelen voor mijn account</span>
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">AI Provider & Model</label>
<select name="ai_model" id="profile_ai_model" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<optgroup label="Google Gemini (Aanbevolen)">
<option value="gemini-1.5-pro" <?= ($userAiSettings['ai_model'] === 'gemini-1.5-pro' || empty($userAiSettings['ai_model'])) ? 'selected' : '' ?>>Google Gemini 1.5 Pro (Aanbevolen)</option>
<option value="gemini-2.0-flash" <?= $userAiSettings['ai_model'] === 'gemini-2.0-flash' ? 'selected' : '' ?>>Google Gemini 2.0 Flash (Razendsnel)</option>
<option value="gemini-1.5-flash" <?= $userAiSettings['ai_model'] === 'gemini-1.5-flash' ? 'selected' : '' ?>>Google Gemini 1.5 Flash</option>
</optgroup>
<optgroup label="OpenAI">
<option value="gpt-4o-mini" <?= $userAiSettings['ai_model'] === 'gpt-4o-mini' ? 'selected' : '' ?>>OpenAI GPT-4o-mini</option>
<option value="gpt-4o" <?= $userAiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
</optgroup>
</select>
<input type="hidden" name="ai_provider" id="profile_ai_provider" value="<?= strpos($userAiSettings['ai_model'], 'gpt') !== false ? 'openai' : 'gemini' ?>">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Persoonlijke Google Gemini API Sleutel
<span class="text-[11px] font-normal text-slate-500 dark:text-slate-400">(Optioneel)</span>
</label>
<div class="relative">
<input
type="password"
name="ai_api_key"
id="profile_ai_api_key"
value="<?= $hasPersonalAiKey ? '••••••••••••••••' : '' ?>"
placeholder="<?= $userAiSettings['has_key'] ? 'Gebruikt organisatie-sleutel (leeg laten)' : 'AIzaSy... (Google AI Studio)' ?>"
class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl pl-3.5 pr-9 py-2.5 text-xs text-slate-900 dark:text-white font-mono placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<button type="button" onclick="togglePasswordVisibility('profile_ai_api_key', this)" class="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
</button>
</div>
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-1">
Gratis sleutel aanmaken? <a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">Google AI Studio &rarr;</a>
</span>
</div>
</div>
<!-- Live Test Resultaat Alert in Profiel -->
<div id="profileAiTestResult" class="hidden p-3 rounded-xl text-xs"></div>
<div class="flex items-center justify-between pt-2 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<button type="button" onclick="testPersonalAiConnection()" id="profileAiTestBtn" class="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 text-xs font-semibold transition cursor-pointer flex items-center space-x-2">
<i data-lucide="activity" class="w-3.5 h-3.5 text-indigo-500"></i>
<span id="profileAiTestBtnText">Test Verbinding</span>
</button>
<?php if ($hasPersonalAiKey): ?>
<button type="submit" form="disconnectPersonalAiForm" class="px-3 py-2 rounded-xl text-xs text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 transition cursor-pointer font-medium">
Persoonlijke Sleutel Verwijderen
</button>
<?php endif; ?>
</div>
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold transition shadow-xs cursor-pointer">
AI Instellingen Opslaan
</button>
</div>
</form>
<?php if ($hasPersonalAiKey): ?>
<form method="POST" id="disconnectPersonalAiForm" class="hidden">
<input type="hidden" name="action" value="disconnect_personal_ai">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
</form>
<?php endif; ?>
</div>
<!-- Sectie 3: Mijn Meldingen & Agenda Push Notificaties -->
<div class="fociz-card p-6 sm:p-7">
<div class="flex items-center space-x-3.5 pb-6 border-b border-slate-100 dark:border-slate-800/80">
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
<i data-lucide="bell" class="w-6 h-6"></i>
</div>
<div>
<h2 class="text-base sm:text-lg font-bold text-slate-900 dark:text-white tracking-tight">Mijn Meldingen & Agenda Herinneringen</h2>
<p class="text-xs text-slate-500 dark:text-slate-400">Krijg realtime meldingen vanuit uw browser zodra een afspraak nadert</p>
</div>
</div>
<div class="mt-6 space-y-5">
<!-- Browser Permissie Banner in Profiel -->
<div id="profileNotifPermGranted" class="hidden p-4 rounded-xl bg-teal-50 dark:bg-teal-500/10 border border-teal-200 dark:border-teal-500/30 text-teal-800 dark:text-teal-300 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center space-x-2.5">
<i data-lucide="check-circle" class="w-5 h-5 text-teal-600 dark:text-teal-400 flex-shrink-0"></i>
<div>
<strong class="block text-xs text-slate-900 dark:text-white">Browser Pushberichten zijn Actief</strong>
<span class="text-[11px] text-teal-700 dark:text-teal-300/80">Uw browser heeft toestemming om bureaubladmeldingen weer te geven.</span>
</div>
</div>
<button type="button" onclick="focizTestNotification()" class="px-3 py-1.5 rounded-xl bg-teal-500/20 hover:bg-teal-500/30 text-teal-800 dark:text-teal-200 font-semibold text-xs transition flex items-center space-x-1.5 flex-shrink-0 cursor-pointer">
<i data-lucide="send" class="w-3.5 h-3.5"></i>
<span>Verzend Test Melding</span>
</button>
</div>
<div id="profileNotifPermRequest" class="p-4 rounded-xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/30 text-indigo-800 dark:text-indigo-200 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center space-x-2.5">
<i data-lucide="bell-ring" class="w-5 h-5 text-indigo-600 dark:text-indigo-400 flex-shrink-0"></i>
<div>
<strong class="block text-xs text-slate-900 dark:text-white">Toestemming van Browser Vereist</strong>
<span class="text-[11px] text-indigo-700 dark:text-indigo-300/80">Klik op de knop om pushmeldingen in te schakelen voor Fociz.</span>
</div>
</div>
<button type="button" onclick="focizRequestPermission()" class="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-semibold text-xs transition shadow-xs flex-shrink-0 cursor-pointer">
Toestemming Verlenen
</button>
</div>
<div id="profileNotifPermDenied" class="hidden p-4 rounded-xl bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/30 text-rose-800 dark:text-rose-300 text-xs leading-relaxed">
<i data-lucide="alert-triangle" class="w-4 h-4 text-rose-600 dark:text-rose-400 inline mr-1 -mt-0.5"></i>
Meldingen zijn momenteel geblokkeerd in uw browser. Klik links in de adresbalk op het slotje/site-instellingen en zet "Meldingen" op "Toestaan".
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="save_notifications">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div class="flex items-center justify-between p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800">
<div>
<span class="text-xs font-semibold text-slate-900 dark:text-white block">Meldingen Actief</span>
<span class="text-[11px] text-slate-500 dark:text-slate-400">Automatisch waarschuwen bij naderende afspraken</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="notif_enabled" <?= !empty($notifSettings['enabled']) ? 'checked' : '' ?> class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 dark: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-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Vooraankondiging
</label>
<select name="lead_time_minutes" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="0" <?= (int)$notifSettings['lead_time_minutes'] === 0 ? 'selected' : '' ?>>0 minuten (Precies op aanvang)</option>
<option value="2" <?= (int)$notifSettings['lead_time_minutes'] === 2 ? 'selected' : '' ?>>2 minuten van tevoren</option>
<option value="5" <?= (int)$notifSettings['lead_time_minutes'] === 5 ? 'selected' : '' ?>>5 minuten van tevoren (Aanbevolen)</option>
<option value="10" <?= (int)$notifSettings['lead_time_minutes'] === 10 ? 'selected' : '' ?>>10 minuten van tevoren</option>
<option value="15" <?= (int)$notifSettings['lead_time_minutes'] === 15 ? 'selected' : '' ?>>15 minuten van tevoren</option>
<option value="30" <?= (int)$notifSettings['lead_time_minutes'] === 30 ? 'selected' : '' ?>>30 minuten van tevoren</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Aantal herhalingen
</label>
<select name="repeat_count" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="1" <?= (int)$notifSettings['repeat_count'] === 1 ? 'selected' : '' ?>>1x (Alleen eerste melding)</option>
<option value="2" <?= (int)$notifSettings['repeat_count'] === 2 ? 'selected' : '' ?>>2x herhalen (Aanbevolen)</option>
<option value="3" <?= (int)$notifSettings['repeat_count'] === 3 ? 'selected' : '' ?>>3x herhalen</option>
<option value="5" <?= (int)$notifSettings['repeat_count'] === 5 ? 'selected' : '' ?>>5x herhalen</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Herhalingsinterval
</label>
<select name="repeat_interval_minutes" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<option value="1" <?= (int)$notifSettings['repeat_interval_minutes'] === 1 ? 'selected' : '' ?>>Elke 1 minuut</option>
<option value="2" <?= (int)$notifSettings['repeat_interval_minutes'] === 2 ? 'selected' : '' ?>>Elke 2 minuten</option>
<option value="3" <?= (int)$notifSettings['repeat_interval_minutes'] === 3 ? 'selected' : '' ?>>Elke 3 minuten</option>
<option value="5" <?= (int)$notifSettings['repeat_interval_minutes'] === 5 ? 'selected' : '' ?>>Elke 5 minuten</option>
</select>
</div>
</div>
<div class="flex items-center justify-between p-4 rounded-xl bg-slate-50/70 dark:bg-slate-900/60 border border-slate-200/80 dark:border-slate-800">
<div class="flex items-center space-x-2.5">
<i data-lucide="volume-2" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<div>
<span class="text-xs font-semibold text-slate-900 dark:text-white block">Subtiel Geluidssignaal</span>
<span class="text-[11px] text-slate-500 dark:text-slate-400">Speel een zachte chime af bij binnenkomst van de herinnering</span>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="sound_enabled" <?= !empty($notifSettings['sound_enabled']) ? 'checked' : '' ?> class="sr-only peer">
<div class="w-11 h-6 bg-slate-200 dark: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-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-semibold transition shadow-xs cursor-pointer">
Notificatievoorkeuren Opslaan
</button>
</div>
</form>
</div>
</div>
<!-- Sectie 4: Account & Beveiliging (Wachtwoord & 2FA) -->
<div class="space-y-6">
<!-- 2FA Beveiliging Card -->
<div class="p-5 rounded-2xl bg-teal-50/70 dark:bg-teal-950/20 border border-teal-200/80 dark:border-teal-800/40 flex flex-col sm:flex-row sm:items-center justify-between gap-4 shadow-xs">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-teal-500/15 border border-teal-500/30 flex items-center justify-center text-teal-700 dark:text-teal-400 font-bold shadow-xs flex-shrink-0">
<i data-lucide="shield-check" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Tweestapsverificatie (2FA) is Actief</h3>
<span class="px-2 py-0.5 rounded-full bg-teal-500/15 text-teal-800 dark:text-teal-300 font-semibold text-[10px] border border-teal-500/20">Beveiligd</span>
</div>
<p class="text-xs text-slate-600 dark:text-slate-400 mt-0.5">
Uw Fociz account is optimaal beveiligd met een Authenticator app (TOTP) voor maximale privacy- en dataveiligheid.
</p>
</div>
</div>
<div class="text-xs font-mono font-medium text-teal-700 dark:text-teal-400 flex items-center space-x-1.5 flex-shrink-0 self-end sm:self-center">
<i data-lucide="lock" class="w-4 h-4"></i>
<span>Verplicht & Actief</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Profielnaam formulier -->
<div class="fociz-card p-6 sm:p-7 space-y-4">
<div class="flex items-center space-x-3 pb-4 border-b border-slate-100 dark:border-slate-800/80">
<i data-lucide="user" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Persoonlijke Gegevens</h3>
</div>
<form method="POST" class="space-y-4">
<input type="hidden" name="action" value="update_profile">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
Weergavenaam
</label>
<input type="text" name="name" required value="<?= htmlspecialchars($userProfile['name']) ?>" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
E-mailadres
</label>
<input type="email" readonly disabled value="<?= htmlspecialchars($userProfile['email']) ?>" class="w-full bg-slate-100/70 dark:bg-slate-900/40 border border-slate-200 dark:border-slate-800/60 rounded-xl px-3.5 py-2.5 text-xs text-slate-500 cursor-not-allowed shadow-2xs">
<span class="text-[10px] text-slate-500 mt-1 block">E-mailadres is gekoppeld aan uw account en 2FA.</span>
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-white text-xs font-semibold transition border border-slate-900 dark:border-slate-700 shadow-xs cursor-pointer">
Naam Opslaan
</button>
</div>
</form>
</div>
<!-- Wachtwoord Wijzigen Formulier -->
<div class="fociz-card p-6 sm:p-7 space-y-4">
<div class="flex items-center space-x-3 pb-4 border-b border-slate-100 dark:border-slate-800/80">
<i data-lucide="key" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Wachtwoord Wijzigen</h3>
</div>
<form method="POST" class="space-y-3">
<input type="hidden" name="action" value="change_password">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Huidig Wachtwoord
</label>
<input type="password" name="current_password" required class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Nieuw Wachtwoord (min. 12 tekens)
</label>
<input type="password" name="new_password" required minlength="12" placeholder="Minimaal 12 tekens" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<p class="text-[10px] text-slate-400 dark:text-slate-500 mt-1">Minimaal 12 tekens (incl. hoofdletter, kleine letter, cijfer en speciaal teken).</p>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Bevestig Nieuw Wachtwoord
</label>
<input type="password" name="confirm_password" required minlength="12" placeholder="Herhaal nieuw wachtwoord" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div class="flex justify-end pt-2">
<button type="submit" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-white text-xs font-semibold transition border border-slate-900 dark:border-slate-700 shadow-xs cursor-pointer">
Wachtwoord Wijzigen
</button>
</div>
</form>
</div>
</div>
<!-- Actieve Apparaten & Sessies Card (SOC 2 CC6.1 / ISO 27001 A.9.4.2) -->
<div class="fociz-card p-6 sm:p-7 space-y-5">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-3.5">
<div class="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs flex-shrink-0">
<i data-lucide="laptop" class="w-6 h-6"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Actieve Apparaten & Sessies</h3>
<span class="px-2 py-0.5 rounded-full bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-200 dark:border-emerald-800/60 text-emerald-700 dark:text-emerald-300 font-semibold text-[10px]">
<?= count($activeSessions) ?> <?= count($activeSessions) === 1 ? 'actieve sessie' : 'actieve sessies' ?>
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Centraal overzicht van alle actieve inlogapparaten conform ISO 27001 en SOC 2 richtlijnen.
</p>
</div>
</div>
<?php if (count($activeSessions) > 1): ?>
<form method="POST" action="/profile.php" onsubmit="return confirm('Weet u zeker dat u alle andere actieve sessies op afstand wilt beëindigen? U blijft op dit apparaat ingelogd.')">
<input type="hidden" name="action" value="revoke_other_sessions">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
<button type="submit" class="inline-flex items-center space-x-2 px-3.5 py-2 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/30 dark:hover:bg-rose-900/40 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-800/60 text-xs font-semibold transition cursor-pointer">
<i data-lucide="log-out" class="w-3.5 h-3.5"></i>
<span>Overal Uitloggen (Overige Sessies)</span>
</button>
</form>
<?php endif; ?>
</div>
<div class="space-y-2.5">
<?php foreach ($activeSessions as $s): ?>
<div class="p-3.5 rounded-2xl <?= $s['is_current'] ? 'bg-indigo-50/40 dark:bg-indigo-950/20 border-indigo-200/80 dark:border-indigo-800/50' : 'bg-slate-50/70 dark:bg-[#0c0f17]/60 border-slate-200 dark:border-slate-800' ?> border flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div class="flex items-center space-x-3">
<div class="w-9 h-9 rounded-xl <?= $s['is_current'] ? 'bg-indigo-600 text-white shadow-xs' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?> flex items-center justify-center flex-shrink-0">
<i data-lucide="<?= stripos($s['device_name'] ?? '', 'Phone') !== false || stripos($s['device_name'] ?? '', 'iPhone') !== false || stripos($s['device_name'] ?? '', 'Android') !== false ? 'smartphone' : 'monitor' ?>" class="w-4 h-4"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<span class="text-xs font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($s['device_name'] ?: 'Onbekend apparaat') ?></span>
<?php if ($s['is_current']): ?>
<span class="px-2 py-0.5 rounded-md bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800/60 text-[10px] font-bold">
Dit apparaat (Huidig)
</span>
<?php endif; ?>
</div>
<div class="text-[11px] text-slate-500 dark:text-slate-400 flex items-center space-x-2 mt-0.5">
<span class="font-mono"><?= htmlspecialchars($s['ip_address']) ?></span>
<span>•</span>
<span>Laatst actief: <?= date('d-m-Y H:i', strtotime($s['last_activity_at'])) ?></span>
</div>
</div>
</div>
<?php if (!$s['is_current']): ?>
<button type="button" onclick="revokeSessionById(<?= (int)$s['id'] ?>)" class="self-end sm:self-center px-3 py-1.5 rounded-xl bg-slate-100 hover:bg-rose-50 dark:bg-slate-800 dark:hover:bg-rose-950/30 text-slate-600 hover:text-rose-600 dark:text-slate-400 dark:hover:text-rose-400 text-xs font-semibold transition border border-slate-200 dark:border-slate-700 hover:border-rose-200 dark:hover:border-rose-800 cursor-pointer">
Sessie Beëindigen
</button>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<script>
// --- Thema Selector Functionaliteit ---
function selectThemePreference(theme) {
if (typeof focizSetTheme === 'function') {
focizSetTheme(theme, true);
}
updateProfileThemeUI(theme);
}
function updateProfileThemeUI(theme) {
const themes = ['dark', 'light', 'system'];
themes.forEach(t => {
const btn = document.getElementById('themeBtn-' + t);
const check = document.getElementById('themeCheck-' + t);
if (!btn || !check) return;
if (t === theme) {
btn.classList.add('ring-2', 'ring-indigo-500', 'border-indigo-500');
check.classList.remove('hidden');
check.classList.add('flex');
} else {
btn.classList.remove('ring-2', 'ring-indigo-500', 'border-indigo-500');
check.classList.add('hidden');
check.classList.remove('flex');
}
});
const badge = document.getElementById('themeStatusBadge');
if (badge) {
const label = theme.charAt(0).toUpperCase() + theme.slice(1);
badge.innerText = 'Actief: ' + label;
}
}
function toggleJiraToken() {
const input = document.getElementById('jiraApiTokenInput');
const btn = document.getElementById('toggleJiraTokenBtn');
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
if (btn) btn.innerHTML = '<i data-lucide="eye-off" class="w-4 h-4"></i>';
} else {
input.type = 'password';
if (btn) btn.innerHTML = '<i data-lucide="eye" class="w-4 h-4"></i>';
}
if (window.lucide && typeof lucide.createIcons === 'function') {
lucide.createIcons();
}
}
function previewUploadAvatar(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
const nameEl = document.getElementById('uploadFileName');
if (nameEl) {
nameEl.textContent = '📄 ' + file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
nameEl.classList.remove('hidden');
}
// Live thumbnail weergave
const reader = new FileReader();
reader.onload = function(e) {
let prevImg = document.getElementById('uploadAvatarLivePreview');
if (!prevImg && nameEl && nameEl.parentNode) {
prevImg = document.createElement('img');
prevImg.id = 'uploadAvatarLivePreview';
prevImg.className = 'w-16 h-16 rounded-full mx-auto my-2 object-cover border-2 border-indigo-500 shadow-sm';
nameEl.parentNode.insertBefore(prevImg, nameEl);
}
if (prevImg) {
prevImg.src = e.target.result;
prevImg.classList.remove('hidden');
}
};
reader.readAsDataURL(file);
}
}
function filterAvatarCategory(category) {
const items = document.querySelectorAll('.avatar-item');
items.forEach(el => {
const cat = el.getAttribute('data-category');
if (category === 'all' || cat === category) {
el.classList.remove('hidden');
} else {
el.classList.add('hidden');
}
});
const btns = document.querySelectorAll('.av-cat-btn');
btns.forEach(btn => {
btn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-semibold transition cursor-pointer bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300';
});
const activeBtn = document.getElementById('avCatBtn-' + category);
if (activeBtn) {
activeBtn.className = 'av-cat-btn px-2.5 py-1 rounded-lg text-[11px] font-bold transition cursor-pointer bg-indigo-600 text-white shadow-xs';
}
}
document.addEventListener('DOMContentLoaded', () => {
// Model en provider sync
const profileModelSelect = document.getElementById('profile_ai_model');
const profileProviderInput = document.getElementById('profile_ai_provider');
if (profileModelSelect && profileProviderInput) {
profileModelSelect.addEventListener('change', function() {
if (this.value.indexOf('gemini') !== -1) {
profileProviderInput.value = 'gemini';
} else {
profileProviderInput.value = 'openai';
}
});
}
// Initialiseer geselecteerde thema-kaart
const activeTheme = localStorage.getItem('fociz_theme') || '<?= $userTheme ?>' || 'dark';
updateProfileThemeUI(activeTheme);
// Permissie status
if (!("Notification" in window)) return;
const grantedEl = document.getElementById('profileNotifPermGranted');
const reqEl = document.getElementById('profileNotifPermRequest');
const deniedEl = document.getElementById('profileNotifPermDenied');
if (Notification.permission === 'granted') {
if (grantedEl) grantedEl.classList.remove('hidden');
if (reqEl) reqEl.classList.add('hidden');
if (deniedEl) deniedEl.classList.add('hidden');
} else if (Notification.permission === 'denied') {
if (grantedEl) grantedEl.classList.add('hidden');
if (reqEl) reqEl.classList.add('hidden');
if (deniedEl) deniedEl.classList.remove('hidden');
} else {
if (grantedEl) grantedEl.classList.add('hidden');
if (reqEl) reqEl.classList.remove('hidden');
if (deniedEl) deniedEl.classList.add('hidden');
}
});
async function testPersonalAiConnection() {
const btn = document.getElementById('profileAiTestBtn');
const btnText = document.getElementById('profileAiTestBtnText');
const resultDiv = document.getElementById('profileAiTestResult');
const apiKey = document.getElementById('profile_ai_api_key').value.trim();
const model = document.getElementById('profile_ai_model').value;
const provider = document.getElementById('profile_ai_provider').value;
resultDiv.classList.add('hidden');
resultDiv.className = 'hidden p-3 rounded-xl text-xs';
btn.disabled = true;
btnText.innerText = 'Testen...';
try {
const payload = {
provider: provider,
model: model,
};
// Stuur alleen de ingevulde sleutel mee als het geen placeholder bolletjes zijn
if (apiKey && apiKey !== '••••••••••••••••') {
payload.api_key = apiKey;
}
const res = await fetch('/api/ai.php?action=test_connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
resultDiv.classList.remove('hidden');
if (data.success) {
resultDiv.className = 'p-3 rounded-xl text-xs bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 flex items-center space-x-2';
resultDiv.innerHTML = `<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i><span><strong>Verbinding Geslaagd!</strong> ${data.message}</span>`;
} else {
resultDiv.className = 'p-3 rounded-xl text-xs bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 flex items-center space-x-2';
resultDiv.innerHTML = `<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i><span><strong>Verbinding Mislukt:</strong> ${data.error || 'Onbekende fout'}</span>`;
}
if (window.lucide) lucide.createIcons();
} catch (err) {
resultDiv.classList.remove('hidden');
resultDiv.className = 'p-3 rounded-xl text-xs bg-rose-50 dark:bg-rose-950/30 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-200 flex items-center space-x-2';
resultDiv.innerHTML = `<i data-lucide="alert-circle" class="w-4 h-4 text-rose-600 dark:text-rose-400 flex-shrink-0"></i><span><strong>Netwerkfout:</strong> ${err.message}</span>`;
if (window.lucide) lucide.createIcons();
} finally {
btn.disabled = false;
btnText.innerText = 'Test Verbinding';
}
}
async function revokeSessionById(sessionId) {
if (!confirm('Weet u zeker dat u deze sessie op afstand wilt beëindigen?')) return;
try {
const formData = new FormData();
formData.append('action', 'revoke_session');
formData.append('session_id', sessionId);
formData.append('csrf_token', '<?= htmlspecialchars(get_csrf_token()) ?>');
const res = await fetch('/api/session.php', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
window.location.reload();
} else {
alert(data.error || 'Fout bij beëindigen van sessie.');
}
} catch (e) {
alert('Netwerkfout bij beëindigen van sessie: ' + e.message);
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>