Implement seamless AJAX date switching and in-place updates in planner to preserve scroll position and eliminate full-page reload flickers

This commit is contained in:
2026-09-09 19:02:40 +02:00
parent 0b9e92eded
commit 2062fd065a
+147 -20
View File
@@ -4,13 +4,22 @@
* Gekoppeld aan Microsoft Outlook 365 (Agenda) en Atlassian Jira (Tickets) * Gekoppeld aan Microsoft Outlook 365 (Agenda) en Atlassian Jira (Tickets)
*/ */
// Controleer of dit een AJAX verzoek is voor een geruisloze datumwisseling
$isAjaxRequest = (!empty($_GET['ajax']) && $_GET['ajax'] === '1');
if ($isAjaxRequest) {
require_once __DIR__ . '/includes/auth.php';
$currentUser = require_auth(true);
} else {
require_once __DIR__ . '/includes/header.php'; require_once __DIR__ . '/includes/header.php';
}
require_once __DIR__ . '/includes/settings.php'; require_once __DIR__ . '/includes/settings.php';
require_once __DIR__ . '/includes/integrations/microsoft.php'; require_once __DIR__ . '/includes/integrations/microsoft.php';
require_once __DIR__ . '/includes/integrations/jira.php'; require_once __DIR__ . '/includes/integrations/jira.php';
$db = get_db_connection(); $db = get_db_connection();
$userId = $currentUser['id']; $userId = (int)($currentUser['id'] ?? 0);
$today = date('Y-m-d'); $today = date('Y-m-d');
$selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today; $selectedDate = (!empty($_GET['date']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $_GET['date'])) ? $_GET['date'] : $today;
$isTodaySelected = ($selectedDate === $today); $isTodaySelected = ($selectedDate === $today);
@@ -294,7 +303,8 @@ for ($h = $startHour; $h <= $endHour; $h++) {
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
</style> </style>
<div class="space-y-6 max-w-7xl mx-auto"> <div id="plannerContentArea" class="space-y-6 max-w-7xl mx-auto transition-opacity duration-200">
<?php if ($isAjaxRequest) ob_start(); ?>
<!-- Top Banner: Datum, Hoofddoel & Integraties Status --> <!-- Top Banner: Datum, Hoofddoel & Integraties Status -->
<div class="fociz-card p-6 sm:p-7 relative overflow-hidden"> <div class="fociz-card p-6 sm:p-7 relative overflow-hidden">
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4"> <div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
@@ -382,16 +392,16 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-4 border-b border-slate-100 dark:border-slate-800/60"> <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 mb-4 border-b border-slate-100 dark:border-slate-800/60">
<!-- Navigatie controls --> <!-- Navigatie controls -->
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<a href="planner.php?date=<?= $prevWeekDate ?>" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Vorige week (Maandag <?= date('j M', strtotime($prevWeekDate)) ?>)"> <a href="planner.php?date=<?= $prevWeekDate ?>" onclick="switchPlannerDate('<?= $prevWeekDate ?>', event)" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Vorige week (Maandag <?= date('j M', strtotime($prevWeekDate)) ?>)">
<i data-lucide="chevron-left" class="w-4 h-4"></i> <i data-lucide="chevron-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">Vorige week</span> <span class="hidden sm:inline">Vorige week</span>
</a> </a>
<a href="planner.php?date=<?= $today ?>" class="px-3.5 py-1.5 rounded-xl <?= $isTodaySelected ? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold shadow-xs' : 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 font-semibold shadow-2xs' ?> text-xs transition" title="Spring direct naar vandaag"> <a href="planner.php?date=<?= $today ?>" onclick="switchPlannerDate('<?= $today ?>', event)" class="px-3.5 py-1.5 rounded-xl <?= $isTodaySelected ? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 font-bold shadow-xs' : 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 font-semibold shadow-2xs' ?> text-xs transition" title="Spring direct naar vandaag">
Vandaag Vandaag
</a> </a>
<a href="planner.php?date=<?= $nextWeekDate ?>" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Volgende week (Maandag <?= date('j M', strtotime($nextWeekDate)) ?>)"> <a href="planner.php?date=<?= $nextWeekDate ?>" onclick="switchPlannerDate('<?= $nextWeekDate ?>', event)" class="flex items-center space-x-1.5 px-3.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800/80 dark:hover:bg-slate-700/80 text-slate-700 dark:text-slate-300 text-xs font-semibold transition shadow-2xs" title="Volgende week (Maandag <?= date('j M', strtotime($nextWeekDate)) ?>)">
<span class="hidden sm:inline">Volgende week</span> <span class="hidden sm:inline">Volgende week</span>
<i data-lucide="chevron-right" class="w-4 h-4"></i> <i data-lucide="chevron-right" class="w-4 h-4"></i>
</a> </a>
@@ -414,7 +424,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
type="date" type="date"
id="jumpDateInput" id="jumpDateInput"
value="<?= htmlspecialchars($selectedDate) ?>" value="<?= htmlspecialchars($selectedDate) ?>"
onchange="if(this.value) window.location.href='planner.php?date=' + this.value;" onchange="if(this.value) switchPlannerDate(this.value, event);"
class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs" class="bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl px-3 py-1.5 text-xs text-slate-800 dark:text-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500/30 font-medium cursor-pointer shadow-2xs"
> >
</div> </div>
@@ -425,6 +435,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
<?php foreach ($weekDays as $wd): ?> <?php foreach ($weekDays as $wd): ?>
<a <a
href="planner.php?date=<?= $wd['date'] ?>" href="planner.php?date=<?= $wd['date'] ?>"
onclick="switchPlannerDate('<?= $wd['date'] ?>', event)"
class="fociz-card-interactive group relative flex flex-col p-3.5 rounded-2xl border transition-all text-left <?= $wd['is_selected'] class="fociz-card-interactive group relative flex flex-col p-3.5 rounded-2xl border transition-all text-left <?= $wd['is_selected']
? 'bg-gradient-to-b from-indigo-500/15 via-indigo-500/5 to-transparent dark:from-indigo-500/25 dark:via-indigo-500/10 dark:to-transparent border-indigo-500 ring-2 ring-indigo-500/40 shadow-sm' ? 'bg-gradient-to-b from-indigo-500/15 via-indigo-500/5 to-transparent dark:from-indigo-500/25 dark:via-indigo-500/10 dark:to-transparent border-indigo-500 ring-2 ring-indigo-500/40 shadow-sm'
: 'bg-slate-50/70 dark:bg-[#0c0f17]/50 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 hover:bg-white dark:hover:bg-[#111624] shadow-2xs' ?>" : 'bg-slate-50/70 dark:bg-[#0c0f17]/50 border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 hover:bg-white dark:hover:bg-[#111624] shadow-2xs' ?>"
@@ -917,6 +928,28 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div> </div>
</div> </div>
</div> </div>
<?php
if ($isAjaxRequest) {
$contentHtml = ob_get_clean();
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'selectedDate' => $selectedDate,
'formattedDate' => $formattedSelectedDate,
'weekStart' => $weekStart,
'weekEnd' => $weekEnd,
'weekNumber' => $weekNumber,
'yearNumber' => $yearNumber,
'formattedWeek' => $formattedWeekRange,
'syncHash' => $initialSyncHash,
'focusGoal' => $dailyPlan['focus_goal'] ?? '',
'dailyNotes' => $dailyPlan['notes'] ?? '',
'title' => 'Fociz — Dagplanning (' . $formattedSelectedDate . ')',
'html' => $contentHtml,
]);
exit;
}
?>
</div> </div>
<!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) --> <!-- Modal: Taak & Tijdblok Bewerken (Vrij invullen van tijden & duur) -->
@@ -1134,8 +1167,103 @@ function selectFocusTask(title) {
document.getElementById('activeTaskDisplay').innerText = title; document.getElementById('activeTaskDisplay').innerText = title;
} }
// --- Globale Planner Status & Naadloos AJAX Datumbeheer ---
let activePlannerDate = '<?= $selectedDate ?>';
let weekStartDate = '<?= $weekStart ?>';
let weekEndDate = '<?= $weekEnd ?>';
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
let isDateSwitching = false;
/**
* Wisselt de actieve plannerdatum geruisloos via AJAX
* Behoudt de huidige schermhoogte (scrollpositie) en voorkomt flitsen/herladen
*/
async function switchPlannerDate(newDate, event = null, pushState = true) {
if (event && event.preventDefault) {
event.preventDefault();
}
if (!newDate || isDateSwitching) return;
const container = document.getElementById('plannerContentArea');
if (!container) {
window.location.href = 'planner.php?date=' + encodeURIComponent(newDate);
return;
}
// Onthoud exact de huidige scrollpositie
const currentScrollY = window.scrollY;
isDateSwitching = true;
// Subtiele visuele feedback zonder layout-shift
container.classList.add('opacity-60', 'pointer-events-none');
try {
const response = await fetch(`planner.php?date=${encodeURIComponent(newDate)}&ajax=1`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP fout ${response.status}`);
}
const data = await response.json();
if (!data || !data.success || !data.html) {
throw new Error(data?.error || 'Ongeldige serverrespons');
}
// Vervang de dynamische plannerinhoud geruisloos
container.innerHTML = data.html;
// Werk globale variabelen bij
activePlannerDate = data.selectedDate;
if (data.weekStart) weekStartDate = data.weekStart;
if (data.weekEnd) weekEndDate = data.weekEnd;
if (data.syncHash) currentSyncHash = data.syncHash;
// Werk paginatitel bij
if (data.title) {
document.title = data.title;
}
// Werk URL bij in adresbalk zonder paginaherlaad
if (pushState && window.history && window.history.pushState) {
const newUrl = `planner.php?date=${encodeURIComponent(newDate)}`;
window.history.pushState({ date: newDate }, data.title || '', newUrl);
}
// Render Lucide SVG-iconen in de nieuwe inhoud
if (typeof lucide !== 'undefined' && lucide.createIcons) {
lucide.createIcons();
}
// Garandeer dat het scherm exact op dezelfde scrollhoogte blijft
window.scrollTo({
top: currentScrollY,
behavior: 'instant'
});
} catch (err) {
console.error('Fout bij wisselen van datum via AJAX:', err);
// Veilige fallback bij netwerkonderbreking
window.location.href = 'planner.php?date=' + encodeURIComponent(newDate);
} finally {
isDateSwitching = false;
container.classList.remove('opacity-60', 'pointer-events-none');
}
}
// Browser Terug/Vooruit knoppen opvangen (popstate)
window.addEventListener('popstate', (event) => {
const urlParams = new URLSearchParams(window.location.search);
const dateFromUrl = urlParams.get('date');
if (dateFromUrl && dateFromUrl !== activePlannerDate) {
switchPlannerDate(dateFromUrl, null, false);
}
});
// --- Dagdoel & Taken AJAX --- // --- Dagdoel & Taken AJAX ---
const activePlannerDate = '<?= $selectedDate ?>';
async function saveGoal() { async function saveGoal() {
const focusGoal = document.getElementById('focusGoalInput').value; const focusGoal = document.getElementById('focusGoalInput').value;
@@ -1348,7 +1476,10 @@ async function submitNewTask() {
if (syncOutlook && !data.outlook_synced) { if (syncOutlook && !data.outlook_synced) {
alert('Let op: De taak is opgeslagen in Fociz, maar kon niet direct naar Microsoft Outlook worden verzonden:\n\n' + (data.outlook_error || 'Controleer uw verbinding in Mijn Profiel.')); alert('Let op: De taak is opgeslagen in Fociz, maar kon niet direct naar Microsoft Outlook worden verzonden:\n\n' + (data.outlook_error || 'Controleer uw verbinding in Mijn Profiel.'));
} }
location.reload(); toggleAddTaskForm();
const titleInput = document.getElementById('newTaskTitle');
if (titleInput) titleInput.value = '';
switchPlannerDate(activePlannerDate, null, false);
} else { } else {
alert(data.error || 'Fout bij opslaan van de taak'); alert(data.error || 'Fout bij opslaan van de taak');
} }
@@ -1528,7 +1659,8 @@ async function submitEditTask() {
}); });
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
location.reload(); closeEditTaskModal();
switchPlannerDate(activePlannerDate, null, false);
} else { } else {
alert(data.error || 'Kon taak niet bijwerken'); alert(data.error || 'Kon taak niet bijwerken');
if (saveBtn) { if (saveBtn) {
@@ -1599,7 +1731,7 @@ async function quickImportJira(btn, title, jiraKey, url) {
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>'; btn.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>';
location.reload(); switchPlannerDate(activePlannerDate, null, false);
} else { } else {
btn.innerHTML = originalHtml; btn.innerHTML = originalHtml;
btn.disabled = false; btn.disabled = false;
@@ -1618,7 +1750,7 @@ async function toggleTask(taskId, isCompleted) {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ task_id: taskId, is_completed: isCompleted }) body: JSON.stringify({ task_id: taskId, is_completed: isCompleted })
}); });
location.reload(); switchPlannerDate(activePlannerDate, null, false);
} }
async function deleteTask(taskId) { async function deleteTask(taskId) {
@@ -1628,7 +1760,7 @@ async function deleteTask(taskId) {
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ task_id: taskId }) body: JSON.stringify({ task_id: taskId })
}); });
document.getElementById(`task-row-${taskId}`)?.remove(); switchPlannerDate(activePlannerDate, null, false);
} }
// --- AI Co-Pilot Slimme Dagplanning (Release v1.5) --- // --- AI Co-Pilot Slimme Dagplanning (Release v1.5) ---
@@ -1652,7 +1784,7 @@ async function aiSmartSchedule(btn) {
if (data.success) { if (data.success) {
alert(`Succes! AI Co-Pilot heeft ${data.scheduled_count} ta(a)k(en) optimaal ingepland op je tijdlijn.`); alert(`Succes! AI Co-Pilot heeft ${data.scheduled_count} ta(a)k(en) optimaal ingepland op je tijdlijn.`);
location.reload(); switchPlannerDate(activePlannerDate, null, false);
} else { } else {
alert(data.error || 'Kon dagplanning niet genereren via AI'); alert(data.error || 'Kon dagplanning niet genereren via AI');
if (btn) { if (btn) {
@@ -1672,10 +1804,7 @@ async function aiSmartSchedule(btn) {
} }
// --- Outlook 365 Auto-Sync & Achtergrond Controle --- // --- Outlook 365 Auto-Sync & Achtergrond Controle ---
const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>; const isOutlookConnected = <?= $isOutlookConnected ? 'true' : 'false' ?>;
let currentSyncHash = '<?= $initialSyncHash ?? '' ?>';
const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>; const syncIntervalSeconds = <?= (int)($syncIntervalSeconds ?? 30) ?>;
const weekStartDate = '<?= $weekStart ?>';
const weekEndDate = '<?= $weekEnd ?>';
let isSyncCheckActive = false; let isSyncCheckActive = false;
async function checkOutlookChanges(isManual = false) { async function checkOutlookChanges(isManual = false) {
@@ -1708,10 +1837,8 @@ async function checkOutlookChanges(isManual = false) {
if (data.has_changes) { if (data.has_changes) {
if (statusText) statusText.innerText = 'Wijzigingen bijwerken...'; if (statusText) statusText.innerText = 'Wijzigingen bijwerken...';
saveTimerStateBeforeReload(); await switchPlannerDate(activePlannerDate, null, false);
setTimeout(() => { if (statusText) statusText.innerText = 'Outlook 365 Live';
location.reload();
}, 400);
return; return;
} else { } else {
currentSyncHash = data.hash; currentSyncHash = data.hash;