Aanpassing planner #60

Merged
NiekRabelink merged 1 commits from dev into main 2026-09-12 09:04:17 +00:00
7 changed files with 666 additions and 26 deletions
+3 -2
View File
@@ -1096,8 +1096,9 @@ require_once __DIR__ . '/includes/header.php';
<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">
<?= $aiSettings['has_key'] ? 'AI Actief (' . htmlspecialchars($aiSettings['ai_model']) . ')' : 'Sleutel Vereist' ?>
<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($aiSettings['ai_model']) . ')' : 'Sleutel Vereist' ?></span>
</span>
</div>
</div>
+7 -1
View File
@@ -62,8 +62,14 @@ try {
$relevantIssues = [];
// Filter issues die specifiek relevant zijn voor deze medewerker
$userFirstName = explode(' ', trim($user['name'] ?? ''))[0] ?? '';
foreach (array_merge($assignedIssues, $teamIssues) as $iss) {
$isAssignedToMe = !empty($iss['assignee_name']) && (stripos($iss['assignee_name'], $user['name']) !== false || stripos($iss['assignee_name'], 'Niek') !== false);
$assignee = $iss['assignee_name'] ?? '';
$isAssignedToMe = !empty($assignee) && (
stripos($assignee, $user['name'] ?? '') !== false ||
(!empty($userFirstName) && mb_strlen($userFirstName) >= 2 && stripos($assignee, $userFirstName) !== false) ||
(!empty($user['email']) && stripos($assignee, $user['email']) !== false)
);
$ol = strtolower(trim($iss['onderwerp_logging'] ?? ''));
$isQueueMatch = false;
+151 -1
View File
@@ -747,8 +747,23 @@ try {
} catch (Throwable $e) {}
}
// 3. Batch Calendar Blocks: haal verlof en geblokkeerde vakantiedagen op voor dit venster
$blockedDates = [];
try {
$stmtBlk = $db->prepare("SELECT start_date, end_date FROM calendar_blocks WHERE user_id = ? AND start_date <= ? AND end_date >= ?");
$stmtBlk->execute([$user['id'], $rangeEndDate, $rangeStartDate]);
while ($b = $stmtBlk->fetch()) {
$bStart = strtotime($b['start_date']);
$bEnd = strtotime($b['end_date']);
for ($d = $bStart; $d <= $bEnd; $d += 86400) {
$blockedDates[date('Y-m-d', $d)] = true;
}
}
} catch (Throwable $e) {}
while ($checkedDays < $maxDays) {
$dayOfWeek = (int)$checkDate->format('N'); // 1 = Ma, 7 = Zo
$dateStr = $checkDate->format('Y-m-d');
// Niet-werkdag / weekend overslaan op basis van organisatie-instellingen
if (!in_array($dayOfWeek, $activeWorkdays, true)) {
@@ -756,7 +771,11 @@ try {
continue;
}
$dateStr = $checkDate->format('Y-m-d');
// Geblokkeerde dagen (bijv. vakantie, verlof) overslaan
if (!empty($blockedDates[$dateStr])) {
$checkDate->modify('+1 day');
continue;
}
$isToday = ($dateStr === $now->format('Y-m-d')) && !$startFromNextWorkday;
// Bepaal vroegste startminuut voor deze dag (op hele kwartieren)
@@ -854,6 +873,137 @@ try {
exit;
}
// 12. Kalenderblokkades ophalen (Vakanties / Verlof / Afwezigheid)
if ($action === 'get_calendar_blocks') {
$stmt = $db->prepare("
SELECT * FROM calendar_blocks
WHERE user_id = ?
ORDER BY start_date DESC
LIMIT 100
");
$stmt->execute([$user['id']]);
$blocks = $stmt->fetchAll() ?: [];
echo json_encode(['success' => true, 'blocks' => $blocks]);
exit;
}
// 13. Kalenderblokkade opslaan (Vakantie / Verlof / Dag(en) blokkeren)
if ($action === 'save_calendar_block') {
$rawInput = json_decode(file_get_contents('php://input'), true);
$data = is_array($rawInput) ? $rawInput : $_POST;
$blockId = (int)($data['id'] ?? 0);
$title = trim($data['title'] ?? '');
$blockType = trim($data['block_type'] ?? 'VACATION');
$startDate = trim($data['start_date'] ?? $today);
$endDate = trim($data['end_date'] ?? $startDate);
$notes = trim($data['notes'] ?? '');
$syncToOutlook = !empty($data['sync_to_outlook']);
if (empty($title)) {
echo json_encode(['success' => false, 'error' => 'Geef een titel op voor de blokkade (bijv. Vakantie of Vrije dag)']);
exit;
}
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
echo json_encode(['success' => false, 'error' => 'Ongeldige datumnotatie']);
exit;
}
if ($endDate < $startDate) {
$temp = $startDate;
$startDate = $endDate;
$endDate = $temp;
}
$outlookEventId = null;
// Indien bewerken en er bestond al een Outlook event, haal het event ID op
if ($blockId > 0) {
$stmtEx = $db->prepare("SELECT outlook_event_id FROM calendar_blocks WHERE id = ? AND user_id = ?");
$stmtEx->execute([$blockId, $user['id']]);
$outlookEventId = $stmtEx->fetchColumn() ?: null;
}
// Optioneel synchroniseren naar Outlook 365
if ($syncToOutlook && function_exists('microsoft_create_event')) {
try {
if (!empty($outlookEventId) && function_exists('microsoft_delete_event')) {
microsoft_delete_event($user['id'], $outlookEventId);
}
$eventDesc = !empty($notes) ? $notes : "Geblokkeerd via Fociz Agenda ({$blockType})";
$msRes = microsoft_create_event($user['id'], [
'subject' => $title,
'description' => $eventDesc,
'date' => $startDate,
'end_date' => $endDate,
'is_all_day' => true,
'show_as' => ($blockType === 'FOCUS') ? 'busy' : 'oof',
]);
if (!empty($msRes['success']) && !empty($msRes['event_id'])) {
$outlookEventId = $msRes['event_id'];
}
} catch (Throwable $e) {
error_log("Fout bij sync calendar_block naar Outlook: " . $e->getMessage());
}
}
if ($blockId > 0) {
$stmt = $db->prepare("
UPDATE calendar_blocks
SET title = ?, block_type = ?, start_date = ?, end_date = ?, notes = ?, outlook_event_id = ?, updated_at = NOW()
WHERE id = ? AND user_id = ?
");
$stmt->execute([$title, $blockType, $startDate, $endDate, $notes, $outlookEventId, $blockId, $user['id']]);
} else {
$stmt = $db->prepare("
INSERT INTO calendar_blocks (user_id, start_date, end_date, title, block_type, notes, outlook_event_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
");
$stmt->execute([$user['id'], $startDate, $endDate, $title, $blockType, $notes, $outlookEventId]);
$blockId = (int)$db->lastInsertId();
}
echo json_encode([
'success' => true,
'message' => 'Agenda-blokkade succesvol opgeslagen',
'block_id' => $blockId,
'start_date'=> $startDate,
'end_date' => $endDate,
'title' => $title
]);
exit;
}
// 14. Kalenderblokkade verwijderen / opheffen
if ($action === 'delete_calendar_block') {
$rawInput = json_decode(file_get_contents('php://input'), true);
$data = is_array($rawInput) ? $rawInput : $_POST;
$blockId = (int)($data['id'] ?? 0);
if ($blockId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ongeldig blok ID']);
exit;
}
$stmtEx = $db->prepare("SELECT outlook_event_id FROM calendar_blocks WHERE id = ? AND user_id = ?");
$stmtEx->execute([$blockId, $user['id']]);
$outlookEventId = $stmtEx->fetchColumn();
if (!empty($outlookEventId) && function_exists('microsoft_delete_event')) {
try {
microsoft_delete_event($user['id'], $outlookEventId);
} catch (Throwable $e) {}
}
$stmtDel = $db->prepare("DELETE FROM calendar_blocks WHERE id = ? AND user_id = ?");
$stmtDel->execute([$blockId, $user['id']]);
echo json_encode(['success' => true, 'message' => 'Blokkade succesvol opgeheven']);
exit;
}
echo json_encode(['error' => 'Onbekende actie']);
} catch (Exception $e) {
http_response_code(500);
+2
View File
@@ -170,6 +170,8 @@ function ensure_tables_exist(PDO $pdo): void {
"CREATE TABLE IF NOT EXISTS `user_known_ips` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `is_verified` TINYINT(1) NOT NULL DEFAULT 1, `verify_token` VARCHAR(64) NULL, `verify_token_expires_at` DATETIME NULL, `first_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_ip` (`user_id`, `ip_address`), INDEX (`verify_token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
// SOC 2 & ISO 27001: Onwijzigbare Audit Trail
"CREATE TABLE IF NOT EXISTS `audit_logs` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NULL, `user_email` VARCHAR(191) NULL, `action` VARCHAR(64) NOT NULL, `resource_type` VARCHAR(64) NULL, `resource_id` INT NULL, `details` TEXT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`user_id`), INDEX (`action`), INDEX (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
// Agenda & Verlof: Hele of meerdere dagen blokkeren (Vakantie, Vrije dag, Ziekte)
"CREATE TABLE IF NOT EXISTS `calendar_blocks` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, `title` VARCHAR(255) NOT NULL, `block_type` VARCHAR(50) NOT NULL DEFAULT 'VACATION', `notes` TEXT NULL, `outlook_event_id` VARCHAR(255) NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX (`user_id`, `start_date`, `end_date`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($colUpgrades as $sql) {
try {
+16
View File
@@ -313,3 +313,19 @@ CREATE TABLE IF NOT EXISTS `user_sessions` (
CONSTRAINT `fk_user_sessions_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Fociz Agenda & Verlof: Hele of meerdere dagen blokkeren (Vakantie, Vrije dag, Ziekte)
CREATE TABLE IF NOT EXISTS `calendar_blocks` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`title` VARCHAR(255) NOT NULL,
`block_type` VARCHAR(50) NOT NULL DEFAULT 'VACATION',
`notes` TEXT NULL,
`outlook_event_id` VARCHAR(255) NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX (`user_id`, `start_date`, `end_date`),
CONSTRAINT `fk_calblocks_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+25 -8
View File
@@ -485,8 +485,24 @@ function microsoft_create_event(int $userId, array $eventData): array {
}
}
$isAllDay = !empty($eventData['is_all_day']);
if ($isAllDay) {
$startDate = $date;
$endDate = !empty($eventData['end_date']) ? trim($eventData['end_date']) : $date;
// In MS Graph API is de einddatum van een all-day event exclusief (dus +1 dag om hele periode te beslaan)
$endPlusOne = date('Y-m-d', strtotime('+1 day', strtotime($endDate)));
$startDateTimeStr = "{$startDate}T00:00:00";
$endDateTimeStr = "{$endPlusOne}T00:00:00";
$showAs = !empty($eventData['show_as']) ? $eventData['show_as'] : 'oof'; // Out of Office / Afwezig
$isReminderOn = false;
$startTime = '00:00';
$endTime = '23:59';
} else {
$startDateTimeStr = "{$date}T{$startTime}:00";
$endDateTimeStr = "{$date}T{$endTime}:00";
$showAs = 'busy';
$isReminderOn = true;
}
$payload = [
'subject' => $subject,
@@ -502,8 +518,9 @@ function microsoft_create_event(int $userId, array $eventData): array {
'dateTime' => $endDateTimeStr,
'timeZone' => 'Europe/Amsterdam',
],
'showAs' => 'busy',
'isReminderOn' => true,
'isAllDay' => $isAllDay,
'showAs' => $showAs,
'isReminderOn' => $isReminderOn,
'reminderMinutesBeforeStart' => 15,
];
@@ -923,11 +940,11 @@ function microsoft_get_mail_demo_data(): array {
[
'id' => 'demo-mail-1',
'subject' => '⚡ Urgent: UWV Koppeling REST-endpoints en Datamigratie timing',
'body_preview' => 'Beste Niek, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...',
'body_html' => '<p>Beste Niek,</p><p>We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de <strong>UWV REST-endpoints</strong> en de validatie van werknemersdossiers.</p><p>Zou je kunnen verifiëren of onze SSO-koppeling en de datadump export hier al op aansluiten? Het zou fijn zijn als we dit vanmiddag kort kunnen afstemmen in de planning.</p><p>Met vriendelijke groet,<br><strong>Directie UWV Koppelingen & Integraties</strong></p>',
'body_preview' => 'Beste collega, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...',
'body_html' => '<p>Beste collega,</p><p>We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de <strong>UWV REST-endpoints</strong> en de validatie van werknemersdossiers.</p><p>Zou je kunnen verifiëren of onze SSO-koppeling en de datadump export hier al op aansluiten? Het zou fijn zijn als we dit vanmiddag kort kunnen afstemmen in de planning.</p><p>Met vriendelijke groet,<br><strong>Directie UWV Koppelingen & Integraties</strong></p>',
'from_name' => 'Directie UWV Integraties',
'from_email' => 'koppelingen@uwv-overheid.nl',
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)), // 12 minuten geleden
'received_human' => '12 min geleden',
@@ -944,7 +961,7 @@ function microsoft_get_mail_demo_data(): array {
'body_html' => '<p>Hallo team,</p><p>Bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de werkweek accorderen in het systeem.</p><p>Laat het weten indien er correcties nodig zijn op de dagregistraties.</p><p>Hartelijke groet,<br><strong>Financiële Administratie Fociz</strong></p>',
'from_name' => 'Financiële Administratie',
'from_email' => 'facturatie@fociz.nl',
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)), // 1 uur geleden
'received_human' => '1 uur geleden',
@@ -961,7 +978,7 @@ function microsoft_get_mail_demo_data(): array {
'body_html' => '<p>Goedemiddag,</p><p>De testomgeving voor de nieuwe Atlassian API migratie (CHANGE-2046) is succesvol klaargezet. We kunnen starten met de verificatietesten op de Servicedesk wachtrijen en DX filters.</p><p>Alle endpoint logs zijn beschikbaar via het beheerpaneel.</p><p>Groeten,<br><strong>Team DevOps & Infrastructuur</strong></p>',
'from_name' => 'DevOps Systeembeheer',
'from_email' => 'infra@fociz.nl',
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)), // 3 uur geleden
'received_human' => '3 uur geleden',
@@ -995,7 +1012,7 @@ function microsoft_get_mail_demo_data(): array {
'body_html' => '<p>Welkom bij Fociz!</p><p>Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw persoonlijke dagplanning?</p><p>Hierdoor houd je maximale grip op je werkdag en behoud je de ultieme focus.</p><p>Veel productiviteit gewenst,<br><strong>Het Fociz Team</strong></p>',
'from_name' => 'Fociz Support Team',
'from_email' => 'no-reply@fociz.nl',
'to_recipients' => [['name' => 'Niek Rabelink', 'address' => 'niek@fociz.nl']],
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)), // 2 dagen geleden
'received_human' => '2 dagen geleden',
+457 -9
View File
@@ -213,6 +213,32 @@ foreach ($weekOutlookEvents as $evt) {
$outlookEventsByDate[$evtDate][] = $evt;
}
// 4b. Haal actieve kalenderblokkades (vakanties / verlof / afwezigheid) op
$allCalendarBlocks = [];
$calendarBlocksByDate = [];
try {
$stmtBlocks = $db->prepare("
SELECT * FROM calendar_blocks
WHERE user_id = ? AND start_date <= ? AND end_date >= ?
ORDER BY start_date ASC
");
$stmtBlocks->execute([$userId, date('Y-m-d', strtotime('+35 days', $sundayTs)), date('Y-m-d', strtotime('-14 days', $mondayTs))]);
$allCalendarBlocks = $stmtBlocks->fetchAll() ?: [];
foreach ($allCalendarBlocks as $cb) {
$cbStart = strtotime($cb['start_date']);
$cbEnd = strtotime($cb['end_date']);
for ($d = $cbStart; $d <= $cbEnd; $d += 86400) {
$dStr = date('Y-m-d', $d);
$calendarBlocksByDate[$dStr][] = $cb;
}
}
} catch (Throwable $e) {
error_log("Fout bij ophalen calendar_blocks in planner: " . $e->getMessage());
}
$selectedDayBlock = $calendarBlocksByDate[$selectedDate][0] ?? null;
$isSelectedDateBlocked = !empty($selectedDayBlock);
// 7 Dagen van de week samenstellen voor de weekselector
$weekDays = [];
for ($i = 0; $i < 7; $i++) {
@@ -223,6 +249,8 @@ for ($i = 0; $i < 7; $i++) {
$tStat = $weekTaskStats[$dayDate] ?? ['total' => 0, 'completed' => 0];
$mCount = count($outlookEventsByDate[$dayDate] ?? []);
$isWorkday = in_array($dayN, $activeWorkdays, true);
$dayBlock = $calendarBlocksByDate[$dayDate][0] ?? null;
$isBlocked = !empty($dayBlock);
$weekDays[] = [
'date' => $dayDate,
@@ -234,6 +262,8 @@ for ($i = 0; $i < 7; $i++) {
'is_selected' => ($dayDate === $selectedDate),
'is_workday' => $isWorkday,
'is_weekend' => !$isWorkday,
'is_blocked' => $isBlocked,
'block_info' => $dayBlock,
'task_count' => $tStat['total'],
'completed_count' => $tStat['completed'],
'meeting_count' => $mCount,
@@ -456,6 +486,12 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<span>Weekend / Vrij</span>
</span>
<?php endif; ?>
<?php if ($isSelectedDateBlocked): ?>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-amber-100 text-amber-900 dark:bg-amber-950/80 dark:text-amber-300 ring-1 ring-amber-500/30 flex items-center gap-1 cursor-pointer" onclick="openCalendarBlockModal(<?= htmlspecialchars(json_encode($selectedDayBlock), ENT_QUOTES) ?>)" title="<?= htmlspecialchars($selectedDayBlock['title']) ?> (klik om te bewerken)">
<i data-lucide="palmtree" class="w-3 h-3 text-amber-600 dark:text-amber-400"></i>
<span><?= htmlspecialchars($selectedDayBlock['title']) ?></span>
</span>
<?php endif; ?>
</div>
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
Fociz
@@ -467,6 +503,11 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<!-- Integratie Status Badges & Widget Beheer Knop -->
<div class="flex flex-wrap items-center gap-2">
<button onclick="openCalendarBlockModal()" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-amber-50 hover:bg-amber-100 dark:bg-amber-500/10 dark:hover:bg-amber-500/20 text-amber-800 dark:text-amber-300 border border-amber-200/80 dark:border-amber-500/30 text-xs font-bold transition shadow-2xs cursor-pointer" title="Hele of meerdere dagen blokkeren voor vakantie of verlof">
<i data-lucide="palmtree" class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400"></i>
<span>Vakantie / Dagen Blokkeren</span>
</button>
<button onclick="openWidgetManagerModal()" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-indigo-50 hover:bg-indigo-100 dark:bg-indigo-500/10 dark:hover:bg-indigo-500/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200/80 dark:border-indigo-500/30 text-xs font-bold transition shadow-2xs cursor-pointer">
<i data-lucide="layout-grid" class="w-3.5 h-3.5"></i>
<span>Widgets & Indeling</span>
@@ -953,8 +994,13 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div>
</div>
<!-- Rechterkant: AI Slim Indelen & Directe datumselector -->
<div class="flex flex-wrap items-center space-x-2 justify-end">
<!-- Rechterkant: Dagen Blokkeren, AI Slim Indelen & Directe datumselector -->
<div class="flex flex-wrap items-center gap-2 justify-end">
<button type="button" onclick="openCalendarBlockModal()" title="Hele of meerdere dagen blokkeren voor vakantie, verlof of afwezigheid" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-amber-50 hover:bg-amber-100 dark:bg-amber-950/40 dark:hover:bg-amber-900/50 text-amber-800 dark:text-amber-300 border border-amber-200 dark:border-amber-800/60 text-xs font-semibold shadow-2xs transition cursor-pointer">
<i data-lucide="palmtree" class="w-3.5 h-3.5 text-amber-600 dark:text-amber-400"></i>
<span class="hidden sm:inline">Dagen Blokkeren</span>
</button>
<?php if ($aiSettings['ai_enabled'] && $aiSettings['has_key']): ?>
<button onclick="aiSmartSchedule(this)" id="aiScheduleBtn" title="Laat AI Co-Pilot openstaande taken optimaal inplannen om meetings en lunch heen" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-indigo-600 to-sky-600 hover:from-indigo-500 hover:to-sky-500 text-white text-xs font-semibold shadow-xs transition cursor-pointer">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
@@ -984,16 +1030,23 @@ for ($h = $startHour; $h <= $endHour; $h++) {
onclick="switchPlannerDate('<?= $wd['date'] ?>', event); return false;"
class="fociz-card-interactive group relative flex flex-col p-3 rounded-xl border transition-all text-left <?= $wd['is_selected']
? 'bg-gradient-to-b from-indigo-500/15 via-indigo-500/5 to-transparent dark:from-indigo-500/25 dark:via-indigo-500/10 dark:to-transparent border-indigo-500 ring-2 ring-indigo-500/40 shadow-sm'
: ($wd['is_blocked']
? 'bg-amber-50/70 dark:bg-amber-950/20 border-amber-300/80 dark:border-amber-800/60 hover:border-amber-400 shadow-2xs'
: ($wd['is_workday']
? 'bg-slate-50/70 dark:bg-[#0c0f17]/50 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 hover:bg-white dark:hover:bg-[#111624] shadow-2xs'
: 'bg-slate-100/60 dark:bg-[#080b11]/60 border-dashed border-amber-300/60 dark:border-amber-900/40 hover:border-amber-400 dark:hover:border-amber-700 hover:bg-white dark:hover:bg-[#0f141f] shadow-2xs') ?>"
: 'bg-slate-100/60 dark:bg-[#080b11]/60 border-dashed border-amber-300/60 dark:border-amber-900/40 hover:border-amber-400 dark:hover:border-amber-700 hover:bg-white dark:hover:bg-[#0f141f] shadow-2xs')) ?>"
>
<div class="flex items-center justify-between mb-1">
<span class="text-[10px] font-bold uppercase tracking-wider <?= $wd['is_selected'] ? 'text-indigo-600 dark:text-indigo-400' : ($wd['is_workday'] ? 'text-slate-500 dark:text-slate-400' : 'text-slate-400 dark:text-slate-500') ?>">
<?= $wd['short_name'] ?>
</span>
<div class="flex items-center gap-1">
<?php if ($wd['is_weekend']): ?>
<?php if ($wd['is_blocked']): ?>
<span class="px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-amber-200/90 dark:bg-amber-900/80 text-amber-950 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80 flex items-center gap-0.5" title="<?= htmlspecialchars($wd['block_info']['title']) ?>">
<i data-lucide="palmtree" class="w-2.5 h-2.5"></i>
<span><?= htmlspecialchars(mb_strimwidth($wd['block_info']['title'], 0, 7, '..')) ?></span>
</span>
<?php elseif ($wd['is_weekend']): ?>
<span class="px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase bg-amber-50 dark:bg-amber-950/60 text-amber-700 dark:text-amber-400 border border-amber-200/80 dark:border-amber-800/60" title="Weekend / Niet-werkdag conform beheerinstellingen">
Weekend
</span>
@@ -1041,27 +1094,68 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<?php endforeach; ?>
</div>
<?php if ($isSelectedDateBlocked): ?>
<!-- Prominente Vakantie / Blokkade Banner -->
<div class="p-4 sm:p-5 rounded-2xl bg-gradient-to-r from-amber-50 via-amber-100/60 to-orange-50 dark:from-amber-950/40 dark:via-[#1a1408] dark:to-orange-950/30 border-2 border-amber-300/90 dark:border-amber-700/70 flex flex-col sm:flex-row sm:items-center justify-between gap-4 shadow-sm">
<div class="flex items-start sm:items-center space-x-3.5">
<div class="w-11 h-11 rounded-2xl bg-amber-200 dark:bg-amber-900/80 text-amber-900 dark:text-amber-200 flex items-center justify-center font-bold text-xl flex-shrink-0 shadow-xs ring-2 ring-amber-300 dark:ring-amber-700/80">
🏖️
</div>
<div>
<div class="flex items-center space-x-2 flex-wrap gap-y-1">
<h4 class="text-sm sm:text-base font-black text-amber-950 dark:text-amber-200">
<?= htmlspecialchars($selectedDayBlock['title']) ?>
</h4>
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-extrabold bg-amber-200/90 text-amber-950 dark:bg-amber-800/80 dark:text-amber-100 uppercase tracking-wider border border-amber-300 dark:border-amber-700">
<?= htmlspecialchars($selectedDayBlock['block_type']) ?>
</span>
</div>
<p class="text-xs text-amber-900/80 dark:text-amber-300/80 mt-1">
Geblokkeerd van <strong><?= date('j M Y', strtotime($selectedDayBlock['start_date'])) ?></strong> t/m <strong><?= date('j M Y', strtotime($selectedDayBlock['end_date'])) ?></strong>
<?php if (!empty($selectedDayBlock['notes'])): ?>
&bull; <?= htmlspecialchars($selectedDayBlock['notes']) ?>
<?php endif; ?>
</p>
</div>
</div>
<div class="flex items-center space-x-2 self-end sm:self-auto flex-shrink-0">
<button onclick="openCalendarBlockModal(<?= htmlspecialchars(json_encode($selectedDayBlock), ENT_QUOTES) ?>)" class="px-3.5 py-1.5 rounded-xl bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200 text-xs font-bold hover:bg-amber-50 dark:hover:bg-slate-800 border border-amber-300/80 dark:border-amber-700 transition shadow-2xs cursor-pointer">
Bewerken
</button>
<button onclick="deleteCalendarBlock(<?= (int)$selectedDayBlock['id'] ?>)" class="px-3.5 py-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/50 dark:hover:bg-rose-900/60 text-rose-700 dark:text-rose-300 text-xs font-bold border border-rose-200 dark:border-rose-800/60 transition shadow-2xs cursor-pointer">
Opheffen
</button>
</div>
</div>
<?php endif; ?>
<!-- 3. Sub-header van de Tijdlijn voor de geselecteerde dag -->
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center space-x-2">
<div class="w-7 h-7 rounded-lg <?= $isSelectedDateWeekend ? 'bg-amber-50 dark:bg-amber-950/50 text-amber-600 dark:text-amber-400' : '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="<?= $isSelectedDateWeekend ? 'coffee' : 'clock' ?>" class="w-3.5 h-3.5"></i>
<div class="w-7 h-7 rounded-lg <?= ($isSelectedDateBlocked || $isSelectedDateWeekend) ? 'bg-amber-50 dark:bg-amber-950/50 text-amber-600 dark:text-amber-400' : '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="<?= $isSelectedDateBlocked ? 'palmtree' : ($isSelectedDateWeekend ? 'coffee' : 'clock') ?>" class="w-3.5 h-3.5"></i>
</div>
<div>
<div class="flex items-center space-x-2">
<h4 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Tijdlijn & Time-blocking voor <?= htmlspecialchars($formattedSelectedDate) ?>
</h4>
<?php if ($isSelectedDateWeekend): ?>
<?php if ($isSelectedDateBlocked): ?>
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-100 text-amber-900 dark:bg-amber-950/80 dark:text-amber-200 border border-amber-300 dark:border-amber-700/80">
<?= htmlspecialchars($selectedDayBlock['title']) ?> (Geblokkeerd)
</span>
<?php elseif ($isSelectedDateWeekend): ?>
<span class="px-2 py-0.5 rounded-full text-[9px] font-extrabold uppercase bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200/80 dark:border-amber-800/60">
Weekend / Vrije Dag
</span>
<?php endif; ?>
</div>
<p class="text-[10px] text-slate-400">
<?= $isSelectedDateWeekend
<?= $isSelectedDateBlocked
? 'Deze dag is geblokkeerd in je agenda &bull; Geen kantooruren ingeroosterd'
: ($isSelectedDateWeekend
? 'Geen reguliere kantooruren gepland conform beheerinstellingen &bull; Vrije dag'
: 'Urenindeling & synchronisatie met Outlook' ?>
: 'Urenindeling & synchronisatie met Outlook') ?>
</p>
</div>
</div>
@@ -2316,6 +2410,123 @@ if ($isAjaxRequest) {
</div>
</div>
<!-- Modal: Agenda Dagen Blokkeren (Vakantie / Verlof / Focus) -->
<div id="calendarBlockModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm 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-[90vh] flex flex-col">
<!-- 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="palmtree" class="w-4 h-4"></i>
</div>
<div>
<h3 id="calendarBlockModalTitle" class="text-sm font-bold text-slate-900 dark:text-white">Dagen Blokkeren / Vakantie</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400">Blokkeer een hele dag of periode in je agenda & planner</p>
</div>
</div>
<button type="button" onclick="closeCalendarBlockModal()" 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>
<!-- Formulier / Content -->
<form id="calendarBlockForm" onsubmit="submitCalendarBlock(event)" class="flex-1 overflow-y-auto pr-1 space-y-3.5">
<input type="hidden" id="calBlockId" value="0">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Type Blokkade</label>
<div class="grid grid-cols-4 gap-2">
<button type="button" onclick="updateCalBlockDefaultTitle('VACATION')" id="calType_VACATION" class="cal-block-type-btn p-2 rounded-xl border text-center transition cursor-pointer flex flex-col items-center">
<span class="text-base mb-0.5">🏖️</span>
<span class="text-[10px] font-bold">Vakantie</span>
</button>
<button type="button" onclick="updateCalBlockDefaultTitle('LEAVE')" id="calType_LEAVE" class="cal-block-type-btn p-2 rounded-xl border text-center transition cursor-pointer flex flex-col items-center">
<span class="text-base mb-0.5">✈️</span>
<span class="text-[10px] font-bold">Verlof / ADV</span>
</button>
<button type="button" onclick="updateCalBlockDefaultTitle('SICK')" id="calType_SICK" class="cal-block-type-btn p-2 rounded-xl border text-center transition cursor-pointer flex flex-col items-center">
<span class="text-base mb-0.5">🤒</span>
<span class="text-[10px] font-bold">Ziekte</span>
</button>
<button type="button" onclick="updateCalBlockDefaultTitle('FOCUS')" id="calType_FOCUS" class="cal-block-type-btn p-2 rounded-xl border text-center transition cursor-pointer flex flex-col items-center">
<span class="text-base mb-0.5">🎯</span>
<span class="text-[10px] font-bold">Focusdag</span>
</button>
</div>
<input type="hidden" id="calBlockType" value="VACATION">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Titel / Omschrijving <span class="text-rose-500">*</span></label>
<input type="text" id="calBlockTitle" required placeholder="Bijv. Zomervakantie, ADV dag, Verlof..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Begindatum <span class="text-rose-500">*</span></label>
<input type="date" id="calBlockStartDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Einddatum (t/m) <span class="text-rose-500">*</span></label>
<input type="date" id="calBlockEndDate" required class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white focus:outline-hidden focus:ring-2 focus:ring-amber-500">
</div>
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Notities (optioneel)</label>
<textarea id="calBlockNotes" rows="2" placeholder="Eventuele overdracht of bijzonderheden..." class="w-full px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 text-xs font-medium text-slate-900 dark:text-white placeholder-slate-400 focus:outline-hidden focus:ring-2 focus:ring-amber-500"></textarea>
</div>
<?php if ($isOutlookConnected): ?>
<div class="p-3 rounded-xl bg-sky-50/60 dark:bg-sky-950/20 border border-sky-200/60 dark:border-sky-800/40 flex items-center justify-between">
<div class="flex items-center space-x-2.5">
<i data-lucide="refresh-cw" class="w-4 h-4 text-sky-600 dark:text-sky-400 flex-shrink-0"></i>
<div>
<p class="text-xs font-bold text-sky-900 dark:text-sky-200">Synchroniseren naar Outlook 365</p>
<p class="text-[10px] text-sky-700/80 dark:text-sky-400/80">Maakt een 'Afwezig' (OOF) dagblokkade aan in je Microsoft agenda</p>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer flex-shrink-0 ml-2">
<input type="checkbox" id="calBlockSyncOutlook" checked class="sr-only peer">
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-slate-600 peer-checked:bg-sky-600"></div>
</label>
</div>
<?php endif; ?>
<!-- Overzicht actieve / geplande blokkades -->
<div class="pt-2 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] font-bold text-slate-600 dark:text-slate-400 uppercase tracking-wider">Actieve & Toekomstige Blokkades</span>
<button type="button" onclick="loadCalendarBlocksList()" class="text-[10px] text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 font-semibold cursor-pointer flex items-center space-x-1">
<i data-lucide="refresh-cw" class="w-2.5 h-2.5"></i>
<span>Vernieuwen</span>
</button>
</div>
<div id="calendarBlocksListContainer" class="space-y-1.5 max-h-36 overflow-y-auto pr-1">
<div class="text-[11px] text-slate-400 text-center py-2">Laden...</div>
</div>
</div>
<!-- Knoppen balk -->
<div class="flex items-center justify-between pt-3 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0">
<button type="button" id="btnDeleteCurrentCalBlock" onclick="deleteCurrentCalendarBlock()" class="hidden px-3 py-2 rounded-xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/50 dark:hover:bg-rose-900/60 text-rose-700 dark:text-rose-300 text-xs font-bold border border-rose-200 dark:border-rose-800/60 transition cursor-pointer items-center space-x-1">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span>Opheffen</span>
</button>
<div class="flex items-center space-x-2 ml-auto">
<button type="button" onclick="closeCalendarBlockModal()" 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 text-xs font-semibold transition cursor-pointer">
Annuleren
</button>
<button type="submit" id="btnSaveCalBlock" class="px-4 py-2 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-3.5 h-3.5"></i>
<span>Opslaan</span>
</button>
</div>
</div>
</form>
</div>
</div>
<script>
// --- Fociz Widget Beheer & Configuratielogica (v2.0) ---
let focizWidgets = <?= json_encode(array_values($userWidgetLayout), JSON_UNESCAPED_UNICODE) ?>;
@@ -2590,6 +2801,243 @@ function showWidgetToast(htmlMessage, type = 'success', duration = 3500) {
}, duration);
}
// --- Agenda Dagen Blokkeren (Vakantie / Verlof / Focus) ---
const CAL_BLOCK_DEFAULT_TITLES = {
'VACATION': 'Vakantie',
'LEAVE': 'Verlof',
'SICK': 'Ziekte / Afwezig',
'FOCUS': 'Focusdag'
};
function selectCalBlockType(type) {
const input = document.getElementById('calBlockType');
if (input) input.value = type;
const types = ['VACATION', 'LEAVE', 'SICK', 'FOCUS'];
types.forEach(t => {
const btn = document.getElementById(`calType_${t}`);
if (!btn) return;
if (t === type) {
btn.className = 'cal-block-type-btn p-2 rounded-xl border border-amber-500 bg-amber-100 dark:bg-amber-950/80 text-amber-950 dark:text-amber-200 font-bold shadow-xs text-center transition cursor-pointer flex flex-col items-center ring-2 ring-amber-500/20';
} else {
btn.className = 'cal-block-type-btn p-2 rounded-xl border border-slate-200 dark:border-slate-700/80 bg-slate-50 dark:bg-slate-800/60 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 text-center transition cursor-pointer flex flex-col items-center';
}
});
}
function updateCalBlockDefaultTitle(type) {
const titleInput = document.getElementById('calBlockTitle');
const currentVal = titleInput ? titleInput.value.trim() : '';
const oldType = document.getElementById('calBlockType')?.value;
selectCalBlockType(type);
if (titleInput && (!currentVal || currentVal === (CAL_BLOCK_DEFAULT_TITLES[oldType] || ''))) {
titleInput.value = CAL_BLOCK_DEFAULT_TITLES[type] || '';
}
}
function openCalendarBlockModal(existingBlock = null, defaultDate = null) {
const modal = document.getElementById('calendarBlockModal');
if (!modal) return;
const idInput = document.getElementById('calBlockId');
const titleInput = document.getElementById('calBlockTitle');
const startInput = document.getElementById('calBlockStartDate');
const endInput = document.getElementById('calBlockEndDate');
const notesInput = document.getElementById('calBlockNotes');
const deleteBtn = document.getElementById('btnDeleteCurrentCalBlock');
const modalTitle = document.getElementById('calendarBlockModalTitle');
const selDate = defaultDate || '<?= $selectedDate ?>';
if (existingBlock && existingBlock.id) {
if (idInput) idInput.value = existingBlock.id;
if (titleInput) titleInput.value = existingBlock.title || '';
if (startInput) startInput.value = existingBlock.start_date || selDate;
if (endInput) endInput.value = existingBlock.end_date || existingBlock.start_date || selDate;
if (notesInput) notesInput.value = existingBlock.notes || '';
selectCalBlockType(existingBlock.block_type || 'VACATION');
if (deleteBtn) {
deleteBtn.classList.remove('hidden');
deleteBtn.classList.add('flex');
}
if (modalTitle) modalTitle.textContent = 'Blokkade Bewerken';
} else {
if (idInput) idInput.value = 0;
selectCalBlockType('VACATION');
if (titleInput) titleInput.value = 'Vakantie';
if (startInput) startInput.value = selDate;
if (endInput) endInput.value = selDate;
if (notesInput) notesInput.value = '';
if (deleteBtn) {
deleteBtn.classList.add('hidden');
deleteBtn.classList.remove('flex');
}
if (modalTitle) modalTitle.textContent = 'Dagen Blokkeren / Vakantie';
}
loadCalendarBlocksList();
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
}
function closeCalendarBlockModal() {
const modal = document.getElementById('calendarBlockModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
}
async function loadCalendarBlocksList() {
const container = document.getElementById('calendarBlocksListContainer');
if (!container) return;
try {
const res = await fetch('/api/planner.php?action=get_calendar_blocks');
const data = await res.json();
if (!data.success || !Array.isArray(data.blocks) || data.blocks.length === 0) {
container.innerHTML = '<p class="text-[11px] text-slate-400 text-center py-2 italic">Geen actieve of geplande blokkades gevonden.</p>';
return;
}
const icons = {
'VACATION': '🏖️',
'LEAVE': '✈️',
'SICK': '🤒',
'FOCUS': '🎯'
};
container.innerHTML = data.blocks.map(b => {
const icon = icons[b.block_type] || '📅';
const safeTitle = escapeHtml(b.title);
const dateStr = b.start_date === b.end_date
? b.start_date
: `${b.start_date} t/m ${b.end_date}`;
const blockJson = JSON.stringify(b).replace(/"/g, '&quot;');
return `
<div class="flex items-center justify-between p-2 rounded-xl bg-slate-50 dark:bg-slate-800/70 border border-slate-200/70 dark:border-slate-700/60 text-xs">
<div class="flex items-center space-x-2 min-w-0 flex-1 cursor-pointer" onclick="openCalendarBlockModal(${blockJson})">
<span class="text-sm flex-shrink-0">${icon}</span>
<div class="min-w-0 flex-1 truncate">
<div class="font-bold text-slate-800 dark:text-slate-200 truncate">${safeTitle}</div>
<div class="text-[10px] text-slate-500 dark:text-slate-400">${dateStr}</div>
</div>
</div>
<div class="flex items-center space-x-1 flex-shrink-0 ml-2">
<button type="button" onclick="openCalendarBlockModal(${blockJson})" class="p-1 text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer" title="Bewerken">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
</button>
<button type="button" onclick="deleteCalendarBlock(${b.id})" class="p-1 text-slate-400 hover:text-rose-600 dark:hover:text-rose-400 transition cursor-pointer" title="Opheffen">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
</div>
`;
}).join('');
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
} catch (e) {
container.innerHTML = '<p class="text-[11px] text-rose-500 text-center py-2">Kon blokkades niet laden.</p>';
}
}
async function submitCalendarBlock(event) {
event.preventDefault();
const btn = document.getElementById('btnSaveCalBlock');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerText = 'Opslaan...';
btn.disabled = true;
}
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
const blockType = document.getElementById('calBlockType')?.value || 'VACATION';
const title = document.getElementById('calBlockTitle')?.value.trim();
const startDate = document.getElementById('calBlockStartDate')?.value;
const endDate = document.getElementById('calBlockEndDate')?.value;
const notes = document.getElementById('calBlockNotes')?.value.trim();
const syncOutlook = document.getElementById('calBlockSyncOutlook')?.checked || false;
if (!title) {
alert('Vul een titel in voor de blokkade.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
return;
}
try {
const res = await fetch('/api/planner.php?action=save_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: id,
title: title,
block_type: blockType,
start_date: startDate,
end_date: endDate,
notes: notes,
sync_to_outlook: syncOutlook
})
});
const data = await res.json();
if (data.success) {
showWidgetToast(id > 0 ? 'Blokkade succesvol bijgewerkt' : 'Dagen succesvol geblokkeerd', 'success');
setTimeout(() => {
window.location.reload();
}, 600);
} else {
alert(data.error || 'Er is een fout opgetreden bij het opslaan.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
}
} catch (err) {
alert('Netwerkfout bij opslaan van blokkade.');
if (btn) { btn.innerHTML = origHtml; btn.disabled = false; }
}
}
async function deleteCalendarBlock(blockId) {
if (!blockId) return;
if (!confirm('Weet je zeker dat je deze agenda-blokkade wilt opheffen?')) return;
try {
const res = await fetch('/api/planner.php?action=delete_calendar_block', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: blockId })
});
const data = await res.json();
if (data.success) {
showWidgetToast('Blokkade opgeheven', 'success');
setTimeout(() => {
window.location.reload();
}, 600);
} else {
alert(data.error || 'Kon blokkade niet opheffen.');
}
} catch (err) {
alert('Netwerkfout bij opheffen van blokkade.');
}
}
function deleteCurrentCalendarBlock() {
const id = parseInt(document.getElementById('calBlockId')?.value, 10) || 0;
if (id > 0) {
deleteCalendarBlock(id);
}
}
// --- Bepaal de actuele widget layout en exacte volgorde uit het scherm (DOM) ---
function getActiveWidgetLayoutPayload() {
const grid = document.getElementById('focizWidgetGrid');