Compare commits
3 Commits
7461231879
...
ad06c67235
| Author | SHA1 | Date | |
|---|---|---|---|
| ad06c67235 | |||
| deba8463b8 | |||
| 7fe7c0f51a |
@@ -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)));
|
||||
@@ -831,7 +831,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Wachtrijfilter Toevoegen / Bewerken -->
|
||||
<div id="jiraFilterModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div id="jiraFilterModal" onclick="if(event.target === this) closeJiraFilterModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
|
||||
@@ -98,7 +98,8 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
$title = "[{$key}] {$summary}";
|
||||
$customTitle = trim($input['title'] ?? '');
|
||||
$title = !empty($customTitle) ? $customTitle : "[{$key}] {$summary}";
|
||||
|
||||
// Zoek of maak daily plan aan voor deze datum
|
||||
$stmtPlan = $db->prepare("SELECT id FROM daily_plans WHERE user_id = ? AND plan_date = ?");
|
||||
|
||||
+58
-25
@@ -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,10 +694,55 @@ 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;
|
||||
|
||||
// Bepaal het totale zoekvenster voor batch-ophalen (O(1) database & Graph API schaalbaarheid)
|
||||
$rangeStartDate = $checkDate->format('Y-m-d');
|
||||
$rangeEndObj = clone $checkDate;
|
||||
$rangeEndObj->modify('+21 days'); // ruim venster incl. weekenden
|
||||
$rangeEndDate = $rangeEndObj->format('Y-m-d');
|
||||
|
||||
// 1. Batch SQL: haal alle bestaande taken van de gebruiker in 1 geoptimaliseerde query op
|
||||
$tasksByDate = [];
|
||||
$stmtTasks = $db->prepare("
|
||||
SELECT t.id, dp.plan_date, t.time_block_start, t.time_block_end, t.duration_minutes
|
||||
FROM tasks t
|
||||
INNER JOIN daily_plans dp ON t.daily_plan_id = dp.id
|
||||
WHERE t.user_id = ?
|
||||
AND dp.plan_date BETWEEN ? AND ?
|
||||
AND t.time_block_start IS NOT NULL
|
||||
AND t.time_block_start != ''
|
||||
AND t.status != 'CANCELLED'
|
||||
");
|
||||
$stmtTasks->execute([$user['id'], $rangeStartDate, $rangeEndDate]);
|
||||
while ($t = $stmtTasks->fetch()) {
|
||||
if ($excludeTaskId > 0 && (int)$t['id'] === $excludeTaskId) {
|
||||
continue;
|
||||
}
|
||||
$tasksByDate[$t['plan_date']][] = $t;
|
||||
}
|
||||
|
||||
// 2. Batch Microsoft Graph: haal alle Outlook events in 1 API call op voor het gehele venster
|
||||
$outlookByDate = [];
|
||||
if (function_exists('microsoft_fetch_events_range')) {
|
||||
try {
|
||||
$allOutlookEvts = microsoft_fetch_events_range($user['id'], $rangeStartDate, $rangeEndDate, false);
|
||||
foreach ($allOutlookEvts as $oe) {
|
||||
if (!empty($oe['date'])) {
|
||||
$outlookByDate[$oe['date']][] = $oe;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
while ($checkedDays < $maxDays) {
|
||||
$dayOfWeek = (int)$checkDate->format('N'); // 1 = Ma, 7 = Zo
|
||||
|
||||
@@ -707,7 +753,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) {
|
||||
@@ -731,22 +777,9 @@ try {
|
||||
$busyIntervals[] = [$lunchStartMin, $lunchEndMin];
|
||||
}
|
||||
|
||||
// Bestaande taken van de gebruiker op deze dag
|
||||
$stmtTasks = $db->prepare("
|
||||
SELECT t.id, t.time_block_start, t.time_block_end, t.duration_minutes
|
||||
FROM tasks t
|
||||
INNER JOIN daily_plans dp ON t.daily_plan_id = dp.id
|
||||
WHERE t.user_id = ?
|
||||
AND dp.plan_date = ?
|
||||
AND t.time_block_start IS NOT NULL
|
||||
AND t.time_block_start != ''
|
||||
AND t.status != 'CANCELLED'
|
||||
");
|
||||
$stmtTasks->execute([$user['id'], $dateStr]);
|
||||
while ($t = $stmtTasks->fetch()) {
|
||||
if ($excludeTaskId > 0 && (int)$t['id'] === $excludeTaskId) {
|
||||
continue;
|
||||
}
|
||||
// Bestaande taken voor deze datum uit het geheugen
|
||||
$dayTasks = $tasksByDate[$dateStr] ?? [];
|
||||
foreach ($dayTasks as $t) {
|
||||
$tStart = $toMins($t['time_block_start']);
|
||||
$tDur = (int)($t['duration_minutes'] ?: 30);
|
||||
$tEnd = !empty($t['time_block_end']) ? $toMins($t['time_block_end']) : ($tStart + $tDur);
|
||||
@@ -755,11 +788,9 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Outlook afspraken
|
||||
if (function_exists('microsoft_fetch_events_range')) {
|
||||
try {
|
||||
$outlookEvts = microsoft_fetch_events_range($user['id'], $dateStr, $dateStr, false);
|
||||
foreach ($outlookEvts as $oe) {
|
||||
// Outlook afspraken voor deze datum uit het geheugen
|
||||
$dayOutlook = $outlookByDate[$dateStr] ?? [];
|
||||
foreach ($dayOutlook as $oe) {
|
||||
if (!empty($oe['is_all_day'])) continue;
|
||||
if (empty($oe['start']) || empty($oe['end'])) continue;
|
||||
$oStart = $toMins($oe['start']);
|
||||
@@ -768,8 +799,6 @@ try {
|
||||
$busyIntervals[] = [$oStart, $oEnd];
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {}
|
||||
}
|
||||
|
||||
// Doorzoek kwartierslots
|
||||
for ($slotStart = $earliestMin; ($slotStart + $duration) <= $workdayEndMin; $slotStart += 15) {
|
||||
@@ -807,10 +836,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.",
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
</div>
|
||||
|
||||
<!-- MODAL 1: Taak (Post-it / Opdracht) Toevoegen of Bewerken -->
|
||||
<div id="cardModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div id="cardModal" onclick="if(event.target === this) closeCardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="fociz-card p-6 w-full max-w-xl shadow-2xl space-y-5 my-8">
|
||||
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
@@ -341,7 +341,7 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
</div>
|
||||
|
||||
<!-- MODAL 2: Bord Instellingen, Kolommen & Leden Autorisatie (Beheerder) -->
|
||||
<div id="manageBoardModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div id="manageBoardModal" onclick="if(event.target === this) closeManageBoardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="fociz-card p-6 w-full max-w-xl shadow-2xl space-y-5 my-8">
|
||||
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
@@ -403,7 +403,7 @@ $isAdmin = ($userRole === 'ADMIN');
|
||||
</div>
|
||||
|
||||
<!-- MODAL 3: Nieuw Bord Aanmaken (Beheerder) -->
|
||||
<div id="createBoardModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="createBoardModal" onclick="if(event.target === this) closeCreateBoardModal()" 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">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<h3 class="text-base font-bold text-slate-900 dark:text-white">Nieuw Bord Toevoegen</h3>
|
||||
@@ -1275,11 +1275,25 @@ function toggleBoardMaximize() {
|
||||
// Pas bewaarde breedte-status direct toe bij het laden
|
||||
applyMaximizeState(isBoardMaximized);
|
||||
|
||||
// Sneltoets Escape om gemaximaliseerd bord te verkleinen (mits geen modal openstaat)
|
||||
// Sneltoets Escape om geopende modals te sluiten of gemaximaliseerd bord te verkleinen
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && isBoardMaximized) {
|
||||
const anyModalOpen = document.querySelector('#cardModal:not(.hidden), #manageBoardModal:not(.hidden), #createBoardModal:not(.hidden)');
|
||||
if (!anyModalOpen) {
|
||||
if (e.key === 'Escape') {
|
||||
const cardModal = document.getElementById('cardModal');
|
||||
if (cardModal && !cardModal.classList.contains('hidden')) {
|
||||
closeCardModal();
|
||||
return;
|
||||
}
|
||||
const manageModal = document.getElementById('manageBoardModal');
|
||||
if (manageModal && !manageModal.classList.contains('hidden')) {
|
||||
closeManageBoardModal();
|
||||
return;
|
||||
}
|
||||
const createModal = document.getElementById('createBoardModal');
|
||||
if (createModal && !createModal.classList.contains('hidden')) {
|
||||
closeCreateBoardModal();
|
||||
return;
|
||||
}
|
||||
if (isBoardMaximized) {
|
||||
toggleBoardMaximize();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -126,7 +126,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<!-- Modal: Nieuwe sessie -->
|
||||
<div id="newSessionModal" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div id="newSessionModal" onclick="if(event.target === this) this.classList.add('hidden')" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-lg w-full space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
@@ -159,7 +159,7 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Doel / Beschrijving</label>
|
||||
<input type="text" name="description" placeholder="Doel van deze sessie..." class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
|
||||
<input type="text" name="description" placeholder="Doel van deze sessie..." class="w-full bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-300 dark:border-slate-800 rounded-xl px-3.5 py-2.5 text-slate-900 dark:text-white text-xs placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 shadow-2xs">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -175,4 +175,13 @@ require_once __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('newSessionModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
@@ -195,7 +195,7 @@ $categories = [
|
||||
</div>
|
||||
|
||||
<!-- Modal: Nieuwe Kaart Toevoegen -->
|
||||
<div id="addCardModal" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div id="addCardModal" onclick="if(event.target === this) closeAddCardModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="fociz-card p-6 shadow-2xl max-w-md w-full space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
@@ -236,7 +236,7 @@ $categories = [
|
||||
</div>
|
||||
|
||||
<!-- Modal: Zet om naar Roadmap -->
|
||||
<div id="convertModal" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div id="convertModal" onclick="if(event.target === this) closeConvertModal()" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="fociz-card p-6 shadow-2xl max-w-lg w-full space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
@@ -554,7 +554,12 @@ async function aiSuggestRoadmapMeta(btn) {
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeAddCardModal();
|
||||
closeConvertModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
+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 {
|
||||
$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';
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -467,8 +467,8 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
</div>
|
||||
|
||||
<!-- MODAL 1: Inplannen in Dagplanner (planner.php) -->
|
||||
<div id="planTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-md shadow-2xl p-6 transition transform">
|
||||
<div id="planTaskModal" onclick="if(event.target === this) closePlanModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl p-6 transition transform">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-50 dark:bg-indigo-950/50 text-indigo-600 dark:text-indigo-400 flex items-center justify-center">
|
||||
@@ -476,7 +476,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Inplannen in Dagplanner</h3>
|
||||
<p class="text-[11px] text-slate-400">Voeg toe aan je dagfocus</p>
|
||||
<p class="text-[11px] text-slate-400">Voeg toe aan je dagfocus op de tijdlijn</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closePlanModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
|
||||
@@ -489,25 +489,81 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
<input type="hidden" id="planIssueSummary" value="">
|
||||
<input type="hidden" id="planIssueUrl" value="">
|
||||
|
||||
<!-- Ticket Info Preview -->
|
||||
<div class="p-3 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200/80 dark:border-slate-800/80">
|
||||
<!-- Ticket Info Preview met uitklapbare inhoud -->
|
||||
<div class="p-3.5 rounded-xl bg-slate-50 dark:bg-slate-950 border border-slate-200/80 dark:border-slate-800/80 space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span id="planTicketKeyBadge" class="font-mono text-xs font-bold text-indigo-600 dark:text-indigo-400">FOC-101</span>
|
||||
<span id="planTicketProject" class="text-[11px] text-slate-400">• Fociz Platform</span>
|
||||
<span id="planSpBadge" class="hidden ml-auto text-[10px] font-bold px-2 py-0.5 rounded-full bg-violet-50 dark:bg-violet-500/10 text-violet-700 dark:text-violet-300 border border-violet-200 dark:border-violet-500/20"></span>
|
||||
</div>
|
||||
<div id="planTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 mt-1 line-clamp-2">
|
||||
<div id="planTicketSummaryText" class="text-xs font-semibold text-slate-800 dark:text-slate-200 line-clamp-2">
|
||||
Tweestapsverificatie implementeren
|
||||
</div>
|
||||
<div id="planSpHint" class="hidden mt-1.5 text-[11px] text-violet-600 dark:text-violet-400 flex items-center space-x-1">
|
||||
<div id="planSpHint" class="hidden mt-1 text-[11px] text-violet-600 dark:text-violet-400 flex items-center space-x-1">
|
||||
<i data-lucide="zap" class="w-3 h-3"></i>
|
||||
<span id="planSpHintText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Uitklapbare beschrijving (10 regels verticaal scrollen) -->
|
||||
<div class="pt-1.5 border-t border-slate-200/60 dark:border-slate-800/60">
|
||||
<button type="button" onclick="togglePlanTicketDesc()" class="text-[11px] text-indigo-600 dark:text-indigo-400 hover:underline flex items-center space-x-1 font-medium cursor-pointer">
|
||||
<i data-lucide="file-text" class="w-3 h-3"></i>
|
||||
<span id="planTicketDescToggleText">Toon ticket inhoud</span>
|
||||
<i data-lucide="chevron-down" id="planTicketDescChevron" class="w-3 h-3 transition-transform duration-200"></i>
|
||||
</button>
|
||||
<div id="planTicketDescContainer" class="hidden mt-2 p-3 rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-[11px] text-slate-700 dark:text-slate-300 max-h-40 overflow-y-auto leading-relaxed whitespace-pre-wrap select-text font-normal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aanpasbare Taaktitel in Agenda -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label for="planTaskTitleInput" class="block text-xs font-bold text-slate-700 dark:text-slate-300">Naam op de tijdlijn</label>
|
||||
<button type="button" onclick="clearPlanTaskTitle()" class="text-[10px] text-slate-400 hover:text-rose-500 cursor-pointer flex items-center space-x-0.5" title="Titel wissen">
|
||||
<i data-lucide="x-circle" class="w-3 h-3"></i>
|
||||
<span>Wissen</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="planTaskTitleInput"
|
||||
required
|
||||
placeholder="Bijv. Oppakken Jira item FOC-101"
|
||||
class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-xs text-slate-900 dark:text-white font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
|
||||
>
|
||||
<button type="button" onclick="clearPlanTaskTitle()" class="absolute right-2.5 top-2.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 cursor-pointer" title="Wissen">
|
||||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datum & Slimme "Plan in open blok" knop -->
|
||||
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||||
<button type="button" onclick="planInNextOpenBlockJira()" id="btnJiraPlanOpenBlock" 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>
|
||||
|
||||
<!-- Open blok banner -->
|
||||
<div id="jiraPlanOpenBlockNotice" 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="jiraPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('jiraPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded cursor-pointer">
|
||||
<i data-lucide="x" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datum Kiezer -->
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
|
||||
<input
|
||||
type="date"
|
||||
id="planDateInput"
|
||||
@@ -518,19 +574,26 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
</div>
|
||||
|
||||
<!-- Tijdsblok Starttijd & Duur -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Starttijd (optioneel)</label>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Starttijd (optioneel)</label>
|
||||
<span id="planEndTimeBadge" class="text-[10px] text-indigo-600 dark:text-indigo-400 font-mono font-semibold"></span>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="time"
|
||||
id="planStartTimeInput"
|
||||
value="09:00"
|
||||
class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
|
||||
oninput="updatePlanEndTimeBadge()"
|
||||
class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Verwachte Duur</label>
|
||||
<select id="planDurationInput" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Verwachte Duur</label>
|
||||
<select id="planDurationInput" onchange="updatePlanEndTimeBadge()" class="w-full bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500/40 cursor-pointer">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30">30 minuten</option>
|
||||
<option value="45" selected>45 minuten</option>
|
||||
<option value="60">1 uur (60 min)</option>
|
||||
@@ -573,8 +636,54 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Waarschuwing Buiten Werktijden (Jira) -->
|
||||
<div id="outsideWorkhoursModalJira" onclick="if(event.target === this) closeOutsideWorkhoursModalJira('CANCEL')" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-slate-900 border border-amber-500/30 rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-4">
|
||||
<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="closeOutsideWorkhoursModalJira('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="outsideWorkhoursSelectedSlotJira" 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="outsideWorkhoursBoundsJira" class="font-mono font-semibold">08:30 – 17:00</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="btnOutsideWorkhoursNoJira" onclick="closeOutsideWorkhoursModalJira('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="btnOutsideWorkhoursYesJira" onclick="closeOutsideWorkhoursModalJira('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 2: Toevoegen aan Kanban Bord (board.php) -->
|
||||
<div id="boardTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div id="boardTaskModal" onclick="if(event.target === this) closeBoardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl w-full max-w-md shadow-2xl p-6 transition transform">
|
||||
<div class="flex items-center justify-between pb-4 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -1399,12 +1508,131 @@ function renderTickets(tickets) {
|
||||
}
|
||||
|
||||
// Modal 1: Plan in Dagplanner
|
||||
let _activeJiraTicket = null;
|
||||
let _outsideWorkhoursPendingJira = null;
|
||||
|
||||
function togglePlanTicketDesc() {
|
||||
const container = document.getElementById('planTicketDescContainer');
|
||||
const chevron = document.getElementById('planTicketDescChevron');
|
||||
const text = document.getElementById('planTicketDescToggleText');
|
||||
if (!container) return;
|
||||
|
||||
if (container.classList.contains('hidden')) {
|
||||
container.classList.remove('hidden');
|
||||
if (chevron) chevron.classList.add('rotate-180');
|
||||
if (text) text.textContent = 'Verberg ticket inhoud';
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
if (chevron) chevron.classList.remove('rotate-180');
|
||||
if (text) text.textContent = 'Toon ticket inhoud';
|
||||
}
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function clearPlanTaskTitle() {
|
||||
const input = document.getElementById('planTaskTitleInput');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlanEndTimeBadge() {
|
||||
const startInput = document.getElementById('planStartTimeInput');
|
||||
const durationInput = document.getElementById('planDurationInput');
|
||||
const badge = document.getElementById('planEndTimeBadge');
|
||||
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 planInNextOpenBlockJira(startFromNextWorkday = false) {
|
||||
const durationInput = document.getElementById('planDurationInput');
|
||||
const duration = parseInt(durationInput?.value || '45', 10);
|
||||
const btn = document.getElementById('btnJiraPlanOpenBlock');
|
||||
const origHtml = btn ? btn.innerHTML : '';
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>`;
|
||||
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('planDateInput');
|
||||
const startInput = document.getElementById('planStartTimeInput');
|
||||
if (dateInput) dateInput.value = slot.date;
|
||||
if (startInput) startInput.value = slot.start;
|
||||
|
||||
updatePlanEndTimeBadge();
|
||||
|
||||
const notice = document.getElementById('jiraPlanOpenBlockNotice');
|
||||
const noticeText = document.getElementById('jiraPlanOpenBlockNoticeText');
|
||||
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 closeOutsideWorkhoursModalJira(choice) {
|
||||
const modal = document.getElementById('outsideWorkhoursModalJira');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
|
||||
if (choice === 'JA') {
|
||||
if (_outsideWorkhoursPendingJira) {
|
||||
_outsideWorkhoursPendingJira.bypassWorkhours = true;
|
||||
submitPlanTask(null, _outsideWorkhoursPendingJira);
|
||||
}
|
||||
} else if (choice === 'NEE') {
|
||||
_outsideWorkhoursPendingJira = null;
|
||||
planInNextOpenBlockJira(true);
|
||||
} else {
|
||||
_outsideWorkhoursPendingJira = null;
|
||||
}
|
||||
}
|
||||
|
||||
function openPlanModal(item) {
|
||||
const ticket = (typeof item === 'string')
|
||||
? (ticketStore.get(item) || (jiraData.assigned_issues || []).concat(jiraData.team_issues || []).find(t => t.key === item))
|
||||
: item;
|
||||
if (!ticket) return;
|
||||
|
||||
_activeJiraTicket = ticket;
|
||||
|
||||
document.getElementById('planIssueKey').value = ticket.key;
|
||||
document.getElementById('planIssueSummary').value = ticket.summary;
|
||||
document.getElementById('planIssueUrl').value = ticket.url;
|
||||
@@ -1413,6 +1641,27 @@ function openPlanModal(item) {
|
||||
document.getElementById('planTicketProject').innerText = '• ' + (ticket.project || 'Project');
|
||||
document.getElementById('planTicketSummaryText').innerText = ticket.summary;
|
||||
|
||||
// Vul aanpasbare taaktitel in
|
||||
const titleInput = document.getElementById('planTaskTitleInput');
|
||||
if (titleInput) {
|
||||
titleInput.value = `Oppakken Jira item ${ticket.key} - ${ticket.summary}`;
|
||||
}
|
||||
|
||||
// Vul ticket omschrijving preview
|
||||
const descContainer = document.getElementById('planTicketDescContainer');
|
||||
if (descContainer) {
|
||||
descContainer.textContent = ticket.description || 'Geen gedetailleerde omschrijving beschikbaar in Jira.';
|
||||
descContainer.classList.add('hidden');
|
||||
}
|
||||
const chevron = document.getElementById('planTicketDescChevron');
|
||||
if (chevron) chevron.classList.remove('rotate-180');
|
||||
const descText = document.getElementById('planTicketDescToggleText');
|
||||
if (descText) descText.textContent = 'Toon ticket inhoud';
|
||||
|
||||
// Verberg open blok notice
|
||||
const notice = document.getElementById('jiraPlanOpenBlockNotice');
|
||||
if (notice) notice.classList.add('hidden');
|
||||
|
||||
// Story Points → Duur conversie (1 SP = 30 minuten, configurabel)
|
||||
const spBadge = document.getElementById('planSpBadge');
|
||||
const spHint = document.getElementById('planSpHint');
|
||||
@@ -1420,7 +1669,7 @@ function openPlanModal(item) {
|
||||
const sp = parseFloat(ticket.story_points);
|
||||
if (!isNaN(sp) && sp > 0) {
|
||||
const spMinutes = Math.round(sp * 30); // 1 SP = 30 minuten
|
||||
const closestDuration = [30, 45, 60, 90, 120].reduce((prev, curr) =>
|
||||
const closestDuration = [15, 30, 45, 60, 90, 120].reduce((prev, curr) =>
|
||||
Math.abs(curr - spMinutes) < Math.abs(prev - spMinutes) ? curr : prev
|
||||
);
|
||||
const durationSelect = document.getElementById('planDurationInput');
|
||||
@@ -1449,6 +1698,8 @@ function openPlanModal(item) {
|
||||
if (r.value === focizPrio) r.checked = true;
|
||||
}
|
||||
|
||||
updatePlanEndTimeBadge();
|
||||
|
||||
const modal = document.getElementById('planTaskModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
@@ -1461,19 +1712,18 @@ function closePlanModal() {
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
async function submitPlanTask(e) {
|
||||
e.preventDefault();
|
||||
async function submitPlanTask(e, options = {}) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('planSubmitBtn');
|
||||
const oldBtnText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
const oldBtnText = btn ? btn.innerHTML : '';
|
||||
|
||||
const key = document.getElementById('planIssueKey').value;
|
||||
const summary = document.getElementById('planIssueSummary').value;
|
||||
const customTitle = (document.getElementById('planTaskTitleInput')?.value || '').trim();
|
||||
const url = document.getElementById('planIssueUrl').value;
|
||||
const planDate = document.getElementById('planDateInput').value;
|
||||
const timeBlockStart = document.getElementById('planStartTimeInput').value;
|
||||
const timeBlockStart = (document.getElementById('planStartTimeInput')?.value || '').trim();
|
||||
const durationMinutes = parseInt(document.getElementById('planDurationInput').value, 10);
|
||||
|
||||
let priority = 'P2';
|
||||
@@ -1482,6 +1732,45 @@ async function submitPlanTask(e) {
|
||||
if (r.checked) priority = r.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) {
|
||||
_outsideWorkhoursPendingJira = { key, summary, title: customTitle, url, planDate, timeBlockStart, durationMinutes, priority };
|
||||
|
||||
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('outsideWorkhoursSelectedSlotJira');
|
||||
if (slotEl) slotEl.innerText = `${timeBlockStart} – ${endStr} (${durationMinutes} min)`;
|
||||
|
||||
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
|
||||
if (outsideModal) {
|
||||
outsideModal.classList.remove('hidden');
|
||||
outsideModal.classList.add('flex');
|
||||
setTimeout(() => {
|
||||
const btnNo = document.getElementById('btnOutsideWorkhoursNoJira');
|
||||
if (btnNo) btnNo.focus();
|
||||
}, 50);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/integrations.php?action=plan_jira_task', {
|
||||
method: 'POST',
|
||||
@@ -1489,6 +1778,7 @@ async function submitPlanTask(e) {
|
||||
body: JSON.stringify({
|
||||
key,
|
||||
summary,
|
||||
title: customTitle || `[${key}] ${summary}`,
|
||||
url,
|
||||
plan_date: planDate,
|
||||
time_block_start: timeBlockStart,
|
||||
@@ -1512,10 +1802,12 @@ async function submitPlanTask(e) {
|
||||
} catch (err) {
|
||||
alert('Netwerkfout: ' + err.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = oldBtnText;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal 2: Plaats op Kanban Bord
|
||||
@@ -1681,11 +1973,23 @@ function escapeHtml(str) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Sluit modals bij Escape toets
|
||||
// Toetsenbordnavigatie voor alle modals (Escape & Enter)
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
|
||||
if (outsideModal && !outsideModal.classList.contains('hidden')) {
|
||||
closeOutsideWorkhoursModalJira('CANCEL');
|
||||
return;
|
||||
}
|
||||
closePlanModal();
|
||||
closeBoardModal();
|
||||
} else if (e.key === 'Enter') {
|
||||
const outsideModal = document.getElementById('outsideWorkhoursModalJira');
|
||||
if (outsideModal && !outsideModal.classList.contains('hidden')) {
|
||||
e.preventDefault();
|
||||
closeOutsideWorkhoursModalJira('NEE');
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
</div>
|
||||
|
||||
<!-- MODAL 1: E-mail Inplannen in Fociz Dagplanning -->
|
||||
<div id="planMailModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="planMailModal" onclick="if(event.target === this) closePlanMailModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -253,7 +253,7 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
<p class="text-[11px] text-slate-400">Plaats dit actiepunt direct op jouw Fociz tijdlijn</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closePlanMailModal()" class="text-slate-400 hover:text-slate-600 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<button onclick="closePlanMailModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 p-1.5 rounded-lg transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -268,21 +268,46 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
<input type="text" id="planMailSubject" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white font-semibold focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<!-- Datum & Slimme "Plan in open blok" knop -->
|
||||
<div class="space-y-2 pt-1 border-t border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||||
<button type="button" onclick="planInNextOpenBlockMail()" id="btnMailPlanOpenBlock" 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>
|
||||
|
||||
<!-- Open blok banner -->
|
||||
<div id="mailPlanOpenBlockNotice" 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="mailPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('mailPlanOpenBlockNotice').classList.add('hidden')" class="p-0.5 text-emerald-600 dark:text-emerald-400 hover:text-emerald-800 rounded cursor-pointer">
|
||||
<i data-lucide="x" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Datum</label>
|
||||
<input type="date" id="planMailDate" value="<?= date('Y-m-d') ?>" required class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Starttijd (optioneel)</label>
|
||||
<input type="text" id="planMailStartTime" placeholder="bijv. 10:30" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300">Starttijd (optioneel)</label>
|
||||
<span id="planMailEndTimeBadge" class="text-[10px] text-indigo-600 dark:text-indigo-400 font-mono font-semibold"></span>
|
||||
</div>
|
||||
<input type="text" id="planMailStartTime" oninput="updatePlanMailEndTimeBadge()" placeholder="bijv. 10:30" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Tijdsduur</label>
|
||||
<select id="planMailDuration" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<select id="planMailDuration" onchange="updatePlanMailEndTimeBadge()" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30" selected>30 minuten</option>
|
||||
<option value="45">45 minuten</option>
|
||||
@@ -292,7 +317,7 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Prioriteit</label>
|
||||
<select id="planMailPriority" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<select id="planMailPriority" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 cursor-pointer">
|
||||
<option value="P1">P1 - Urgent / Brandend</option>
|
||||
<option value="P2" selected>P2 - Belangrijk</option>
|
||||
<option value="P3">P3 - Regulier</option>
|
||||
@@ -313,8 +338,54 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Waarschuwing Buiten Werktijden (Mail) -->
|
||||
<div id="outsideWorkhoursModalMail" onclick="if(event.target === this) closeOutsideWorkhoursModalMail('CANCEL')" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-amber-500/30 rounded-2xl w-full max-w-md shadow-2xl p-6 space-y-4">
|
||||
<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="closeOutsideWorkhoursModalMail('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="outsideWorkhoursSelectedSlotMail" 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="outsideWorkhoursBoundsMail" class="font-mono font-semibold">08:30 – 17:00</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="btnOutsideWorkhoursNoMail" onclick="closeOutsideWorkhoursModalMail('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="btnOutsideWorkhoursYesMail" onclick="closeOutsideWorkhoursModalMail('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 2: E-mail Beantwoorden -->
|
||||
<div id="replyModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="replyModal" onclick="if(event.target === this) closeReplyModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -360,7 +431,7 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
|
||||
</div>
|
||||
|
||||
<!-- MODAL 3: E-mail Doorsturen -->
|
||||
<div id="forwardModal" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="forwardModal" onclick="if(event.target === this) closeForwardModal()" class="fixed inset-0 z-50 bg-slate-950/60 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div class="bg-white dark:bg-[#111624] border border-slate-200 dark:border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-7 shadow-2xl space-y-5 animate-in fade-in zoom-in-95 duration-150">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -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 = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Zoeken...</span>`;
|
||||
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;
|
||||
const origHtml = btn ? btn.innerHTML : '';
|
||||
if (btn) {
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i><span>Inplannen...</span>`;
|
||||
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,10 +935,12 @@ async function submitPlanEmail(e) {
|
||||
} catch(err) {
|
||||
alert('Netwerkfout: ' + err.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openReplyModal() {
|
||||
@@ -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) {
|
||||
|
||||
+468
-23
@@ -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];
|
||||
@@ -1771,7 +1771,7 @@ if ($isAjaxRequest) {
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Modal: Jira Ticket Inplannen (met datum, tijdsuggestie & aanpasbare titel) -->
|
||||
<div id="jiraPlanModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="jiraPlanModal" onclick="if(event.target === this) closeJiraPlanModal()" 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 max-h-[90vh] overflow-y-auto shadow-2xl space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -1825,21 +1825,46 @@ if ($isAjaxRequest) {
|
||||
<input type="text" id="jiraPlanCustomTitle" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500 placeholder-slate-400" placeholder="Oppakken Jira item ...">
|
||||
</div>
|
||||
|
||||
<!-- 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-[11px] font-bold text-slate-700 dark:text-slate-300">Datum & Tijdblok</span>
|
||||
<button type="button" onclick="planJiraInNextOpenBlock()" id="btnJiraPlanOpenBlock" 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="jiraPlanOpenBlockNotice" 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="jiraPlanOpenBlockNoticeText" class="truncate font-semibold"></span>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('jiraPlanOpenBlockNotice').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>
|
||||
|
||||
<!-- Datum & Starttijd -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label for="jiraPlanDate" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Datum
|
||||
</label>
|
||||
<input type="date" id="jiraPlanDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<input type="date" id="jiraPlanDate" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" onchange="updateJiraPlanEndTimeDisplay()">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="jiraPlanStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label for="jiraPlanStartTime" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300">
|
||||
Starttijd
|
||||
</label>
|
||||
<span id="jiraPlanEndTimeBadge" class="text-[10px] font-mono text-indigo-600 dark:text-indigo-400 font-semibold"></span>
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<input type="time" id="jiraPlanStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
|
||||
<input type="time" id="jiraPlanStartTime" class="w-full bg-white dark:bg-[#0c0f17] border border-slate-200 dark:border-slate-800 rounded-xl pl-3 pr-8 py-2 text-sm text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" oninput="updateJiraPlanEndTimeDisplay()">
|
||||
<button type="button" onclick="clearJiraPlanStartTime()" title="Starttijd wissen" class="absolute right-2 p-1 text-slate-400 hover:text-rose-500 dark:hover:text-rose-400 rounded transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
@@ -1962,7 +1987,7 @@ if ($isAjaxRequest) {
|
||||
</div>
|
||||
|
||||
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
|
||||
<div id="editTaskModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4">
|
||||
<div id="editTaskModal" onclick="if(event.target === this) closeEditTaskModal()" 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="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -2084,8 +2109,54 @@ if ($isAjaxRequest) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Waarschuwing Buiten Werktijden -->
|
||||
<div id="outsideWorkhoursModal" onclick="if(event.target === this) closeOutsideWorkhoursModal('CANCEL')" 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 id="planEmailDirectModal" onclick="if(event.target === this) closePlanEmailDirectModal()" 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="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
@@ -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 = '<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
|
||||
})
|
||||
});
|
||||
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;
|
||||
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,14 +3677,18 @@ 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) {
|
||||
@@ -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;
|
||||
|
||||
+7
-2
@@ -206,7 +206,7 @@ $quarters = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
</div>
|
||||
|
||||
<!-- Modal: Nieuw Roadmap Item Handmatig Toevoegen -->
|
||||
<div id="newItemModal" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div id="newItemModal" onclick="if(event.target === this) this.classList.add('hidden')" class="hidden fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="fociz-card p-6 sm:p-7 shadow-2xl max-w-lg w-full space-y-4">
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800">
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white flex items-center space-x-2">
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
+7
-1
@@ -224,7 +224,7 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
|
||||
</div>
|
||||
|
||||
<!-- Gedeelde MODAL 1: Taak Toevoegen of Bewerken (100% gesynchroniseerd met Kanban bord) -->
|
||||
<div id="cardModal" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div id="cardModal" onclick="if(event.target === this) closeCardModal()" class="fixed inset-0 z-50 bg-slate-950/70 backdrop-blur-sm hidden items-center justify-center p-4 overflow-y-auto">
|
||||
<div class="fociz-card p-6 w-full max-w-xl shadow-2xl space-y-5 my-8">
|
||||
|
||||
<div class="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80">
|
||||
@@ -1073,6 +1073,12 @@ function escapeHtml(str) {
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeCardModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user