${fmtMin(c.planned_minutes)} planlagt${fmtMin(c.remaining_minutes)} tilbage
`).join(''):'
Ingen sager matcher filtrene
';document.querySelectorAll('.case-card').forEach(el=>el.ondragstart=e=>{e.dataTransfer.setData('application/x-case',el.dataset.case);setCompactDragImage(e,el)});document.querySelectorAll('[data-time-case]').forEach(el=>el.onclick=e=>{e.stopPropagation();showTimeEntries(Number(el.dataset.timeCase))});document.querySelectorAll('[data-estimate-case]').forEach(el=>el.onclick=e=>{e.stopPropagation();setEstimate(Number(el.dataset.estimateCase))})}
async function setEstimate(caseId){const c=state.cases.find(x=>x.id===caseId),answer=prompt('Forventet tidsforbrug i timer (fx 2,5):',c?.estimated_minutes?String(c.estimated_minutes/60).replace('.',','):'');if(answer===null)return;const hours=Number(answer.replace(',','.'));if(!Number.isFinite(hours)||hours<0){toast('Angiv et gyldigt antal timer');return}try{await api(`/api/v1/planner/cases/${caseId}/estimate`,{method:'PATCH',body:JSON.stringify({estimated_minutes:Math.round(hours*60)})});toast('Forventet tid er gemt');await loadCases()}catch(e){toast(e.message)}}
async function showTimeEntries(caseId){const c=state.cases.find(x=>x.id===caseId);$('#timeDialogTitle').textContent=`#${caseId} · ${c?.title||'Sag'}`;$('#timeDialogTotal').textContent='Henter tidsregistreringer…';$('#timeEntryList').innerHTML='';$('#timeDialogCaseLink').href=`/sag/${caseId}/v3`;$('#timeDialog').showModal();try{const data=await api(`/api/v1/planner/cases/${caseId}/time-entries`);$('#timeDialogTotal').textContent=`I alt ${fmtMin(data.total_minutes)} brugt`;$('#timeEntryList').innerHTML=data.items.length?data.items.map(x=>{const when=x.start_tid?new Date(x.start_tid).toLocaleString('da-DK'):(x.worked_date?new Date(x.worked_date+'T12:00').toLocaleDateString('da-DK'):'Ukendt tidspunkt');return `
`}}
function render(a,b){const opts={day:'numeric',month:'short'};$('#periodLabel').textContent=state.view==='month'?a.toLocaleDateString('da-DK',{month:'long',year:'numeric'}):`${a.toLocaleDateString('da-DK',opts)} – ${new Date(b-1).toLocaleDateString('da-DK',{...opts,year:'numeric'})}`;state.view==='month'?renderMonth(a):renderTimeline(a,b)}
function cap(uid,key){return state.data.capacity.find(c=>c.user_id===uid&&c.date===key)||{state:'gray',label:'Ingen arbejdstid'}}
function canEdit(uid){return (state.meta.editable_user_ids||[]).includes(Number(uid))}
function integrationWarning(){return (state.data.integration_statuses||[]).some(x=>x.status==='degraded')?'
Ekstern travl tid kan mangle. Intern planlægning virker fortsat.
':''}
function blockStyle(start,end,lane={index:0,count:1}){const first=7*60,last=18*60,total=last-first,s=new Date(start),e=new Date(end),sm=s.getHours()*60+s.getMinutes(),em=e.getHours()*60+e.getMinutes(),top=Math.max(0,(sm-first)/total*660),height=Math.max(24,(Math.min(last,em)-Math.max(first,sm))/total*660),left=lane.index*(100/lane.count),leftPx=26-lane.index*(29/lane.count),width=100/lane.count,widthPx=29/lane.count;return `top:${top}px;height:${height}px;left:calc(${left}% + ${leftPx}px);right:auto;width:calc(${width}% - ${widthPx}px)`}
function overlapLayout(planned,registered){const all=[...planned.map(item=>({key:`p:${item.id}`,item})),...registered.map(item=>({key:`r:${item.id}`,item}))].sort((a,b)=>new Date(a.item.starts_at)-new Date(b.item.starts_at));const result=new Map();let group=[],groupEnd=0;const flush=()=>{if(!group.length)return;const laneEnds=[];for(const event of group){const start=+new Date(event.item.starts_at);let lane=laneEnds.findIndex(end=>end<=start);if(lane<0)lane=laneEnds.length;laneEnds[lane]=+new Date(event.item.ends_at);event.lane=lane}for(const event of group)result.set(event.key,{index:event.lane,count:laneEnds.length});group=[];groupEnd=0};for(const event of all){const start=+new Date(event.item.starts_at),end=+new Date(event.item.ends_at);if(group.length&&start>=groupEnd)flush();group.push(event);groupEnd=Math.max(groupEnd,end)}flush();return result}
function hourMarks(){return Array.from({length:12},(_,i)=>`${pad(i+7)}:00`).join('')}
function registeredHtml(x,lane){const s=new Date(x.starts_at),e=new Date(x.ends_at);return `
`;for(const d of days){const key=dateKey(d),c=cap(u.id,key),items=state.data.allocations.filter(x=>x.user_id===u.id&&dateKey(new Date(x.starts_at))===key),allRegistered=(state.data.time_entries||[]).filter(x=>x.user_id===u.id&&dateKey(new Date(x.starts_at))===key),registered=allRegistered.filter(x=>x.has_time),untimed=allRegistered.filter(x=>!x.has_time),busy=state.data.external_busy.filter(x=>x.user_id===u.id&&dateKey(new Date(x.starts_at))===key),lanes=overlapLayout(items,registered);html+=`