@@ -3297,6 +3368,11 @@ function clearJiraPlanStartTime() {
input.value = '';
input.focus();
}
+ const notice = document.getElementById('jiraPlanOpenBlockNotice');
+ if (notice) notice.classList.add('hidden');
+ if (typeof updateJiraPlanEndTimeDisplay === 'function') {
+ updateJiraPlanEndTimeDisplay();
+ }
}
function toggleJiraPlanDescription() {
@@ -3386,6 +3462,10 @@ function openJiraPlanModal(issueKey) {
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
document.getElementById('jiraPlanStartTime').value = suggestTime;
+ const notice = document.getElementById('jiraPlanOpenBlockNotice');
+ if (notice) notice.classList.add('hidden');
+ updateJiraPlanEndTimeDisplay();
+
const modal = document.getElementById('jiraPlanModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
@@ -3399,29 +3479,120 @@ function closeJiraPlanModal() {
_jiraPlanCurrentIssue = null;
}
+function updateJiraPlanEndTimeDisplay() {
+ const startTimeInput = document.getElementById('jiraPlanStartTime');
+ const badge = document.getElementById('jiraPlanEndTimeBadge');
+ const durationInput = document.getElementById('jiraPlanDuration');
+ if (!badge) return;
+
+ const startTime = startTimeInput ? startTimeInput.value.trim() : '';
+ const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
+
+ if (!startTime) {
+ badge.textContent = '';
+ return;
+ }
+
+ const parts = startTime.split(':').map(Number);
+ if (parts.length < 2 || isNaN(parts[0]) || isNaN(parts[1])) {
+ badge.textContent = '';
+ return;
+ }
+
+ const endTotalMin = parts[0] * 60 + parts[1] + duration;
+ const endH = Math.floor(endTotalMin / 60);
+ const endM = endTotalMin % 60;
+ const endTimeStr = `${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')}`;
+ badge.textContent = `(tot ${endTimeStr})`;
+}
+
+async function planJiraInNextOpenBlock() {
+ const durationInput = document.getElementById('jiraPlanDuration');
+ const duration = durationInput ? (parseInt(durationInput.value, 10) || 15) : 15;
+ const btn = document.getElementById('btnJiraPlanOpenBlock');
+ const notice = document.getElementById('jiraPlanOpenBlockNotice');
+ const noticeText = document.getElementById('jiraPlanOpenBlockNoticeText');
+
+ 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
+ })
+ });
+ const data = await res.json();
+
+ if (data.success && data.slot) {
+ const slot = data.slot;
+ const dateInput = document.getElementById('jiraPlanDate');
+ if (dateInput) dateInput.value = slot.date;
+
+ const startInput = document.getElementById('jiraPlanStartTime');
+ if (startInput) startInput.value = slot.start;
+
+ updateJiraPlanEndTimeDisplay();
+
+ 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 setJiraPlanDuration(minutes) {
document.getElementById('jiraPlanDuration').value = minutes;
document.querySelectorAll('.jira-plan-dur-btn').forEach(btn => {
const dur = parseInt(btn.getAttribute('data-dur'), 10);
if (dur === minutes) {
- btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white';
+ btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-indigo-600 border-indigo-600 text-white shadow-xs';
} else {
btn.className = 'jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 hover:border-indigo-400';
}
});
+ updateJiraPlanEndTimeDisplay();
}
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 +3605,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;
- btn.innerHTML = 'Inplannen...';
- btn.disabled = true;
+ const origHtml = btn ? btn.innerHTML : '';
+ if (btn) {
+ btn.innerHTML = 'Inplannen...';
+ btn.disabled = true;
+ }
try {
const payload = {
@@ -3478,13 +3677,17 @@ async function submitJiraPlanModal() {
}
} else {
alert(data.error || 'Kon ticket niet inplannen');
- btn.innerHTML = origHtml;
- btn.disabled = false;
+ if (btn) {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ }
}
} catch(e) {
alert('Netwerkfout bij inplannen');
- btn.innerHTML = origHtml;
- btn.disabled = false;
+ if (btn) {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ }
}
}
@@ -3530,6 +3733,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 +3774,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 +3897,177 @@ 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();
+ setTimeout(() => {
+ const noBtn = document.getElementById('btnOutsideWorkhoursNo');
+ if (noBtn) noBtn.focus();
+ }, 50);
+ } 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.');
+}
+
+// Globale sneltoetsafhandeling voor modals (Escape om te sluiten, Enter voor actie)
+window.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ const outsideModal = document.getElementById('outsideWorkhoursModal');
+ if (outsideModal && !outsideModal.classList.contains('hidden')) {
+ closeOutsideWorkhoursModal('CANCEL');
+ return;
+ }
+ const jiraModal = document.getElementById('jiraPlanModal');
+ if (jiraModal && !jiraModal.classList.contains('hidden')) {
+ closeJiraPlanModal();
+ return;
+ }
+ const editModal = document.getElementById('editTaskModal');
+ if (editModal && !editModal.classList.contains('hidden')) {
+ closeEditTaskModal();
+ return;
+ }
+ const emailModal = document.getElementById('planEmailDirectModal');
+ if (emailModal && !emailModal.classList.contains('hidden')) {
+ closePlanEmailDirectModal();
+ return;
+ }
+ const detailModal = document.getElementById('jiraTicketDetailModal');
+ if (detailModal && !detailModal.classList.contains('hidden')) {
+ closeJiraTicketDetailModal();
+ return;
+ }
+ } else if (e.key === 'Enter') {
+ const outsideModal = document.getElementById('outsideWorkhoursModal');
+ if (outsideModal && !outsideModal.classList.contains('hidden')) {
+ e.preventDefault();
+ closeOutsideWorkhoursModal('NEE');
+ return;
+ }
+ }
+});
/**
* Wisselt de actieve plannerdatum geruisloos via AJAX
@@ -3964,6 +4363,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 +4396,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 +4407,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 +4638,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 +4649,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;
diff --git a/roadmap.php b/roadmap.php
index 536ae56..b806c53 100644
--- a/roadmap.php
+++ b/roadmap.php
@@ -206,7 +206,7 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
-
+
@@ -333,7 +333,12 @@ async function submitManualItem() {
if (data.success) {
location.reload();
}
-}
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ const modal = document.getElementById('newItemModal');
+ if (modal) modal.classList.add('hidden');
+ }
+});
diff --git a/timeline.php b/timeline.php
index 8206987..31ac026 100644
--- a/timeline.php
+++ b/timeline.php
@@ -224,7 +224,7 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
-
+
@@ -1073,6 +1073,12 @@ function escapeHtml(str) {
.replace(/"/g, '"')
.replace(/'/g, ''');
}
+
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ closeCardModal();
+ }
+});