bmc_hub/static/js/case-slot-resize.js

84 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

/* Resize case bookings through the existing versioned planner API. */
(() => {
'use strict';
const host = document.getElementById('casePlannerWeek');
if (!host) return;
let drag = null;
let saving = false;
const duration = button => (new Date(button.dataset.end) - new Date(button.dataset.start)) / 60000;
const endAt = (button, minutes) => new Date(new Date(button.dataset.start).getTime() + minutes * 60000);
function preview(button, minutes) {
button.closest('.case-planner-calendar-item').querySelector('strong').textContent =
`${new Date(button.dataset.start).toLocaleTimeString('da-DK', {hour:'2-digit', minute:'2-digit'})}–${endAt(button, minutes).toLocaleTimeString('da-DK', {hour:'2-digit', minute:'2-digit'})} · ${minutes} min`;
const card = button.closest('.v5-time-event');
if (card && button.dataset.pixelsPerStep) card.style.height = `${Math.max(24, card.offsetHeight + (minutes - Number(button.dataset.previewMinutes || duration(button))) * 1.6)}px`;
button.dataset.previewMinutes = String(minutes);
}
async function save(button, minutes) {
if (minutes === duration(button)) return;
saving = true;
button.disabled = true;
const payload = {version: Number(button.dataset.version), ends_at: endAt(button, minutes).toISOString()};
try {
for (;;) {
const response = await fetch(`/api/v1/planner/allocations/${Number(button.dataset.allocationId)}`, {
method: 'PATCH', credentials: 'include',
headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload),
});
const body = await response.json();
if (response.ok) break;
const message = typeof body.detail === 'string' ? body.detail : body.detail?.message;
if (response.status === 409 && body.detail?.conflict_status && !payload.confirm_conflicts &&
window.confirm(`${message}. Vil du gemme alligevel?`)) {
payload.confirm_conflicts = true;
continue;
}
throw new Error(message || 'Kunne ikke ændre varigheden');
}
window.showCaseFeedback('Aftalens varighed er gemt', 'success');
} catch (error) {
window.showCaseFeedback(error.message);
} finally {
await window.loadEmbeddedCasePlanner(true);
saving = false;
button.disabled = false;
}
}
host.addEventListener('click', event => {
if (event.target.closest('.case-slot-resize')) event.stopPropagation();
});
host.addEventListener('pointerdown', event => {
const button = event.target.closest('.case-slot-resize');
if (!button || saving || drag || (event.button !== 0 && event.pointerType !== 'touch')) return;
event.preventDefault();
event.stopPropagation();
drag = {button, y: event.clientY, original: duration(button), minutes: duration(button), pointer: event.pointerId};
button.setPointerCapture(event.pointerId);
button.classList.add('is-resizing');
});
host.addEventListener('pointermove', event => {
if (!drag || drag.pointer !== event.pointerId) return;
drag.minutes = Math.max(15, drag.original + Math.round((event.clientY - drag.y) / (Number(drag.button.dataset.pixelsPerStep) || 12)) * 15);
preview(drag.button, drag.minutes);
});
function finish(event, cancel) {
if (!drag || drag.pointer !== event.pointerId) return;
const current = drag;
drag = null;
current.button.classList.remove('is-resizing');
if (current.button.hasPointerCapture(event.pointerId)) current.button.releasePointerCapture(event.pointerId);
if (cancel) preview(current.button, current.original);
else void save(current.button, current.minutes);
}
host.addEventListener('pointerup', event => finish(event, false));
host.addEventListener('pointercancel', event => finish(event, true));
host.addEventListener('lostpointercapture', event => finish(event, true));
host.addEventListener('keydown', event => {
const button = event.target.closest('.case-slot-resize');
if (!button || saving || drag || !['ArrowUp', 'ArrowDown'].includes(event.key)) return;
event.preventDefault();
event.stopPropagation();
void save(button, Math.max(15, duration(button) + (event.key === 'ArrowDown' ? 15 : -15)));
});
})();