@@ -671,7 +742,97 @@ function closeMobileDetail() {
document.getElementById('noMailSelectedState').classList.remove('hidden');
}
-// Modal open/close helpers
+// Modal open/close helpers & Open blok inplannen
+let _outsideWorkhoursPendingMail = null;
+
+function updatePlanMailEndTimeBadge() {
+ const startInput = document.getElementById('planMailStartTime');
+ const durationInput = document.getElementById('planMailDuration');
+ const badge = document.getElementById('planMailEndTimeBadge');
+ if (!startInput || !durationInput || !badge) return;
+
+ const startVal = (startInput.value || '').trim();
+ const duration = parseInt(durationInput.value || '30', 10);
+ if (!startVal || !startVal.includes(':') || isNaN(duration)) {
+ badge.innerText = '';
+ return;
+ }
+
+ const [hStr, mStr] = startVal.split(':');
+ const totalMinutes = (parseInt(hStr, 10) * 60 + parseInt(mStr, 10)) + duration;
+ const endH = Math.floor((totalMinutes / 60) % 24);
+ const endM = totalMinutes % 60;
+ const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
+ badge.innerText = `(tot ${endStr})`;
+}
+
+async function planInNextOpenBlockMail(startFromNextWorkday = false) {
+ const durationInput = document.getElementById('planMailDuration');
+ const duration = parseInt(durationInput?.value || '30', 10);
+ const btn = document.getElementById('btnMailPlanOpenBlock');
+ const origHtml = btn ? btn.innerHTML : '';
+
+ if (btn) {
+ btn.disabled = true;
+ btn.innerHTML = `
Zoeken...`;
+ if (typeof lucide !== 'undefined') lucide.createIcons();
+ }
+
+ try {
+ let url = `/api/planner.php?action=find_next_free_slot&duration=${duration}`;
+ if (startFromNextWorkday) {
+ url += '&start_from_next_workday=1';
+ }
+ const resp = await fetch(url);
+ const data = await resp.json();
+
+ if (data.success && data.slot) {
+ const slot = data.slot;
+ const dateInput = document.getElementById('planMailDate');
+ const startInput = document.getElementById('planMailStartTime');
+ if (dateInput) dateInput.value = slot.date;
+ if (startInput) startInput.value = slot.start;
+
+ updatePlanMailEndTimeBadge();
+
+ const notice = document.getElementById('mailPlanOpenBlockNotice');
+ const noticeText = document.getElementById('mailPlanOpenBlockNoticeText');
+ if (notice && noticeText) {
+ const formattedDate = new Date(slot.date + 'T00:00:00').toLocaleDateString('nl-NL', { weekday: 'short', day: 'numeric', month: 'short' });
+ noticeText.innerText = `Vrij blok gevonden: ${formattedDate} om ${slot.start} – ${slot.end} (${duration} min)`;
+ notice.classList.remove('hidden');
+ }
+ } else {
+ alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen de komende 14 dagen.');
+ }
+ } catch (err) {
+ alert('Netwerkfout bij zoeken van open blok: ' + err.message);
+ } finally {
+ if (btn) {
+ btn.disabled = false;
+ btn.innerHTML = origHtml;
+ if (typeof lucide !== 'undefined') lucide.createIcons();
+ }
+ }
+}
+
+function closeOutsideWorkhoursModalMail(choice) {
+ const modal = document.getElementById('outsideWorkhoursModalMail');
+ if (modal) modal.classList.add('hidden');
+
+ if (choice === 'JA') {
+ if (_outsideWorkhoursPendingMail) {
+ _outsideWorkhoursPendingMail.bypassWorkhours = true;
+ submitPlanEmail(null, _outsideWorkhoursPendingMail);
+ }
+ } else if (choice === 'NEE') {
+ _outsideWorkhoursPendingMail = null;
+ planInNextOpenBlockMail(true);
+ } else {
+ _outsideWorkhoursPendingMail = null;
+ }
+}
+
function openPlanMailModal() {
if (!selectedMessage) return;
document.getElementById('planMailId').value = selectedMessage.id;
@@ -679,9 +840,15 @@ function openPlanMailModal() {
document.getElementById('planMailWebLink').value = selectedMessage.web_link || '';
document.getElementById('planMailSender').value = selectedMessage.from_name || '';
+ const notice = document.getElementById('mailPlanOpenBlockNotice');
+ if (notice) notice.classList.add('hidden');
+
+ updatePlanMailEndTimeBadge();
+
const modal = document.getElementById('planMailModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
+ if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closePlanMailModal() {
@@ -690,26 +857,72 @@ function closePlanMailModal() {
modal.classList.remove('flex');
}
-async function submitPlanEmail(e) {
- e.preventDefault();
+async function submitPlanEmail(e, options = {}) {
+ if (e) e.preventDefault();
+
+ const messageId = document.getElementById('planMailId').value;
+ const subject = document.getElementById('planMailSubject').value;
+ const planDate = document.getElementById('planMailDate').value;
+ const timeBlockStart = (document.getElementById('planMailStartTime')?.value || '').trim();
+ const durationMinutes = parseInt(document.getElementById('planMailDuration').value, 10);
+ const priority = document.getElementById('planMailPriority').value;
+ const webLink = document.getElementById('planMailWebLink').value;
+ const sender = document.getElementById('planMailSender').value;
+
+ // Controleer of de geplande tijd buiten de gezette werktijden valt (standaard 08:30 - 17:00)
+ if (!options.bypassWorkhours && timeBlockStart && timeBlockStart.includes(':')) {
+ const [shStr, smStr] = timeBlockStart.split(':');
+ const startMin = parseInt(shStr, 10) * 60 + parseInt(smStr, 10);
+ const endMin = startMin + durationMinutes;
+
+ const workStartMin = 8 * 60 + 30; // 08:30
+ const workEndMin = 17 * 60; // 17:00
+
+ if (startMin < workStartMin || endMin > workEndMin) {
+ _outsideWorkhoursPendingMail = { messageId, subject, planDate, timeBlockStart, durationMinutes, priority, webLink, sender };
+
+ const endH = Math.floor((endMin / 60) % 24);
+ const endM = endMin % 60;
+ const endStr = `${String(endH).padStart(2, '0')}:${String(endM).padStart(2, '0')}`;
+
+ const slotEl = document.getElementById('outsideWorkhoursSelectedSlotMail');
+ if (slotEl) slotEl.innerText = `${timeBlockStart} – ${endStr} (${durationMinutes} min)`;
+
+ const outsideModal = document.getElementById('outsideWorkhoursModalMail');
+ if (outsideModal) {
+ outsideModal.classList.remove('hidden');
+ outsideModal.classList.add('flex');
+ setTimeout(() => {
+ const btnNo = document.getElementById('btnOutsideWorkhoursNoMail');
+ if (btnNo) btnNo.focus();
+ }, 50);
+ if (typeof lucide !== 'undefined') lucide.createIcons();
+ }
+ return;
+ }
+ }
+
const btn = document.getElementById('planMailSubmitBtn');
- const origHtml = btn.innerHTML;
- btn.innerHTML = `
Inplannen...`;
- btn.disabled = true;
+ const origHtml = btn ? btn.innerHTML : '';
+ if (btn) {
+ btn.innerHTML = `
Inplannen...`;
+ btn.disabled = true;
+ if (typeof lucide !== 'undefined') lucide.createIcons();
+ }
try {
const res = await fetch('/api/mail.php?action=plan_email', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
- message_id: document.getElementById('planMailId').value,
- subject: document.getElementById('planMailSubject').value,
- plan_date: document.getElementById('planMailDate').value,
- time_block_start: document.getElementById('planMailStartTime').value,
- duration_minutes: parseInt(document.getElementById('planMailDuration').value, 10),
- priority: document.getElementById('planMailPriority').value,
- web_link: document.getElementById('planMailWebLink').value,
- sender: document.getElementById('planMailSender').value
+ message_id: messageId,
+ subject: subject,
+ plan_date: planDate,
+ time_block_start: timeBlockStart,
+ duration_minutes: durationMinutes,
+ priority: priority,
+ web_link: webLink,
+ sender: sender
})
});
const data = await res.json();
@@ -722,9 +935,11 @@ async function submitPlanEmail(e) {
} catch(err) {
alert('Netwerkfout: ' + err.message);
} finally {
- btn.innerHTML = origHtml;
- btn.disabled = false;
- if (typeof lucide !== 'undefined') lucide.createIcons();
+ if (btn) {
+ btn.innerHTML = origHtml;
+ btn.disabled = false;
+ if (typeof lucide !== 'undefined') lucide.createIcons();
+ }
}
}
@@ -858,6 +1073,27 @@ function escapeHtml(str) {
.replace(/'/g, ''');
}
+// Toetsenbordnavigatie voor alle modals (Escape & Enter)
+document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ const outsideModal = document.getElementById('outsideWorkhoursModalMail');
+ if (outsideModal && !outsideModal.classList.contains('hidden')) {
+ closeOutsideWorkhoursModalMail('CANCEL');
+ return;
+ }
+ closePlanMailModal();
+ closeReplyModal();
+ closeForwardModal();
+ } else if (e.key === 'Enter') {
+ const outsideModal = document.getElementById('outsideWorkhoursModalMail');
+ if (outsideModal && !outsideModal.classList.contains('hidden')) {
+ e.preventDefault();
+ closeOutsideWorkhoursModalMail('NEE');
+ return;
+ }
+ }
+});
+
document.addEventListener('DOMContentLoaded', () => {
applyMailSearch();
if (allMessages && allMessages.length > 0) {
diff --git a/planner.php b/planner.php
index b053cd5..fc86ae8 100644
--- a/planner.php
+++ b/planner.php
@@ -1771,7 +1771,7 @@ if ($isAjaxRequest) {
-
+
@@ -1825,21 +1825,46 @@ if ($isAjaxRequest) {
+
+
+
+ Datum & Tijdblok
+
+
+
+
+
+
+
-
+
-
+
+
+
+
-
+
@@ -1962,7 +1987,7 @@ if ($isAjaxRequest) {
-
+
@@ -2085,7 +2110,7 @@ if ($isAjaxRequest) {
-
+
@@ -2131,7 +2156,7 @@ if ($isAjaxRequest) {
-
+
@@ -3343,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() {
@@ -3432,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');
@@ -3445,16 +3479,96 @@ 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() {
@@ -3873,6 +3987,10 @@ function promptOutsideWorkhoursModal(checkResult) {
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');
@@ -3913,6 +4031,44 @@ async function findSlotOnNextWorkday(duration, taskId = 0) {
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
* Behoudt de huidige schermhoogte (scrollpositie) en voorkomt flitsen/herladen
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();
+ }
+});