bmc_hub/static/js/case-week-calendar.js

60 lines
5.2 KiB
JavaScript
Raw Normal View History

/* Case-local time grid; the shared planner remains responsible for loading. */
(() => {
'use strict';
window.renderCaseWeekCalendar = function(host, anchor, items) {
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'}[c]));
const key = date => `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
const time = date => date.toLocaleTimeString('da-DK', {hour:'2-digit', minute:'2-digit'});
const monday = new Date(anchor);
monday.setHours(0,0,0,0);
const days = [];
let firstHour = 7, lastHour = 17;
for (let index = 0; index < 7; index++) {
const date = new Date(monday); date.setDate(date.getDate()+index);
const next = new Date(date); next.setDate(next.getDate()+1);
const segments = items.filter(item => new Date(item.starts_at) < next && new Date(item.ends_at) > date).map(item => {
const start = new Date(Math.max(new Date(item.starts_at).getTime(), date.getTime()));
const end = new Date(Math.min(new Date(item.ends_at).getTime(), next.getTime()));
const from = start.getHours()*60+start.getMinutes();
const to = end.getTime() === next.getTime() ? 1440 : end.getHours()*60+end.getMinutes();
firstHour = Math.min(firstHour, Math.floor(from/60));
lastHour = Math.max(lastHour, Math.ceil(to/60));
return {item, start, end, from, to};
}).sort((a,b) => a.from-b.from);
if (index < 5 || segments.length) days.push({date, segments});
}
const height = (lastHour-firstHour)*96;
host.classList.add('v5-time-grid');
host.style.setProperty('--week-days', days.length);
host.dataset.firstHour = String(firstHour);
host.dataset.lastHour = String(lastHour);
host.innerHTML = '<div class="v5-time-corner">Kl.</div>' + days.map(({date}) => `<div class="v5-time-heading ${key(date) === key(new Date()) ? 'is-today' : ''}">${esc(date.toLocaleDateString('da-DK',{weekday:'short',day:'numeric',month:'short'}))}</div>`).join('') +
`<div class="v5-time-labels" style="height:${height}px">${Array.from({length:lastHour-firstHour+1},(_,i)=>`<span style="top:${i*96}px">${String(firstHour+i).padStart(2,'0')}:00</span>`).join('')}</div>` +
days.map(({date,segments}) => {
const lanes = [];
for (const segment of segments) {
let lane = lanes.findIndex(end => end <= segment.from);
if (lane < 0) lane = lanes.length;
lanes[lane] = segment.to; segment.lane = lane;
}
return `<div class="v5-time-day" data-date="${key(date)}" style="height:${height}px">${segments.map(({item,start,end,from,to,lane}) => {
const canResize = !item.locked && new Date(item.ends_at).getTime() === end.getTime();
return `<div class="case-planner-calendar-item v5-time-event" ${!item.locked ? `draggable="true" data-move-id="${Number(item.id)}" data-version="${Number(item.version)}" data-start="${esc(item.starts_at)}" data-end="${esc(item.ends_at)}" data-segment-start="${esc(start.toISOString())}" title="Træk aftalen til en anden dag eller tid"` : ''} style="top:${(from-firstHour*60)*1.6}px;height:${(to-from)*1.6}px;left:calc(${lane*100/lanes.length}% + 2px);width:calc(${100/lanes.length}% - 4px)"><strong>${time(start)}–${time(end)}</strong><div>${esc(item.user_name || 'Ukendt')}</div>${item.note ? `<div>${esc(item.note)}</div>` : ''}${item.conflict_status && item.conflict_status !== 'none' ? '<span class="badge text-bg-warning">Overlap</span>' : ''}${canResize ? `<button type="button" class="case-slot-resize" data-allocation-id="${Number(item.id)}" data-version="${Number(item.version)}" data-start="${esc(item.starts_at)}" data-end="${esc(item.ends_at)}" data-pixels-per-step="24" aria-label="Ændr varighed: træk eller brug piletaster" title="Træk for at ændre varighed">━</button>` : ''}</div>`;
}).join('')}</div>`;
}).join('');
const friday = days[days.length-1].date;
document.getElementById('casePlannerWeekLabel').textContent = `${monday.toLocaleDateString('da-DK',{day:'numeric',month:'short'})} – ${friday.toLocaleDateString('da-DK',{day:'numeric',month:'short',year:'numeric'})}`;
if (!host.dataset.timeGridBound) {
host.dataset.timeGridBound = 'true';
host.addEventListener('click', event => {
if (event.target.closest('.v5-time-event')) return;
const day = event.target.closest('.v5-time-day');
if (!day) return;
const minutes = Math.max(0, Math.min(1425, Number(host.dataset.firstHour)*60 + Math.floor((event.clientY-day.getBoundingClientRect().top)/24)*15));
window.openEmbeddedPlannerSlot(day.dataset.date);
document.getElementById('casePlannerSlotTime').value = `${String(Math.floor(minutes/60)).padStart(2,'0')}:${String(minutes%60).padStart(2,'0')}`;
});
}
};
})();