feat(planner): add functionality to find and plan tasks in the next available time slot, including date selection and UI updates
This commit is contained in:
+209
-11
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user