diff --git a/jira.php b/jira.php index 76b0e88..92b7608 100644 --- a/jira.php +++ b/jira.php @@ -156,18 +156,7 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT Koppel Jira Cloud - - -
-
- - Verbonden met als -
-
- Laatste synchronisatie: -
-
- +
@@ -177,273 +166,280 @@ $boardsJson = json_encode($boards, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT Controleer instellingen
- - -
- -
-
- Aan mij toegewezen -
- -
+ +
+
+ + + Verbonden met als + + + + + Demodataset actief met voorbeeldtickets +
-
- - persoonlijke taken -
-
- -
-
- Team & Projecten -
- -
-
-
- - in actieve projecten -
-
- - -
-
- In Uitvoering -
- -
-
-
- ... - actief mee bezig -
-
- - -
-
- Hoge Prioriteit / Urgent -
- -
-
-
- ... - Highest / High -
-
-
- - -
-
-
-
- -
-
-

Jira Projecten

-

Klik op een project om direct te filteren

-
-
- -
- -
- - - - - $pData): ?> - - +
-
- -
-
-
-
- + + - -
+ +
+ + + + +
$widget): if (empty($widget['enabled'])) continue; $span = (int)($widget['col_span'] ?? 6); @@ -583,7 +598,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
-
+
@@ -1526,6 +1541,7 @@ for ($h = $startHour; $h <= $endHour; $h++) {
+
{ - handle.setAttribute('draggable', 'true'); + handle.setAttribute('draggable', 'false'); // Uitschakelen van krakkemikkige native browser drag + handle.style.touchAction = 'none'; + + handle.onpointerdown = (e) => { + // Alleen primaire muisklik (links) + if (e.button !== 0) return; + // Negeer als er op knoppen in de buurt geklikt wordt + if (e.target.closest('.widget-settings-btn') || e.target.closest('.widget-settings-dropdown')) return; - handle.ondragstart = (e) => { const wrapper = handle.closest('.fociz-widget-wrapper'); if (!wrapper) return; - draggedWidgetEl = wrapper; - // Bewaar beginpositie om te controleren op wijzigingen - const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')); - dragInitialIndex = allWrappers.indexOf(draggedWidgetEl); - - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', wrapper.getAttribute('data-widget-id')); - - // Sluit eventuele open popover menu's - document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden')); - - setTimeout(() => { - if (draggedWidgetEl) { - draggedWidgetEl.classList.add('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl'); - } - }, 0); - }; - - handle.ondragend = (e) => { - if (draggedWidgetEl) { - draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl'); - } - saveCurrentGridOrderIfChanged(); - draggedWidgetEl = null; + dragCandidate = { + wrapper: wrapper, + handle: handle, + startX: e.clientX, + startY: e.clientY + }; }; }); - - // Dragover op grid: verplaatst de widget live in het DOM zodat andere blokken direct opzij schuiven - grid.ondragover = (e) => { - if (!draggedWidgetEl) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - - // Check of cursor onderaan het grid zweeft - const gridRect = grid.getBoundingClientRect(); - if (e.clientY > gridRect.bottom - 40) { - if (grid.lastElementChild !== draggedWidgetEl) { - grid.appendChild(draggedWidgetEl); - } - return; - } - - // Check of cursor bovenaan het grid zweeft - if (e.clientY < gridRect.top + 40) { - if (grid.firstElementChild !== draggedWidgetEl) { - grid.prepend(draggedWidgetEl); - } - return; - } - - const target = e.target.closest('.fociz-widget-wrapper'); - if (target && target !== draggedWidgetEl && target.parentElement === grid) { - const rect = target.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - const midX = rect.left + rect.width / 2; - - // Bepaal of de muis voorbij het midden is (verticaal of horizontaal in dezelfde rij) - const isAfter = (e.clientY > midY) || (Math.abs(e.clientY - midY) < rect.height * 0.35 && e.clientX > midX); - - if (isAfter) { - if (target.nextElementSibling !== draggedWidgetEl) { - target.after(draggedWidgetEl); - } - } else { - if (target.previousElementSibling !== draggedWidgetEl) { - target.before(draggedWidgetEl); - } - } - } - }; - - grid.ondrop = (e) => { - e.preventDefault(); - if (draggedWidgetEl) { - draggedWidgetEl.classList.remove('opacity-40', 'scale-[0.98]', 'ring-2', 'ring-indigo-500/70', 'shadow-2xl'); - } - saveCurrentGridOrderIfChanged(); - draggedWidgetEl = null; - }; } +// Globale pointer listeners (blijven soepel actief zelfs als cursor snel beweegt) +window.addEventListener('pointermove', (e) => { + currentPointerX = e.clientX; + currentPointerY = e.clientY; + + if (!isDraggingActive && dragCandidate) { + // Controleer of de gebruiker minimaal 5px heeft bewogen (voorkomt valse triggers bij simpele kliks) + const dist = Math.hypot(e.clientX - dragCandidate.startX, e.clientY - dragCandidate.startY); + if (dist > 5) { + startWidgetDragging(dragCandidate.wrapper, dragCandidate.startX, dragCandidate.startY); + } + } + + if (isDraggingActive) { + updateFloatingGhostPosition(); + updateDropTargetFromPointer(); + } +}); + +window.addEventListener('pointerup', (e) => { + if (isDraggingActive) { + finishWidgetDragging(); + } + dragCandidate = null; +}); + +window.addEventListener('pointercancel', () => { + if (isDraggingActive) { + cancelWidgetDragging(); + } + dragCandidate = null; +}); + +window.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && isDraggingActive) { + cancelWidgetDragging(); + } +}); + +// Start het oppakken & slepen van de widget +function startWidgetDragging(wrapper, startX, startY) { + const grid = document.getElementById('focizWidgetGrid'); + if (!grid || !wrapper) return; + + isDraggingActive = true; + activeDraggedWrapper = wrapper; + + // Sluit openstaande popovers + document.querySelectorAll('.widget-settings-dropdown').forEach(d => d.classList.add('hidden')); + + // Bepaal beginpositie + const allWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')); + dragInitialIndex = allWrappers.indexOf(wrapper); + + // 1. Toon het 12-koloms Blueprint Achtergrond Raster + const blueprint = document.getElementById('widgetGridBlueprint'); + if (blueprint) { + blueprint.classList.remove('hidden'); + blueprint.classList.add('block'); + } + + // 2. Creëer de zwevende Hover Ghost kaart + const rect = wrapper.getBoundingClientRect(); + grabOffsetX = startX - rect.left; + grabOffsetY = startY - rect.top; + + floatingGhost = wrapper.cloneNode(true); + floatingGhost.id = 'widgetFloatingGhost'; + floatingGhost.removeAttribute('data-widget-id'); + floatingGhost.querySelectorAll('[id]').forEach(el => el.removeAttribute('id')); + floatingGhost.querySelectorAll('.widget-settings-dropdown').forEach(d => d.remove()); + + floatingGhost.style.position = 'fixed'; + floatingGhost.style.left = `${rect.left}px`; + floatingGhost.style.top = `${rect.top}px`; + floatingGhost.style.width = `${rect.width}px`; + floatingGhost.style.height = `${rect.height}px`; + floatingGhost.style.pointerEvents = 'none'; + floatingGhost.style.zIndex = '999999'; + floatingGhost.style.transform = 'scale(1.02) rotate(1.2deg)'; + floatingGhost.style.opacity = '0.94'; + floatingGhost.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease'; + floatingGhost.style.boxShadow = '0 25px 60px -10px rgba(79, 70, 229, 0.45), 0 0 0 2px rgba(99, 102, 241, 0.7)'; + floatingGhost.style.cursor = 'grabbing'; + document.body.appendChild(floatingGhost); + + // 3. Plaats een duidelijke Drop Slot Placeholder in het raster + const span = wrapper.getAttribute('data-col-span') || '6'; + dropPlaceholder = document.createElement('div'); + dropPlaceholder.id = 'widgetDropPlaceholder'; + dropPlaceholder.className = `lg:col-span-${span} col-span-1 min-w-0 rounded-2xl border-2 border-dashed border-indigo-500 dark:border-indigo-400 bg-indigo-50/70 dark:bg-indigo-950/50 flex flex-col items-center justify-center p-6 transition-all duration-200 shadow-inner`; + dropPlaceholder.style.minHeight = `${Math.max(130, Math.min(rect.height, 280))}px`; + dropPlaceholder.innerHTML = ` +
+ + Laat los om hier te plaatsen +
+ `; + wrapper.after(dropPlaceholder); + wrapper.style.display = 'none'; + + document.body.classList.add('select-none', 'cursor-grabbing'); + + // 4. Start automatische soepele scroll bij nadering van schermranden + startAutoScroller(); +} + +function updateFloatingGhostPosition() { + if (!floatingGhost) return; + floatingGhost.style.left = `${currentPointerX - grabOffsetX}px`; + floatingGhost.style.top = `${currentPointerY - grabOffsetY}px`; +} + +function updateDropTargetFromPointer() { + const grid = document.getElementById('focizWidgetGrid'); + if (!grid || !dropPlaceholder || !activeDraggedWrapper) return; + + const wrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')).filter(w => w !== activeDraggedWrapper); + let targetFound = false; + + for (const wrap of wrappers) { + const rect = wrap.getBoundingClientRect(); + if (currentPointerY >= rect.top - 15 && currentPointerY <= rect.bottom + 15 && + currentPointerX >= rect.left - 15 && currentPointerX <= rect.right + 15) { + + const midY = rect.top + rect.height / 2; + const midX = rect.left + rect.width / 2; + const isAfter = (currentPointerY > midY) || (Math.abs(currentPointerY - midY) < rect.height * 0.35 && currentPointerX > midX); + + if (isAfter) { + if (wrap.nextElementSibling !== dropPlaceholder) { + wrap.after(dropPlaceholder); + } + } else { + if (wrap.previousElementSibling !== dropPlaceholder) { + wrap.before(dropPlaceholder); + } + } + targetFound = true; + break; + } + } + + if (!targetFound && wrappers.length > 0) { + const firstRect = wrappers[0].getBoundingClientRect(); + const lastRect = wrappers[wrappers.length - 1].getBoundingClientRect(); + if (currentPointerY < firstRect.top) { + if (grid.firstElementChild !== dropPlaceholder) { + grid.prepend(dropPlaceholder); + } + } else if (currentPointerY > lastRect.bottom) { + if (grid.lastElementChild !== dropPlaceholder) { + grid.appendChild(dropPlaceholder); + } + } + } +} + +// Auto-scroller wanneer je met de widget naar de boven- of onderkant van het scherm beweegt +function startAutoScroller() { + stopAutoScroller(); + function scrollStep() { + if (!isDraggingActive) return; + const viewportHeight = window.innerHeight; + const threshold = 110; + + if (currentPointerY < threshold && window.scrollY > 0) { + const factor = (threshold - currentPointerY) / threshold; + const speed = Math.max(3, Math.round(factor * 26)); + window.scrollBy(0, -speed); + updateDropTargetFromPointer(); + } else if (currentPointerY > viewportHeight - threshold) { + const factor = (currentPointerY - (viewportHeight - threshold)) / threshold; + const speed = Math.max(3, Math.round(factor * 26)); + window.scrollBy(0, speed); + updateDropTargetFromPointer(); + } + autoScrollAnimId = requestAnimationFrame(scrollStep); + } + autoScrollAnimId = requestAnimationFrame(scrollStep); +} + +function stopAutoScroller() { + if (autoScrollAnimId) { + cancelAnimationFrame(autoScrollAnimId); + autoScrollAnimId = null; + } +} + +// Voltooi het slepen (loslaten & AJAX opslaan) +function finishWidgetDragging() { + isDraggingActive = false; + stopAutoScroller(); + + document.body.classList.remove('select-none', 'cursor-grabbing'); + + // Verberg blueprint raster + const blueprint = document.getElementById('widgetGridBlueprint'); + if (blueprint) { + blueprint.classList.add('hidden'); + blueprint.classList.remove('block'); + } + + // Verwijder zwevende ghost + if (floatingGhost) { + floatingGhost.remove(); + floatingGhost = null; + } + + // Plaats originele widget terug op de plek van de placeholder + if (dropPlaceholder && activeDraggedWrapper) { + dropPlaceholder.replaceWith(activeDraggedWrapper); + activeDraggedWrapper.style.display = ''; + dropPlaceholder = null; + + // Subtiel succes flitsje op de neergezette widget + activeDraggedWrapper.classList.add('ring-2', 'ring-indigo-500', 'shadow-xl'); + setTimeout(() => { + activeDraggedWrapper?.classList.remove('ring-2', 'ring-indigo-500', 'shadow-xl'); + }, 400); + } + + // Controleer of de volgorde gewijzigd is en sla op via AJAX + saveCurrentGridOrderIfChanged(); + activeDraggedWrapper = null; +} + +// Annuleer slepen (bijv. via Escape) +function cancelWidgetDragging() { + isDraggingActive = false; + stopAutoScroller(); + + document.body.classList.remove('select-none', 'cursor-grabbing'); + + const blueprint = document.getElementById('widgetGridBlueprint'); + if (blueprint) { + blueprint.classList.add('hidden'); + blueprint.classList.remove('block'); + } + + if (floatingGhost) { + floatingGhost.remove(); + floatingGhost = null; + } + + if (dropPlaceholder && activeDraggedWrapper) { + dropPlaceholder.remove(); + dropPlaceholder = null; + activeDraggedWrapper.style.display = ''; + } + + activeDraggedWrapper = null; +} + +// Controleer gewijzigde volgorde en sla asynchroon op via AJAX function saveCurrentGridOrderIfChanged() { const grid = document.getElementById('focizWidgetGrid'); if (!grid) return; const currentWrappers = Array.from(grid.querySelectorAll('.fociz-widget-wrapper')); - const currentIndex = currentWrappers.indexOf(draggedWidgetEl); + const currentIndex = activeDraggedWrapper ? currentWrappers.indexOf(activeDraggedWrapper) : -1; let hasOrderChanged = false; currentWrappers.forEach((el, index) => { @@ -2219,6 +2434,7 @@ function saveCurrentGridOrderIfChanged() { }); if (hasOrderChanged || (dragInitialIndex !== -1 && currentIndex !== dragInitialIndex)) { + // Geruisloos opslaan via AJAX persistWidgetLayoutAuto(); showWidgetToast('Widget verplaatst & indeling opgeslagen', 'success'); }