diff --git a/api/planner.php b/api/planner.php index d5abaac..cc8ddd3 100644 --- a/api/planner.php +++ b/api/planner.php @@ -4,6 +4,7 @@ */ require_once __DIR__ . '/../includes/auth.php'; +require_once __DIR__ . '/../includes/settings.php'; require_once __DIR__ . '/../includes/integrations/microsoft.php'; $user = require_auth(true); $db = get_db_connection(); @@ -237,13 +238,28 @@ try { } catch (Throwable $e) {} } + $targetDate = !empty($data['date']) ? trim($data['date']) : null; + $planDate = null; + $newPlanId = null; + + if ($targetDate && preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) { + $stmtPlan = $db->prepare(" + INSERT INTO daily_plans (user_id, plan_date) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id) + "); + $stmtPlan->execute([$user['id'], $targetDate]); + $newPlanId = (int)$db->lastInsertId(); + $planDate = $targetDate; + } else { + $stmtPlan = $db->prepare("SELECT plan_date FROM daily_plans WHERE id = ?"); + $stmtPlan->execute([$existingTask['daily_plan_id']]); + $planDate = $stmtPlan->fetchColumn() ?: date('Y-m-d'); + } + // Indien gekoppeld aan Outlook, werk afspraak bij via Graph API if (!empty($existingTask['external_id']) && $existingTask['source'] === 'OUTLOOK') { try { - $stmtPlan = $db->prepare("SELECT plan_date FROM daily_plans WHERE id = ?"); - $stmtPlan->execute([$existingTask['daily_plan_id']]); - $planDate = $stmtPlan->fetchColumn() ?: date('Y-m-d'); - microsoft_update_event($user['id'], $existingTask['external_id'], [ 'subject' => $title, 'date' => $planDate, @@ -255,18 +271,28 @@ try { } } - $stmtUpdate = $db->prepare(" - UPDATE tasks - SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? - WHERE id = ? AND user_id = ? - "); - $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]); + if ($newPlanId) { + $stmtUpdate = $db->prepare(" + UPDATE tasks + SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ?, daily_plan_id = ? + WHERE id = ? AND user_id = ? + "); + $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $newPlanId, $taskId, $user['id']]); + } else { + $stmtUpdate = $db->prepare(" + UPDATE tasks + SET title = ?, priority = ?, time_block_start = ?, time_block_end = ?, duration_minutes = ? + WHERE id = ? AND user_id = ? + "); + $stmtUpdate->execute([$title, $priority, $timeBlockStart, $timeBlockEnd, $durationMinutes, $taskId, $user['id']]); + } echo json_encode([ 'success' => true, 'time_block_start' => $timeBlockStart, 'time_block_end' => $timeBlockEnd, - 'duration_minutes' => $durationMinutes + 'duration_minutes' => $durationMinutes, + 'date' => $planDate ]); exit; } @@ -619,6 +645,178 @@ try { exit; } + // 11. Zoek eerstvolgende vrije open blok binnen werktijden (Release v2.1) + if ($action === 'find_next_free_slot') { + $rawInput = json_decode(file_get_contents('php://input'), true); + $data = is_array($rawInput) ? $rawInput : $_GET; + $duration = max(5, (int)($data['duration_minutes'] ?? 30)); + $excludeTaskId = (int)($data['task_id'] ?? 0); + + $workdaySettings = get_workday_settings(); + $workdayStartStr = $workdaySettings['workday_start'] ?? '08:30'; + $workdayEndStr = $workdaySettings['workday_end'] ?? '17:30'; + $lunchStartStr = $workdaySettings['lunch_start'] ?? '12:30'; + $lunchEndStr = $workdaySettings['lunch_end'] ?? '13:00'; + + $toMins = function(string $timeStr): int { + $parts = explode(':', trim($timeStr)); + return ((int)($parts[0] ?? 0) * 60) + (int)($parts[1] ?? 0); + }; + + $toTimeStr = function(int $mins): string { + $h = (int)floor($mins / 60); + $m = $mins % 60; + return sprintf('%02d:%02d', $h, $m); + }; + + $workdayStartMin = $toMins($workdayStartStr); + $workdayEndMin = $toMins($workdayEndStr); + $lunchStartMin = $toMins($lunchStartStr); + $lunchEndMin = $toMins($lunchEndStr); + + $tz = new DateTimeZone('Europe/Amsterdam'); + $now = new DateTime('now', $tz); + + // Huidige tijd afronden naar hele kwartieren + $nowH = (int)$now->format('G'); + $nowM = (int)$now->format('i'); + if ($nowM % 15 !== 0) { + $nowM = (int)(ceil($nowM / 15) * 15); + if ($nowM >= 60) { + $nowH += (int)floor($nowM / 60); + $nowM = $nowM % 60; + } + } + $currentQuarterMin = $nowH * 60 + $nowM; + + $dutchDays = [1 => 'Maandag', 2 => 'Dinsdag', 3 => 'Woensdag', 4 => 'Donderdag', 5 => 'Vrijdag', 6 => 'Zaterdag', 7 => 'Zondag']; + $dutchMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec']; + + $checkDate = clone $now; + $foundSlot = null; + $maxDays = 14; + $checkedDays = 0; + + while ($checkedDays < $maxDays) { + $dayOfWeek = (int)$checkDate->format('N'); // 1 = Ma, 7 = Zo + + // Weekend overslaan + if ($dayOfWeek === 6 || $dayOfWeek === 7) { + $checkDate->modify('+1 day'); + continue; + } + + $dateStr = $checkDate->format('Y-m-d'); + $isToday = ($dateStr === $now->format('Y-m-d')); + + // Bepaal vroegste startminuut voor deze dag (op hele kwartieren) + if ($isToday) { + $earliestMin = max($workdayStartMin, $currentQuarterMin); + if ($earliestMin % 15 !== 0) { + $earliestMin = (int)(ceil($earliestMin / 15) * 15); + } + } else { + $earliestMin = $workdayStartMin; + if ($earliestMin % 15 !== 0) { + $earliestMin = (int)(ceil($earliestMin / 15) * 15); + } + } + + // Alleen zoeken als er binnen de werkdag nog voldoende tijd is voor duration + if ($earliestMin + $duration <= $workdayEndMin) { + $busyIntervals = []; + + // Lunchpauze + if ($lunchEndMin > $lunchStartMin) { + $busyIntervals[] = [$lunchStartMin, $lunchEndMin]; + } + + // Bestaande taken van de gebruiker op deze dag + $stmtTasks = $db->prepare(" + SELECT t.id, t.time_block_start, t.time_block_end, t.duration_minutes + FROM tasks t + INNER JOIN daily_plans dp ON t.daily_plan_id = dp.id + WHERE t.user_id = ? + AND dp.plan_date = ? + AND t.time_block_start IS NOT NULL + AND t.time_block_start != '' + AND t.status != 'CANCELLED' + "); + $stmtTasks->execute([$user['id'], $dateStr]); + while ($t = $stmtTasks->fetch()) { + if ($excludeTaskId > 0 && (int)$t['id'] === $excludeTaskId) { + continue; + } + $tStart = $toMins($t['time_block_start']); + $tDur = (int)($t['duration_minutes'] ?: 30); + $tEnd = !empty($t['time_block_end']) ? $toMins($t['time_block_end']) : ($tStart + $tDur); + if ($tEnd > $tStart) { + $busyIntervals[] = [$tStart, $tEnd]; + } + } + + // Outlook afspraken + if (function_exists('microsoft_fetch_events_range')) { + try { + $outlookEvts = microsoft_fetch_events_range($user['id'], $dateStr, $dateStr, false); + foreach ($outlookEvts as $oe) { + if (!empty($oe['is_all_day'])) continue; + if (empty($oe['start']) || empty($oe['end'])) continue; + $oStart = $toMins($oe['start']); + $oEnd = $toMins($oe['end']); + if ($oEnd > $oStart) { + $busyIntervals[] = [$oStart, $oEnd]; + } + } + } catch (Throwable $e) {} + } + + // Doorzoek kwartierslots + for ($slotStart = $earliestMin; ($slotStart + $duration) <= $workdayEndMin; $slotStart += 15) { + $slotEnd = $slotStart + $duration; + $hasCollision = false; + + foreach ($busyIntervals as [$bStart, $bEnd]) { + if ($slotStart < $bEnd && $slotEnd > $bStart) { + $hasCollision = true; + break; + } + } + + if (!$hasCollision) { + $foundSlot = [ + 'date' => $dateStr, + 'start' => $toTimeStr($slotStart), + 'end' => $toTimeStr($slotEnd), + 'duration_minutes' => $duration, + 'is_today' => $isToday, + 'formatted_date' => $isToday + ? 'Vandaag (' . date('j', strtotime($dateStr)) . ' ' . $dutchMonths[(int)date('n', strtotime($dateStr))] . ')' + : $dutchDays[(int)$checkDate->format('N')] . ' ' . date('j', strtotime($dateStr)) . ' ' . $dutchMonths[(int)date('n', strtotime($dateStr))], + ]; + break 2; + } + } + } + + $checkDate->modify('+1 day'); + $checkedDays++; + } + + if ($foundSlot) { + echo json_encode([ + 'success' => true, + 'slot' => $foundSlot, + ]); + } else { + echo json_encode([ + 'success' => false, + 'error' => "Geen vrij blok van {$duration} minuten gevonden binnen de gezette werktijden ({$workdayStartStr} - {$workdayEndStr}) voor de komende {$maxDays} werkdagen.", + ]); + } + exit; + } + echo json_encode(['error' => 'Onbekende actie']); } catch (Exception $e) { http_response_code(500); diff --git a/planner.php b/planner.php index 26a995c..88c7e7e 100644 --- a/planner.php +++ b/planner.php @@ -860,7 +860,8 @@ for ($h = $startHour; $h <= $endHour; $h++) { 'end' => $task['time_block_end'] ?? '', 'duration' => (int)($task['duration_minutes'] ?: 30), 'source' => $task['source'], - 'external_id' => $task['external_id'] ?? '' + 'external_id' => $task['external_id'] ?? '', + 'date' => $selectedDate ]), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijd aanpassen" class="p-1.5 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-500/10 opacity-0 group-hover:opacity-100 transition cursor-pointer"> @@ -1178,6 +1179,19 @@ for ($h = $startHour; $h <= $endHour; $h++) { + @@ -2005,9 +2019,38 @@ if ($isAjaxRequest) { -
+ +
+
+ Datum & Tijdblok + +
+ + + +
+ +
- + + +
+
+
+ + +
@@ -2016,10 +2059,10 @@ if ($isAjaxRequest) {
- Vrij invulbaar + Auto/Vrij
- +
@@ -3961,6 +4004,16 @@ function openEditTaskModal(task) { document.getElementById('editTaskStartTime').value = task.start || ''; document.getElementById('editTaskEndTime').value = task.end || ''; + // Datum + const dateInput = document.getElementById('editTaskDate'); + if (dateInput) { + dateInput.value = task.date || (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10)); + } + + // Reset statusbericht van open blok + const notice = document.getElementById('editTaskOpenBlockNotice'); + if (notice) notice.classList.add('hidden'); + const dur = task.duration || 30; setSelectedEditTaskDuration(dur); @@ -3990,6 +4043,76 @@ function closeEditTaskModal() { } } +function clearEditTaskTimes() { + const s = document.getElementById('editTaskStartTime'); + const e = document.getElementById('editTaskEndTime'); + if (s) s.value = ''; + if (e) e.value = ''; + const notice = document.getElementById('editTaskOpenBlockNotice'); + if (notice) notice.classList.add('hidden'); +} + +async function planInNextOpenBlock() { + const duration = getSelectedEditTaskDurationMinutes(); + const taskId = parseInt(document.getElementById('editTaskId')?.value, 10) || 0; + const btn = document.getElementById('btnPlanOpenBlock'); + const notice = document.getElementById('editTaskOpenBlockNotice'); + const noticeText = document.getElementById('editTaskOpenBlockNoticeText'); + + let origBtnHtml = ''; + if (btn) { + origBtnHtml = btn.innerHTML; + btn.innerHTML = 'Zoeken...'; + btn.disabled = true; + } + + try { + const res = await fetch('/api/planner.php?action=find_next_free_slot', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + duration_minutes: duration, + task_id: taskId + }) + }); + const data = await res.json(); + + if (data.success && data.slot) { + const slot = data.slot; + + // Stel datum en tijden in + const dateInput = document.getElementById('editTaskDate'); + if (dateInput) { + dateInput.value = slot.date; + } + const startInput = document.getElementById('editTaskStartTime'); + if (startInput) { + startInput.value = slot.start; + } + const endInput = document.getElementById('editTaskEndTime'); + if (endInput) { + endInput.value = slot.end; + } + + // Toon statusmelding in dialoog + if (notice && noticeText) { + noticeText.textContent = `Open blok gevonden: ${slot.formatted_date} van ${slot.start} tot ${slot.end} (${slot.duration_minutes}m)`; + notice.classList.remove('hidden'); + } + } else { + alert(data.error || 'Geen vrij blok gevonden binnen de gezette werktijden.'); + } + } catch (err) { + alert('Netwerkfout bij zoeken naar een open blok.'); + } finally { + if (btn) { + btn.innerHTML = origBtnHtml; + btn.disabled = false; + } + if (typeof lucide !== 'undefined') lucide.createIcons(); + } +} + function onEditTaskTimeOrDurationChange(source) { const startInput = document.getElementById('editTaskStartTime'); const endInput = document.getElementById('editTaskEndTime'); @@ -4093,6 +4216,8 @@ async function submitEditTask() { const taskId = parseInt(document.getElementById('editTaskId').value, 10); const title = document.getElementById('editTaskTitle').value.trim(); const priority = document.getElementById('editTaskPriority').value; + const dateInput = document.getElementById('editTaskDate'); + const taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10)); const startTime = document.getElementById('editTaskStartTime').value.trim(); const endTime = document.getElementById('editTaskEndTime').value.trim(); const duration = getSelectedEditTaskDurationMinutes(); @@ -4118,13 +4243,20 @@ async function submitEditTask() { priority: priority, time_block_start: startTime, time_block_end: endTime, - duration_minutes: duration + duration_minutes: duration, + date: taskDate }) }); const data = await res.json(); if (data.success) { closeEditTaskModal(); - switchPlannerDate(activePlannerDate, null, false); + const targetDate = data.date || taskDate; + if (typeof switchPlannerDate === 'function') { + switchPlannerDate(targetDate, null, false); + } + if (typeof showWidgetToast === 'function') { + showWidgetToast(`Taak ${escapeHtml(title)} succesvol bijgewerkt`, 'success'); + } } else { alert(data.error || 'Kon taak niet bijwerken'); if (saveBtn) {