Dev #54
@@ -175,7 +175,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
|
// 3. Organisatie Werkdag Standaarden opslaan (Release v1.4)
|
||||||
elseif ($action === 'save_workday_settings') {
|
elseif ($action === 'save_workday_settings') {
|
||||||
$workdayStart = trim($_POST['workday_start'] ?? '08:30');
|
$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');
|
$lunchStart = trim($_POST['lunch_start'] ?? '12:30');
|
||||||
$lunchEnd = trim($_POST['lunch_end'] ?? '13:00');
|
$lunchEnd = trim($_POST['lunch_end'] ?? '13:00');
|
||||||
$pomoFocus = max(10, min(120, (int)($_POST['pomodoro_focus'] ?? 25)));
|
$pomoFocus = max(10, min(120, (int)($_POST['pomodoro_focus'] ?? 25)));
|
||||||
|
|||||||
+13
-2
@@ -651,10 +651,11 @@ try {
|
|||||||
$data = is_array($rawInput) ? $rawInput : $_GET;
|
$data = is_array($rawInput) ? $rawInput : $_GET;
|
||||||
$duration = max(5, (int)($data['duration_minutes'] ?? 30));
|
$duration = max(5, (int)($data['duration_minutes'] ?? 30));
|
||||||
$excludeTaskId = (int)($data['task_id'] ?? 0);
|
$excludeTaskId = (int)($data['task_id'] ?? 0);
|
||||||
|
$startFromNextWorkday = !empty($data['start_from_next_workday']);
|
||||||
|
|
||||||
$workdaySettings = get_workday_settings();
|
$workdaySettings = get_workday_settings();
|
||||||
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
||||||
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
|
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:00';
|
||||||
$lunchStartStr = $workdaySettings['lunch_start'] ?? '12:30';
|
$lunchStartStr = $workdaySettings['lunch_start'] ?? '12:30';
|
||||||
$lunchEndStr = $workdaySettings['lunch_end'] ?? '13:00';
|
$lunchEndStr = $workdaySettings['lunch_end'] ?? '13:00';
|
||||||
|
|
||||||
@@ -693,6 +694,12 @@ try {
|
|||||||
$dutchMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
|
$dutchMonths = ['', 'jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'];
|
||||||
|
|
||||||
$checkDate = clone $now;
|
$checkDate = clone $now;
|
||||||
|
if ($startFromNextWorkday) {
|
||||||
|
$checkDate->modify('+1 day');
|
||||||
|
while ((int)$checkDate->format('N') >= 6) {
|
||||||
|
$checkDate->modify('+1 day');
|
||||||
|
}
|
||||||
|
}
|
||||||
$foundSlot = null;
|
$foundSlot = null;
|
||||||
$maxDays = 14;
|
$maxDays = 14;
|
||||||
$checkedDays = 0;
|
$checkedDays = 0;
|
||||||
@@ -707,7 +714,7 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$dateStr = $checkDate->format('Y-m-d');
|
$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)
|
// Bepaal vroegste startminuut voor deze dag (op hele kwartieren)
|
||||||
if ($isToday) {
|
if ($isToday) {
|
||||||
@@ -807,10 +814,14 @@ try {
|
|||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'slot' => $foundSlot,
|
'slot' => $foundSlot,
|
||||||
|
'workday_start' => $workdayStartStr,
|
||||||
|
'workday_end' => $workdayEndStr,
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'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.",
|
'error' => "Geen vrij blok van {$duration} minuten gevonden binnen de gezette werktijden ({$workdayStartStr} - {$workdayEndStr}) voor de komende {$maxDays} werkdagen.",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
function fociz_ai_smart_schedule(array $tasks, array $events, array $workdaySettings): array {
|
||||||
$startH = (int)explode(':', $workdaySettings['workday_start'] ?? '08:30')[0];
|
$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';
|
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
|
||||||
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function set_system_setting(string $key, $value): bool {
|
|||||||
function get_workday_settings(): array {
|
function get_workday_settings(): array {
|
||||||
return [
|
return [
|
||||||
'workday_start' => get_system_setting('workday_start', '08:30'),
|
'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_start' => get_system_setting('lunch_start', '12:30'),
|
||||||
'lunch_end' => get_system_setting('lunch_end', '13:00'),
|
'lunch_end' => get_system_setting('lunch_end', '13:00'),
|
||||||
'pomodoro_focus' => (int)get_system_setting('pomodoro_focus', 25),
|
'pomodoro_focus' => (int)get_system_setting('pomodoro_focus', 25),
|
||||||
|
|||||||
+305
-16
@@ -378,7 +378,7 @@ $initialSyncHash = md5(json_encode([
|
|||||||
|
|
||||||
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
|
// 6. Tijdlijn uren & Werkdag Standaarden (Release v1.4)
|
||||||
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
$workdayStartStr = $workdaySettings['workday_start'] ?? '08:30';
|
||||||
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:30';
|
$workdayEndStr = $workdaySettings['workday_end'] ?? '17:00';
|
||||||
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
|
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
|
||||||
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
||||||
$lunchHour = (int)explode(':', $lunchStart)[0];
|
$lunchHour = (int)explode(':', $lunchStart)[0];
|
||||||
@@ -2084,6 +2084,52 @@ if ($isAjaxRequest) {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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 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">
|
<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() {
|
function suggestNextFreeSlot() {
|
||||||
// Zoek het volgende ronde kwartier na het huidige moment
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let h = now.getHours();
|
let h = now.getHours();
|
||||||
let m = now.getMinutes();
|
let m = now.getMinutes();
|
||||||
// Rond op naar boven naar het volgende kwartier
|
|
||||||
m = Math.ceil((m + 1) / 15) * 15;
|
m = Math.ceil((m + 1) / 15) * 15;
|
||||||
if (m >= 60) { h += 1; m = 0; }
|
if (m >= 60) { h += 1; m = 0; }
|
||||||
// Begrenzen op werktijd 08:00-18:00
|
|
||||||
if (h < 8) { h = 8; m = 0; }
|
const wStart = (typeof FOCIZ_WORKDAY_START !== 'undefined' && FOCIZ_WORKDAY_START) ? FOCIZ_WORKDAY_START : '08:30';
|
||||||
if (h >= 18) { h = 8; m = 0; } // Morgen → val terug op 08:00
|
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')}`;
|
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 finalTitle = customTitle || `Oppakken Jira item ${issue.key}`;
|
||||||
|
|
||||||
const dateInput = document.getElementById('jiraPlanDate');
|
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 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;
|
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 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.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -3478,15 +3563,19 @@ async function submitJiraPlanModal() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Kon ticket niet inplannen');
|
alert(data.error || 'Kon ticket niet inplannen');
|
||||||
|
if (btn) {
|
||||||
btn.innerHTML = origHtml;
|
btn.innerHTML = origHtml;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
alert('Netwerkfout bij inplannen');
|
alert('Netwerkfout bij inplannen');
|
||||||
|
if (btn) {
|
||||||
btn.innerHTML = origHtml;
|
btn.innerHTML = origHtml;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function quickImportJiraByKey(btn, issueKey) {
|
async function quickImportJiraByKey(btn, issueKey) {
|
||||||
// Plan knop → open de plan modal (niet direct importeren)
|
// Plan knop → open de plan modal (niet direct importeren)
|
||||||
@@ -3530,6 +3619,31 @@ async function submitPlanEmailDirect() {
|
|||||||
return;
|
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 btn = document.getElementById('btnSubmitPlanEmailDirect');
|
||||||
const origHtml = btn.innerHTML;
|
const origHtml = btn.innerHTML;
|
||||||
btn.innerText = 'Inplannen...';
|
btn.innerText = 'Inplannen...';
|
||||||
@@ -3546,13 +3660,13 @@ async function submitPlanEmailDirect() {
|
|||||||
duration_minutes: duration,
|
duration_minutes: duration,
|
||||||
time_block_start: startTime,
|
time_block_start: startTime,
|
||||||
sync_outlook: syncOutlook,
|
sync_outlook: syncOutlook,
|
||||||
date: activePlannerDate
|
date: planDate
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
closePlanEmailDirectModal();
|
closePlanEmailDirectModal();
|
||||||
switchPlannerDate(activePlannerDate, null, false);
|
switchPlannerDate(planDate, null, false);
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Kon e-mail niet inplannen');
|
alert(data.error || 'Kon e-mail niet inplannen');
|
||||||
}
|
}
|
||||||
@@ -3669,6 +3783,135 @@ let weekStartDate = '<?= $weekStart ?>';
|
|||||||
let weekEndDate = '<?= $weekEnd ?>';
|
let weekEndDate = '<?= $weekEnd ?>';
|
||||||
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
|
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
|
||||||
let isDateSwitching = false;
|
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
|
* Wisselt de actieve plannerdatum geruisloos via AJAX
|
||||||
@@ -3964,6 +4207,28 @@ async function submitNewTask() {
|
|||||||
return;
|
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 {
|
try {
|
||||||
const res = await fetch('/api/planner.php?action=add_task', {
|
const res = await fetch('/api/planner.php?action=add_task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -3975,7 +4240,7 @@ async function submitNewTask() {
|
|||||||
time_block_end: endTime,
|
time_block_end: endTime,
|
||||||
duration_minutes: duration,
|
duration_minutes: duration,
|
||||||
sync_to_outlook: syncOutlook,
|
sync_to_outlook: syncOutlook,
|
||||||
date: activePlannerDate
|
date: planDate
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -3986,7 +4251,7 @@ async function submitNewTask() {
|
|||||||
toggleAddTaskForm();
|
toggleAddTaskForm();
|
||||||
const titleInput = document.getElementById('newTaskTitle');
|
const titleInput = document.getElementById('newTaskTitle');
|
||||||
if (titleInput) titleInput.value = '';
|
if (titleInput) titleInput.value = '';
|
||||||
switchPlannerDate(activePlannerDate, null, false);
|
switchPlannerDate(planDate, null, false);
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Fout bij opslaan van de taak');
|
alert(data.error || 'Fout bij opslaan van de taak');
|
||||||
}
|
}
|
||||||
@@ -4217,9 +4482,9 @@ async function submitEditTask() {
|
|||||||
const title = document.getElementById('editTaskTitle').value.trim();
|
const title = document.getElementById('editTaskTitle').value.trim();
|
||||||
const priority = document.getElementById('editTaskPriority').value;
|
const priority = document.getElementById('editTaskPriority').value;
|
||||||
const dateInput = document.getElementById('editTaskDate');
|
const dateInput = document.getElementById('editTaskDate');
|
||||||
const taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
let taskDate = (dateInput && dateInput.value) ? dateInput.value.trim() : (typeof activePlannerDate !== 'undefined' ? activePlannerDate : new Date().toISOString().slice(0, 10));
|
||||||
const startTime = document.getElementById('editTaskStartTime').value.trim();
|
let startTime = document.getElementById('editTaskStartTime').value.trim();
|
||||||
const endTime = document.getElementById('editTaskEndTime').value.trim();
|
let endTime = document.getElementById('editTaskEndTime').value.trim();
|
||||||
const duration = getSelectedEditTaskDurationMinutes();
|
const duration = getSelectedEditTaskDurationMinutes();
|
||||||
const saveBtn = document.getElementById('btnSaveEditTask');
|
const saveBtn = document.getElementById('btnSaveEditTask');
|
||||||
|
|
||||||
@@ -4228,6 +4493,30 @@ async function submitEditTask() {
|
|||||||
return;
|
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) {
|
if (saveBtn) {
|
||||||
saveBtn.innerText = 'Opslaan...';
|
saveBtn.innerText = 'Opslaan...';
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user