feat(outlook): fix calendar sync format and enable bidirectional task-to-outlook scheduling
This commit is contained in:
+162
-36
@@ -50,15 +50,13 @@ $isLiveOutlook = false;
|
||||
|
||||
if ($isOutlookConnected) {
|
||||
$outlookEvents = microsoft_fetch_today_events($userId, $today);
|
||||
$isLiveOutlook = !empty($outlookEvents) || true; // Live koppeling is actief
|
||||
}
|
||||
|
||||
// Fallback naar sandbox data indien Outlook niet live gekoppeld is
|
||||
if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
$isLiveOutlook = true;
|
||||
} else {
|
||||
// Alleen tonen indien de gebruiker Microsoft Outlook nog niet gekoppeld heeft
|
||||
$outlookEvents = [
|
||||
[
|
||||
'id' => 'evt-1',
|
||||
'subject' => 'Daily Team Standup & Focus Sync',
|
||||
'subject' => 'Voorbeeld: Team Standup & Sync',
|
||||
'start' => '09:00',
|
||||
'end' => '09:30',
|
||||
'location' => 'Microsoft Teams',
|
||||
@@ -67,7 +65,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
],
|
||||
[
|
||||
'id' => 'evt-2',
|
||||
'subject' => 'Sprint & Roadmap Review met Stakeholders',
|
||||
'subject' => 'Voorbeeld: Roadmap Review met Stakeholders',
|
||||
'start' => '11:00',
|
||||
'end' => '12:00',
|
||||
'location' => 'Vergaderruimte A & Teams',
|
||||
@@ -76,7 +74,7 @@ if (!$isOutlookConnected || empty($outlookEvents)) {
|
||||
],
|
||||
[
|
||||
'id' => 'evt-3',
|
||||
'subject' => 'Architectuur & MariaDB Database Evaluatie',
|
||||
'subject' => 'Voorbeeld: Architectuur & DB Evaluatie',
|
||||
'start' => '14:30',
|
||||
'end' => '15:30',
|
||||
'location' => 'Online Teams Call',
|
||||
@@ -91,16 +89,38 @@ $jiraData = jira_get_user_issues($userId);
|
||||
$jiraIssues = $jiraData['issues'] ?? [];
|
||||
$isJiraLive = !empty($jiraData['success']) && empty($jiraData['is_demo']);
|
||||
|
||||
// Filter Outlook events die afkomstig zijn van Fociz taken om duplicaten op de tijdlijn te voorkomen
|
||||
$syncedTaskExternalIds = [];
|
||||
foreach ($tasks as $t) {
|
||||
if (!empty($t['external_id'])) {
|
||||
$syncedTaskExternalIds[$t['external_id']] = true;
|
||||
}
|
||||
}
|
||||
$displayedOutlookEvents = [];
|
||||
foreach ($outlookEvents as $evt) {
|
||||
if (!empty($evt['id']) && isset($syncedTaskExternalIds[$evt['id']])) {
|
||||
continue;
|
||||
}
|
||||
$displayedOutlookEvents[] = $evt;
|
||||
}
|
||||
|
||||
// 5. Tijdlijn uren bepalen
|
||||
$startHour = 8;
|
||||
$endHour = 18;
|
||||
foreach ($outlookEvents as $e) {
|
||||
foreach ($displayedOutlookEvents as $e) {
|
||||
if (!empty($e['start'])) {
|
||||
$h = (int)explode(':', $e['start'])[0];
|
||||
if ($h < $startHour && $h >= 6) $startHour = $h;
|
||||
if ($h > $endHour && $h <= 22) $endHour = $h + 1;
|
||||
}
|
||||
}
|
||||
foreach ($tasks as $t) {
|
||||
if (!empty($t['time_block_start'])) {
|
||||
$h = (int)explode(':', $t['time_block_start'])[0];
|
||||
if ($h < $startHour && $h >= 6) $startHour = $h;
|
||||
if ($h > $endHour && $h <= 22) $endHour = $h + 1;
|
||||
}
|
||||
}
|
||||
$hours = [];
|
||||
for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
$hours[] = sprintf('%02d:00', $h);
|
||||
@@ -254,11 +274,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Taaktitel</label>
|
||||
<input type="text" id="newTaskTitle" placeholder="Wat moet er vandaag gedaan worden?" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-3 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-emerald-500">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Prioriteit</label>
|
||||
<select id="newTaskPriority" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs focus:outline-none">
|
||||
<option value="P1">P1 - Brandend / Hoogste Focus</option>
|
||||
<option value="P1">P1 - Brandend / Focus</option>
|
||||
<option value="P2" selected>P2 - Belangrijk (Standaard)</option>
|
||||
<option value="P3">P3 - Regulier / Lage druk</option>
|
||||
</select>
|
||||
@@ -267,7 +287,31 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Tijdblok (Starttijd)</label>
|
||||
<input type="text" id="newTaskTime" placeholder="Bijv. 09:30" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs font-mono focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-600 dark:text-slate-400 mb-1">Tijdsduur</label>
|
||||
<select id="newTaskDuration" class="w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-800 rounded-xl px-2.5 py-1.5 text-slate-900 dark:text-white text-xs focus:outline-none">
|
||||
<option value="15">15 minuten</option>
|
||||
<option value="30" selected>30 minuten</option>
|
||||
<option value="45">45 minuten</option>
|
||||
<option value="60">60 minuten (1 uur)</option>
|
||||
<option value="90">90 minuten (1,5 uur)</option>
|
||||
<option value="120">120 minuten (2 uur)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($isOutlookConnected): ?>
|
||||
<div class="p-2.5 rounded-xl bg-blue-50/70 dark:bg-blue-950/40 border border-blue-200/70 dark:border-blue-800/40">
|
||||
<label class="flex items-center space-x-2 text-xs text-blue-900 dark:text-blue-200 cursor-pointer select-none">
|
||||
<input type="checkbox" id="newTaskSyncOutlook" checked class="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 border-slate-300 dark:border-slate-700">
|
||||
<span class="flex items-center space-x-1.5 font-medium">
|
||||
<i data-lucide="calendar" class="w-3.5 h-3.5 text-blue-500"></i>
|
||||
<span>Direct inplannen in Microsoft Outlook agenda</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="flex space-x-2 pt-1">
|
||||
<button onclick="submitNewTask()" class="px-4 py-1.5 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-bold rounded-lg text-xs transition cursor-pointer">Opslaan</button>
|
||||
<button onclick="toggleAddTaskForm()" class="px-3 py-1.5 bg-slate-200 hover:bg-slate-300 dark:bg-slate-800 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-400 rounded-lg text-xs transition cursor-pointer">Annuleren</button>
|
||||
@@ -296,6 +340,11 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<span class="px-1.5 py-0.2 rounded bg-sky-500/10 border border-sky-500/20 text-sky-600 dark:text-sky-400 font-mono text-[9px] font-bold">
|
||||
JIRA
|
||||
</span>
|
||||
<?php elseif ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])): ?>
|
||||
<span class="px-1.5 py-0.2 rounded bg-blue-500/10 border border-blue-500/20 text-blue-600 dark:text-blue-400 font-mono text-[9px] font-bold flex items-center space-x-0.5">
|
||||
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
|
||||
<span>OUTLOOK</span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="text-xs font-medium truncate <?= $task['is_completed'] ? 'line-through text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-200' ?>">
|
||||
<?= htmlspecialchars($task['title']) ?>
|
||||
@@ -306,12 +355,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?php if (!empty($task['time_block_start'])): ?>
|
||||
<span class="text-[10px] text-slate-500 dark:text-slate-400 font-mono flex items-center space-x-1">
|
||||
<i data-lucide="clock" class="w-2.5 h-2.5"></i>
|
||||
<span><?= htmlspecialchars($task['time_block_start']) ?></span>
|
||||
<span><?= htmlspecialchars($task['time_block_start']) ?><?= !empty($task['time_block_end']) ? ' - ' . htmlspecialchars($task['time_block_end']) : '' ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($task['external_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] text-sky-600 dark:text-sky-400 hover:underline flex items-center space-x-0.5">
|
||||
<span>Open in Jira</span>
|
||||
<a href="<?= htmlspecialchars($task['external_url']) ?>" target="_blank" class="text-[10px] <?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'text-blue-600 dark:text-blue-400' : 'text-sky-600 dark:text-sky-400' ?> hover:underline flex items-center space-x-0.5">
|
||||
<span><?= ($task['source'] === 'OUTLOOK' || !empty($task['external_id'])) ? 'Open in Outlook' : 'Open in Jira' ?></span>
|
||||
<i data-lucide="external-link" class="w-2.5 h-2.5"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
@@ -324,6 +373,12 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?= htmlspecialchars($task['priority']) ?>
|
||||
</span>
|
||||
|
||||
<?php if ($isOutlookConnected && empty($task['external_id']) && !$task['is_completed']): ?>
|
||||
<button onclick="syncTaskToOutlook(<?= $task['id'] ?>, this)" title="Direct synchroniseren naar Outlook Agenda" class="p-1.5 rounded-lg text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-500/10 transition">
|
||||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$task['is_completed']): ?>
|
||||
<button onclick="selectFocusTask('<?= htmlspecialchars(addslashes($task['title'])) ?>')" title="Koppel aan Focus Timer" class="p-1.5 rounded-lg text-slate-400 hover:text-emerald-600 dark:hover:text-emerald-400 hover:bg-emerald-500/10 transition">
|
||||
<i data-lucide="flame" class="w-3.5 h-3.5"></i>
|
||||
@@ -431,8 +486,8 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?php foreach ($hours as $hour):
|
||||
$hourInt = (int)explode(':', $hour)[0];
|
||||
|
||||
// Zoek Outlook meetings die binnen dit uur vallen
|
||||
$slotEvents = array_filter($outlookEvents, function($e) use ($hourInt) {
|
||||
// Zoek Outlook meetings die binnen dit uur vallen (gefilterd op Fociz duplicaten)
|
||||
$slotEvents = array_filter($displayedOutlookEvents, function($e) use ($hourInt) {
|
||||
if (empty($e['start'])) return false;
|
||||
return (int)explode(':', $e['start'])[0] === $hourInt;
|
||||
});
|
||||
@@ -459,12 +514,19 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<?= htmlspecialchars($evt['subject']) ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (!empty($evt['meeting_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded-lg flex-shrink-0 transition">
|
||||
<i data-lucide="video" class="w-3 h-3"></i>
|
||||
<span>Deelnemen</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<?php if (!empty($evt['meeting_url'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['meeting_url']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-white bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded-lg flex-shrink-0 transition">
|
||||
<i data-lucide="video" class="w-3 h-3"></i>
|
||||
<span>Deelnemen</span>
|
||||
</a>
|
||||
<?php elseif (!empty($evt['web_link'])): ?>
|
||||
<a href="<?= htmlspecialchars($evt['web_link']) ?>" target="_blank" class="flex items-center space-x-1 text-[10px] text-blue-600 dark:text-blue-300 hover:underline px-1.5 py-0.5 transition">
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
<span>Outlook</span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -473,13 +535,26 @@ $formattedDate = $dutchDays[date('w')] . ' ' . date('j') . ' ' . $dutchMonths[(i
|
||||
<div class="flex items-center justify-between p-2 rounded-xl border text-xs <?= $t['is_completed'] ? 'bg-slate-100 dark:bg-slate-900/40 border-slate-200 dark:border-slate-800 text-slate-400 dark:text-slate-500 line-through' : 'bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-800/40 text-emerald-900 dark:text-emerald-100' ?>">
|
||||
<div class="flex items-center space-x-2 truncate mr-2">
|
||||
<span class="px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 font-mono text-[10px] font-bold">
|
||||
<?= htmlspecialchars($t['time_block_start']) ?>
|
||||
<?= htmlspecialchars($t['time_block_start']) ?><?= !empty($t['time_block_end']) ? ' - ' . htmlspecialchars($t['time_block_end']) : '' ?>
|
||||
</span>
|
||||
<span class="font-semibold truncate"><?= htmlspecialchars($t['title']) ?></span>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-700 dark:text-emerald-400">
|
||||
<?= $t['priority'] ?>
|
||||
</span>
|
||||
<div class="flex items-center space-x-1.5 flex-shrink-0">
|
||||
<?php if (!empty($t['external_id']) && $t['source'] === 'OUTLOOK'): ?>
|
||||
<span class="text-[9px] font-bold px-1.5 py-0.5 rounded bg-blue-500/15 text-blue-700 dark:text-blue-400 border border-blue-500/20 flex items-center space-x-1" title="In Outlook agenda">
|
||||
<i data-lucide="calendar" class="w-2.5 h-2.5"></i>
|
||||
<span class="hidden sm:inline">In Outlook</span>
|
||||
</span>
|
||||
<?php elseif ($isOutlookConnected && !$t['is_completed']): ?>
|
||||
<button onclick="syncTaskToOutlook(<?= $t['id'] ?>, this)" title="Synchroniseer naar Outlook Agenda" class="text-[9px] font-bold px-1.5 py-0.5 rounded bg-blue-500/10 hover:bg-blue-500/20 text-blue-700 dark:text-blue-300 border border-blue-500/20 flex items-center space-x-1 transition cursor-pointer">
|
||||
<i data-lucide="calendar-plus" class="w-2.5 h-2.5"></i>
|
||||
<span class="hidden sm:inline">Naar Outlook</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<span class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-700 dark:text-emerald-400">
|
||||
<?= $t['priority'] ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -597,24 +672,75 @@ function toggleAddTaskForm() {
|
||||
|
||||
function quickSchedule(hour) {
|
||||
toggleAddTaskForm();
|
||||
document.getElementById('newTaskTime').value = hour;
|
||||
const timeInput = document.getElementById('newTaskTime');
|
||||
if (timeInput) timeInput.value = hour;
|
||||
const outlookCheckbox = document.getElementById('newTaskSyncOutlook');
|
||||
if (outlookCheckbox) outlookCheckbox.checked = true;
|
||||
}
|
||||
|
||||
async function submitNewTask() {
|
||||
const title = document.getElementById('newTaskTitle').value.trim();
|
||||
const priority = document.getElementById('newTaskPriority').value;
|
||||
const time = document.getElementById('newTaskTime').value.trim();
|
||||
const duration = document.getElementById('newTaskDuration') ? parseInt(document.getElementById('newTaskDuration').value) : 30;
|
||||
const syncOutlook = document.getElementById('newTaskSyncOutlook') ? document.getElementById('newTaskSyncOutlook').checked : false;
|
||||
|
||||
if (!title) return;
|
||||
|
||||
const res = await fetch('/api/planner.php?action=add_task', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ title, priority, time_block_start: time, date: '<?= $today ?>' })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
try {
|
||||
const res = await fetch('/api/planner.php?action=add_task', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
priority,
|
||||
time_block_start: time,
|
||||
duration_minutes: duration,
|
||||
sync_to_outlook: syncOutlook,
|
||||
date: '<?= $today ?>'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Fout bij opslaan van de taak');
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij opslaan van de taak');
|
||||
}
|
||||
}
|
||||
|
||||
async function syncTaskToOutlook(taskId, btn) {
|
||||
if (!btn) return;
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="text-[9px] animate-pulse font-medium">Syncen...</span>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/planner.php?action=sync_task_to_outlook', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ task_id: taskId })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
btn.innerHTML = '<i data-lucide="check" class="w-3.5 h-3.5 text-emerald-500"></i>';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 600);
|
||||
} else {
|
||||
alert(data.error || 'Kon taak niet synchroniseren naar Outlook');
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Netwerkfout bij synchroniseren naar Outlook');
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.disabled = false;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,7 +788,7 @@ async function toggleTask(taskId, isCompleted) {
|
||||
}
|
||||
|
||||
async function deleteTask(taskId) {
|
||||
if (!confirm('Taak verwijderen?')) return;
|
||||
if (!confirm('Taak verwijderen? (Indien gekoppeld aan Outlook wordt ook de agenda-afspraak verwijderd)')) return;
|
||||
await fetch('/api/planner.php?action=delete_task', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
|
||||
Reference in New Issue
Block a user