2535 lines
175 KiB
PHP
2535 lines
175 KiB
PHP
<?php
|
|
/**
|
|
* Fociz Systeembeheer & Centrale Integraties Portaal (Uitsluitend voor Beheerders)
|
|
*/
|
|
|
|
require_once __DIR__ . '/includes/auth.php';
|
|
require_once __DIR__ . '/includes/integrations/microsoft.php';
|
|
require_once __DIR__ . '/includes/integrations/jira.php';
|
|
require_once __DIR__ . '/includes/settings.php';
|
|
|
|
// Strikte autorisatie: alleen gebruikers met de rol 'ADMIN' hebben toegang
|
|
$adminUser = require_admin();
|
|
$currentUser = $adminUser;
|
|
$userId = (int)$adminUser['id'];
|
|
$db = get_db_connection();
|
|
|
|
$message = null;
|
|
$messageType = 'success';
|
|
|
|
$validTabs = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
|
$activeTab = $_GET['tab'] ?? 'users';
|
|
if (!in_array($activeTab, $validTabs, true)) {
|
|
$activeTab = 'users';
|
|
}
|
|
|
|
if (!empty($_GET['success'])) {
|
|
$message = $_GET['success'];
|
|
$messageType = 'success';
|
|
} elseif (!empty($_GET['error'])) {
|
|
$message = $_GET['error'];
|
|
$messageType = 'error';
|
|
}
|
|
|
|
// POST Verwerking VÓÓR header output zodat redirects en opslaan vlekkeloos werken
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!empty($_POST['active_tab']) && in_array($_POST['active_tab'], $validTabs, true)) {
|
|
$activeTab = $_POST['active_tab'];
|
|
}
|
|
|
|
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. Centrale Azure App Registratie opslaan
|
|
if ($action === 'save_azure') {
|
|
$activeTab = 'microsoft';
|
|
$clientId = trim($_POST['client_id'] ?? '');
|
|
$clientSecret = trim($_POST['client_secret'] ?? '');
|
|
$tenantId = trim($_POST['tenant_id'] ?? '');
|
|
if (empty($tenantId) || $tenantId === 'common') {
|
|
$tenantId = 'planningsagenda.nl';
|
|
}
|
|
$isDemo = isset($_POST['is_demo_mode']) ? 1 : 0;
|
|
|
|
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
|
|
$stmtExisting->execute([$userId]);
|
|
$existing = $stmtExisting->fetch();
|
|
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
|
|
|
|
if (($clientSecret === '' || $clientSecret === '••••••••••••••••') && !empty($existingSettings['client_secret']) && $existingSettings['client_secret'] !== '••••••••••••••••') {
|
|
$clientSecret = $existingSettings['client_secret'];
|
|
}
|
|
|
|
$newSettings = array_merge($existingSettings, [
|
|
'client_id' => $clientId,
|
|
'client_secret' => $clientSecret,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
$stmt = $db->prepare("
|
|
INSERT INTO integrations (user_id, provider, is_enabled, is_demo_mode, settings, last_sync_at)
|
|
VALUES (?, 'OUTLOOK', 1, ?, ?, NOW())
|
|
ON DUPLICATE KEY UPDATE
|
|
is_demo_mode = VALUES(is_demo_mode),
|
|
settings = VALUES(settings),
|
|
last_sync_at = NOW()
|
|
");
|
|
$stmt->execute([$userId, $isDemo, json_encode($newSettings)]);
|
|
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'integration', null, ['provider' => 'OUTLOOK', 'tenant_id' => $tenantId], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Centrale Microsoft 365 instellingen (Agenda, E-mail & Teams) zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
}
|
|
|
|
// 2. Centraal Jira Bedrijfsdomein opslaan
|
|
elseif ($action === 'save_jira_domain') {
|
|
$activeTab = 'jira';
|
|
$rawDomain = trim($_POST['jira_domain'] ?? '');
|
|
$companyDomain = !empty($rawDomain) ? jira_normalize_domain($rawDomain) : '';
|
|
|
|
$stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'JIRA'");
|
|
$stmtExisting->execute([$userId]);
|
|
$existing = $stmtExisting->fetch();
|
|
$existingSettings = json_decode($existing['settings'] ?? '{}', true) ?: [];
|
|
|
|
$existingSettings['jira_domain'] = $companyDomain;
|
|
|
|
$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
|
|
settings = VALUES(settings),
|
|
last_sync_at = NOW()
|
|
");
|
|
$stmt->execute([$userId, json_encode($existingSettings)]);
|
|
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'integration', null, ['provider' => 'JIRA', 'domain' => $companyDomain], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Standaard Atlassian Jira bedrijfsdomein is succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
}
|
|
|
|
// 2b. Jira Wachtrijfilter toevoegen of bewerken
|
|
elseif ($action === 'save_jira_queue_filter') {
|
|
$activeTab = 'jira';
|
|
$filterId = (int)($_POST['filter_id'] ?? 0);
|
|
$filterCode = strtoupper(trim($_POST['filter_code'] ?? ''));
|
|
$filterName = trim($_POST['filter_name'] ?? '');
|
|
$projectKey = strtoupper(trim($_POST['project_key'] ?? ''));
|
|
$jqlClause = trim($_POST['jql_clause'] ?? '');
|
|
$description = trim($_POST['description'] ?? '');
|
|
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
|
$sortOrder = (int)($_POST['sort_order'] ?? 0);
|
|
|
|
if (empty($filterCode) || empty($filterName) || empty($jqlClause)) {
|
|
$message = "Filtercode, naam en JQL-clausule zijn verplicht.";
|
|
$messageType = 'error';
|
|
} else {
|
|
if ($filterId > 0) {
|
|
$stmt = $db->prepare("
|
|
UPDATE jira_queue_filters
|
|
SET filter_code = ?, filter_name = ?, project_key = ?, jql_clause = ?, description = ?, is_active = ?, sort_order = ?, updated_at = NOW()
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder, $filterId]);
|
|
$message = "Wachtrijfilter '{$filterName}' is succesvol bijgewerkt.";
|
|
} else {
|
|
$stmt = $db->prepare("
|
|
INSERT INTO jira_queue_filters (filter_code, filter_name, project_key, jql_clause, description, is_active, sort_order)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
filter_name = VALUES(filter_name),
|
|
project_key = VALUES(project_key),
|
|
jql_clause = VALUES(jql_clause),
|
|
description = VALUES(description),
|
|
is_active = VALUES(is_active),
|
|
sort_order = VALUES(sort_order),
|
|
updated_at = NOW()
|
|
");
|
|
$stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]);
|
|
$message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd.";
|
|
}
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_queue_filter', $filterId ?: null, ['filter_code' => $filterCode, 'filter_name' => $filterName], 'SUCCESS', $adminUser['email']);
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
// 2c. Jira Wachtrijfilter verwijderen
|
|
elseif ($action === 'delete_jira_queue_filter') {
|
|
$activeTab = 'jira';
|
|
$filterId = (int)($_POST['filter_id'] ?? 0);
|
|
if ($filterId > 0) {
|
|
$stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?");
|
|
$stmt->execute([$filterId]);
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_queue_filter', $filterId, ['action' => 'delete'], 'SUCCESS', $adminUser['email']);
|
|
$message = "Wachtrijfilter is succesvol verwijderd.";
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
// 2d. Teamleden toewijzen aan Jira Wachtrijfilter
|
|
elseif ($action === 'save_user_jira_assignments') {
|
|
$activeTab = 'jira';
|
|
$assignments = $_POST['user_filter'] ?? [];
|
|
if (is_array($assignments)) {
|
|
$upd = $db->prepare("UPDATE users SET default_jira_filter_code = ? WHERE id = ?");
|
|
foreach ($assignments as $uId => $fCode) {
|
|
$fCode = trim($fCode);
|
|
$upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]);
|
|
}
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_assignments', null, [], 'SUCCESS', $adminUser['email']);
|
|
$message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
|
|
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
|
|
elseif ($action === 'save_workday_settings') {
|
|
$rawWorkdays = isset($_POST['workdays']) && is_array($_POST['workdays'])
|
|
? array_values(array_filter(array_map('intval', $_POST['workdays']), fn($d) => $d >= 1 && $d <= 7))
|
|
: [1, 2, 3, 4, 5];
|
|
if (empty($rawWorkdays)) {
|
|
$rawWorkdays = [1, 2, 3, 4, 5];
|
|
}
|
|
sort($rawWorkdays);
|
|
$workdaysStr = implode(',', $rawWorkdays);
|
|
|
|
$workdayStart = trim($_POST['workday_start'] ?? '08:30');
|
|
$workdayEnd = trim($_POST['workday_end'] ?? '17:00');
|
|
$lunchStart = trim($_POST['lunch_start'] ?? '12:30');
|
|
$lunchEnd = trim($_POST['lunch_end'] ?? '13:00');
|
|
$pomoFocus = max(10, min(120, (int)($_POST['pomodoro_focus'] ?? 25)));
|
|
$pomoDeep = max(20, min(180, (int)($_POST['pomodoro_deep'] ?? 50)));
|
|
$pomoBreak = max(2, min(30, (int)($_POST['pomodoro_break'] ?? 5)));
|
|
$taskDuration = in_array((int)($_POST['default_task_duration'] ?? 30), [15, 30, 45, 60, 90, 120]) ? (int)$_POST['default_task_duration'] : 30;
|
|
|
|
set_system_setting('workdays', $workdaysStr);
|
|
set_system_setting('workday_start', $workdayStart);
|
|
set_system_setting('workday_end', $workdayEnd);
|
|
set_system_setting('lunch_start', $lunchStart);
|
|
set_system_setting('lunch_end', $lunchEnd);
|
|
set_system_setting('pomodoro_focus', $pomoFocus);
|
|
set_system_setting('pomodoro_deep', $pomoDeep);
|
|
set_system_setting('pomodoro_break', $pomoBreak);
|
|
set_system_setting('default_task_duration', $taskDuration);
|
|
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'workday_settings', null, ['workdays' => $workdaysStr, 'workday_start' => $workdayStart, 'workday_end' => $workdayEnd], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Organisatie werkdagstandaarden zijn succesvol bijgewerkt.";
|
|
$messageType = 'success';
|
|
$activeTab = 'calendar';
|
|
}
|
|
|
|
// 3b. Agenda Blokkade Opties / Redenen opslaan
|
|
elseif ($action === 'save_calendar_block_reasons') {
|
|
$rawReasons = $_POST['reasons'] ?? [];
|
|
$sanitizedReasons = [];
|
|
if (is_array($rawReasons)) {
|
|
foreach ($rawReasons as $r) {
|
|
$code = strtoupper(trim((string)($r['code'] ?? '')));
|
|
$code = preg_replace('/[^A-Z0-9_]/', '', $code);
|
|
$title = trim((string)($r['title'] ?? ''));
|
|
$icon = trim((string)($r['icon'] ?? '📅'));
|
|
$isActive = isset($r['is_active']) && ($r['is_active'] == '1' || $r['is_active'] === 'on') ? 1 : 0;
|
|
$isDelete = isset($r['is_delete']) && ($r['is_delete'] == '1' || $r['is_delete'] === 'on');
|
|
|
|
if ($isDelete || empty($code) || empty($title)) {
|
|
continue;
|
|
}
|
|
|
|
$sanitizedReasons[] = [
|
|
'code' => $code,
|
|
'title' => $title,
|
|
'icon' => !empty($icon) ? $icon : '📅',
|
|
'is_active' => $isActive,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Nieuwe optie toevoegen indien ingevuld
|
|
$newCode = strtoupper(trim((string)($_POST['new_code'] ?? '')));
|
|
$newCode = preg_replace('/[^A-Z0-9_]/', '', $newCode);
|
|
$newTitle = trim((string)($_POST['new_title'] ?? ''));
|
|
$newIcon = trim((string)($_POST['new_icon'] ?? '📅'));
|
|
|
|
if (!empty($newCode) && !empty($newTitle)) {
|
|
$exists = false;
|
|
foreach ($sanitizedReasons as &$sr) {
|
|
if ($sr['code'] === $newCode) {
|
|
$sr['title'] = $newTitle;
|
|
$sr['icon'] = !empty($newIcon) ? $newIcon : '📅';
|
|
$sr['is_active'] = 1;
|
|
$exists = true;
|
|
break;
|
|
}
|
|
}
|
|
unset($sr);
|
|
if (!$exists) {
|
|
$sanitizedReasons[] = [
|
|
'code' => $newCode,
|
|
'title' => $newTitle,
|
|
'icon' => !empty($newIcon) ? $newIcon : '📅',
|
|
'is_active' => 1,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (empty($sanitizedReasons)) {
|
|
$sanitizedReasons = [
|
|
['code' => 'VACATION', 'title' => 'Vakantie', 'icon' => '🏖️', 'is_active' => 1],
|
|
['code' => 'LEAVE', 'title' => 'Verlof / Vrij', 'icon' => '✈️', 'is_active' => 1],
|
|
['code' => 'SICK', 'title' => 'Ziek / Afwezig', 'icon' => '🤒', 'is_active' => 1],
|
|
['code' => 'FOCUS', 'title' => 'Focusdag', 'icon' => '🎯', 'is_active' => 1],
|
|
['code' => 'ADV', 'title' => 'ADV Dag', 'icon' => '📋', 'is_active' => 1],
|
|
];
|
|
}
|
|
|
|
set_system_setting('calendar_block_reasons', $sanitizedReasons);
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'calendar_block_reasons', null, ['count' => count($sanitizedReasons)], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Agenda blokkade opties zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
$activeTab = 'calendar';
|
|
}
|
|
|
|
// 3c. Brainstorm Categorieën opslaan
|
|
elseif ($action === 'save_brainstorm_categories') {
|
|
$rawCategories = $_POST['categories'] ?? [];
|
|
$sanitizedCategories = [];
|
|
if (is_array($rawCategories)) {
|
|
foreach ($rawCategories as $c) {
|
|
$name = trim((string)($c['name'] ?? ''));
|
|
$desc = trim((string)($c['description'] ?? ''));
|
|
$isActive = isset($c['is_active']) && ($c['is_active'] == '1' || $c['is_active'] === 'on') ? 1 : 0;
|
|
$isDelete = isset($c['is_delete']) && ($c['is_delete'] == '1' || $c['is_delete'] === 'on');
|
|
|
|
if ($isDelete || empty($name)) {
|
|
continue;
|
|
}
|
|
|
|
$sanitizedCategories[] = [
|
|
'name' => $name,
|
|
'description' => $desc,
|
|
'is_active' => $isActive,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Nieuwe categorie toevoegen indien ingevuld
|
|
$newName = trim((string)($_POST['new_category_name'] ?? ''));
|
|
$newDesc = trim((string)($_POST['new_category_desc'] ?? ''));
|
|
if (!empty($newName)) {
|
|
$exists = false;
|
|
foreach ($sanitizedCategories as &$sc) {
|
|
if (strcasecmp($sc['name'], $newName) === 0) {
|
|
$sc['name'] = $newName;
|
|
if (!empty($newDesc)) $sc['description'] = $newDesc;
|
|
$sc['is_active'] = 1;
|
|
$exists = true;
|
|
break;
|
|
}
|
|
}
|
|
unset($sc);
|
|
if (!$exists) {
|
|
$sanitizedCategories[] = [
|
|
'name' => $newName,
|
|
'description' => $newDesc,
|
|
'is_active' => 1,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (empty($sanitizedCategories)) {
|
|
$sanitizedCategories = [
|
|
['name' => 'Product & Strategie', 'description' => 'Strategische richting, nieuwe producten en visie', 'is_active' => 1],
|
|
['name' => 'Procesverbetering', 'description' => 'Optimalisatie van werkprocessen en efficiëntie', 'is_active' => 1],
|
|
['name' => 'Klant & Service', 'description' => 'Klanttevredenheid, support en klantervaring', 'is_active' => 1],
|
|
['name' => 'Technologie & IT', 'description' => 'Architectuur, tooling, infrastructuur en tech stack','is_active' => 1],
|
|
['name' => 'Marketing & Groei', 'description' => 'Leadgeneratie, branding en marktpositie', 'is_active' => 1],
|
|
];
|
|
}
|
|
|
|
set_system_setting('brainstorm_categories', $sanitizedCategories);
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'brainstorm_categories', null, ['count' => count($sanitizedCategories)], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Brainstorm categorieën zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
$activeTab = 'system';
|
|
}
|
|
|
|
// 4. AI Co-Pilot instellingen opslaan (Release v1.5)
|
|
elseif ($action === 'save_ai_settings') {
|
|
$activeTab = 'ai';
|
|
try {
|
|
$aiEnabled = isset($_POST['ai_enabled']) ? 1 : 0;
|
|
$aiModel = trim((string)($_POST['ai_model'] ?? 'gemini-1.5-pro'));
|
|
$aiProvider = trim((string)($_POST['ai_provider'] ?? ''));
|
|
if (empty($aiProvider)) {
|
|
$aiProvider = str_contains($aiModel, 'gpt') ? 'openai' : 'gemini';
|
|
}
|
|
$apiKeyIn = trim((string)($_POST['ai_api_key'] ?? ''));
|
|
|
|
if ($apiKeyIn !== '' && $apiKeyIn !== '••••••••••••••••') {
|
|
set_system_setting('ai_api_key', fociz_encrypt($apiKeyIn));
|
|
}
|
|
|
|
set_system_setting('ai_enabled', $aiEnabled);
|
|
set_system_setting('ai_provider', $aiProvider);
|
|
set_system_setting('ai_model', $aiModel);
|
|
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'ai_settings', null, [
|
|
'ai_enabled' => $aiEnabled,
|
|
'ai_provider' => $aiProvider,
|
|
'ai_model' => $aiModel
|
|
], 'SUCCESS', $adminUser['email'] ?? null);
|
|
|
|
$message = "AI Co-Pilot instellingen zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij opslaan AI instellingen: " . $e->getMessage());
|
|
$message = "Fout bij opslaan van AI instellingen: " . htmlspecialchars($e->getMessage());
|
|
$messageType = 'error';
|
|
}
|
|
}
|
|
|
|
// 5. Test e-mail versturen via SMTP (Release v1.6)
|
|
elseif ($action === 'send_test_email') {
|
|
$activeTab = 'system';
|
|
require_once __DIR__ . '/includes/mailer.php';
|
|
$testEmail = trim($_POST['test_email'] ?? '');
|
|
if (empty($testEmail) || !filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$message = "Vul een geldig e-mailadres in om een test e-mail te sturen.";
|
|
$messageType = 'error';
|
|
} else {
|
|
$mailResult = FocizMailer::sendTestEmail($testEmail, $currentUser['name']);
|
|
if ($mailResult['success']) {
|
|
$message = "Test e-mail succesvol verzonden via mail.fociz.nl naar " . htmlspecialchars($testEmail) . "!";
|
|
$messageType = 'success';
|
|
} else {
|
|
$message = "Fout bij verzenden test e-mail via mail.fociz.nl: " . htmlspecialchars($mailResult['error'] ?? 'Onbekende fout');
|
|
$messageType = 'error';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6. Sessie- & Beveiligingsinstellingen opslaan (ISO 27001 / NIST SP 800-63B)
|
|
elseif ($action === 'save_security_settings') {
|
|
$activeTab = 'security';
|
|
$timeout = (int)($_POST['session_idle_timeout'] ?? 1800);
|
|
if (!in_array($timeout, [900, 1800, 3600, 7200], true)) {
|
|
$timeout = 1800;
|
|
}
|
|
set_system_setting('session_idle_timeout', $timeout);
|
|
|
|
fociz_log_audit($userId, 'SETTINGS_UPDATED', 'security_settings', null, ['session_idle_timeout' => $timeout], 'SUCCESS', $adminUser['email']);
|
|
|
|
$message = "Sessie- en beveiligingsinstellingen zijn succesvol opgeslagen.";
|
|
$messageType = 'success';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Haal actuele configuratie op
|
|
$appConfig = microsoft_get_app_config();
|
|
$companyJiraDomain = jira_get_company_domain();
|
|
$workdaySettings = get_workday_settings();
|
|
$aiSettings = get_ai_settings();
|
|
$aiUsageStats = get_ai_usage_stats();
|
|
$sessionIdleTimeout = get_session_idle_timeout();
|
|
$calendarBlockReasons = get_calendar_block_reasons();
|
|
$brainstormCategories = get_brainstorm_categories();
|
|
|
|
// Redirect URI
|
|
$redirectUri = rtrim(APP_URL, '/') . '/auth/callback_microsoft.php';
|
|
|
|
// Admin Consent URL
|
|
$tenantForConsent = !empty($appConfig['tenant_id']) ? $appConfig['tenant_id'] : 'common';
|
|
$adminConsentUrl = "https://login.microsoftonline.com/" . urlencode($tenantForConsent) . "/adminconsent?client_id=" . urlencode($appConfig['client_id']) . "&redirect_uri=" . urlencode($redirectUri);
|
|
|
|
// Statistieken ophalen voor Systeem & Team Monitor
|
|
$totalUsers = 0;
|
|
$totalAdmins = 0;
|
|
$totalMembers = 0;
|
|
$pendingInvites = 0;
|
|
$outlookConnectedCount = 0;
|
|
$jiraConnectedCount = 0;
|
|
$recentUsers = [];
|
|
|
|
try {
|
|
$totalUsers = (int)$db->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
|
$totalAdmins = (int)$db->query("SELECT COUNT(*) FROM users WHERE role = 'ADMIN'")->fetchColumn();
|
|
$totalMembers = $totalUsers - $totalAdmins;
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen user stats: " . $e->getMessage());
|
|
}
|
|
|
|
try {
|
|
$pendingInvites = (int)$db->query("SELECT COUNT(*) FROM invites WHERE used_at IS NULL AND expires_at > NOW()")->fetchColumn();
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen invites stats: " . $e->getMessage());
|
|
ensure_tables_exist($db);
|
|
}
|
|
|
|
try {
|
|
$outlookConnectedCount = (int)$db->query("SELECT COUNT(*) FROM integrations WHERE provider = 'OUTLOOK' AND access_token IS NOT NULL")->fetchColumn();
|
|
$jiraConnectedCount = (int)$db->query("SELECT COUNT(*) FROM integrations WHERE provider = 'JIRA' AND settings LIKE '%jira_api_token%' AND settings NOT LIKE '%\"jira_api_token\":\"\"%'")->fetchColumn();
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen integrations stats: " . $e->getMessage());
|
|
ensure_tables_exist($db);
|
|
}
|
|
|
|
try {
|
|
$recentUsers = $db->query("
|
|
SELECT u.id, u.name, u.email, u.role, u.created_at,
|
|
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'OUTLOOK' AND i.access_token IS NOT NULL) as has_outlook,
|
|
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'JIRA' AND i.settings LIKE '%jira_api_token%' AND i.settings NOT LIKE '%\"jira_api_token\":\"\"%') as has_jira
|
|
FROM users u
|
|
ORDER BY u.created_at DESC
|
|
LIMIT 6
|
|
")->fetchAll() ?: [];
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen recente gebruikers: " . $e->getMessage());
|
|
}
|
|
|
|
$allUsersList = [];
|
|
try {
|
|
$allUsersList = $db->query("
|
|
SELECT u.id, u.name, u.email, u.role, u.created_at,
|
|
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'OUTLOOK' AND i.access_token IS NOT NULL) as has_outlook,
|
|
(SELECT COUNT(*) FROM integrations i WHERE i.user_id = u.id AND i.provider = 'JIRA' AND i.settings LIKE '%jira_api_token%' AND i.settings NOT LIKE '%\"jira_api_token\":\"\"%') as has_jira
|
|
FROM users u
|
|
ORDER BY u.name ASC
|
|
")->fetchAll() ?: [];
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen allUsersList: " . $e->getMessage());
|
|
}
|
|
|
|
$allUsersForJira = [];
|
|
$allJiraFilters = [];
|
|
try {
|
|
$allUsersForJira = $db->query("SELECT id, name, email, role, avatar_url, default_jira_filter_code FROM users ORDER BY name ASC")->fetchAll() ?: [];
|
|
$allJiraFilters = $db->query("SELECT * FROM jira_queue_filters ORDER BY sort_order ASC, id ASC")->fetchAll() ?: [];
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen jira filters/users: " . $e->getMessage());
|
|
}
|
|
|
|
$auditLogs = [];
|
|
try {
|
|
$auditLogs = $db->query("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 50")->fetchAll() ?: [];
|
|
} catch (Throwable $e) {
|
|
error_log("Fout bij ophalen audit logs: " . $e->getMessage());
|
|
}
|
|
|
|
$pageTitle = 'Systeembeheer & Integraties — Fociz';
|
|
require_once __DIR__ . '/includes/header.php';
|
|
?>
|
|
|
|
<div class="space-y-8 max-w-7xl mx-auto pb-12">
|
|
<!-- Top Header Banner -->
|
|
<div class="relative overflow-hidden fociz-card p-6 sm:p-8">
|
|
<div class="absolute -top-24 -right-24 w-80 h-80 bg-indigo-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
|
<div class="absolute -bottom-24 -left-24 w-80 h-80 bg-sky-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
|
|
|
<div class="relative z-10 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
<div class="space-y-2">
|
|
<div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-300 text-xs font-semibold tracking-wide">
|
|
<i data-lucide="shield-alert" class="w-3.5 h-3.5"></i>
|
|
<span>Beheerderspaneel • Alleen toegankelijk voor Beheer</span>
|
|
</div>
|
|
<h1 class="text-3xl sm:text-4xl font-black text-slate-900 dark:text-white tracking-tight">
|
|
Systeembeheer & Integraties
|
|
</h1>
|
|
<p class="text-sm text-slate-600 dark:text-slate-400 max-w-2xl leading-relaxed">
|
|
Beheer hier de centrale organisatiekoppelingen zoals Microsoft 365 Azure App Registratie en Jira, inspecteer de teamstatus en configureer toekomstige modules.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<a href="/users.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/90 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="users" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
|
<span>Team & Invites (<?= $totalUsers ?>)</span>
|
|
</a>
|
|
<a href="/profile.php" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs">
|
|
<i data-lucide="user" class="w-4 h-4"></i>
|
|
<span>Mijn Persoonlijk Profiel</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Meldingen Banner -->
|
|
<?php if ($message): ?>
|
|
<div class="p-4 rounded-2xl flex items-start space-x-3 text-sm animate-in fade-in duration-200 <?= $messageType === 'success' ? 'bg-teal-50/80 dark:bg-teal-950/30 border border-teal-200 dark:border-teal-800/60 text-teal-800 dark:text-teal-200' : 'bg-red-500/10 border border-red-500/30 text-red-800 dark:text-red-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-red-600 dark:text-red-400' ?>"></i>
|
|
<div class="flex-1 font-medium"><?= htmlspecialchars($message) ?></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Stat kaarten rij -->
|
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="fociz-card p-5 flex items-center space-x-4">
|
|
<div class="w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 flex-shrink-0">
|
|
<i data-lucide="users" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium block">Totaal Teamleden</span>
|
|
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $totalUsers ?></span>
|
|
<span class="text-[10px] text-slate-500 dark:text-slate-500 block"><?= $totalAdmins ?> Beheerders • <?= $totalMembers ?> Collega's</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fociz-card p-5 flex items-center space-x-4">
|
|
<div class="w-12 h-12 rounded-xl bg-sky-50 dark:bg-sky-950/40 border border-sky-200 dark:border-sky-800/60 flex items-center justify-center text-sky-600 dark:text-sky-400 flex-shrink-0">
|
|
<i data-lucide="calendar" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium block">Outlook Live Gekoppeld</span>
|
|
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $outlookConnectedCount ?></span>
|
|
<span class="text-[10px] text-teal-600 dark:text-teal-400 font-medium block"><?= $appConfig['is_configured'] ? 'Azure App Actief' : 'Configuratie nodig' ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fociz-card p-5 flex items-center space-x-4">
|
|
<div class="w-12 h-12 rounded-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 flex-shrink-0">
|
|
<i data-lucide="check-square" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium block">Jira Gekoppeld</span>
|
|
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $jiraConnectedCount ?></span>
|
|
<span class="text-[10px] text-slate-500 dark:text-slate-500 block"><?= !empty($companyJiraDomain) ? 'Domein ingesteld' : 'Geen domein' ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fociz-card p-5 flex items-center space-x-4">
|
|
<div class="w-12 h-12 rounded-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 flex-shrink-0">
|
|
<i data-lucide="mail" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium block">Openstaande Invites</span>
|
|
<span class="text-2xl font-black text-slate-900 dark:text-white"><?= $pendingInvites ?></span>
|
|
<a href="/users.php" class="text-[10px] text-amber-600 dark:text-amber-400 hover:underline block font-medium">Beheer uitnodigingen →</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Tab Navigatie Header (7 Tabs) -->
|
|
<div class="fociz-card p-2 shadow-xs">
|
|
<div class="flex items-center space-x-1 sm:space-x-2 overflow-x-auto no-scrollbar" id="adminTabsNav">
|
|
<button type="button" onclick="switchAdminTab('users')" id="tab-btn-users" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="users" class="w-4 h-4"></i>
|
|
<span>Gebruikers</span>
|
|
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-indigo-500/20 text-indigo-300 font-extrabold"><?= count($allUsersList) ?></span>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('calendar')" id="tab-btn-calendar" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="calendar" class="w-4 h-4"></i>
|
|
<span>Agenda</span>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('microsoft')" id="tab-btn-microsoft" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="cloud" class="w-4 h-4"></i>
|
|
<span>Microsoft 365</span>
|
|
<?php if ($appConfig['is_configured']): ?>
|
|
<span class="w-2 h-2 rounded-full bg-teal-400"></span>
|
|
<?php endif; ?>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('jira')" id="tab-btn-jira" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="check-square" class="w-4 h-4"></i>
|
|
<span>Jira</span>
|
|
<?php if ($jiraConnectedCount > 0): ?>
|
|
<span class="ml-1 px-1.5 py-0.5 rounded-full text-[10px] bg-sky-500/20 text-sky-400 font-extrabold"><?= $jiraConnectedCount ?></span>
|
|
<?php endif; ?>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('ai')" id="tab-btn-ai" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
|
<span>AI Co-Pilot</span>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('security')" id="tab-btn-security" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
|
<span>Beveiliging & Audit</span>
|
|
</button>
|
|
<button type="button" onclick="switchAdminTab('system')" id="tab-btn-system" class="admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer">
|
|
<i data-lucide="server" class="w-4 h-4"></i>
|
|
<span>Systeem & Database</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Inhoud Panelen -->
|
|
<div id="adminTabPanesContainer" class="space-y-8">
|
|
|
|
<!-- Tab 1: Gebruikers -->
|
|
<div id="tab-pane-users" class="admin-tab-pane space-y-6">
|
|
<!-- Team Status & Gebruikersbeheer Card -->
|
|
<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-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-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
<i data-lucide="users" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Team & Medewerkers</h2>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-300 text-[10px] font-bold">
|
|
<?= count($allUsersList) ?> Leden
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Centraal overzicht van alle teamaccounts, beheerdersrollen en gekoppelde integraties</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2.5">
|
|
<a href="/users.php" class="inline-flex items-center space-x-2 px-3.5 py-2 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">
|
|
<i data-lucide="mail" class="w-4 h-4 text-amber-500"></i>
|
|
<span>Uitnodigingen (<?= $pendingInvites ?>)</span>
|
|
</a>
|
|
<a href="/users.php" class="inline-flex items-center space-x-2 px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs">
|
|
<i data-lucide="user-plus" class="w-4 h-4"></i>
|
|
<span>Nieuwe Collega Uitnodigen</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gebruikers Tabel -->
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-xs text-slate-600 dark:text-slate-300">
|
|
<thead>
|
|
<tr class="border-b border-slate-200 dark:border-slate-800 text-[11px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
|
<th class="py-3 px-3">Medewerker</th>
|
|
<th class="py-3 px-3">E-mailadres</th>
|
|
<th class="py-3 px-3">Rol</th>
|
|
<th class="py-3 px-3 text-center">Outlook 365</th>
|
|
<th class="py-3 px-3 text-center">Jira</th>
|
|
<th class="py-3 px-3">Geregistreerd</th>
|
|
<th class="py-3 px-3 text-right">Beheer</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/60 font-sans">
|
|
<?php foreach ($allUsersList as $u): ?>
|
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition">
|
|
<td class="py-3 px-3 font-semibold text-slate-900 dark:text-white">
|
|
<div class="flex items-center space-x-2.5">
|
|
<div class="w-8 h-8 rounded-full bg-indigo-50 dark:bg-indigo-950/50 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-xs flex-shrink-0">
|
|
<?= strtoupper(substr($u['name'], 0, 1)) ?>
|
|
</div>
|
|
<span class="truncate"><?= htmlspecialchars($u['name']) ?></span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-3 font-mono text-[11px] text-slate-500 dark:text-slate-400">
|
|
<?= htmlspecialchars($u['email']) ?>
|
|
</td>
|
|
<td class="py-3 px-3">
|
|
<?php if ($u['role'] === 'ADMIN'): ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-50 dark:bg-indigo-950/50 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60">
|
|
Beheerder
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
|
Medewerker
|
|
</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="py-3 px-3 text-center">
|
|
<?php if ($u['has_outlook']): ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60" title="Outlook 365 Agenda & E-mail Live">
|
|
<i data-lucide="cloud" class="w-3 h-3 mr-1"></i> Gekoppeld
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] text-slate-400 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
|
Niet gekoppeld
|
|
</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="py-3 px-3 text-center">
|
|
<?php if ($u['has_jira']): ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60" title="Atlassian Jira Gekoppeld">
|
|
<i data-lucide="check-square" class="w-3 h-3 mr-1"></i> Gekoppeld
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] text-slate-400 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
|
|
Niet gekoppeld
|
|
</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="py-3 px-3 text-slate-500 font-mono text-[11px]">
|
|
<?= date('d-m-Y', strtotime($u['created_at'])) ?>
|
|
</td>
|
|
<td class="py-3 px-3 text-right">
|
|
<a href="/users.php" class="inline-flex items-center space-x-1 text-indigo-600 dark:text-indigo-400 hover:underline font-semibold text-xs">
|
|
<span>Beheren</span>
|
|
<i data-lucide="external-link" class="w-3 h-3"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: Agenda (Werkdagen & Blokkade Opties) -->
|
|
<div id="tab-pane-calendar" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 3. Organisatie Werkdag Standaarden (Release v1.4) -->
|
|
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
|
<div class="flex items-center justify-between pb-4 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-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
<i data-lucide="clock" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Werkdag & Dagplanning Standaarden</h2>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Centrale kantoortijden, lunchpauzes en Pomodoro presets voor het team</p>
|
|
</div>
|
|
</div>
|
|
<span class="px-2.5 py-1 rounded-full bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 text-xs font-bold border border-teal-200 dark:border-teal-800/60">
|
|
Release v1.4 Actief
|
|
</span>
|
|
</div>
|
|
|
|
<form method="POST" action="/admin.php" class="space-y-4">
|
|
<input type="hidden" name="action" value="save_workday_settings">
|
|
<input type="hidden" name="active_tab" value="calendar">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<!-- Actieve Werkdagen & Weekend Configuratie -->
|
|
<div class="p-4 sm:p-5 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-3">
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-1">
|
|
<div class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
|
<i data-lucide="calendar-days" class="w-4 h-4 text-indigo-500"></i>
|
|
<span>Actieve Werkdagen vs. Weekend</span>
|
|
</div>
|
|
<span class="text-[10px] text-slate-500 dark:text-slate-400">
|
|
Ma t/m Vr zijn standaard werkdagen; Za & Zo gelden als weekend
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
|
Vink de dagen aan die voor uw organisatie als reguliere werkdagen gelden. Niet-aangevinkte dagen worden in Fociz visueel duidelijk gemarkeerd als <strong>Weekend / Vrije dag</strong> in de dagplanning, weekoverzichten en tijdlijnen.
|
|
</p>
|
|
|
|
<?php
|
|
$dayOptions = [
|
|
1 => ['short' => 'Ma', 'full' => 'Maandag', 'type' => 'Werkdag'],
|
|
2 => ['short' => 'Di', 'full' => 'Dinsdag', 'type' => 'Werkdag'],
|
|
3 => ['short' => 'Wo', 'full' => 'Woensdag', 'type' => 'Werkdag'],
|
|
4 => ['short' => 'Do', 'full' => 'Donderdag', 'type' => 'Werkdag'],
|
|
5 => ['short' => 'Vr', 'full' => 'Vrijdag', 'type' => 'Werkdag'],
|
|
6 => ['short' => 'Za', 'full' => 'Zaterdag', 'type' => 'Weekend'],
|
|
7 => ['short' => 'Zo', 'full' => 'Zondag', 'type' => 'Weekend'],
|
|
];
|
|
?>
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-7 gap-2.5 pt-1">
|
|
<?php foreach ($dayOptions as $dNum => $dInfo):
|
|
$isChecked = in_array($dNum, $workdaySettings['workdays'], true);
|
|
?>
|
|
<label class="relative flex flex-col p-3 rounded-xl border cursor-pointer select-none transition-all <?= $isChecked ? 'bg-indigo-50/70 dark:bg-indigo-950/30 border-indigo-400 dark:border-indigo-600 shadow-2xs' : 'bg-white dark:bg-[#111622] border-slate-200/80 dark:border-slate-800 opacity-75 hover:opacity-100' ?>">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<span class="text-xs font-black <?= $isChecked ? 'text-indigo-600 dark:text-indigo-400' : 'text-slate-700 dark:text-slate-300' ?>">
|
|
<?= $dInfo['short'] ?>
|
|
</span>
|
|
<input type="checkbox" name="workdays[]" value="<?= $dNum ?>" <?= $isChecked ? 'checked' : '' ?> class="rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700 w-3.5 h-3.5 cursor-pointer">
|
|
</div>
|
|
<div class="text-[11px] font-semibold text-slate-800 dark:text-slate-200 leading-tight">
|
|
<?= $dInfo['full'] ?>
|
|
</div>
|
|
<span class="mt-1 text-[9px] font-bold uppercase tracking-wider <?= $dInfo['type'] === 'Weekend' ? 'text-amber-600 dark:text-amber-400' : 'text-slate-400 dark:text-slate-500' ?>">
|
|
<?= $dInfo['type'] ?>
|
|
</span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<!-- Werkdag uren -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-3">
|
|
<div class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
|
<i data-lucide="sun" class="w-3.5 h-3.5 text-amber-500"></i>
|
|
<span>Kantooruren / Tijdlijn Bereik</span>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Starttijd</label>
|
|
<input type="time" name="workday_start" value="<?= htmlspecialchars($workdaySettings['workday_start']) ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Eindtijd</label>
|
|
<input type="time" name="workday_end" value="<?= htmlspecialchars($workdaySettings['workday_end']) ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
</div>
|
|
<p class="text-[10px] text-slate-400 dark:text-slate-500">Bepaalt het standaard start- en einduur op de dagelijkse tijdlijn.</p>
|
|
</div>
|
|
|
|
<!-- Lunchpauze -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-3">
|
|
<div class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
|
<i data-lucide="coffee" class="w-3.5 h-3.5 text-amber-500"></i>
|
|
<span>Automatisch Lunchpauze Blok</span>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Start lunch</label>
|
|
<input type="time" name="lunch_start" value="<?= htmlspecialchars($workdaySettings['lunch_start']) ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Einde lunch</label>
|
|
<input type="time" name="lunch_end" value="<?= htmlspecialchars($workdaySettings['lunch_end']) ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
</div>
|
|
<p class="text-[10px] text-slate-400 dark:text-slate-500">Wordt visueel gemarkeerd als pauzeblok op de tijdlijn.</p>
|
|
</div>
|
|
|
|
<!-- Pomodoro presets -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-3">
|
|
<div class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
|
<i data-lucide="flame" class="w-3.5 h-3.5 text-rose-500"></i>
|
|
<span>Focus & Pomodoro Presets (minuten)</span>
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<div>
|
|
<label class="block text-[10px] font-medium text-slate-600 dark:text-slate-400 mb-1">Focus</label>
|
|
<input type="number" name="pomodoro_focus" min="10" max="120" value="<?= (int)$workdaySettings['pomodoro_focus'] ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-medium text-slate-600 dark:text-slate-400 mb-1">Deep</label>
|
|
<input type="number" name="pomodoro_deep" min="20" max="180" value="<?= (int)$workdaySettings['pomodoro_deep'] ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-medium text-slate-600 dark:text-slate-400 mb-1">Pauze</label>
|
|
<input type="number" name="pomodoro_break" min="2" max="30" value="<?= (int)$workdaySettings['pomodoro_break'] ?>" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-2.5 py-1.5 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Standaard taakduur -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-3">
|
|
<div class="text-xs font-bold text-slate-900 dark:text-white flex items-center space-x-1.5">
|
|
<i data-lucide="check-square" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
<span>Standaard Taakduur bij Aanmaken</span>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Standaard tijdslot</label>
|
|
<select name="default_task_duration" class="w-full bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
<option value="10" <?= $workdaySettings['default_task_duration'] == 10 ? 'selected' : '' ?>>10 minuten</option>
|
|
<option value="15" <?= $workdaySettings['default_task_duration'] == 15 ? 'selected' : '' ?>>15 minuten</option>
|
|
<option value="20" <?= $workdaySettings['default_task_duration'] == 20 ? 'selected' : '' ?>>20 minuten</option>
|
|
<option value="30" <?= $workdaySettings['default_task_duration'] == 30 ? 'selected' : '' ?>>30 minuten (Standaard)</option>
|
|
<option value="45" <?= $workdaySettings['default_task_duration'] == 45 ? 'selected' : '' ?>>45 minuten</option>
|
|
<option value="60" <?= $workdaySettings['default_task_duration'] == 60 ? 'selected' : '' ?>>60 minuten (1 uur)</option>
|
|
<option value="90" <?= $workdaySettings['default_task_duration'] == 90 ? 'selected' : '' ?>>90 minuten (1,5 uur)</option>
|
|
<option value="120" <?= $workdaySettings['default_task_duration'] == 120 ? 'selected' : '' ?>>120 minuten (2 uur)</option>
|
|
</select>
|
|
</div>
|
|
<p class="text-[10px] text-slate-400 dark:text-slate-500">Selectie bij het toevoegen van nieuwe dagtaken.</p>
|
|
</div>
|
|
</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-bold transition shadow-xs cursor-pointer">
|
|
Werkdag Standaarden Opslaan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
<!-- 3b. Agenda Blokkade Opties & Redenen -->
|
|
<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-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-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 font-bold shadow-xs">
|
|
<i data-lucide="palmtree" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Agenda Blokkade Opties & Redenen</h2>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 text-amber-700 dark:text-amber-300 text-[10px] font-bold">
|
|
Beheerbaar
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Configureer de afwezigheidsopties die verschijnen bij rechtermuisklik op een dag en in het blokkeervenster</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" class="space-y-5">
|
|
<input type="hidden" name="action" value="save_calendar_block_reasons">
|
|
<input type="hidden" name="active_tab" value="calendar">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<div class="space-y-2">
|
|
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
|
<div class="col-span-3 sm:col-span-2 text-center">Icoon</div>
|
|
<div class="col-span-3 sm:col-span-2">Systeemcode</div>
|
|
<div class="col-span-4 sm:col-span-6">Zichtbare Titel / Omschrijving</div>
|
|
<div class="col-span-1 text-center">Actief</div>
|
|
<div class="col-span-1 text-right">Verwijder</div>
|
|
</div>
|
|
|
|
<?php foreach ($calendarBlockReasons as $idx => $reason): ?>
|
|
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
<div class="col-span-3 sm:col-span-2 flex items-center space-x-1.5">
|
|
<button type="button" onclick="openIconPickerFor('reason_icon_<?= $idx ?>', 'reason_icon_preview_<?= $idx ?>')" class="w-10 h-10 flex-shrink-0 text-xl flex items-center justify-center bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 hover:border-amber-400 dark:hover:border-amber-500 rounded-xl transition cursor-pointer shadow-2xs hover:scale-105" title="Klik om een icoon te kiezen uit de lijst">
|
|
<span id="reason_icon_preview_<?= $idx ?>"><?= htmlspecialchars($reason['icon'] ?? '📅') ?></span>
|
|
</button>
|
|
<input type="text" id="reason_icon_<?= $idx ?>" name="reasons[<?= $idx ?>][icon]" value="<?= htmlspecialchars($reason['icon'] ?? '📅') ?>" oninput="document.getElementById('reason_icon_preview_<?= $idx ?>').innerText = this.value || '📅'" class="w-12 text-center text-xs py-2 px-1 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
|
</div>
|
|
<div class="col-span-3 sm:col-span-2">
|
|
<input type="hidden" name="reasons[<?= $idx ?>][code]" value="<?= htmlspecialchars($reason['code']) ?>">
|
|
<span class="font-mono text-xs font-bold text-slate-700 dark:text-slate-300 px-2.5 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 inline-block truncate w-full shadow-2xs"><?= htmlspecialchars($reason['code']) ?></span>
|
|
</div>
|
|
<div class="col-span-4 sm:col-span-6">
|
|
<input type="text" name="reasons[<?= $idx ?>][title]" value="<?= htmlspecialchars($reason['title']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500" required>
|
|
</div>
|
|
<div class="col-span-1 text-center">
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" name="reasons[<?= $idx ?>][is_active]" value="1" <?= !empty($reason['is_active']) ? 'checked' : '' ?> class="w-4 h-4 text-amber-600 rounded border-slate-300 dark:border-slate-700 focus:ring-amber-500">
|
|
</label>
|
|
</div>
|
|
<div class="col-span-1 text-right">
|
|
<label class="cursor-pointer text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 transition" title="Vink aan om deze reden te verwijderen">
|
|
<input type="checkbox" name="reasons[<?= $idx ?>][is_delete]" value="1" class="w-4 h-4 text-rose-600 rounded border-slate-300 dark:border-slate-700 focus:ring-rose-500">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<!-- Nieuwe reden toevoegen regel -->
|
|
<div class="p-4 rounded-2xl bg-amber-50/40 dark:bg-amber-950/20 border border-dashed border-amber-300/80 dark:border-amber-800/60 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-2 text-xs font-bold text-amber-900 dark:text-amber-200">
|
|
<i data-lucide="plus-circle" class="w-4 h-4 text-amber-600 dark:text-amber-400"></i>
|
|
<span>Nieuwe Blokkadereden Toevoegen</span>
|
|
</div>
|
|
<span class="text-[10px] text-amber-700 dark:text-amber-400">Klik op de icoonknop om uit de standaard iconenset te kiezen</span>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
|
<div class="sm:col-span-3 flex items-center space-x-1.5">
|
|
<button type="button" onclick="openIconPickerFor('new_icon_input', 'new_icon_preview')" class="w-10 h-10 flex-shrink-0 text-xl flex items-center justify-center bg-white dark:bg-[#111624] border border-amber-300 dark:border-amber-700 hover:border-amber-500 rounded-xl transition cursor-pointer shadow-2xs hover:scale-105" title="Kies een icoon uit de lijst">
|
|
<span id="new_icon_preview">🎯</span>
|
|
</button>
|
|
<div class="flex-1">
|
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Icoon (Kies of typ)</label>
|
|
<input type="text" id="new_icon_input" name="new_icon" value="🎯" placeholder="🎯" oninput="document.getElementById('new_icon_preview').innerText = this.value || '📅'" class="w-full text-center text-xs py-1.5 px-2 bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 rounded-xl focus:outline-none focus:ring-1 focus:ring-amber-500">
|
|
</div>
|
|
</div>
|
|
<div class="sm:col-span-3">
|
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Code (bv. STUDIEDAG)</label>
|
|
<input type="text" name="new_code" placeholder="BIJV_CURSUS" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-mono text-slate-900 dark:text-white uppercase focus:outline-none focus:ring-1 focus:ring-amber-500">
|
|
</div>
|
|
<div class="sm:col-span-6">
|
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Zichtbare Titel</label>
|
|
<input type="text" name="new_title" placeholder="Bijv. Studiedag / Opleiding" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-amber-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-2">
|
|
<button type="submit" class="px-5 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
|
|
<i data-lucide="check" class="w-4 h-4"></i>
|
|
<span>Agenda Blokkade Opties Opslaan</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 3: Microsoft 365 -->
|
|
<div id="tab-pane-microsoft" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 1. Microsoft 365 Azure App Registratie (Agenda, E-mail & Teams) -->
|
|
<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-xl bg-sky-50 dark:bg-sky-950/40 border border-sky-200 dark:border-sky-800/60 flex items-center justify-center text-sky-600 dark:text-sky-400 font-bold shadow-xs">
|
|
<i data-lucide="cloud" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Microsoft 365 Koppeling (Agenda, E-mail & Teams)</h2>
|
|
<?php if ($appConfig['is_configured']): ?>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-950/40 border border-teal-200 dark:border-teal-800/60 text-teal-700 dark:text-teal-400 text-[10px] font-bold">
|
|
Geconfigureerd
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 text-amber-700 dark:text-amber-400 text-[10px] font-bold">
|
|
Configuratie Vereist
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Centrale OAuth 2.0 koppeling voor Outlook Agenda, E-mail (Postvak IN) en Microsoft Teams</p>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($appConfig['is_configured']): ?>
|
|
<a href="<?= htmlspecialchars($adminConsentUrl) ?>" target="_blank" rel="noopener noreferrer" class="inline-flex items-center space-x-2 px-3.5 py-2 rounded-xl bg-sky-50 hover:bg-sky-100 dark:bg-sky-950/40 dark:hover:bg-sky-900/40 text-sky-700 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 text-xs font-semibold transition" title="Verleen namens alle gebruikers beheerdersgoedkeuring in Azure voor Agenda, E-mail en Teams">
|
|
<i data-lucide="shield-check" class="w-4 h-4 text-sky-600 dark:text-sky-400"></i>
|
|
<span>Admin Consent Verlenen</span>
|
|
<i data-lucide="external-link" class="w-3.5 h-3.5 text-sky-600 dark:text-sky-400"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<form method="POST" class="mt-6 space-y-5">
|
|
<input type="hidden" name="action" value="save_azure">
|
|
<input type="hidden" name="active_tab" value="microsoft">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<!-- Redirect URI Kopieerbox -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<label class="text-xs font-semibold text-slate-700 dark:text-slate-300 flex items-center space-x-1.5">
|
|
<i data-lucide="link" class="w-3.5 h-3.5 text-sky-500"></i>
|
|
<span>Redirect URI (Terugbel-URL voor Azure Portal)</span>
|
|
</label>
|
|
<span class="text-[10px] text-slate-500">Kopieer dit exact naar de Azure Web Redirect URIs</span>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<input type="text" readonly id="adminRedirectUri" value="<?= htmlspecialchars($redirectUri) ?>" class="flex-1 bg-white dark:bg-[#111622] border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-xs font-mono text-indigo-600 dark:text-indigo-400 select-all focus:outline-none">
|
|
<button type="button" onclick="copyAdminRedirectUri()" class="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-white text-xs font-semibold flex items-center space-x-1.5 transition border border-slate-200 dark:border-transparent cursor-pointer">
|
|
<i data-lucide="copy" class="w-3.5 h-3.5" id="copyIcon"></i>
|
|
<span id="copyText">Kopiëren</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Client ID & Tenant ID Grid -->
|
|
<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">
|
|
Application (Client) ID <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" name="client_id" required value="<?= htmlspecialchars($appConfig['client_id']) ?>" placeholder="bijv. 38314451-1d05-4205-88ec-..." class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 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:border-indigo-500 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.5">
|
|
Directory (Tenant) ID / Domeinnaam
|
|
</label>
|
|
<input type="text" name="tenant_id" value="<?= htmlspecialchars((!empty($appConfig['tenant_id']) && $appConfig['tenant_id'] !== 'common') ? $appConfig['tenant_id'] : 'planningsagenda.nl') ?>" placeholder="bijv. planningsagenda.nl of Azure Tenant GUID" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 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:border-indigo-500 focus:ring-1 focus:ring-indigo-500 font-mono">
|
|
<span class="text-[10px] text-slate-500 dark:text-slate-400 mt-1 block">Vul uw domeinnaam in (<code class="text-indigo-600 dark:text-indigo-400 font-mono">planningsagenda.nl</code>) of de Directory (tenant) ID uit Azure. <em>Let op: Gebruik géén 'common' voor single-tenant apps.</em></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Client Secret met toggle -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<label class="text-xs font-semibold text-slate-700 dark:text-slate-300">
|
|
Client Secret (Value) <span class="text-red-500">*</span>
|
|
</label>
|
|
<span class="text-[10px] text-slate-500">Wordt veilig en versleuteld opgeslagen</span>
|
|
</div>
|
|
<div class="relative">
|
|
<input type="password" id="adminClientSecret" name="client_secret" value="<?= htmlspecialchars($appConfig['client_secret'] ?? '') ?>" placeholder="Voer de Secret Value in uit Azure..." class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 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:border-indigo-500 focus:ring-1 focus:ring-indigo-500">
|
|
<button type="button" onclick="toggleAdminSecret()" id="toggleSecretBtn" title="Toon / verberg secret" 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="eyeSecretIcon"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-2 flex items-center justify-between">
|
|
<div class="text-[11px] text-slate-500 dark:text-slate-400">
|
|
Zodra opgeslagen en goedgekeurd kunnen al uw collega's met 1 klik hun Outlook agenda én e-mail koppelen.
|
|
</div>
|
|
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer">
|
|
Microsoft Gegevens Opslaan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Azure Setup Documentatie Accordion -->
|
|
<div class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800/80">
|
|
<button type="button" onclick="toggleAdminGuide()" class="w-full flex items-center justify-between text-xs font-semibold text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white transition cursor-pointer">
|
|
<span class="flex items-center space-x-2">
|
|
<i data-lucide="help-circle" class="w-4 h-4 text-sky-500"></i>
|
|
<span>Hoe configureer ik de complete Microsoft Koppeling (Agenda, E-mail & Teams) in 4 stappen?</span>
|
|
</span>
|
|
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform duration-200" id="guideArrow"></i>
|
|
</button>
|
|
|
|
<div id="adminGuideContent" class="hidden mt-4 p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-xs text-slate-600 dark:text-slate-300 space-y-3 leading-relaxed">
|
|
<div class="flex items-start space-x-2.5">
|
|
<span class="w-5 h-5 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">1</span>
|
|
<div>
|
|
<strong class="text-slate-900 dark:text-white">Azure Portal:</strong> Ga naar <a href="https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps" target="_blank" class="text-sky-600 dark:text-sky-400 underline">portal.azure.com</a> → <em>Microsoft Entra ID</em> → <em>App registrations</em> → <em>New registration</em> (of open de bestaande Fociz app). Kies "Accounts in this organizational directory only" (single-tenant).
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start space-x-2.5">
|
|
<span class="w-5 h-5 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">2</span>
|
|
<div>
|
|
<strong class="text-slate-900 dark:text-white">Redirect URI:</strong> Ga naar <em>Authentication</em>, kies platform <strong>Web</strong> en vul exact in: <code class="text-indigo-600 dark:text-indigo-400 font-mono"><?= htmlspecialchars($redirectUri) ?></code>.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start space-x-2.5">
|
|
<span class="w-5 h-5 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">3</span>
|
|
<div>
|
|
<strong class="text-slate-900 dark:text-white">API Permissions (Agenda, E-mail & Teams):</strong> Ga naar <em>API permissions</em> → <em>Add a permission</em> → <em>Microsoft Graph</em> → <strong>Delegated permissions</strong> → Vink de volgende machtigingen aan:
|
|
<ul class="list-disc list-inside mt-1.5 space-y-1 text-[11px] text-slate-700 dark:text-slate-300">
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">User.Read</code> — Aanmelden en basisprofiel</li>
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">Calendars.ReadWrite</code> — Outlook agenda synchronisatie & afspraken inplannen</li>
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">Mail.ReadWrite</code> — Outlook inbox e-mails lezen, inplannen & beheren</li>
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">Mail.Send</code> — E-mails direct beantwoorden en doorsturen vanuit Fociz</li>
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">OnlineMeetings.ReadWrite</code> — Automatische Teams-vergaderingen koppelen</li>
|
|
<li><code class="text-sky-600 dark:text-sky-400 font-mono">offline_access</code> — Verbinding actief houden op de achtergrond</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start space-x-2.5">
|
|
<span class="w-5 h-5 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center font-bold text-[10px] flex-shrink-0 mt-0.5">4</span>
|
|
<div>
|
|
<strong class="text-slate-900 dark:text-white">Beheerdersgoedkeuring (Admin Consent):</strong> Klik in Azure op <em>"Grant admin consent for [Uw Organisatie]"</em> (of gebruik de knop <em>"Admin Consent Verlenen"</em> hierboven). Er verschijnt direct een groen vinkje bij alle regels. Hiermee hoeven collega's niet afzonderlijk om beheerderstoestemming te vragen en werkt zowel de agenda als de mailbox direct live!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Tab 4: Jira -->
|
|
<div id="tab-pane-jira" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 2. Atlassian Jira Bedrijfsinstellingen -->
|
|
<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-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
<i data-lucide="trello" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Atlassian Jira Organisatie-instellingen</h2>
|
|
<?php if (!empty($companyJiraDomain)): ?>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-teal-50 dark:bg-teal-950/40 border border-teal-200 dark:border-teal-800/60 text-teal-700 dark:text-teal-400 text-[10px] font-bold">
|
|
Actief
|
|
</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-bold">
|
|
Optioneel
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Standaard Jira Cloud domein voor het hele team</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" class="mt-6 space-y-4">
|
|
<input type="hidden" name="action" value="save_jira_domain">
|
|
<input type="hidden" name="active_tab" value="jira">
|
|
<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">
|
|
Standaard Jira Bedrijfsdomein
|
|
</label>
|
|
<input type="text" name="jira_domain" value="<?= htmlspecialchars($companyJiraDomain) ?>" placeholder="https://uwbedrijf.atlassian.net" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-300 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:border-indigo-500 focus:ring-1 focus:ring-indigo-500 font-mono">
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-1.5 leading-relaxed">
|
|
Wanneer u dit instelt, wordt dit domein automatisch vooraf ingevuld op de profielpagina van alle collega's. Zij hoeven dan enkel nog hun eigen persoonlijke e-mailadres en Atlassian API-token in te voeren.
|
|
</p>
|
|
</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-bold transition shadow-xs cursor-pointer">
|
|
Jira Domein Opslaan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 2b. Atlassian Jira Wachtrijfilters & Gebruikerskoppelingen -->
|
|
<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-xl bg-gradient-to-tr from-sky-500 to-indigo-600 text-white flex items-center justify-center font-bold shadow-md shadow-indigo-500/20 flex-shrink-0">
|
|
<i data-lucide="sliders" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Jira Wachtrijfilters & Toewijzingen</h2>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-400 text-[10px] font-bold">
|
|
ServiceDesk & Teams
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
Beheer aangepaste JQL-wachtrijfilters (zoals DX en SD) en wijs teamleden toe aan hun standaard wachtrij.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="openJiraFilterModal()" class="inline-flex items-center space-x-2 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer self-start sm:self-center">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
<span>Nieuw Wachtrijfilter</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Overzicht van actieve wachtrijfilters -->
|
|
<div>
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
|
|
<i data-lucide="filter" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
<span>Geconfigureerde Wachtrijfilters (<?= count($allJiraFilters) ?>)</span>
|
|
</h3>
|
|
<span class="text-[11px] text-slate-400">Worden getoond als snelle filtertabs op de Jira pagina</span>
|
|
</div>
|
|
|
|
<?php if (empty($allJiraFilters)): ?>
|
|
<div class="p-6 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 text-center">
|
|
<p class="text-xs text-slate-500">Nog geen wachtrijfilters ingesteld. Klik op "Nieuw Wachtrijfilter" om te beginnen.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php
|
|
$userNamesByFilter = [];
|
|
foreach ($allUsersForJira as $u) {
|
|
if (!empty($u['default_jira_filter_code'])) {
|
|
$userNamesByFilter[$u['default_jira_filter_code']][] = $u['name'];
|
|
}
|
|
}
|
|
?>
|
|
<div class="grid grid-cols-1 gap-3.5">
|
|
<?php foreach ($allJiraFilters as $filter): ?>
|
|
<?php
|
|
$assignedColleagues = $userNamesByFilter[$filter['filter_code']] ?? [];
|
|
$filterJsonSafe = htmlspecialchars(json_encode($filter), ENT_QUOTES, 'UTF-8');
|
|
?>
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 hover:border-indigo-500/40 transition flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div class="space-y-2 flex-1 min-w-0">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="font-mono text-xs font-black px-2.5 py-0.5 rounded-lg bg-indigo-100 text-indigo-800 dark:bg-indigo-950/70 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/80">
|
|
<?= htmlspecialchars($filter['filter_code']) ?>
|
|
</span>
|
|
<span class="text-xs font-bold text-slate-900 dark:text-white">
|
|
<?= htmlspecialchars($filter['filter_name']) ?>
|
|
</span>
|
|
<?php if (!empty($filter['project_key'])): ?>
|
|
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-200/80 dark:bg-slate-800 text-slate-700 dark:text-slate-300">
|
|
Project: <?= htmlspecialchars($filter['project_key']) ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="text-[11px] font-medium px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800/60 text-slate-500">
|
|
Alle projecten
|
|
</span>
|
|
<?php endif; ?>
|
|
<?php if (empty($filter['is_active'])): ?>
|
|
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-50 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400">
|
|
Inactief
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php if (!empty($filter['description'])): ?>
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400">
|
|
<?= htmlspecialchars($filter['description']) ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
|
|
<!-- JQL preview box -->
|
|
<div class="p-2.5 rounded-xl bg-white dark:bg-[#111622] border border-slate-200/90 dark:border-slate-800 text-[11px] font-mono text-slate-700 dark:text-indigo-300 break-all select-all">
|
|
<span class="text-slate-400 dark:text-slate-500 mr-1.5 font-sans font-bold">JQL:</span><?= htmlspecialchars($filter['jql_clause']) ?>
|
|
</div>
|
|
|
|
<!-- Toegewezen collega's -->
|
|
<div class="flex items-center space-x-2 text-[11px] text-slate-500 dark:text-slate-400 pt-0.5">
|
|
<span class="font-semibold text-slate-700 dark:text-slate-300">Standaard gekoppeld aan:</span>
|
|
<?php if (!empty($assignedColleagues)): ?>
|
|
<span class="inline-flex items-center gap-1.5 flex-wrap">
|
|
<?php foreach ($assignedColleagues as $cName): ?>
|
|
<span class="px-2 py-0.5 rounded-md bg-sky-50 text-sky-700 dark:bg-sky-950/50 dark:text-sky-300 border border-sky-200 dark:border-sky-800/60 font-medium">
|
|
<?= htmlspecialchars($cName) ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="italic text-slate-400">Nog niemand specifiek toegewezen</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actieknoppen -->
|
|
<div class="flex items-center space-x-2 self-start md:self-center flex-shrink-0 pt-2 md:pt-0 border-t md:border-t-0 border-slate-200 dark:border-slate-800 w-full md:w-auto justify-end">
|
|
<button type="button" onclick="editJiraFilter(<?= $filterJsonSafe ?>)" class="px-3 py-1.5 rounded-xl bg-slate-200/80 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 text-xs font-semibold transition cursor-pointer">
|
|
Bewerken
|
|
</button>
|
|
<form method="POST" action="/admin.php" onsubmit="return confirm('Weet u zeker dat u het filter \'<?= htmlspecialchars(addslashes($filter['filter_name'])) ?>\' wilt verwijderen?');" class="inline">
|
|
<input type="hidden" name="action" value="delete_jira_queue_filter">
|
|
<input type="hidden" name="active_tab" value="jira">
|
|
<input type="hidden" name="filter_id" value="<?= (int)$filter['id'] ?>">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<button type="submit" class="p-2 rounded-xl text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer" title="Verwijderen">
|
|
<i data-lucide="trash-2" class="w-4 h-4 inline"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Team Wachtrijmatrix: Standaard Wachtrij per Teamlid -->
|
|
<div class="pt-6 border-t border-slate-100 dark:border-slate-800/80 space-y-4">
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
|
<div>
|
|
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider flex items-center space-x-1.5">
|
|
<i data-lucide="users" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
<span>Team Wachtrijmatrix: Standaard Wachtrij per Collega</span>
|
|
</h3>
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">
|
|
Wijs collega's toe aan hun primaire wachtrij (bijv. DX of SD). Wanneer zij de Jira pagina openen, wordt deze wachtrij direct als standaard getoond.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" action="/admin.php" class="space-y-4">
|
|
<input type="hidden" name="action" value="save_user_jira_assignments">
|
|
<input type="hidden" name="active_tab" value="jira">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<div class="overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800">
|
|
<table class="w-full text-left text-xs">
|
|
<thead class="bg-slate-100 dark:bg-[#0c0f17] text-slate-600 dark:text-slate-400 font-bold border-b border-slate-200 dark:border-slate-800">
|
|
<tr>
|
|
<th class="py-3 px-4">Teamlid</th>
|
|
<th class="py-3 px-4">E-mailadres</th>
|
|
<th class="py-3 px-4">Rol</th>
|
|
<th class="py-3 px-4 w-72">Standaard Wachtrijfilter</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/80 bg-white dark:bg-[#111622]">
|
|
<?php foreach ($allUsersForJira as $usr): ?>
|
|
<tr class="hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition">
|
|
<td class="py-3 px-4">
|
|
<div class="flex items-center space-x-2.5">
|
|
<?php if (!empty($usr['avatar_url'])): ?>
|
|
<img src="<?= htmlspecialchars($usr['avatar_url']) ?>" alt="Avatar" class="w-7 h-7 rounded-full object-cover border border-slate-200 dark:border-slate-700" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
|
|
<div class="hidden w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
|
|
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
|
|
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<span class="font-bold text-slate-900 dark:text-white"><?= htmlspecialchars($usr['name']) ?></span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 font-mono text-slate-600 dark:text-slate-400">
|
|
<?= htmlspecialchars($usr['email']) ?>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-bold <?= $usr['role'] === 'ADMIN' ? 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800/60' : 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300' ?>">
|
|
<?= $usr['role'] === 'ADMIN' ? 'Beheerder' : 'Collega' ?>
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4">
|
|
<select name="user_filter[<?= (int)$usr['id'] ?>]" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
|
|
<option value="">Geen voorkeur (alles tonen)</option>
|
|
<?php foreach ($allJiraFilters as $f): ?>
|
|
<?php if ($f['is_active']): ?>
|
|
<option value="<?= htmlspecialchars($f['filter_code']) ?>" <?= ($usr['default_jira_filter_code'] === $f['filter_code']) ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($f['filter_code']) ?> — <?= htmlspecialchars($f['filter_name']) ?>
|
|
</option>
|
|
<?php endif; ?>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</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-bold transition shadow-xs cursor-pointer">
|
|
Wachtrijtoewijzingen Opslaan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
|
|
<div id="jiraFilterModal" onclick="if(event.target === this) closeJiraFilterModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
|
|
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
|
|
<div class="flex items-center space-x-2.5">
|
|
<div class="w-9 h-9 rounded-xl bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center font-bold">
|
|
<i data-lucide="filter" class="w-4 h-4"></i>
|
|
</div>
|
|
<div>
|
|
<h3 id="jiraFilterModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Wachtrijfilter Configureren</h3>
|
|
<p class="text-[11px] text-slate-400">Beheer JQL zoekclausule en koppeling</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="closeJiraFilterModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form method="POST" action="/admin.php" class="mt-4 space-y-4">
|
|
<input type="hidden" name="action" value="save_jira_queue_filter">
|
|
<input type="hidden" name="active_tab" value="jira">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<input type="hidden" id="filterModal_id" name="filter_id" value="0">
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
|
Filter Code <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" id="filterModal_code" name="filter_code" required placeholder="bijv. DX of SD" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase 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">
|
|
Doel Project (optioneel)
|
|
</label>
|
|
<input type="text" id="filterModal_project" name="project_key" placeholder="bijv. SD of SERVICEDESK" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
|
Filter Naam <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" id="filterModal_name" name="filter_name" required placeholder="bijv. DX — Koppelingen, SSO & Migratie" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 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">
|
|
JQL Clausule <span class="text-red-500">*</span>
|
|
</label>
|
|
<textarea id="filterModal_jql" name="jql_clause" rows="3" required placeholder='bijv. "Onderwerp logging[dropdown]" in (Koppelingen, SSO-koppeling, Rest-endpoints, Datadump, UWV-koppeling, Migratie)' class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500"></textarea>
|
|
<span class="text-[10px] text-slate-400">Voer de exacte JQL filterclausule in die in Jira op de issues wordt toegepast.</span>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
|
|
Omschrijving / Toelichting
|
|
</label>
|
|
<input type="text" id="filterModal_desc" name="description" placeholder="Korte toelichting voor het team" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between pt-2">
|
|
<label class="flex items-center space-x-2 text-xs text-slate-700 dark:text-slate-300 cursor-pointer">
|
|
<input type="checkbox" id="filterModal_active" name="is_active" value="1" checked class="w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
|
<span>Filter is actief</span>
|
|
</label>
|
|
<div class="flex items-center space-x-2">
|
|
<button type="button" onclick="closeJiraFilterModal()" class="px-4 py-2 rounded-xl text-xs font-semibold text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
|
Annuleren
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-bold bg-indigo-600 hover:bg-indigo-500 text-white transition cursor-pointer shadow-xs">
|
|
Filter Opslaan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Tab 5: AI Co-Pilot -->
|
|
<div id="tab-pane-ai" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 4. AI & Brainstorm Co-Pilot (Release v1.5) -->
|
|
<div class="fociz-card p-6 sm:p-7 space-y-6">
|
|
<div class="flex items-center justify-between pb-4 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-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 font-bold shadow-xs">
|
|
<i data-lucide="sparkles" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">AI-Co-pilot</h2>
|
|
</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</span>
|
|
</span>
|
|
<span class="px-2.5 py-1 rounded-full <?= $aiSettings['has_key'] ? 'bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60' : 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800/60' ?> text-xs font-bold inline-flex items-center space-x-1.5">
|
|
<i data-lucide="<?= $aiSettings['has_key'] ? 'check-circle-2' : 'key' ?>" class="w-3.5 h-3.5 flex-shrink-0"></i>
|
|
<span><?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars((string)($aiSettings['ai_model'] ?? 'Gemini Pro')) . ')' : 'Sleutel Vereist' ?></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Verbruik & Statistieken Dashboard -->
|
|
<div class="p-4 rounded-2xl bg-slate-50/80 dark:bg-[#0c0f17]/90 border border-slate-200/80 dark:border-slate-800/80 space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<i data-lucide="bar-chart-2" class="w-4 h-4 text-indigo-500"></i>
|
|
<span class="text-xs font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">AI Verbruik</span>
|
|
</div>
|
|
<?php if (!empty($aiUsageStats['last_used_at'])): ?>
|
|
<span class="text-[11px] text-slate-400">Laatst gebruikt: <?= htmlspecialchars(date('d-m-Y H:i', strtotime($aiUsageStats['last_used_at']))) ?></span>
|
|
<?php else: ?>
|
|
<span class="text-[11px] text-slate-400 italic">Nog geen verbruik geregistreerd</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Aanroepen</span>
|
|
<span class="text-lg font-black text-slate-900 dark:text-white"><?= number_format((int)($aiUsageStats['total_requests'] ?? 0), 0, ',', '.') ?></span>
|
|
</div>
|
|
|
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Totaal Tokens</span>
|
|
<span class="text-lg font-black text-indigo-600 dark:text-indigo-400"><?= number_format((int)($aiUsageStats['total_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
</div>
|
|
|
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Prompt / Invoer</span>
|
|
<span class="text-lg font-black text-slate-700 dark:text-slate-300"><?= number_format((int)($aiUsageStats['total_prompt_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
</div>
|
|
|
|
<div class="p-3 rounded-xl bg-white dark:bg-slate-900 border border-slate-200/60 dark:border-slate-800 shadow-2xs">
|
|
<span class="block text-[10px] uppercase font-bold text-slate-400">Output / Gegenereerd</span>
|
|
<span class="text-lg font-black text-teal-600 dark:text-teal-400"><?= number_format((int)($aiUsageStats['total_completion_tokens'] ?? 0), 0, ',', '.') ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($aiUsageStats['by_model'])): ?>
|
|
<div class="pt-2 flex flex-wrap items-center gap-2 text-[11px] border-t border-slate-200/40 dark:border-slate-800/40">
|
|
<span class="font-semibold text-slate-500 dark:text-slate-400">Per model:</span>
|
|
<?php foreach ($aiUsageStats['by_model'] as $mName => $mCount): ?>
|
|
<span class="px-2 py-0.5 rounded-md bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
|
<strong><?= htmlspecialchars((string)$mName) ?></strong>: <?= (int)$mCount ?>x
|
|
</span>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- ZERO-DATA LEAKAGE PRIVACY FIREWALL BANNER -->
|
|
<div class="p-3.5 rounded-2xl bg-emerald-50/80 dark:bg-emerald-950/20 border border-emerald-200/80 dark:border-emerald-800/40 flex items-start space-x-3">
|
|
<div class="w-8 h-8 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 space-y-1">
|
|
<strong class="text-emerald-950 dark:text-emerald-200 font-bold block text-xs">Strikte Zero-Data Leakage Privacy Garantie</strong>
|
|
<p class="text-emerald-800/90 dark:text-emerald-300/90 text-[11px] leading-relaxed">
|
|
Er wordt <strong>onder géén enkele voorwaarde data uit Jira tickets, Outlook e-mails of Microsoft Teams</strong> naar de AI verzonden. De ingebouwde privacy firewall onderschept en filtert automatisch ticketnummers, Atlassian-links, mailheaders en communicatietraces vóór verzending.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" action="/admin.php" id="adminAiForm" class="space-y-4">
|
|
<input type="hidden" name="action" value="save_ai_settings">
|
|
<input type="hidden" name="active_tab" value="ai">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<div class="p-3.5 rounded-2xl bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-200/60 dark:border-indigo-800/40">
|
|
<label class="flex items-center space-x-2.5 text-xs text-indigo-950 dark:text-indigo-200 cursor-pointer select-none">
|
|
<input type="checkbox" name="ai_enabled" value="1" <?= $aiSettings['ai_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-bold">AI Co-Pilot inschakelen voor alle gebruikers in de organisatie</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="admin_ai_model" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-3 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
<optgroup label="Google Gemini (Aanbevolen)">
|
|
<option value="gemini-3.6-flash" <?= ($aiSettings['ai_model'] === 'gemini-3.6-flash' || empty($aiSettings['ai_model']) || $aiSettings['ai_model'] === 'gemini') ? 'selected' : '' ?>>Google Gemini 3.6 Flash (Aanbevolen, Snel & Nieuwste)</option>
|
|
<option value="gemini-1.5-pro" <?= $aiSettings['ai_model'] === 'gemini-1.5-pro' ? 'selected' : '' ?>>Google Gemini 1.5 Pro (Slim & Uitgebreid)</option>
|
|
<option value="gemini-2.0-flash" <?= $aiSettings['ai_model'] === 'gemini-2.0-flash' ? 'selected' : '' ?>>Google Gemini 2.0 Flash</option>
|
|
<option value="gemini-1.5-flash" <?= $aiSettings['ai_model'] === 'gemini-1.5-flash' ? 'selected' : '' ?>>Google Gemini 1.5 Flash</option>
|
|
</optgroup>
|
|
<optgroup label="OpenAI">
|
|
<option value="gpt-4o-mini" <?= $aiSettings['ai_model'] === 'gpt-4o-mini' ? 'selected' : '' ?>>OpenAI GPT-4o-mini</option>
|
|
<option value="gpt-4o" <?= $aiSettings['ai_model'] === 'gpt-4o' ? 'selected' : '' ?>>OpenAI GPT-4o</option>
|
|
</optgroup>
|
|
</select>
|
|
<input type="hidden" name="ai_provider" id="admin_ai_provider" value="<?= str_contains((string)($aiSettings['ai_model'] ?? ''), 'gpt') ? 'openai' : 'gemini' ?>">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">Google Gemini / AI API Sleutel</label>
|
|
<div class="relative">
|
|
<input
|
|
type="password"
|
|
name="ai_api_key"
|
|
id="admin_ai_api_key"
|
|
value="<?= $aiSettings['has_key'] ? '••••••••••••••••' : '' ?>"
|
|
placeholder="AIzaSy... (Google) of sk-... (OpenAI)"
|
|
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-9 py-2 text-slate-900 dark:text-white text-xs font-mono placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
>
|
|
<button type="button" onclick="togglePasswordVisibility('admin_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">Haal een gratis of betaalde Gemini API sleutel via <a href="https://aistudio.google.com/app/apikey" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">Google AI Studio →</a></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Test Resultaat Alert -->
|
|
<div id="adminAiTestResult" 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">
|
|
<button type="button" onclick="testAdminAiConnection()" id="adminAiTestBtn" 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="adminAiTestBtnText">Test Verbinding</span>
|
|
</button>
|
|
|
|
<button type="submit" class="px-5 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center space-x-2">
|
|
<i data-lucide="save" class="w-3.5 h-3.5"></i>
|
|
<span>AI Instellingen Opslaan</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Tab 6: Beveiliging & Audit -->
|
|
<div id="tab-pane-security" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 6. Sessie Inactiviteit & Beveiligingsbeleid (Release v1.6) -->
|
|
<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-3 pb-4 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-xl bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800/60 flex items-center justify-center text-amber-600 dark:text-amber-400 font-bold shadow-xs">
|
|
<i data-lucide="shield-alert" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Sessie Inactiviteit & Beveiligingsbeleid</h2>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Automatische afmelding bij inactiviteit conform NIST SP 800-63B, OWASP & ISO 27001</p>
|
|
</div>
|
|
</div>
|
|
<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 self-start sm:self-auto flex items-center space-x-1.5">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
<span>SOC 2 / ISO 27001 Compliant</span>
|
|
</span>
|
|
</div>
|
|
|
|
<form method="POST" action="/admin.php" class="space-y-5">
|
|
<input type="hidden" name="action" value="save_security_settings">
|
|
<input type="hidden" name="active_tab" value="security">
|
|
<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-2">
|
|
Inactiviteitstimeout (Session Idle Expiration)
|
|
</label>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<!-- 15 minuten -->
|
|
<label class="relative flex items-start p-3.5 rounded-2xl border cursor-pointer transition <?= $sessionIdleTimeout === 900 ? 'bg-amber-50/60 dark:bg-amber-950/20 border-amber-400 dark:border-amber-600 ring-1 ring-amber-400/40' : 'bg-slate-50 dark:bg-[#0c0f17] border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700' ?>">
|
|
<input type="radio" name="session_idle_timeout" value="900" <?= $sessionIdleTimeout === 900 ? 'checked' : '' ?> class="mt-0.5 text-amber-600 focus:ring-amber-500 border-slate-300 dark:border-slate-700">
|
|
<div class="ml-3">
|
|
<span class="block text-xs font-bold text-slate-900 dark:text-white">15 minuten</span>
|
|
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">Strikt — ISO 27001 & Overheid BIO richtlijn</span>
|
|
</div>
|
|
</label>
|
|
|
|
<!-- 30 minuten (aanbevolen) -->
|
|
<label class="relative flex items-start p-3.5 rounded-2xl border cursor-pointer transition <?= $sessionIdleTimeout === 1800 ? 'bg-indigo-50/60 dark:bg-indigo-950/20 border-indigo-400 dark:border-indigo-600 ring-1 ring-indigo-400/40' : 'bg-slate-50 dark:bg-[#0c0f17] border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700' ?>">
|
|
<input type="radio" name="session_idle_timeout" value="1800" <?= $sessionIdleTimeout === 1800 ? 'checked' : '' ?> class="mt-0.5 text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
|
<div class="ml-3">
|
|
<div class="flex items-center space-x-1.5">
|
|
<span class="text-xs font-bold text-slate-900 dark:text-white">30 minuten</span>
|
|
<span class="px-1.5 py-0.5 rounded text-[9px] font-extrabold bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300">Aanbevolen</span>
|
|
</div>
|
|
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">NIST SP 800-63B & OWASP ASVS v4.0 standaard</span>
|
|
</div>
|
|
</label>
|
|
|
|
<!-- 60 minuten -->
|
|
<label class="relative flex items-start p-3.5 rounded-2xl border cursor-pointer transition <?= $sessionIdleTimeout === 3600 ? 'bg-indigo-50/60 dark:bg-indigo-950/20 border-indigo-400 dark:border-indigo-600 ring-1 ring-indigo-400/40' : 'bg-slate-50 dark:bg-[#0c0f17] border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700' ?>">
|
|
<input type="radio" name="session_idle_timeout" value="3600" <?= $sessionIdleTimeout === 3600 ? 'checked' : '' ?> class="mt-0.5 text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
|
<div class="ml-3">
|
|
<span class="block text-xs font-bold text-slate-900 dark:text-white">60 minuten</span>
|
|
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">Kantoorstandaard — Balans tussen gebruiksgemak & veiligheid</span>
|
|
</div>
|
|
</label>
|
|
|
|
<!-- 120 minuten -->
|
|
<label class="relative flex items-start p-3.5 rounded-2xl border cursor-pointer transition <?= $sessionIdleTimeout === 7200 ? 'bg-indigo-50/60 dark:bg-indigo-950/20 border-indigo-400 dark:border-indigo-600 ring-1 ring-indigo-400/40' : 'bg-slate-50 dark:bg-[#0c0f17] border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700' ?>">
|
|
<input type="radio" name="session_idle_timeout" value="7200" <?= $sessionIdleTimeout === 7200 ? 'checked' : '' ?> class="mt-0.5 text-indigo-600 focus:ring-indigo-500 border-slate-300 dark:border-slate-700">
|
|
<div class="ml-3">
|
|
<span class="block text-xs font-bold text-slate-900 dark:text-white">120 minuten (2 uur)</span>
|
|
<span class="block text-[11px] text-slate-500 dark:text-slate-400 mt-0.5">Flexibel — Minimale onderbreking bij geconcentreerd werk</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Beveiligingsmaatregelen infopanel -->
|
|
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800/80 space-y-2 text-xs">
|
|
<div class="font-bold text-slate-800 dark:text-slate-200 flex items-center space-x-1.5">
|
|
<i data-lucide="info" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
<span>Actieve beveiligingsmaatregelen:</span>
|
|
</div>
|
|
<ul class="space-y-1.5 text-[11px] text-slate-600 dark:text-slate-400 list-disc list-inside">
|
|
<li><strong>Interactieve waarschuwing:</strong> 2 minuten (120 sec) voor time-out verschijnt een aftelmodal. Actieve gebruikers kunnen met 1 klik ingelogd blijven.</li>
|
|
<li><strong>Periodieke Sessierotatie:</strong> Sessie-ID's worden conform OWASP ASVS v4.0 automatisch elke 30 minuten geroteerd tegen session hijacking.</li>
|
|
<li><strong>Absolute daglimiet:</strong> Maximaal 10 uur aaneengesloten sessie per kalenderdag; om middernacht vervalt elke sessie automatisch.</li>
|
|
<li><strong>Tabblad-synchronisatie:</strong> Activiteit in één browsertabblad verlengt automatisch alle open Fociz-tabbladen.</li>
|
|
</ul>
|
|
</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-bold transition shadow-xs cursor-pointer">
|
|
Sessie-instellingen Opslaan
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Audit Trail Logboek (SOC 2 / ISO 27001) -->
|
|
<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 border-b border-slate-200 dark:border-slate-800 pb-5">
|
|
<div>
|
|
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
|
|
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
|
<span>Compliance & Veiligheidslogboek (SOC 2 / ISO 27001)</span>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
|
|
Onwijzigbare Audit Trail
|
|
</h2>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B.
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center space-x-2 flex-shrink-0">
|
|
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20" title="Tijdzone: Europe/Amsterdam (UTC+2 in zomertijd / UTC+1 in wintertijd)">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
|
Live Audit Logging Actief • NL Tijd
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left text-xs text-slate-600 dark:text-slate-300">
|
|
<thead>
|
|
<tr class="border-b border-slate-200 dark:border-slate-800 text-[11px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
|
<th class="py-3 px-3">Tijdstip (NL)</th>
|
|
<th class="py-3 px-3">Gebruiker / Doel</th>
|
|
<th class="py-3 px-3">Actie</th>
|
|
<th class="py-3 px-3">IP-adres</th>
|
|
<th class="py-3 px-3">Status</th>
|
|
<th class="py-3 px-3">Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/60 font-sans">
|
|
<?php if (empty($auditLogs)): ?>
|
|
<tr>
|
|
<td colspan="6" class="py-8 text-center text-slate-400 dark:text-slate-500">
|
|
<i data-lucide="inbox" class="w-8 h-8 mx-auto mb-2 opacity-40"></i>
|
|
Nog geen beveiligingsgebeurtenissen geregistreerd.
|
|
</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($auditLogs as $log):
|
|
$status = strtoupper($log['status'] ?? 'SUCCESS');
|
|
$statusColor = match($status) {
|
|
'SUCCESS' => 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20',
|
|
'FAILED', 'FAILURE' => 'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20',
|
|
'WARNING' => 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20',
|
|
'PENDING' => 'bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20',
|
|
default => 'bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20',
|
|
};
|
|
$action = $log['action'] ?? '';
|
|
$actionBadge = match($action) {
|
|
'LOGIN_SUCCESS' => 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800/60',
|
|
'LOGIN_FAILED', 'LOGIN_MFA_FAILED' => 'bg-rose-50 dark:bg-rose-950/40 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800/60',
|
|
'LOGIN_NEW_IP_CHALLENGE', 'IP_VERIFIED' => 'bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border-sky-200 dark:border-sky-800/60',
|
|
'SETTINGS_UPDATED' => 'bg-purple-50 dark:bg-purple-950/40 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800/60',
|
|
'USER_INVITED', 'INVITE_RESENT', 'USER_REGISTERED_VIA_INVITE' => 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-800/60',
|
|
'USER_DELETED', 'INVITE_REVOKED' => 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-800/60',
|
|
default => 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-700',
|
|
};
|
|
$detailsStr = '';
|
|
if (!empty($log['details'])) {
|
|
$decoded = json_decode($log['details'], true);
|
|
if (is_array($decoded)) {
|
|
$parts = [];
|
|
foreach ($decoded as $k => $v) {
|
|
if (is_scalar($v)) {
|
|
$parts[] = "{$k}: {$v}";
|
|
}
|
|
}
|
|
$detailsStr = implode(', ', $parts);
|
|
} else {
|
|
$detailsStr = (string)$log['details'];
|
|
}
|
|
}
|
|
?>
|
|
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition">
|
|
<td class="py-2.5 px-3 font-mono text-[11px] text-slate-500 whitespace-nowrap">
|
|
<?= date('d-m-Y H:i:s', strtotime($log['created_at'])) ?>
|
|
</td>
|
|
<td class="py-2.5 px-3 font-medium text-slate-800 dark:text-slate-200 max-w-[200px] truncate" title="<?= htmlspecialchars($log['user_email'] ?? 'Systeem') ?>">
|
|
<?= htmlspecialchars($log['user_email'] ?? 'Systeem / Onbekend') ?>
|
|
</td>
|
|
<td class="py-2.5 px-3 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-mono font-medium border <?= $actionBadge ?>">
|
|
<?= htmlspecialchars($action) ?>
|
|
</span>
|
|
</td>
|
|
<td class="py-2.5 px-3 font-mono text-[11px] text-slate-500 whitespace-nowrap">
|
|
<?= htmlspecialchars($log['ip_address'] ?? '—') ?>
|
|
</td>
|
|
<td class="py-2.5 px-3 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border <?= $statusColor ?>">
|
|
<?= htmlspecialchars($status) ?>
|
|
</span>
|
|
</td>
|
|
<td class="py-2.5 px-3 text-[11px] text-slate-500 max-w-[280px] truncate" title="<?= htmlspecialchars($detailsStr) ?>">
|
|
<?= htmlspecialchars($detailsStr ?: '—') ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 7: Systeem & Database (inclusief SMTP Mail Server) -->
|
|
<div id="tab-pane-system" class="admin-tab-pane space-y-8 hidden">
|
|
<!-- 5. SMTP Mail Server & Notificaties (Release v1.6) -->
|
|
<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-3 pb-4 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-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
<i data-lucide="mail" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">SMTP Mail Server & Notificaties</h2>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Uitgaande e-mailserver voor wachtwoord-vergeten, uitnodigingen en notificaties</p>
|
|
</div>
|
|
</div>
|
|
<span class="px-2.5 py-1 rounded-full bg-teal-50 dark:bg-teal-950/40 text-teal-700 dark:text-teal-400 border border-teal-200 dark:border-teal-800/60 text-xs font-bold self-start sm:self-auto">
|
|
mail.fociz.nl:587 • Actief
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Configuratie Overzicht -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
|
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800/80 flex justify-between items-center">
|
|
<span class="text-slate-500 dark:text-slate-400">SMTP Host & Poort:</span>
|
|
<span class="font-mono font-bold text-slate-900 dark:text-white"><?= htmlspecialchars(defined('SMTP_HOST') ? SMTP_HOST : 'mail.fociz.nl') ?>:<?= htmlspecialchars((string)(defined('SMTP_PORT') ? SMTP_PORT : 587)) ?> (STARTTLS)</span>
|
|
</div>
|
|
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800/80 flex justify-between items-center">
|
|
<span class="text-slate-500 dark:text-slate-400">SMTP Gebruiker:</span>
|
|
<span class="font-mono font-bold text-slate-900 dark:text-white"><?= htmlspecialchars(defined('SMTP_USER') ? SMTP_USER : 'info@fociz.nl') ?></span>
|
|
</div>
|
|
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800/80 flex justify-between items-center">
|
|
<span class="text-slate-500 dark:text-slate-400">Zichtbare Afzender (From):</span>
|
|
<span class="font-semibold text-slate-900 dark:text-white"><?= htmlspecialchars(defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'Fociz') ?> <<?= htmlspecialchars(defined('SMTP_FROM_EMAIL') ? SMTP_FROM_EMAIL : 'no-reply@fociz.nl') ?>></span>
|
|
</div>
|
|
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800/80 flex justify-between items-center">
|
|
<span class="text-slate-500 dark:text-slate-400">Antwoordadres (Reply-To):</span>
|
|
<span class="font-semibold text-slate-900 dark:text-white"><?= htmlspecialchars(defined('SMTP_REPLY_TO') ? SMTP_REPLY_TO : 'info@fociz.nl') ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test E-mail Versturen Formulier -->
|
|
<div class="pt-2 border-t border-slate-100 dark:border-slate-800/80">
|
|
<h3 class="text-xs font-bold text-slate-800 dark:text-slate-200 mb-2 flex items-center space-x-1.5">
|
|
<i data-lucide="send" class="w-3.5 h-3.5 text-indigo-500"></i>
|
|
<span>Directe SMTP Verbindingstest</span>
|
|
</h3>
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400 mb-3">
|
|
Verstuur een realtime testmail met het Fociz HTML-sjabloon om de verbinding, STARTTLS-handshake en aflevering direct te verifiëren.
|
|
</p>
|
|
|
|
<form method="POST" action="/admin.php" class="flex flex-col sm:flex-row gap-2.5">
|
|
<input type="hidden" name="action" value="send_test_email">
|
|
<input type="hidden" name="active_tab" value="system">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
<div class="relative flex-1">
|
|
<input
|
|
type="email"
|
|
name="test_email"
|
|
required
|
|
value="<?= htmlspecialchars($currentUser['email'] ?? '') ?>"
|
|
placeholder="test.ontvanger@bedrijf.nl"
|
|
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-slate-900 dark:text-white text-xs placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
>
|
|
</div>
|
|
<button type="submit" class="px-5 py-2.5 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 flex-shrink-0">
|
|
<i data-lucide="send" class="w-3.5 h-3.5"></i>
|
|
<span>Testmail Verzenden</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Systeem Gezondheid & Beveiliging -->
|
|
<div class="fociz-card p-6 space-y-4">
|
|
<div class="flex items-center space-x-2 text-teal-600 dark:text-teal-400 text-xs font-semibold">
|
|
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
|
<span>Systeem & Beveiliging</span>
|
|
</div>
|
|
<div class="space-y-2.5 text-xs text-slate-700 dark:text-slate-300">
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Tweestapsverificatie (2FA)</span>
|
|
<span class="text-teal-600 dark:text-teal-400 font-bold flex items-center space-x-1">
|
|
<i data-lucide="check" class="w-3 h-3"></i>
|
|
<span>Verplicht</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Sessie Idle Timeout</span>
|
|
<span class="text-indigo-600 dark:text-indigo-400 font-bold"><?= (int)($sessionIdleTimeout / 60) ?> min</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Sessie ID Rotatie</span>
|
|
<span class="text-teal-600 dark:text-teal-400 font-bold">Elke 30 min</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Wachtwoordbeleid</span>
|
|
<span class="text-teal-600 dark:text-teal-400 font-bold">NIST 12+ tekens</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Database Versie</span>
|
|
<span class="text-slate-500 dark:text-slate-400 font-mono text-[11px]">MariaDB 10.x</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Push Notificaties</span>
|
|
<span class="text-teal-600 dark:text-teal-400 font-bold">Actief</span>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between p-2.5 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span>Graph API Sync</span>
|
|
<span class="text-sky-600 dark:text-sky-400 font-bold">Live v1.0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Database & Systeem Status -->
|
|
<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">
|
|
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center justify-center text-slate-700 dark:text-slate-300">
|
|
<i data-lucide="database" class="w-5 h-5"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Database & Systeem Gezondheid</h3>
|
|
<p class="text-[11px] text-slate-500 dark:text-slate-400">Kerntabellen status, caching en configuratie integriteit</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
|
<div class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span class="text-slate-500 dark:text-slate-400 block text-[11px] mb-1">MariaDB Engine</span>
|
|
<span class="font-bold text-slate-900 dark:text-white">InnoDB utf8mb4</span>
|
|
</div>
|
|
<div class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span class="text-slate-500 dark:text-slate-400 block text-[11px] mb-1">Instellingen Cache</span>
|
|
<span class="font-bold text-teal-600 dark:text-teal-400 flex items-center space-x-1">
|
|
<i data-lucide="check" class="w-3.5 h-3.5"></i>
|
|
<span>Synchroon (In-Memory)</span>
|
|
</span>
|
|
</div>
|
|
<div class="p-3 rounded-xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800">
|
|
<span class="text-slate-500 dark:text-slate-400 block text-[11px] mb-1">Tijdzone & Locale</span>
|
|
<span class="font-bold text-slate-900 dark:text-white">Europe/Amsterdam (NL)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Brainstorm Categorieën Beheer Card -->
|
|
<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-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-xl bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold shadow-xs">
|
|
<i data-lucide="lightbulb" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center space-x-2">
|
|
<h2 class="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Brainstorm Categorieën</h2>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800/60 text-indigo-700 dark:text-indigo-300 text-[10px] font-bold">
|
|
Beheerbaar
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">Beheer de beschikbare categorieën voor nieuwe brainstormsessies</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form method="POST" class="space-y-5">
|
|
<input type="hidden" name="action" value="save_brainstorm_categories">
|
|
<input type="hidden" name="active_tab" value="system">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(get_csrf_token()) ?>">
|
|
|
|
<div class="space-y-2">
|
|
<div class="grid grid-cols-12 gap-3 px-3 py-1.5 text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider border-b border-slate-100 dark:border-slate-800">
|
|
<div class="col-span-5 sm:col-span-4">Categorienaam</div>
|
|
<div class="col-span-5 sm:col-span-6">Omschrijving / Toelichting</div>
|
|
<div class="col-span-1 text-center">Actief</div>
|
|
<div class="col-span-1 text-right">Verwijder</div>
|
|
</div>
|
|
|
|
<?php foreach ($brainstormCategories as $bIdx => $bCat): ?>
|
|
<div class="grid grid-cols-12 gap-3 items-center p-3 rounded-2xl bg-slate-50/70 dark:bg-[#0c0f17] border border-slate-200/70 dark:border-slate-800/70 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
<div class="col-span-5 sm:col-span-4">
|
|
<input type="text" name="categories[<?= $bIdx ?>][name]" value="<?= htmlspecialchars($bCat['name']) ?>" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" required>
|
|
</div>
|
|
<div class="col-span-5 sm:col-span-6">
|
|
<input type="text" name="categories[<?= $bIdx ?>][description]" value="<?= htmlspecialchars($bCat['description'] ?? '') ?>" placeholder="Optionele beschrijving..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div class="col-span-1 text-center">
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" name="categories[<?= $bIdx ?>][is_active]" value="1" <?= !empty($bCat['is_active']) ? 'checked' : '' ?> class="w-4 h-4 text-indigo-600 rounded border-slate-300 dark:border-slate-700 focus:ring-indigo-500">
|
|
</label>
|
|
</div>
|
|
<div class="col-span-1 text-right">
|
|
<label class="cursor-pointer text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 transition" title="Vink aan om deze categorie te verwijderen">
|
|
<input type="checkbox" name="categories[<?= $bIdx ?>][is_delete]" value="1" class="w-4 h-4 text-rose-600 rounded border-slate-300 dark:border-slate-700 focus:ring-rose-500">
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
|
|
<!-- Nieuwe categorie toevoegen regel -->
|
|
<div class="p-4 rounded-2xl bg-indigo-50/40 dark:bg-indigo-950/20 border border-dashed border-indigo-300/80 dark:border-indigo-800/60 space-y-3">
|
|
<div class="flex items-center space-x-2 text-xs font-bold text-indigo-900 dark:text-indigo-200">
|
|
<i data-lucide="plus-circle" class="w-4 h-4 text-indigo-600 dark:text-indigo-400"></i>
|
|
<span>Nieuwe Categorie Toevoegen</span>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
|
<div class="sm:col-span-5">
|
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Categorienaam</label>
|
|
<input type="text" name="new_category_name" placeholder="Bijv. Duurzaamheid & MVO" class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-semibold text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
<div class="sm:col-span-7">
|
|
<label class="block text-[10px] font-bold text-slate-500 dark:text-slate-400 mb-1">Omschrijving</label>
|
|
<input type="text" name="new_category_desc" placeholder="Doel of richting van deze categorie..." class="w-full px-3 py-1.5 rounded-xl bg-white dark:bg-[#111624] border border-slate-300 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
|
</div>
|
|
</div>
|
|
</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-bold transition shadow-xs cursor-pointer flex items-center space-x-1.5">
|
|
<i data-lucide="check" class="w-4 h-4"></i>
|
|
<span>Brainstorm Categorieën Opslaan</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Modal: Icoon Kiezer voor Agenda Blokkades -->
|
|
<div id="iconPickerModal" class="fixed inset-0 z-[99999] bg-slate-950/70 backdrop-blur-xs hidden items-center justify-center p-4">
|
|
<div class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4 max-h-[85vh] flex flex-col animate-in fade-in zoom-in-95 duration-100">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
|
<div class="flex items-center space-x-2.5">
|
|
<div class="w-8 h-8 rounded-xl bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 text-amber-600 dark:text-amber-400 flex items-center justify-center">
|
|
<i data-lucide="smile" class="w-4 h-4"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Kies een Icoon</h3>
|
|
<p class="text-[10px] text-slate-500 dark:text-slate-400">Selecteer een standaard icoon voor de dagblokkadereden</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" onclick="closeIconPicker()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Zoekbalk & Categorie Tabs -->
|
|
<div class="space-y-2.5 flex-shrink-0">
|
|
<div class="relative">
|
|
<i data-lucide="search" class="w-3.5 h-3.5 absolute left-3 top-3 text-slate-400"></i>
|
|
<input type="text" id="iconPickerSearch" placeholder="Zoek op categorie of naam (bijv. vakantie, ziek, focus, kantoor, studie)..." oninput="filterIconPicker(this.value)" class="w-full pl-8 pr-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-amber-500">
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-1.5 overflow-x-auto no-scrollbar py-0.5" id="iconPickerCategoryTabs">
|
|
<!-- Dynamische categorieknoppen -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grid met Iconen -->
|
|
<div id="iconPickerGrid" class="flex-1 overflow-y-auto p-1.5 grid grid-cols-6 sm:grid-cols-8 gap-2 min-h-[220px] max-h-[320px] rounded-xl bg-slate-50/50 dark:bg-[#0c0f17]/50 border border-slate-200/50 dark:border-slate-800/50">
|
|
<!-- Wordt via JS gevuld met grote, klikbare iconen -->
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0 text-xs">
|
|
<span class="text-[11px] text-slate-400">Tip: Je kunt ook zelf een aangepaste emoji typen.</span>
|
|
<button type="button" onclick="closeIconPicker()" 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-300 font-semibold transition cursor-pointer">
|
|
Sluiten
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const ADMIN_TABS = ['users', 'calendar', 'microsoft', 'jira', 'ai', 'security', 'system'];
|
|
|
|
function switchAdminTab(tabName, updateUrl = true) {
|
|
if (!ADMIN_TABS.includes(tabName)) {
|
|
tabName = 'users';
|
|
}
|
|
|
|
ADMIN_TABS.forEach(t => {
|
|
const btn = document.getElementById('tab-btn-' + t);
|
|
const pane = document.getElementById('tab-pane-' + t);
|
|
if (btn) {
|
|
if (t === tabName) {
|
|
btn.className = 'admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-bold transition whitespace-nowrap cursor-pointer bg-indigo-600 text-white shadow-xs';
|
|
} else {
|
|
btn.className = 'admin-tab-btn flex items-center space-x-2 px-4 py-2.5 rounded-xl text-xs font-semibold transition whitespace-nowrap cursor-pointer bg-slate-100/70 hover:bg-slate-200 dark:bg-slate-800/70 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300';
|
|
}
|
|
}
|
|
if (pane) {
|
|
if (t === tabName) {
|
|
pane.classList.remove('hidden');
|
|
pane.classList.add('block');
|
|
} else {
|
|
pane.classList.add('hidden');
|
|
pane.classList.remove('block');
|
|
}
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('input[name="active_tab"]').forEach(input => {
|
|
input.value = tabName;
|
|
});
|
|
|
|
if (updateUrl) {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('tab', tabName);
|
|
history.replaceState(null, '', url.toString());
|
|
}
|
|
|
|
if (window.lucide && typeof lucide.createIcons === 'function') {
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
let initialTab = urlParams.get('tab') || window.location.hash.replace('#', '') || '<?= htmlspecialchars($activeTab) ?>';
|
|
if (!ADMIN_TABS.includes(initialTab)) {
|
|
initialTab = 'users';
|
|
}
|
|
switchAdminTab(initialTab, false);
|
|
});
|
|
|
|
function copyAdminRedirectUri() {
|
|
const input = document.getElementById('adminRedirectUri');
|
|
if (!input) return;
|
|
input.select();
|
|
navigator.clipboard.writeText(input.value).then(() => {
|
|
const copyText = document.getElementById('copyText');
|
|
const copyIcon = document.getElementById('copyIcon');
|
|
if (copyText) copyText.innerText = 'Gekopieerd!';
|
|
if (copyIcon) copyIcon.setAttribute('data-lucide', 'check');
|
|
if (window.lucide) lucide.createIcons();
|
|
setTimeout(() => {
|
|
if (copyText) copyText.innerText = 'Kopiëren';
|
|
if (copyIcon) copyIcon.setAttribute('data-lucide', 'copy');
|
|
if (window.lucide) lucide.createIcons();
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function toggleAdminSecret() {
|
|
const input = document.getElementById('adminClientSecret');
|
|
const btn = document.getElementById('toggleSecretBtn');
|
|
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 toggleAdminGuide() {
|
|
const content = document.getElementById('adminGuideContent');
|
|
const arrow = document.getElementById('guideArrow');
|
|
if (!content) return;
|
|
content.classList.toggle('hidden');
|
|
if (arrow) {
|
|
arrow.classList.toggle('rotate-180');
|
|
}
|
|
}
|
|
|
|
// Jira Wachtrijfilter Modalfuncties
|
|
function openJiraFilterModal() {
|
|
document.getElementById('jiraFilterModalTitle').innerText = 'Nieuw Wachtrijfilter Toevoegen';
|
|
document.getElementById('filterModal_id').value = '0';
|
|
document.getElementById('filterModal_code').value = '';
|
|
document.getElementById('filterModal_name').value = '';
|
|
document.getElementById('filterModal_project').value = 'SD';
|
|
document.getElementById('filterModal_jql').value = '';
|
|
document.getElementById('filterModal_desc').value = '';
|
|
document.getElementById('filterModal_active').checked = true;
|
|
|
|
const modal = document.getElementById('jiraFilterModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function editJiraFilter(filter) {
|
|
document.getElementById('jiraFilterModalTitle').innerText = `Wachtrijfilter '${filter.filter_name}' Bewerken`;
|
|
document.getElementById('filterModal_id').value = filter.id || '0';
|
|
document.getElementById('filterModal_code').value = filter.filter_code || '';
|
|
document.getElementById('filterModal_name').value = filter.filter_name || '';
|
|
document.getElementById('filterModal_project').value = filter.project_key || '';
|
|
document.getElementById('filterModal_jql').value = filter.jql_clause || '';
|
|
document.getElementById('filterModal_desc').value = filter.description || '';
|
|
document.getElementById('filterModal_active').checked = (filter.is_active != 0);
|
|
|
|
const modal = document.getElementById('jiraFilterModal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function closeJiraFilterModal() {
|
|
const modal = document.getElementById('jiraFilterModal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const modelSelect = document.getElementById('admin_ai_model');
|
|
const providerInput = document.getElementById('admin_ai_provider');
|
|
if (modelSelect && providerInput) {
|
|
modelSelect.addEventListener('change', function() {
|
|
if (this.value.indexOf('gemini') !== -1) {
|
|
providerInput.value = 'gemini';
|
|
} else {
|
|
providerInput.value = 'openai';
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
async function testAdminAiConnection() {
|
|
const btn = document.getElementById('adminAiTestBtn');
|
|
const btnText = document.getElementById('adminAiTestBtnText');
|
|
const resultDiv = document.getElementById('adminAiTestResult');
|
|
const apiKey = document.getElementById('admin_ai_api_key').value.trim();
|
|
const model = document.getElementById('admin_ai_model').value;
|
|
let provider = document.getElementById('admin_ai_provider').value;
|
|
if (!provider) {
|
|
provider = (model && model.indexOf('gpt') !== -1) ? 'openai' : 'gemini';
|
|
}
|
|
|
|
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 rawText = await res.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(rawText);
|
|
} catch (parseErr) {
|
|
throw new Error(`Ongeldig serverantwoord (HTTP ${res.status}): ${rawText.slice(0, 200)}`);
|
|
}
|
|
|
|
resultDiv.classList.remove('hidden');
|
|
if (data && 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 || 'API verbinding is geverifieerd.'}</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 opgetreden'}</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';
|
|
}
|
|
}
|
|
|
|
// --- Agenda Blokkades Icoon Kiezer ---
|
|
let currentIconTargetInputId = null;
|
|
let currentIconTargetPreviewId = null;
|
|
let currentIconCategory = 'all';
|
|
|
|
const ICON_PICKER_DATA = {
|
|
'Vakantie & Vrij': ['🏖️', '✈️', '🌴', '⛺', '☀️', '⛵', '🎒', '🗺️', '🍹', '🏔️', '🚗', '🛳️', '🏕️', '🕶️', '🏄', '🏊', '⛷️', '🚴', '🎣', '🌅', '🌌', '🌍'],
|
|
'Werk & Focus': ['🎯', '📋', '💻', '📊', '📈', '🏢', '💼', '📁', '✍️', '💡', '⚡', '🔍', '📅', '🗓️', '⏰', '⏳', '🔒', '🛠️', '📞', '🤝', '📌', '📑', '🖊️', '🖥️', '⌨️', '📂'],
|
|
'Ziekte & Zorg': ['🤒', '🏥', '💊', '🩺', '🩹', '🛌', '🧑⚕️', '🦷', '🧘', '🍵', '💉', '😷', '🤧', '🚑', '🌡️', '🧑🦽', '🧴', '🍎', '❤️🩹'],
|
|
'Studie & Opleiding': ['🎓', '📚', '📖', '🏫', '🧑🏫', '📝', '🧠', '🔬', '📐', '📜', '🏆', '🎨', '🧪', '🔭', '💡', '🎒'],
|
|
'Persoonlijk & Verlof': ['👶', '💍', '🏠', '🚚', '🎂', '🎉', '🕊️', '⚖️', '👥', '☕', '🍴', '👨👩👧', '🎈', '🎁', '🐶', '🐱', '🧹', '🪴', '🛒', '🔑', '💤']
|
|
};
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
return text.toString().replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
}
|
|
|
|
function openIconPickerFor(inputId, previewId) {
|
|
currentIconTargetInputId = inputId;
|
|
currentIconTargetPreviewId = previewId;
|
|
|
|
const modal = document.getElementById('iconPickerModal');
|
|
if (!modal) return;
|
|
|
|
currentIconCategory = 'all';
|
|
renderIconCategoryTabs();
|
|
renderIconGrid();
|
|
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
|
|
if (window.lucide && typeof lucide.createIcons === 'function') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const searchInput = document.getElementById('iconPickerSearch');
|
|
if (searchInput) {
|
|
searchInput.value = '';
|
|
searchInput.focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function closeIconPicker() {
|
|
const modal = document.getElementById('iconPickerModal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
currentIconTargetInputId = null;
|
|
currentIconTargetPreviewId = null;
|
|
}
|
|
|
|
function selectIcon(icon) {
|
|
if (currentIconTargetInputId) {
|
|
const input = document.getElementById(currentIconTargetInputId);
|
|
if (input) {
|
|
input.value = icon;
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
if (currentIconTargetPreviewId) {
|
|
const preview = document.getElementById(currentIconTargetPreviewId);
|
|
if (preview) preview.innerText = icon;
|
|
}
|
|
closeIconPicker();
|
|
}
|
|
|
|
function renderIconCategoryTabs() {
|
|
const container = document.getElementById('iconPickerCategoryTabs');
|
|
if (!container) return;
|
|
|
|
let html = `
|
|
<button type="button" onclick="switchIconCategory('all')" class="px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap transition cursor-pointer ${currentIconCategory === 'all' ? 'bg-amber-600 text-white shadow-xs' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'}">
|
|
Alle
|
|
</button>
|
|
`;
|
|
|
|
Object.keys(ICON_PICKER_DATA).forEach(cat => {
|
|
const isActive = currentIconCategory === cat;
|
|
html += `
|
|
<button type="button" onclick="switchIconCategory('${escapeHtml(cat)}')" class="px-2.5 py-1 rounded-lg text-xs font-semibold whitespace-nowrap transition cursor-pointer ${isActive ? 'bg-amber-600 text-white shadow-xs' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'}">
|
|
${escapeHtml(cat)}
|
|
</button>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function switchIconCategory(category) {
|
|
currentIconCategory = category;
|
|
renderIconCategoryTabs();
|
|
const searchInput = document.getElementById('iconPickerSearch');
|
|
const query = searchInput ? searchInput.value : '';
|
|
renderIconGrid(query);
|
|
}
|
|
|
|
function renderIconGrid(filterText = '') {
|
|
const grid = document.getElementById('iconPickerGrid');
|
|
if (!grid) return;
|
|
|
|
let iconsToDisplay = [];
|
|
const query = filterText.toLowerCase().trim();
|
|
|
|
if (currentIconCategory === 'all') {
|
|
Object.entries(ICON_PICKER_DATA).forEach(([cat, icons]) => {
|
|
if (!query || cat.toLowerCase().includes(query)) {
|
|
iconsToDisplay.push(...icons);
|
|
} else {
|
|
icons.forEach(ic => {
|
|
if (ic.includes(query)) iconsToDisplay.push(ic);
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
const catIcons = ICON_PICKER_DATA[currentIconCategory] || [];
|
|
if (!query) {
|
|
iconsToDisplay = catIcons;
|
|
} else {
|
|
iconsToDisplay = catIcons.filter(ic => ic.includes(query) || currentIconCategory.toLowerCase().includes(query));
|
|
}
|
|
}
|
|
|
|
iconsToDisplay = Array.from(new Set(iconsToDisplay));
|
|
|
|
if (iconsToDisplay.length === 0) {
|
|
grid.innerHTML = '<div class="col-span-full py-8 text-center text-xs text-slate-400">Geen iconen gevonden voor deze zoekopdracht.</div>';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = iconsToDisplay.map(ic => `
|
|
<button type="button" onclick="selectIcon('${ic}')" class="h-11 flex items-center justify-center text-2xl rounded-xl bg-white hover:bg-amber-50 dark:bg-slate-900 dark:hover:bg-amber-950/40 border border-slate-200/80 hover:border-amber-400 dark:border-slate-800 dark:hover:border-amber-500 hover:scale-110 transition duration-150 cursor-pointer shadow-2xs group" title="${ic}">
|
|
<span class="group-hover:scale-110 transition-transform duration-150">${ic}</span>
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
function filterIconPicker(query) {
|
|
renderIconGrid(query);
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeJiraFilterModal();
|
|
closeIconPicker();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|