This commit is contained in:
2026-09-12 18:12:30 +02:00
parent 5f15eb67fe
commit f63ea740bf
4 changed files with 426 additions and 164 deletions
+185 -66
View File
@@ -186,16 +186,31 @@ try {
error_log("Fout bij ophalen taken: " . $e->getMessage());
}
// 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs)
// 3b. Haal alle reeds ingeplande externe items op (Jira keys & Outlook Mail IDs) inclusief taakgegevens voor herplannen
$plannedExternalIds = [];
try {
$stmtPlannedExt = $db->prepare("
SELECT external_id FROM tasks
WHERE user_id = ? AND external_id IS NOT NULL AND external_id != ''
SELECT t.id as task_id, t.external_id, t.title, t.priority, t.time_block_start, t.time_block_end,
t.duration_minutes, dp.plan_date
FROM tasks t
LEFT JOIN daily_plans dp ON dp.id = t.daily_plan_id
WHERE t.user_id = ? AND t.external_id IS NOT NULL AND t.external_id != ''
ORDER BY t.id DESC
");
$stmtPlannedExt->execute([$userId]);
foreach ($stmtPlannedExt->fetchAll() as $row) {
$plannedExternalIds[$row['external_id']] = true;
$extId = $row['external_id'];
if (!isset($plannedExternalIds[$extId])) {
$plannedExternalIds[$extId] = [
'task_id' => (int)$row['task_id'],
'title' => $row['title'],
'priority' => $row['priority'],
'time_block_start' => $row['time_block_start'] ? substr($row['time_block_start'], 0, 5) : '',
'time_block_end' => $row['time_block_end'] ? substr($row['time_block_end'], 0, 5) : '',
'duration_minutes' => (int)($row['duration_minutes'] ?: 30),
'date' => $row['plan_date'] ?: date('Y-m-d')
];
}
}
} catch (Throwable $e) {
error_log("Fout bij ophalen planned external IDs: " . $e->getMessage());
@@ -330,7 +345,7 @@ $availableWidgets = [
'timeline' => ['id' => 'timeline', 'title' => 'Tijdlijn & Dag/Weekoverzicht', 'category' => 'Agenda', 'icon' => 'calendar', 'enabled' => true, 'col_span' => 12, 'order' => 2, 'desc' => 'Gecombineerd weekoverzicht, dagselectie en uren-tijdlijn met time-blocking en AI'],
'tasks' => ['id' => 'tasks', 'title' => 'Dagtaken & Prioriteiten', 'category' => 'Planning', 'icon' => 'check-square', 'enabled' => true, 'col_span' => 7, 'order' => 3, 'desc' => 'Volledige takenlijst met P1/P2/P3, tijdvakken en Outlook synchronisatie'],
'outlook_mail' => ['id' => 'outlook_mail', 'title' => 'Outlook E-mail Widget', 'category' => 'Communicatie', 'icon' => 'mail', 'enabled' => true, 'col_span' => 6, 'order' => 4, 'desc' => 'Inkomende e-mails direct inzien en met 1 klik inplannen als taak'],
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets & Wachtrijen', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets (DX/SD wachtrijen) met directe importknop'],
'jira_queue' => ['id' => 'jira_queue', 'title' => 'Jira Tickets', 'category' => 'Ontwikkeling', 'icon' => 'layers', 'enabled' => true, 'col_span' => 6, 'order' => 5, 'desc' => 'Openstaande Jira tickets met directe importknop'],
'calendar_meetings' => ['id' => 'calendar_meetings', 'title' => 'Agenda & Teams Meetings', 'category' => 'Agenda', 'icon' => 'video', 'enabled' => true, 'col_span' => 6, 'order' => 6, 'desc' => 'Vandaag geplande afspraken met 1-klik Teams deelnemen knop'],
'scratchpad' => ['id' => 'scratchpad', 'title' => 'Dagnotities & Kladblok', 'category' => 'Productiviteit', 'icon' => 'edit-3', 'enabled' => true, 'col_span' => 6, 'order' => 7, 'desc' => 'Vrij notitieblok met automatische achtergrond-opslag'],
'stats' => ['id' => 'stats', 'title' => 'Productiviteit & KPI\'s', 'category' => 'Inzicht', 'icon' => 'bar-chart-2', 'enabled' => true, 'col_span' => 12, 'order' => 8, 'desc' => 'Voortgangsbalk, voltooide taken, focus uren en meeting totalen'],
@@ -1478,7 +1493,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div>
<?php elseif ($wId === 'jira_queue'): ?>
<!-- Widget 5: Jira Tickets & Wachtrijen (Interactive v2.0) -->
<!-- Widget 5: Jira Tickets (Interactive v2.0) -->
<div id="jiraPanel" class="fociz-card p-5 sm:p-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800/80 mb-3 gap-2">
<div class="flex items-center space-x-2.5">
@@ -1487,7 +1502,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
</div>
<div>
<h3 class="text-xs font-bold text-slate-900 dark:text-white uppercase tracking-wider">
Jira Wachtrijen & Tickets
Jira Tickets
</h3>
</div>
<span id="plannerJiraCountBadge" class="px-2 py-0.5 rounded-full text-[10px] font-bold <?= $isJiraLive ? 'bg-sky-50 dark:bg-sky-500/15 text-sky-700 dark:text-sky-400 border border-sky-200 dark:border-sky-500/30' : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400' ?>">
@@ -1559,7 +1574,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
onclick="quickImportJiraByKey(this, '<?= htmlspecialchars($issue['key'] ?? '') ?>')"
data-jira-key="<?= htmlspecialchars($issue['key'] ?? '') ?>"
class="plan-btn-jira flex items-center space-x-1 text-[11px] <?= $isJiraPlanned ? 'font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs' : 'font-semibold text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 bg-indigo-50 dark:bg-indigo-500/10 hover:bg-indigo-100 dark:hover:bg-indigo-500/20 border border-indigo-200 dark:border-indigo-500/20' ?> px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)' : 'Plan in als dagtaak' ?>"
title="<?= $isJiraPlanned ? 'Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)' : 'Plan in als dagtaak' ?>"
>
<?php if ($isJiraPlanned): ?>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
@@ -1988,7 +2003,7 @@ if ($isAjaxRequest) {
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
</div>
<div>
<h3 class="text-sm font-bold text-slate-900 dark:text-white">Jira Ticket Inplannen</h3>
<h3 class="text-sm font-bold text-slate-900 dark:text-white" id="jiraPlanModalTitle">Jira Ticket Inplannen</h3>
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p>
</div>
</div>
@@ -1997,6 +2012,22 @@ if ($isAjaxRequest) {
</button>
</div>
<!-- Hidden veld voor bestaande gekoppelde taak (bij herplannen) -->
<input type="hidden" id="jiraPlanTaskId" value="">
<!-- Banner: Al Ingepland (zichtbaar wanneer ticket reeds gepland is) -->
<div id="jiraPlanAlreadyScheduledBanner" class="hidden p-3 rounded-xl bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-300 dark:border-emerald-700 text-emerald-900 dark:text-emerald-200 text-xs">
<div class="flex items-start space-x-2">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600 dark:text-emerald-400 flex-shrink-0 mt-0.5"></i>
<div class="flex-1 min-w-0">
<p class="font-bold">Reeds ingepland in je dagplanning</p>
<p class="text-[11px] text-emerald-800 dark:text-emerald-300 mt-0.5" id="jiraPlanAlreadyScheduledDetails">
Ingepland op ...
</p>
</div>
</div>
</div>
<!-- Ticket samenvatting & Uitklapbare beschrijving -->
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3.5 space-y-2.5">
<div>
@@ -2028,7 +2059,7 @@ if ($isAjaxRequest) {
<div class="space-y-3.5">
<!-- Titel voor de agenda / dagtaak -->
<div>
<label for="jiraPlanCustomTitle" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
<label for="jiraPlanCustomTitle" id="jiraPlanCustomTitleLabel" class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">
Hoe het in de agenda komt (Titel)
</label>
<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 ...">
@@ -4612,7 +4643,8 @@ plannerJiraAssigned.forEach(iss => {
if (iss && iss.key && !plannerTicketStore.has(iss.key)) plannerTicketStore.set(iss.key, iss);
});
const plannedExternalIdsSet = new Set(<?= json_encode(array_keys($plannedExternalIds)) ?>);
const plannedExternalTasksMap = <?= json_encode($plannedExternalIds, JSON_UNESCAPED_UNICODE) ?> || {};
const plannedExternalIdsSet = new Set(Object.keys(plannedExternalTasksMap));
let activePlannerJiraQueue = ''; // '': Alle, 'my': Mijn, 'DX': DX Queue, 'SD': SD Queue
function isIssueInQueue(iss, qCode) {
@@ -4720,7 +4752,7 @@ function filterPlannerJira() {
onclick="quickImportJiraByKey(this, '${keyEsc}')"
data-jira-key="${keyEsc}"
class="plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer"
title="Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)"
title="Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)"
>
<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i>
<span>Ingepland</span>
@@ -4926,42 +4958,80 @@ function openJiraPlanModal(issueKey) {
if (descChevron) descChevron.classList.remove('rotate-180');
if (descLabel) descLabel.textContent = 'Inhoud van het item bekijken';
// Titel hoe het in de agenda komt: standaard "Oppakken Jira item <issue.key>"
// Controleer of dit ticket al is ingepland
const existingTask = (typeof plannedExternalTasksMap !== 'undefined' && plannedExternalTasksMap[issue.key]) ? plannedExternalTasksMap[issue.key] : null;
const isAlreadyPlanned = Boolean(existingTask);
const taskIdInput = document.getElementById('jiraPlanTaskId');
if (taskIdInput) {
taskIdInput.value = (existingTask && existingTask.task_id) ? existingTask.task_id : '';
}
const modalTitle = document.getElementById('jiraPlanModalTitle');
const scheduledBanner = document.getElementById('jiraPlanAlreadyScheduledBanner');
const scheduledDetails = document.getElementById('jiraPlanAlreadyScheduledDetails');
const titleInput = document.getElementById('jiraPlanCustomTitle');
if (titleInput) {
titleInput.value = `Oppakken Jira item ${issue.key}`;
}
// Datum: standaard de actieve planner datum of vandaag
const dateInput = document.getElementById('jiraPlanDate');
const targetDate = (typeof activePlannerDate !== 'undefined' && activePlannerDate) ? activePlannerDate : new Date().toISOString().slice(0, 10);
if (dateInput) {
dateInput.value = targetDate;
const startTimeInput = document.getElementById('jiraPlanStartTime');
if (isAlreadyPlanned && existingTask) {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Herplannen';
if (scheduledBanner) scheduledBanner.classList.remove('hidden');
if (scheduledDetails) {
const timeInfo = existingTask.time_block_start
? `om ${escapeHtml(existingTask.time_block_start)}${existingTask.time_block_end ? ' - ' + escapeHtml(existingTask.time_block_end) : ''} (${existingTask.duration_minutes}m)`
: `(zonder specifiek tijdblok, ${existingTask.duration_minutes}m)`;
scheduledDetails.innerHTML = `Momenteel ingepland op <strong>${escapeHtml(existingTask.date)}</strong> ${timeInfo}. Pas hieronder de datum, tijd of duur aan om te herplannen.`;
}
if (titleInput) {
titleInput.value = existingTask.title || `Oppakken Jira item ${issue.key}`;
}
if (dateInput) {
dateInput.value = existingTask.date || targetDate;
}
setJiraPlanDuration(existingTask.duration_minutes || 30);
if (existingTask.priority) {
document.getElementById('jiraPlanPriority').value = existingTask.priority;
}
if (startTimeInput) {
startTimeInput.value = existingTask.time_block_start || '';
}
} else {
if (modalTitle) modalTitle.textContent = 'Jira Ticket Inplannen';
if (scheduledBanner) scheduledBanner.classList.add('hidden');
if (titleInput) {
titleInput.value = `Oppakken Jira item ${issue.key}`;
}
if (dateInput) {
dateInput.value = targetDate;
}
// Standaard duur: 15 min (of op basis van SP: 1 SP = 30 min)
let defaultDuration = 15;
if (issue.story_points && issue.story_points > 0) {
const spMin = Math.round(issue.story_points * 30);
defaultDuration = [15, 30, 60, 90].reduce((prev, curr) =>
Math.abs(curr - spMin) < Math.abs(prev - spMin) ? curr : prev
);
}
setJiraPlanDuration(defaultDuration);
// Prioriteit: vertaal Jira priority naar Fociz priority
const jiraPrio = (issue.priority || '').toLowerCase();
const focizPrio = (jiraPrio === 'highest' || jiraPrio === 'critical') ? 'P1'
: (jiraPrio === 'high') ? 'P2'
: 'P3';
document.getElementById('jiraPlanPriority').value = focizPrio;
// Slimme starttijd: stel het volgende vrije kwartier voor
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
if (startTimeInput) {
startTimeInput.value = suggestTime;
}
}
// Standaard duur: 15 min (of op basis van SP: 1 SP = 30 min)
let defaultDuration = 15;
if (issue.story_points && issue.story_points > 0) {
const spMin = Math.round(issue.story_points * 30);
defaultDuration = [15, 30, 60, 90].reduce((prev, curr) =>
Math.abs(curr - spMin) < Math.abs(prev - spMin) ? curr : prev
);
}
setJiraPlanDuration(defaultDuration);
// Prioriteit: vertaal Jira priority naar Fociz priority
const jiraPrio = (issue.priority || '').toLowerCase();
const focizPrio = (jiraPrio === 'highest' || jiraPrio === 'critical') ? 'P1'
: (jiraPrio === 'high') ? 'P2'
: 'P3';
document.getElementById('jiraPlanPriority').value = focizPrio;
// Slimme starttijd: stel het volgende vrije kwartier voor
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const suggestTime = targetDate === todayStr ? suggestNextFreeSlot() : '09:00';
document.getElementById('jiraPlanStartTime').value = suggestTime;
const notice = document.getElementById('jiraPlanOpenBlockNotice');
if (notice) notice.classList.add('hidden');
updateJiraPlanEndTimeDisplay();
@@ -4970,7 +5040,13 @@ function openJiraPlanModal(issueKey) {
const submitBtn = document.getElementById('btnSubmitJiraPlan');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
if (isAlreadyPlanned) {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-amber-600 hover:bg-amber-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i><span>Herplannen</span>';
} else {
submitBtn.className = "flex-1 px-4 py-2.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-sm transition shadow-xs flex items-center justify-center space-x-2 cursor-pointer";
submitBtn.innerHTML = '<i data-lucide="calendar-plus" class="w-4 h-4"></i><span>Inplannen</span>';
}
}
const modal = document.getElementById('jiraPlanModal');
@@ -5164,42 +5240,85 @@ async function submitJiraPlanModal() {
btn.disabled = true;
}
const existingTaskId = parseInt(document.getElementById('jiraPlanTaskId')?.value || '0', 10);
const isUpdate = existingTaskId > 0;
try {
const payload = {
title: finalTitle,
priority: priority,
source: 'JIRA',
external_id: issue.key,
external_url: issue.url || '',
duration_minutes: duration,
date: planDate
};
let res, data;
let finalStartTime = startTime;
let finalEndTime = '';
if (startTime) {
payload.time_block_start = startTime;
// Bereken eindtijd
const [hh, mm] = startTime.split(':').map(Number);
const endMin = hh * 60 + mm + duration;
payload.time_block_end = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
finalEndTime = `${String(Math.floor(endMin / 60)).padStart(2,'0')}:${String(endMin % 60).padStart(2,'0')}`;
}
const res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (isUpdate) {
// Bestaande taak bijwerken / verplaatsen naar eventueel andere datum
res = await fetch('/api/planner.php?action=update_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
task_id: existingTaskId,
title: finalTitle,
priority: priority,
duration_minutes: duration,
time_block_start: finalStartTime,
time_block_end: finalEndTime,
date: planDate
})
});
data = await res.json();
} else {
// Nieuwe taak toevoegen
const payload = {
title: finalTitle,
priority: priority,
source: 'JIRA',
external_id: issue.key,
external_url: issue.url || '',
duration_minutes: duration,
date: planDate
};
if (finalStartTime) {
payload.time_block_start = finalStartTime;
payload.time_block_end = finalEndTime;
}
res = await fetch('/api/planner.php?action=add_task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
data = await res.json();
}
if (data.success) {
closeJiraPlanModal();
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> ingepland als dagtaak`, 'success');
const actionVerb = isUpdate ? 'geherpland' : 'ingepland';
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> succesvol ${actionVerb}`, 'success');
// Live update: voeg toe aan plannedExternalIdsSet en transformeer knoppen naar groen 'Ingepland'
// Live update van cache en sets
const savedTaskId = isUpdate ? existingTaskId : (data.task ? data.task.id : existingTaskId);
if (typeof plannedExternalTasksMap !== 'undefined') {
plannedExternalTasksMap[issue.key] = {
task_id: savedTaskId,
title: finalTitle,
priority: priority,
time_block_start: finalStartTime || '',
time_block_end: finalEndTime || '',
duration_minutes: duration,
date: planDate
};
}
if (typeof plannedExternalIdsSet !== 'undefined') {
plannedExternalIdsSet.add(issue.key);
}
// Knoppen bijwerken
document.querySelectorAll(`.plan-btn-jira[data-jira-key="${issue.key}"]`).forEach(btn => {
btn.className = "plan-btn-jira flex items-center space-x-1 text-[11px] font-bold text-white bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 shadow-xs px-2.5 py-1 rounded-lg transition flex-shrink-0 cursor-pointer";
btn.title = "Dit ticket is al ingepland als dagtaak (klik om opnieuw/anders in te plannen)";
btn.title = "Dit ticket is al ingepland als dagtaak (klik om status te bekijken of te herplannen)";
btn.innerHTML = '<i data-lucide="check" class="w-3 h-3 stroke-[2.5]"></i><span>Ingepland</span>';
});
const detailPlanBtn = document.getElementById('btnJiraDetailPlan');