diff --git a/admin.php b/admin.php index 8d4145e..a9fc39e 100644 --- a/admin.php +++ b/admin.php @@ -1096,8 +1096,9 @@ require_once __DIR__ . '/includes/header.php'; Zero-Data Leakage - - + + + diff --git a/api/live_sync.php b/api/live_sync.php index 4afef42..45c1fe6 100644 --- a/api/live_sync.php +++ b/api/live_sync.php @@ -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; diff --git a/api/planner.php b/api/planner.php index 320a023..05c1afa 100644 --- a/api/planner.php +++ b/api/planner.php @@ -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); diff --git a/config/database.php b/config/database.php index ac3bed4..85208ad 100644 --- a/config/database.php +++ b/config/database.php @@ -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 { diff --git a/config/schema.sql b/config/schema.sql index a164fff..84881db 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -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; + diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php index 656fc03..d0a737a 100644 --- a/includes/integrations/microsoft.php +++ b/includes/integrations/microsoft.php @@ -485,8 +485,24 @@ function microsoft_create_event(int $userId, array $eventData): array { } } - $startDateTimeStr = "{$date}T{$startTime}:00"; - $endDateTimeStr = "{$date}T{$endTime}:00"; + $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' => '

Beste Niek,

We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers.

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.

Met vriendelijke groet,
Directie UWV Koppelingen & Integraties

', + 'body_preview' => 'Beste collega, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...', + 'body_html' => '

Beste collega,

We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers.

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.

Met vriendelijke groet,
Directie UWV Koppelingen & Integraties

', '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' => '

Hallo team,

Bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de werkweek accorderen in het systeem.

Laat het weten indien er correcties nodig zijn op de dagregistraties.

Hartelijke groet,
Financiële Administratie Fociz

', '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' => '

Goedemiddag,

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.

Alle endpoint logs zijn beschikbaar via het beheerpaneel.

Groeten,
Team DevOps & Infrastructuur

', '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' => '

Welkom bij Fociz!

Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw persoonlijke dagplanning?

Hierdoor houd je maximale grip op je werkdag en behoud je de ultieme focus.

Veel productiviteit gewenst,
Het Fociz Team

', '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', diff --git a/planner.php b/planner.php index 7d84583..8098fee 100644 --- a/planner.php +++ b/planner.php @@ -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++) { Weekend / Vrij
+ + + + + +

Fociz @@ -467,6 +503,11 @@ for ($h = $startHour; $h <= $endHour; $h++) {
+ +
- -
+ +
+ + + +
+
+ +
-
- +
+

Tijdlijn & Time-blocking voor

- + + + (Geblokkeerd) + + Weekend / Vrije Dag

- +

@@ -2316,6 +2410,123 @@ if ($isAjaxRequest) {
+ + +