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:
2026-09-11 16:35:43 +02:00
parent d15303ff6c
commit 6ebaf027bf
2 changed files with 348 additions and 18 deletions
+139 -7
View File
@@ -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">
<i data-lucide="edit-3" class="w-3.5 h-3.5"></i>
</button>
@@ -1178,6 +1179,19 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full <?= $t['priority'] === 'P1' ? 'bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 border border-rose-200 dark:border-rose-500/20' : ($t['priority'] === 'P2' ? 'bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700') ?>">
<?= $t['priority'] ?>
</span>
<button onclick="openEditTaskModal(<?= htmlspecialchars(json_encode([
'id' => (int)$t['id'],
'title' => $t['title'],
'priority' => $t['priority'],
'start' => $t['time_block_start'] ?? '',
'end' => $t['time_block_end'] ?? '',
'duration' => (int)($t['duration_minutes'] ?: 30),
'source' => $t['source'],
'external_id' => $t['external_id'] ?? '',
'date' => $selectedDate
]), ENT_QUOTES, 'UTF-8') ?>)" title="Taak & Tijdblok bewerken" class="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
<i data-lucide="edit-3" class="w-3 h-3"></i>
</button>
</div>
</div>
</div>
@@ -2005,9 +2019,38 @@ if ($isAjaxRequest) {
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<!-- Datum & Tijdblok met slimme "Plan in open blok" actieknop -->
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800/80">
<div class="flex items-center justify-between">
<span class="text-xs font-semibold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
<button type="button" onclick="planInNextOpenBlock()" id="btnPlanOpenBlock" class="inline-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-bold transition shadow-xs cursor-pointer active:scale-95" title="Vind automatisch het eerstvolgende vrije kwartierblok binnen werktijden">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
<span>Plan in open blok</span>
</button>
</div>
<!-- Succes / statusmelding wanneer een open blok gevonden is -->
<div id="editTaskOpenBlockNotice" class="hidden p-2.5 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-800 dark:text-emerald-300 text-[11px] flex items-center justify-between">
<div class="flex items-center space-x-1.5 min-w-0 flex-1 mr-2">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 flex-shrink-0"></i>
<span id="editTaskOpenBlockNoticeText" class="truncate font-semibold"></span>
</div>
<button type="button" onclick="document.getElementById('editTaskOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd</label>
<label for="editTaskDate" class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
<input type="date" id="editTaskDate" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-2 text-slate-900 dark:text-white text-xs focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Starttijd</label>
<button type="button" onclick="clearEditTaskTimes()" title="Tijd wissen" class="text-[10px] text-slate-400 hover:text-rose-500 cursor-pointer">Wissen</button>
</div>
<div class="relative">
<input type="text" id="editTaskStartTime" placeholder="Bijv. 09:30" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('start')">
<i data-lucide="clock" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
@@ -2016,10 +2059,10 @@ if ($isAjaxRequest) {
<div>
<div class="flex items-center justify-between mb-1">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Eindtijd</label>
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Vrij invulbaar</span>
<span class="text-[9px] text-indigo-600 dark:text-indigo-400 font-medium">Auto/Vrij</span>
</div>
<div class="relative">
<input type="text" id="editTaskEndTime" placeholder="Bijv. 09:40" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('end')">
<input type="text" id="editTaskEndTime" placeholder="Bijv. 10:00" class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-slate-900 dark:text-white text-xs font-mono focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs" oninput="onEditTaskTimeOrDurationChange('end')">
<i data-lucide="clock-arrow-down" class="w-4 h-4 text-slate-400 absolute right-2.5 top-2.5 pointer-events-none"></i>
</div>
</div>
@@ -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 = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin text-white"></i><span>Zoeken...</span>';
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 <strong>${escapeHtml(title)}</strong> succesvol bijgewerkt`, 'success');
}
} else {
alert(data.error || 'Kon taak niet bijwerken');
if (saveBtn) {