function createWeekCalendarImport() { const maxBytes=1024*1024; function clock(value) { const match=/^(\d{2}):(\d{2})$/.exec(String(value||'')); if(!match)return null; const minutes=Number(match[1])*60+Number(match[2]); return Number(match[1])<24&&Number(match[2])<60?minutes:null; } function instant(value) { const match=/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)$/.exec(value||''); if(!match)return null; const parts=match.slice(1,7).map(Number); const milliseconds=match[7]?Date.UTC(parts[0],parts[1]-1,parts[2],parts[3],parts[4],parts[5]): new Date(parts[0],parts[1]-1,parts[2],parts[3],parts[4],parts[5]).getTime(); return Number.isFinite(milliseconds)?milliseconds:null; } function events(source) { if(typeof source!=='string'||!source.includes('BEGIN:VCALENDAR'))throw new Error('Choose a valid .ics calendar file.'); if(new TextEncoder().encode(source).length>maxBytes)throw new Error('Calendar files must be 1 MB or smaller.'); const lines=source.replace(/\r\n[ \t]/g,'').split(/\r?\n/); const result=[];let current=null; lines.forEach(line=>{ if(line==='BEGIN:VEVENT'){current={};return;} if(line==='END:VEVENT'){ if(current?.start!=null&¤t?.end>current.start)result.push(current); current=null;return; } if(!current)return; const separator=line.indexOf(':');if(separator<0)return; const name=line.slice(0,separator).split(';')[0],value=line.slice(separator+1); if(name==='DTSTART')current.start=instant(value); if(name==='DTEND')current.end=instant(value); }); return result; } function dayBoundary(date,minutes) { const [year,month,day]=date.split('-').map(Number); return new Date(year,month-1,day,Math.floor(minutes/60),minutes%60).getTime(); } function review(source,{dates,workdayStart='09:00',workdayEnd='17:00'}={}) { if(!Array.isArray(dates)||dates.length!==7)throw new Error('Week Ahead must contain seven dates.'); const startMinute=clock(workdayStart),endMinute=clock(workdayEnd); if(startMinute==null||endMinute==null||endMinute<=startMinute)throw new Error('Working hours must end after they start.'); const calendarEvents=events(source); return dates.map(plan_date=>{ const start=dayBoundary(plan_date,startMinute),end=dayBoundary(plan_date,endMinute); const ranges=calendarEvents.map(event=>[Math.max(start,event.start),Math.min(end,event.end)]) .filter(range=>range[1]>range[0]).sort((left,right)=>left[0]-right[0]); const merged=[]; ranges.forEach(range=>{ const previous=merged[merged.length-1]; if(previous&&range[0]<=previous[1])previous[1]=Math.max(previous[1],range[1]); else merged.push([...range]); }); const busy_minutes=Math.round(merged.reduce((total,range)=>total+range[1]-range[0],0)/60000); return {plan_date,busy_minutes,capacity_minutes:endMinute-startMinute-busy_minutes}; }); } function createWorkflow({controller,qs,onApplied=()=>{}}={}) { let reviewed=null; const root=()=>qs('#week-capacity-import'); function clear() { reviewed=null; qs('#week-capacity-file').value=''; qs('#week-capacity-review').hidden=true; qs('#week-capacity-days').innerHTML=''; } function open() { clear();root().hidden=false;qs('#week-capacity-status').textContent=''; qs('#week-capacity-file').focus?.();return true; } function cancel() {clear();root().hidden=true;return true;} function reviewSource(source) { const display=controller.dates(); reviewed=review(source,{dates:display.map(item=>item.date),workdayStart:qs('#week-capacity-start').value, workdayEnd:qs('#week-capacity-end').value}); qs('#week-capacity-days').innerHTML=reviewed.map((day,index)=>'
'+ display[index].label+''+day.capacity_minutes+' min available'+day.busy_minutes+ ' min busy during working hours
').join(''); qs('#week-capacity-review').hidden=false; qs('#week-capacity-status').textContent='Review seven capacity totals. Calendar details stay on this device.'; return reviewed.map(day=>({...day})); } async function apply() { if(!reviewed||!controller.stageCapacities(reviewed))return false; qs('#apply-week-capacities').disabled=true; try {await controller.flush();cancel();onApplied();return true;} finally {qs('#apply-week-capacities').disabled=false;} } qs('#week-capacity-file')?.addEventListener('change',async event=>{ const file=event.currentTarget.files?.[0];if(!file)return; try { if(file.size>maxBytes)throw new Error('Calendar files must be 1 MB or smaller.'); reviewSource(await file.text()); } catch(error) {clear();qs('#week-capacity-status').textContent=error.message||'Calendar could not be read.';} }); qs('#cancel-week-capacity-import')?.addEventListener('click',cancel); qs('#apply-week-capacities')?.addEventListener('click',()=>apply().catch(error=>{ qs('#week-capacity-status').textContent=(error.message||'Week Ahead sync is unavailable.')+' Capacities remain saved on this phone.'; })); return {open,cancel,review:reviewSource,apply,state:()=>reviewed?reviewed.map(day=>({...day})):null}; } function mount(controller,weekWorkflow,qs) { const workflow=createWorkflow({controller,qs,onApplied:weekWorkflow.renderReview}); qs('#open-week-capacity-import').addEventListener('click',workflow.open); return workflow; } return {review,createWorkflow,mount}; } const weekCalendarImport=createWeekCalendarImport(); if(typeof module!=='undefined'&&module.exports)module.exports=weekCalendarImport;