Planner changes

This commit is contained in:
2026-09-11 17:30:18 +02:00
parent 6ebaf027bf
commit 7fe7c0f51a
5 changed files with 331 additions and 31 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
elseif ($action === 'save_workday_settings') {
$workdayStart = trim($_POST['workday_start'] ?? '08:30');
$workdayEnd = trim($_POST['workday_end'] ?? '17:30');
$workdayEnd = trim($_POST['workday_end'] ?? '17:00');
$lunchStart = trim($_POST['lunch_start'] ?? '12:30');
$lunchEnd = trim($_POST['lunch_end'] ?? '13:00');
$pomoFocus = max(10, min(120, (int)($_POST['pomodoro_focus'] ?? 25)));
+13 -2
View File
@@ -651,10 +651,11 @@ try {
$data = is_array($rawInput) ? $rawInput : $_GET;
$duration = max(5, (int)($data['duration_minutes'] ?? 30));
$excludeTaskId = (int)($data['task_id'] ?? 0);
$startFromNextWorkday = !empty($data['start_from_next_workday']);
$workdaySettings = get_workday_settings();
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:00';
$lunchStartStr = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEndStr = $workdaySettings['lunch_end'] ?? '13:00';
@@ -693,6 +694,12 @@ try {
$dutchMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
$checkDate = clone $now;
if ($startFromNextWorkday) {
$checkDate->modify('+1 day');
while ((int)$checkDate->format('N') >= 6) {
$checkDate->modify('+1 day');
}
}
$foundSlot = null;
$maxDays = 14;
$checkedDays = 0;
@@ -707,7 +714,7 @@ try {
}
$dateStr = $checkDate->format('Y-m-d');
$isToday = ($dateStr === $now->format('Y-m-d'));
$isToday = ($dateStr === $now->format('Y-m-d')) && !$startFromNextWorkday;
// Bepaal vroegste startminuut voor deze dag (op hele kwartieren)
if ($isToday) {
@@ -807,10 +814,14 @@ try {
echo json_encode([
'success' => true,
'slot' => $foundSlot,
'workday_start' => $workdayStartStr,
'workday_end' => $workdayEndStr,
]);
} else {
echo json_encode([
'success' => false,
'workday_start' => $workdayStartStr,
'workday_end' => $workdayEndStr,
'error' => "Geen vrij blok van {$duration} minuten gevonden binnen de gezette werktijden ({$workdayStartStr} - {$workdayEndStr}) voor de komende {$maxDays} werkdagen.",
]);
}
+1 -1
View File
@@ -168,7 +168,7 @@ function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = '
*/
function fociz_ai_smart_schedule(array $tasks, array $events, array $workdaySettings): array {
$startH = (int)explode(':', $workdaySettings['workday_start'] ?? '08:30')[0];
$endH = (int)explode(':', $workdaySettings['workday_end'] ?? '17:30')[0];
$endH = (int)explode(':', $workdaySettings['workday_end'] ?? '17:00')[0];
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
+1 -1
View File
@@ -95,7 +95,7 @@ function set_system_setting(string $key, $value): bool {
function get_workday_settings(): array {
return [
'workday_start' => get_system_setting('workday_start', '08:30'),
'workday_end' => get_system_setting('workday_end', '17:30'),
'workday_end' => get_system_setting('workday_end', '17:00'),
'lunch_start' => get_system_setting('lunch_start', '12:30'),
'lunch_end' => get_system_setting('lunch_end', '13:00'),
'pomodoro_focus' => (int)get_system_setting('pomodoro_focus', 25),
+305 -16
View File
@@ -378,7 +378,7 @@ $initialSyncHash = md5(json_encode([
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:00';
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
$lunchHour = (int)explode(':', $lunchStart)[0];
@@ -2084,6 +2084,52 @@ if ($isAjaxRequest) {
</div>
</div>
<!-- Modal: Waarschuwing Buiten Werktijden -->
<div id="outsideWorkhoursModal" 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-md shadow-2xl space-y-4 border border-amber-500/30">
<div class="flex items-start space-x-3">
<div class="w-10 h-10 rounded-2xl bg-amber-500/10 border border-amber-500/20 text-amber-500 flex items-center justify-center flex-shrink-0">
<i data-lucide="alert-triangle" class="w-5 h-5"></i>
</div>
<div class="min-w-0 flex-1">
<h3 class="text-base font-bold text-slate-900 dark:text-white leading-tight">
Let op! Je plant buiten werktijden
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Het ingeplande tijdblok valt buiten de gezette werktijden.
</p>
</div>
<button type="button" onclick="closeOutsideWorkhoursModal('CANCEL')" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg transition cursor-pointer flex-shrink-0">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800/40 rounded-xl p-3.5 text-xs text-amber-800 dark:text-amber-300 space-y-1.5">
<div class="flex items-center justify-between text-[11px] font-semibold text-amber-900 dark:text-amber-200">
<span>Gekozen tijdblok:</span>
<span id="outsideWorkhoursSelectedSlot" class="font-mono font-bold"></span>
</div>
<div class="flex items-center justify-between text-[11px] text-amber-700 dark:text-amber-400">
<span>Gezette werktijden:</span>
<span id="outsideWorkhoursBounds" class="font-mono font-semibold"></span>
</div>
<p class="text-[11px] text-amber-700/80 dark:text-amber-400/80 pt-1 border-t border-amber-200/60 dark:border-amber-800/30">
Kies <strong>Nee</strong> om het item automatisch in te plannen in het eerstvolgende open blok op de volgende werkdag, of kies <strong>Ja</strong> om toch nu buiten werktijd te plannen.
</p>
</div>
<div class="flex items-center space-x-2.5 pt-2">
<button type="button" id="btnOutsideWorkhoursNo" onclick="closeOutsideWorkhoursModal('NEE')" class="flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition shadow-xs flex items-center justify-center space-x-1.5 cursor-pointer">
<i data-lucide="calendar-arrow-up" class="w-4 h-4"></i>
<span>Nee (Volgende werkdag)</span>
</button>
<button type="button" id="btnOutsideWorkhoursYes" onclick="closeOutsideWorkhoursModal('JA')" class="px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold text-xs transition cursor-pointer">
<span>Ja, toch inplannen</span>
</button>
</div>
</div>
</div>
<!-- Modal: Direct E-mail Inplannen als Dagtaak -->
<div id="planEmailDirectModal" 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">
@@ -3412,16 +3458,27 @@ function setJiraPlanDuration(minutes) {
}
function suggestNextFreeSlot() {
// Zoek het volgende ronde kwartier na het huidige moment
const now = new Date();
let h = now.getHours();
let m = now.getMinutes();
// Rond op naar boven naar het volgende kwartier
m = Math.ceil((m + 1) / 15) * 15;
if (m >= 60) { h += 1; m = 0; }
// Begrenzen op werktijd 08:00-18:00
if (h < 8) { h = 8; m = 0; }
if (h >= 18) { h = 8; m = 0; } // Morgen → val terug op 08:00
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
const [startH, startM] = wStart.split(':').map(Number);
const [endH, endM] = wEnd.split(':').map(Number);
const curMin = h * 60 + m;
const startMin = startH * 60 + startM;
const endMin = endH * 60 + endM;
if (curMin < startMin) {
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
}
if (curMin >= endMin) {
return `${String(startH).padStart(2,'0')}:${String(startM).padStart(2,'0')}`;
}
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
}
@@ -3434,16 +3491,44 @@ async function submitJiraPlanModal() {
const finalTitle = customTitle || `Oppakken Jira item ${issue.key}`;
const dateInput = document.getElementById('jiraPlanDate');
const planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
let planDate = (dateInput && dateInput.value) ? dateInput.value : ((typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10));
const duration = parseInt(document.getElementById('jiraPlanDuration').value, 10) || 15;
const startTime = document.getElementById('jiraPlanStartTime').value.trim();
let startTime = document.getElementById('jiraPlanStartTime').value.trim();
const priority = document.getElementById('jiraPlanPriority').value;
// Controleer of de ingeplande tijd buiten de gezette werktijden valt
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
startTime = nextSlot.start;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
}
}
const btn = document.getElementById('btnSubmitJiraPlan');
const origHtml = btn.innerHTML;
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
btn.disabled = true;
}
try {
const payload = {
@@ -3478,15 +3563,19 @@ async function submitJiraPlanModal() {
}
} else {
alert(data.error || 'Kon ticket niet inplannen');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
}
}
} catch(e) {
alert('Netwerkfout bij inplannen');
if (btn) {
btn.innerHTML = origHtml;
btn.disabled = false;
}
}
}
async function quickImportJiraByKey(btn, issueKey) {
// Plan knop → open de plan modal (niet direct importeren)
@@ -3530,6 +3619,31 @@ async function submitPlanEmailDirect() {
return;
}
let planDate = activePlannerDate;
if (startTime) {
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
const endTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
const check = checkWorkdayBounds(planDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
startTime = nextSlot.start;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
}
}
const btn = document.getElementById('btnSubmitPlanEmailDirect');
const origHtml = btn.innerHTML;
btn.innerText = 'Inplannen...';
@@ -3546,13 +3660,13 @@ async function submitPlanEmailDirect() {
duration_minutes: duration,
time_block_start: startTime,
sync_outlook: syncOutlook,
date: activePlannerDate
date: planDate
})
});
const data = await res.json();
if (data.success) {
closePlanEmailDirectModal();
switchPlannerDate(activePlannerDate, null, false);
switchPlannerDate(planDate, null, false);
} else {
alert(data.error || 'Kon e-mail niet inplannen');
}
@@ -3669,6 +3783,135 @@ let weekStartDate = '<?= $weekStart ?>';
let weekEndDate = '<?= $weekEnd ?>';
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
let isDateSwitching = false;
const FOCIZ_WORKDAY_START = '<?= $workdayStartStr ?>';
const FOCIZ_WORKDAY_END = '<?= $workdayEndStr ?>';
/**
* Controleert of een gepland tijdstip buiten de gezette werktijden of in het weekend valt
*/
function checkWorkdayBounds(planDate, startTime, endTime, duration) {
if (!startTime || !startTime.trim()) {
return { isOutside: false };
}
const start = startTime.trim();
let [sH, sM] = start.split(':').map(Number);
if (isNaN(sH) || isNaN(sM)) return { isOutside: false };
let dur = parseInt(duration, 10);
if (isNaN(dur) || dur <= 0) dur = 30;
let end = (endTime && endTime.trim()) ? endTime.trim() : '';
let eH, eM;
if (end) {
[eH, eM] = end.split(':').map(Number);
}
if (isNaN(eH) || isNaN(eM)) {
const totalEndMin = sH * 60 + sM + dur;
eH = Math.floor(totalEndMin / 60);
eM = totalEndMin % 60;
end = `${String(eH).padStart(2,'0')}:${String(eM).padStart(2,'0')}`;
}
const startMin = sH * 60 + sM;
const endMin = eH * 60 + eM;
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
const wEnd = (typeof FOCIZ_WORKDAY_END !== 'undefined' && FOCIZ_WORKDAY_END) ? FOCIZ_WORKDAY_END : '17:00';
const [wsH, wsM] = wStart.split(':').map(Number);
const [weH, weM] = wEnd.split(':').map(Number);
const wStartMin = wsH * 60 + wsM;
const wEndMin = weH * 60 + weM;
let isOutside = false;
let reason = '';
if (planDate) {
const d = new Date(planDate + 'T12:00:00');
const dayOfWeek = d.getDay(); // 0 = Zo, 6 = Za
if (dayOfWeek === 0 || dayOfWeek === 6) {
isOutside = true;
reason = 'Het gekozen tijdstip valt in het weekend.';
}
}
if (startMin < wStartMin || endMin > wEndMin || startMin >= wEndMin) {
isOutside = true;
reason = `Het gekozen tijdstip (${start} - ${end}) valt buiten de gezette werktijden (${wStart} - ${wEnd}).`;
}
return {
isOutside,
reason,
start,
end,
duration: dur,
workdayStart: wStart,
workdayEnd: wEnd,
date: planDate
};
}
let _outsideWorkhoursResolver = null;
function promptOutsideWorkhoursModal(checkResult) {
return new Promise((resolve) => {
_outsideWorkhoursResolver = resolve;
const modal = document.getElementById('outsideWorkhoursModal');
const selectedEl = document.getElementById('outsideWorkhoursSelectedSlot');
const boundsEl = document.getElementById('outsideWorkhoursBounds');
if (selectedEl) {
selectedEl.textContent = `${checkResult.start} - ${checkResult.end} (${checkResult.duration}m)`;
}
if (boundsEl) {
boundsEl.textContent = `${checkResult.workdayStart} tot ${checkResult.workdayEnd}`;
}
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
if (typeof lucide !== 'undefined') lucide.createIcons();
} else {
const ok = confirm(`Let op! Je plant buiten werktijden (${checkResult.workdayStart} - ${checkResult.workdayEnd}).\n\nKlik OK (Ja) om toch zo in te plannen, of Annuleren (Nee) om automatisch naar de volgende werkdag te gaan.`);
resolve(ok ? 'JA' : 'NEE');
}
});
}
function closeOutsideWorkhoursModal(choice) {
const modal = document.getElementById('outsideWorkhoursModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (typeof _outsideWorkhoursResolver === 'function') {
const res = _outsideWorkhoursResolver;
_outsideWorkhoursResolver = null;
res(choice);
}
}
/**
* Zoekt automatisch het eerstvolgende open kwartierblok op de volgende werkdag
*/
async function findSlotOnNextWorkday(duration, taskId = 0) {
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,
start_from_next_workday: true
})
});
const data = await res.json();
if (data.success && data.slot) {
return data.slot;
}
throw new Error(data.error || 'Geen vrij open blok gevonden op de volgende werkdag.');
}
/**
* Wisselt de actieve plannerdatum geruisloos via AJAX
@@ -3964,6 +4207,28 @@ async function submitNewTask() {
return;
}
let planDate = activePlannerDate;
if (time) {
const check = checkWorkdayBounds(planDate, time, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
try {
const nextSlot = await findSlotOnNextWorkday(duration, 0);
planDate = nextSlot.date;
time = nextSlot.start;
endTime = nextSlot.end;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
}
}
try {
const res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
@@ -3975,7 +4240,7 @@ async function submitNewTask() {
time_block_end: endTime,
duration_minutes: duration,
sync_to_outlook: syncOutlook,
date: activePlannerDate
date: planDate
})
});
const data = await res.json();
@@ -3986,7 +4251,7 @@ async function submitNewTask() {
toggleAddTaskForm();
const titleInput = document.getElementById('newTaskTitle');
if (titleInput) titleInput.value = '';
switchPlannerDate(activePlannerDate, null, false);
switchPlannerDate(planDate, null, false);
} else {
alert(data.error || 'Fout bij opslaan van de taak');
}
@@ -4217,9 +4482,9 @@ async function submitEditTask() {
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();
let taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
let startTime = document.getElementById('editTaskStartTime').value.trim();
let endTime = document.getElementById('editTaskEndTime').value.trim();
const duration = getSelectedEditTaskDurationMinutes();
const saveBtn = document.getElementById('btnSaveEditTask');
@@ -4228,6 +4493,30 @@ async function submitEditTask() {
return;
}
// Controleer of de geplande tijd buiten de gezette werktijden valt
if (startTime) {
const check = checkWorkdayBounds(taskDate, startTime, endTime, duration);
if (check.isOutside) {
const answer = await promptOutsideWorkhoursModal(check);
if (answer === 'CANCEL') {
return;
}
if (answer === 'NEE') {
// Automatisch naar volgende werkdag met eerder gezette voorwaarden
try {
const nextSlot = await findSlotOnNextWorkday(duration, taskId);
taskDate = nextSlot.date;
startTime = nextSlot.start;
endTime = nextSlot.end;
} catch (err) {
alert(err.message || 'Kon geen open blok vinden op de volgende werkdag.');
return;
}
}
// Bij 'JA': ga gewoon door met het buiten werktijd ingevulde tijdstip
}
}
if (saveBtn) {
saveBtn.innerText = 'Opslaan...';
saveBtn.disabled = true;