Postits en editable items.

This commit is contained in:
2026-09-12 17:14:47 +02:00
parent 7c27949cc2
commit 94d6325693
9 changed files with 1680 additions and 56 deletions
+170
View File
@@ -353,6 +353,25 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
</div>
</div>
<!-- Knop Vrij Blok Zoeken voor Assignee -->
<div class="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 flex items-center justify-between gap-2">
<div class="text-[11px]">
<span class="font-bold text-amber-900 dark:text-amber-200 flex items-center space-x-1">
<i data-lucide="sparkles" class="w-3 h-3 text-amber-500"></i>
<span>Vrij agendablok zoeken</span>
</span>
<span class="text-[10px] text-amber-700 dark:text-amber-400">Zoekt binnen werktijden van de toegewezen collega</span>
</div>
<button type="button" id="btnTimelineFindSlot" onclick="findFreeSlotForTimelineAssignee()" class="px-2.5 py-1 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-bold text-[10px] shadow-xs transition flex items-center space-x-1 flex-shrink-0 cursor-pointer">
<i data-lucide="search" class="w-3 h-3"></i>
<span>Zoek vrij blok</span>
</button>
</div>
<div id="timelineFoundSlotNotice" class="hidden p-2 rounded-lg bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-300 dark:border-emerald-800 text-[11px] text-emerald-800 dark:text-emerald-300 font-medium flex items-center space-x-1.5">
<i data-lucide="check-circle" class="w-3.5 h-3.5 text-emerald-500 flex-shrink-0"></i>
<span id="timelineFoundSlotText"></span>
</div>
<!-- Outlook Agenda Synchronisatie -->
<div class="flex items-center justify-between pt-2 border-t border-slate-200/50 dark:border-slate-800/50">
<div class="flex items-center space-x-2">
@@ -393,6 +412,26 @@ html.dark #timelineGridWrapper::-webkit-scrollbar-thumb {
</div>
</div>
<!-- Rechtermuisknop Contextmenu voor Tijdlijn Items -->
<div id="timelineContextMenu" class="hidden fixed z-50 w-56 bg-white dark:bg-slate-900 rounded-xl shadow-2xl border border-slate-200 dark:border-slate-800 py-1.5 text-xs text-slate-700 dark:text-slate-200">
<button onclick="triggerTimelineCtxEdit()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-indigo-50 dark:hover:bg-indigo-950/50 hover:text-indigo-600 dark:hover:text-indigo-400 transition cursor-pointer">
<i data-lucide="edit-3" class="w-4 h-4 text-indigo-500"></i>
<span>Taak bewerken</span>
</button>
<button onclick="triggerTimelineCtxQuickSlot()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 hover:bg-amber-50 dark:hover:bg-amber-950/40 hover:text-amber-600 dark:hover:text-amber-400 transition cursor-pointer">
<i data-lucide="zap" class="w-4 h-4 text-amber-500"></i>
<span>⚡ Zoek direct vrij blok</span>
</button>
<div class="my-1 border-t border-slate-100 dark:border-slate-800/80"></div>
<button onclick="triggerTimelineCtxDelete()" class="w-full px-3.5 py-2 text-left flex items-center space-x-2.5 text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition cursor-pointer">
<i data-lucide="trash-2" class="w-4 h-4"></i>
<span>Taak verwijderen</span>
</button>
</div>
<script>
// --- Globale Status voor Tijdlijn ---
let currentBoardId = null;
@@ -733,6 +772,11 @@ function renderTimeline() {
leftItem.setAttribute('draggable', 'true');
leftItem.setAttribute('data-card-id', card.id);
leftItem.oncontextmenu = (e) => {
openTimelineContextMenu(e, card.id);
return false;
};
leftItem.ondragstart = (e) => {
e.dataTransfer.setData('text/plain', String(card.id));
e.dataTransfer.effectAllowed = 'move';
@@ -779,6 +823,11 @@ function renderTimeline() {
rightRow.className = 'timeline-task-row timeline-row h-12 relative flex items-center';
rightRow.setAttribute('data-card-id', card.id);
rightRow.oncontextmenu = (e) => {
openTimelineContextMenu(e, card.id);
return false;
};
// Klikken op een lege plek in de rij om direct in te plannen
rightRow.onclick = (e) => {
if (e.target.closest('.timeline-bar')) return;
@@ -803,6 +852,10 @@ function renderTimeline() {
barEl.style.left = `${leftPx}px`;
barEl.style.width = `${widthPx}px`;
barEl.title = `${escapeHtml(card.title)} (${card.start_date} t/m ${card.end_date})`;
barEl.oncontextmenu = (ev) => {
openTimelineContextMenu(ev, card.id);
return false;
};
barEl.onclick = (ev) => {
ev.stopPropagation();
openCardModal(card.id);
@@ -1172,9 +1225,126 @@ function escapeHtml(str) {
.replace(/'/g, '&#039;');
}
// --- Rechtermuisknop Context Menu voor Tijdlijn ---
let activeTimelineCtxCardId = null;
function openTimelineContextMenu(e, cardId) {
e.preventDefault();
e.stopPropagation();
activeTimelineCtxCardId = cardId;
const menu = document.getElementById('timelineContextMenu');
if (!menu) return;
menu.classList.remove('hidden');
const x = Math.min(e.clientX, window.innerWidth - 240);
const y = Math.min(e.clientY, window.innerHeight - 240);
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function closeTimelineContextMenu() {
const menu = document.getElementById('timelineContextMenu');
if (menu) menu.classList.add('hidden');
activeTimelineCtxCardId = null;
}
document.addEventListener('click', (e) => {
if (!e.target.closest('#timelineContextMenu')) {
closeTimelineContextMenu();
}
});
function triggerTimelineCtxEdit() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
openCardModal(cid);
}
function triggerTimelineCtxQuickSlot() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
openCardModal(cid);
setTimeout(() => {
findFreeSlotForTimelineAssignee();
}, 150);
}
async function triggerTimelineCtxDelete() {
if (!activeTimelineCtxCardId) return;
const cid = activeTimelineCtxCardId;
closeTimelineContextMenu();
if (!confirm('Weet je zeker dat je deze taak wilt verwijderen?')) return;
try {
const res = await fetch('/api/board.php?action=delete_card', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ card_id: cid })
});
const data = await res.json();
if (data.success) {
await loadBoardData(currentBoardId);
} else {
alert(data.error || 'Kon taak niet verwijderen');
}
} catch(e) {
alert('Netwerkfout bij verwijderen');
}
}
// Zoek vrij agendablok voor de in de tijdlijn-modal geselecteerde assignee
async function findFreeSlotForTimelineAssignee() {
const sel = document.getElementById('cardAssigneeSelect');
const targetUserId = sel.value ? parseInt(sel.value, 10) : currentUserId;
const schedToggle = document.getElementById('cardScheduleToggle');
if (!schedToggle.checked) {
schedToggle.checked = true;
toggleScheduleFields(true);
}
const btn = document.getElementById('btnTimelineFindSlot');
const origHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i><span>Zoeken...</span>';
if (typeof lucide !== 'undefined') lucide.createIcons();
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: 60,
target_user_id: targetUserId
})
});
const data = await res.json();
if (data.success && data.slot) {
const slot = data.slot;
document.getElementById('cardStartDate').value = slot.date;
document.getElementById('cardEndDate').value = slot.date;
const notice = document.getElementById('timelineFoundSlotNotice');
const noticeText = document.getElementById('timelineFoundSlotText');
noticeText.innerHTML = `Vrij agendablok gevonden: <strong>${slot.formatted_date}</strong> om <strong>${slot.start} – ${slot.end}</strong>`;
notice.classList.remove('hidden');
} else {
alert(data.error || 'Geen beschikbaar tijdblok gevonden binnen werktijden voor deze collega.');
}
} catch (err) {
alert('Netwerkfout bij zoeken van vrij agendablok: ' + err.message);
} finally {
btn.disabled = false;
btn.innerHTML = origHtml;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeCardModal();
closeTimelineContextMenu();
}
});
</script>