feat(login): update TOTP input field to accept 6 digits and improve accessibility
feat(planner): add Jira ticket detail modal with scheduling options and time suggestions
This commit is contained in:
+1
-4
@@ -249,10 +249,7 @@ $pageTitle = "Fociz — Inloggen";
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="text" name="totp_code" required autofocus maxlength="9" placeholder="000 000" class="w-full text-center tracking-[0.4em] font-mono text-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl py-3 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition">
|
||||
<span class="text-[11px] text-slate-500 dark:text-slate-500 text-center block mt-1">
|
||||
Tip: U kunt ook een 8-cijferige herstelcode invoeren.
|
||||
</span>
|
||||
<input type="text" name="totp_code" required autofocus maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="000 000" class="w-full text-center tracking-[0.4em] font-mono text-2xl bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl py-3 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 transition">
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
|
||||
+376
-17
@@ -1296,7 +1296,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
<?php else: ?>
|
||||
<?php foreach (array_slice($jiraAllIssues, 0, 8) as $issue): ?>
|
||||
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
||||
<div class="truncate mr-2 min-w-0 flex-1">
|
||||
<div class="truncate mr-2 min-w-0 flex-1 cursor-pointer" onclick="openJiraTicketDetail('<?= htmlspecialchars($issue['key'] ?? '') ?>')" title="Klik voor ticketdetails">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
||||
<?= htmlspecialchars($issue['key'] ?? '') ?>
|
||||
@@ -1315,7 +1315,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
|
||||
class="flex items-center space-x-1 text-[11px] 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="Plan in als dagtaak"
|
||||
>
|
||||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||||
<span>Plan</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1713,6 +1713,146 @@ if ($isAjaxRequest) {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Modal: Jira Ticket Inplannen (met tijdsuggestie) -->
|
||||
<div id="jiraPlanModal" 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">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<div class="w-8 h-8 rounded-xl bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 text-sky-600 dark:text-sky-400 flex items-center justify-center">
|
||||
<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>
|
||||
<p class="text-[10px] text-slate-500 dark:text-slate-400" id="jiraPlanModalKey">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeJiraPlanModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ticket samenvatting -->
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-200 dark:border-slate-800 p-3">
|
||||
<p class="text-xs font-semibold text-slate-800 dark:text-slate-200 leading-snug" id="jiraPlanModalSummary">—</p>
|
||||
<div class="flex items-center space-x-2 mt-1.5">
|
||||
<span class="text-[10px] text-slate-500" id="jiraPlanModalStatus">—</span>
|
||||
<span class="text-slate-300 dark:text-slate-600">•</span>
|
||||
<span class="text-[10px] text-slate-500" id="jiraPlanModalPriority">—</span>
|
||||
<span id="jiraPlanModalSpBadge" class="hidden px-1.5 py-0.5 rounded bg-violet-50 dark:bg-violet-500/10 border border-violet-200 dark:border-violet-500/20 text-violet-700 dark:text-violet-400 font-mono text-[9px] font-bold"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tijdsuggestie -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Starttijd (suggestie)</label>
|
||||
<input type="time" id="jiraPlanStartTime" 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">
|
||||
<p class="text-[10px] text-slate-400 mt-1">Laat leeg voor handmatig inplannen op de tijdlijn</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Duur</label>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<button type="button" onclick="setJiraPlanDuration(15)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="15">15m</button>
|
||||
<button type="button" onclick="setJiraPlanDuration(30)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="30">30m</button>
|
||||
<button type="button" onclick="setJiraPlanDuration(60)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="60">1u</button>
|
||||
<button type="button" onclick="setJiraPlanDuration(90)" class="jira-plan-dur-btn px-2 py-1.5 rounded-xl border text-[11px] font-bold transition cursor-pointer" data-dur="90">1,5u</button>
|
||||
</div>
|
||||
<input type="hidden" id="jiraPlanDuration" value="15">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-bold text-slate-700 dark:text-slate-300 mb-1.5">Prioriteit</label>
|
||||
<select id="jiraPlanPriority" 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">
|
||||
<option value="P1">P1 — Urgent</option>
|
||||
<option value="P2">P2 — Hoog</option>
|
||||
<option value="P3" selected>P3 — Normaal</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3 pt-1">
|
||||
<button type="button" onclick="closeJiraPlanModal()" class="flex-1 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-300 font-semibold text-sm transition cursor-pointer">
|
||||
Annuleren
|
||||
</button>
|
||||
<button type="button" id="btnSubmitJiraPlan" onclick="submitJiraPlanModal()" class="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">
|
||||
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
|
||||
<span>Inplannen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Jira Ticket Detail (popup bij klikken op ticket) -->
|
||||
<div id="jiraTicketDetailModal" 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-0 w-full max-w-lg shadow-2xl overflow-hidden flex flex-col max-h-[85vh]">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between p-5 border-b border-slate-100 dark:border-slate-800/80 flex-shrink-0">
|
||||
<div class="space-y-1 min-w-0 flex-1 mr-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-mono text-[11px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-0.5 rounded flex-shrink-0" id="jiraDetailKey">—</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailStatusBadge">—</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full font-bold" id="jiraDetailPriorityBadge">—</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-slate-900 dark:text-white leading-snug" id="jiraDetailSummary">—</h3>
|
||||
</div>
|
||||
<button onclick="closeJiraTicketDetailModal()" class="p-1.5 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition cursor-pointer flex-shrink-0">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body: scrollable -->
|
||||
<div class="overflow-y-auto flex-1 p-5 space-y-4">
|
||||
<!-- Meta info -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Aangemeld bij</p>
|
||||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailAssignee">—</p>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Story Points</p>
|
||||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailSp">—</p>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Project</p>
|
||||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailProject">—</p>
|
||||
</div>
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-0.5">Type</p>
|
||||
<p class="text-xs font-bold text-slate-900 dark:text-white" id="jiraDetailType">—</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beschrijving -->
|
||||
<div id="jiraDetailDescSection" class="hidden">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Beschrijving</p>
|
||||
<div class="bg-slate-50 dark:bg-slate-900/50 rounded-xl p-3 border border-slate-200 dark:border-slate-800 text-xs text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-wrap max-h-40 overflow-y-auto" id="jiraDetailDesc"></div>
|
||||
</div>
|
||||
|
||||
<!-- Labels -->
|
||||
<div id="jiraDetailLabelsSection" class="hidden">
|
||||
<p class="text-[10px] text-slate-500 font-semibold uppercase tracking-wide mb-1.5">Labels</p>
|
||||
<div class="flex flex-wrap gap-1.5" id="jiraDetailLabels"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-between p-4 border-t border-slate-100 dark:border-slate-800/80 flex-shrink-0 gap-3">
|
||||
<a href="#" target="_blank" id="jiraDetailOpenLink" class="flex items-center space-x-1.5 text-xs font-semibold text-sky-600 dark:text-sky-400 hover:underline cursor-pointer">
|
||||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||||
<span>Openen in Jira</span>
|
||||
</a>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="button" onclick="closeJiraTicketDetailModal()" class="px-3 py-1.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-300 font-semibold text-xs transition cursor-pointer">
|
||||
Sluiten
|
||||
</button>
|
||||
<button type="button" onclick="openJiraPlanFromDetail()" class="flex items-center space-x-1.5 px-3 py-1.5 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs transition cursor-pointer">
|
||||
<i data-lucide="calendar-plus" class="w-3.5 h-3.5"></i>
|
||||
<span>Inplannen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 class="fociz-card p-6 w-full max-w-lg shadow-2xl space-y-4">
|
||||
@@ -2638,25 +2778,19 @@ function saveCurrentGridOrderIfChanged() {
|
||||
const grid = document.getElementById('focizWidgetGrid');
|
||||
if (!grid) return;
|
||||
const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper'));
|
||||
const currentIndex = activeDraggedWrapper ? currentWrappers.indexOf(activeDraggedWrapper) : -1;
|
||||
|
||||
let hasOrderChanged = false;
|
||||
// Synchroniseer de order in focizWidgets met de actuele DOM-volgorde
|
||||
currentWrappers.forEach((el, index) => {
|
||||
const wId = el.getAttribute('data-widget-id');
|
||||
const item = focizWidgets.find(x => x.id === wId);
|
||||
if (item) {
|
||||
if (item.order !== index + 1) {
|
||||
item.order = index + 1;
|
||||
hasOrderChanged = true;
|
||||
}
|
||||
item.order = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) {
|
||||
// Geruisloos opslaan via AJAX
|
||||
persistWidgetLayoutAuto();
|
||||
showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success');
|
||||
}
|
||||
// Altijd opslaan na een drag (zet ook de volgorde opnieuw neer)
|
||||
persistWidgetLayoutAuto();
|
||||
showWidgetToast('Widget indeling opgeslagen', 'success');
|
||||
}
|
||||
|
||||
// Sluit popup menu's als er buiten wordt geklikt
|
||||
@@ -2777,7 +2911,7 @@ function filterPlannerJira() {
|
||||
|
||||
return `
|
||||
<div class="fociz-card-interactive flex items-center justify-between p-2.5 rounded-xl bg-slate-50/70 dark:bg-[#0c0f17]/60 border border-slate-200/80 dark:border-slate-800/80 hover:border-indigo-300 dark:hover:border-indigo-600/50 transition shadow-2xs">
|
||||
<div class="truncate mr-2 min-w-0 flex-1">
|
||||
<div class="truncate mr-2 min-w-0 flex-1 cursor-pointer" onclick="openJiraTicketDetail('${keyEsc}')" title="Klik voor ticketdetails">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-mono text-[10px] font-bold text-sky-700 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-1.5 py-0.5 rounded">
|
||||
${keyEsc}
|
||||
@@ -2797,7 +2931,7 @@ function filterPlannerJira() {
|
||||
class="flex items-center space-x-1 text-[11px] 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="Plan in als dagtaak"
|
||||
>
|
||||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||
<i data-lucide="calendar-plus" class="w-3 h-3"></i>
|
||||
<span>Plan</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -2809,12 +2943,237 @@ function filterPlannerJira() {
|
||||
}
|
||||
}
|
||||
|
||||
async function quickImportJiraByKey(btn, issueKey) {
|
||||
// --- Jira Ticket Detail Modal ---
|
||||
let _jiraDetailCurrentIssue = null;
|
||||
|
||||
function openJiraTicketDetail(issueKey) {
|
||||
const issue = plannerTicketStore.get(issueKey);
|
||||
if (!issue) return;
|
||||
return quickImportJira(btn, issue.summary || issueKey, issue.key, issue.url || '');
|
||||
_jiraDetailCurrentIssue = issue;
|
||||
|
||||
// Vul header
|
||||
document.getElementById('jiraDetailKey').textContent = issue.key || '—';
|
||||
document.getElementById('jiraDetailSummary').textContent = issue.summary || '—';
|
||||
|
||||
// Status badge
|
||||
const statusEl = document.getElementById('jiraDetailStatusBadge');
|
||||
const status = issue.status || '—';
|
||||
statusEl.textContent = status;
|
||||
statusEl.className = 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
|
||||
|
||||
// Priority badge
|
||||
const priorityEl = document.getElementById('jiraDetailPriorityBadge');
|
||||
const prio = issue.priority || '—';
|
||||
priorityEl.textContent = prio;
|
||||
const prioClass = prio === 'Highest' || prio === 'High'
|
||||
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400'
|
||||
: prio === 'Medium'
|
||||
? 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400'
|
||||
: 'text-[10px] px-2 py-0.5 rounded-full font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400';
|
||||
priorityEl.className = prioClass;
|
||||
|
||||
// Meta velden
|
||||
document.getElementById('jiraDetailAssignee').textContent = issue.assignee_name || issue.assignee || 'Niet toegewezen';
|
||||
document.getElementById('jiraDetailSp').textContent = issue.story_points ? `${issue.story_points} SP` : '—';
|
||||
document.getElementById('jiraDetailProject').textContent = issue.project_key || issue.project || (issue.key ? issue.key.split('-')[0] : '—');
|
||||
document.getElementById('jiraDetailType').textContent = issue.issue_type || issue.issuetype || 'Story';
|
||||
|
||||
// Beschrijving
|
||||
const descSection = document.getElementById('jiraDetailDescSection');
|
||||
const descEl = document.getElementById('jiraDetailDesc');
|
||||
if (issue.description) {
|
||||
descEl.textContent = issue.description;
|
||||
descSection.classList.remove('hidden');
|
||||
} else {
|
||||
descSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Labels
|
||||
const labelsSection = document.getElementById('jiraDetailLabelsSection');
|
||||
const labelsEl = document.getElementById('jiraDetailLabels');
|
||||
if (issue.labels && issue.labels.length > 0) {
|
||||
labelsEl.innerHTML = issue.labels.map(l =>
|
||||
`<span class="px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-[10px] font-semibold border border-slate-200 dark:border-slate-700">${escapeHtml(l)}</span>`
|
||||
).join('');
|
||||
labelsSection.classList.remove('hidden');
|
||||
} else {
|
||||
labelsSection.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Link naar Jira
|
||||
const linkEl = document.getElementById('jiraDetailOpenLink');
|
||||
if (issue.url) {
|
||||
linkEl.href = issue.url;
|
||||
linkEl.classList.remove('hidden');
|
||||
} else {
|
||||
linkEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('jiraTicketDetailModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeJiraTicketDetailModal() {
|
||||
const modal = document.getElementById('jiraTicketDetailModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
|
||||
function openJiraPlanFromDetail() {
|
||||
closeJiraTicketDetailModal();
|
||||
if (_jiraDetailCurrentIssue) {
|
||||
openJiraPlanModal(_jiraDetailCurrentIssue.key);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Jira Plan Modal (met tijdsuggestie) ---
|
||||
let _jiraPlanCurrentIssue = null;
|
||||
|
||||
function openJiraPlanModal(issueKey) {
|
||||
const issue = plannerTicketStore.get(issueKey);
|
||||
if (!issue) return;
|
||||
_jiraPlanCurrentIssue = issue;
|
||||
|
||||
// Vul header
|
||||
document.getElementById('jiraPlanModalKey').textContent = issue.key || '—';
|
||||
document.getElementById('jiraPlanModalSummary').textContent = issue.summary || '—';
|
||||
document.getElementById('jiraPlanModalStatus').textContent = issue.status || '—';
|
||||
document.getElementById('jiraPlanModalPriority').textContent = issue.priority || '—';
|
||||
|
||||
// SP badge
|
||||
const spBadge = document.getElementById('jiraPlanModalSpBadge');
|
||||
if (issue.story_points) {
|
||||
spBadge.textContent = `${issue.story_points} SP`;
|
||||
spBadge.classList.remove('hidden');
|
||||
} else {
|
||||
spBadge.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 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 suggestTime = activePlannerDate === now.toISOString().slice(0,10)
|
||||
? suggestNextFreeSlot()
|
||||
: '09:00';
|
||||
document.getElementById('jiraPlanStartTime').value = suggestTime;
|
||||
|
||||
const modal = document.getElementById('jiraPlanModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeJiraPlanModal() {
|
||||
const modal = document.getElementById('jiraPlanModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
_jiraPlanCurrentIssue = null;
|
||||
}
|
||||
|
||||
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';
|
||||
} 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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`;
|
||||
}
|
||||
|
||||
async function submitJiraPlanModal() {
|
||||
if (!_jiraPlanCurrentIssue) return;
|
||||
const issue = _jiraPlanCurrentIssue;
|
||||
|
||||
const duration = parseInt(document.getElementById('jiraPlanDuration').value, 10) || 15;
|
||||
const startTime = document.getElementById('jiraPlanStartTime').value.trim();
|
||||
const priority = document.getElementById('jiraPlanPriority').value;
|
||||
|
||||
const btn = document.getElementById('btnSubmitJiraPlan');
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="text-xs font-bold animate-pulse">Inplannen...</span>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
title: `[${issue.key}] ${issue.summary || issue.key}`,
|
||||
priority: priority,
|
||||
source: 'JIRA',
|
||||
external_id: issue.key,
|
||||
external_url: issue.url || '',
|
||||
duration_minutes: duration,
|
||||
date: activePlannerDate
|
||||
};
|
||||
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')}`;
|
||||
}
|
||||
|
||||
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 (data.success) {
|
||||
closeJiraPlanModal();
|
||||
showWidgetToast(`<strong>${escapeHtml(issue.key)}</strong> ingepland als dagtaak`, 'success');
|
||||
switchPlannerDate(activePlannerDate, null, false);
|
||||
} else {
|
||||
alert(data.error || 'Kon ticket niet inplannen');
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Netwerkfout bij inplannen');
|
||||
btn.innerHTML = origHtml;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function quickImportJiraByKey(btn, issueKey) {
|
||||
// Plan knop → open de plan modal (niet direct importeren)
|
||||
openJiraPlanModal(issueKey);
|
||||
}
|
||||
|
||||
|
||||
// --- Direct E-mail Inplannen als Dagtaak ---
|
||||
function openPlanEmailDirect(mailId, subject, sender, webLink) {
|
||||
document.getElementById('planEmailId').value = mailId;
|
||||
|
||||
Reference in New Issue
Block a user