function createTodayWeekReschedule({ week, api, getToday, adoptToday = () => {}, operationId = () => globalThis.crypto?.randomUUID?.() || String(Date.now()), } = {}) { let current = null; async function open(identity) { const today = await api('api/v1/today'); if (!identity || !today?.ids?.includes(identity)) { throw new Error('The active Today item changed. Reopen rescheduling.'); } const loaded = await week.load(); if (loaded?.offline_snapshot || loaded?.sync_pending) { throw new Error('Reconnect before rescheduling Today into Week Ahead.'); } const estimate = Number(today.estimates?.[identity]); const estimateMinutes = Number.isFinite(estimate) && estimate > 0 ? estimate : null; const days = week.review().days.map(day => { const ids = (day.ids || []).filter(id => id !== identity); const planned = Number(day.planned_minutes) || 0; const existing = Number(day.estimates?.[identity]) || 0; return { ...day, ids, planned_minutes: Math.max(0, planned - existing), load: `${Math.max(0, planned - existing)} / ${Number(day.capacity_minutes) || 0} min`, eligible: ids.length < 5, }; }); current = { identity, today_revision: today.revision, week_revision: loaded.revision, operation_id: operationId(), estimate_minutes: estimateMinutes, days, }; return {...current, days:days.map(day => ({...day, ids:[...day.ids]}))}; } async function confirm(planDate, estimateMinutes, {allowOverload = false} = {}) { if (!current) throw new Error('Open rescheduling before choosing a day.'); const day = current.days.find(value => value.plan_date === planDate); if (!day) throw new Error('Choose one of the next seven days.'); const estimate = Number(estimateMinutes); if (!Number.isFinite(estimate) || estimate < 5 || estimate > 1440) { throw new Error('Estimate must be between 5 and 1440 minutes.'); } if (!day.eligible) throw new Error('That Week Ahead day already has five items.'); const capacity = Number(day.capacity_minutes) || 0; if (capacity && day.planned_minutes + estimate > capacity && !allowOverload) { throw new Error("That move exceeds the day's capacity. Confirm overload before rescheduling."); } const result = await api('api/v1/week/reschedule', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ operation_id:current.operation_id, identity:current.identity, estimate_minutes:estimate, plan_date:planDate, today_revision:current.today_revision, week_revision:current.week_revision, allow_over_capacity:allowOverload, }), }); adoptToday(result.today); week.adopt(result.week); current = null; return result; } function cancel() { current = null; } return {open, confirm, cancel, state:() => current}; } function mountTodayWeekReschedule({ qs, document=globalThis.document, week, api, getToday, adoptToday, currentTarget, closeActions, refresh, warm, continueToday, announce, schedule=callback=>requestAnimationFrame(callback), }={}) { const dialog=qs('#today-week-reschedule'),daysRoot=qs('#today-week-reschedule-days'); const estimate=qs('#today-week-reschedule-estimate'),status=qs('#today-week-reschedule-status'); const confirm=qs('#confirm-today-week-reschedule'),launcher=qs('[data-work-session-reschedule-week]'); let selectedDate=null,allowOverload=false; const controller=createTodayWeekReschedule({week,api,getToday,adoptToday}); function close() { controller.cancel();selectedDate=null;allowOverload=false; if(dialog.open)dialog.close();schedule(()=>{ qs('[data-mobile-today-more]').click();launcher.focus(); }); } function render(opened) { daysRoot.replaceChildren(...opened.days.map(day=>{ const button=document.createElement('button'); button.type='button';button.dataset.planDate=day.plan_date;button.setAttribute('role','radio'); button.setAttribute('aria-checked','false');button.disabled=!day.eligible; const label=document.createElement('strong');label.textContent=day.label; const load=document.createElement('span');load.className='small'; load.textContent=day.eligible?day.load:'5 items · full';button.append(label,load); button.addEventListener('click',()=>{ selectedDate=day.plan_date;allowOverload=false; daysRoot.querySelectorAll('button').forEach(choice=>choice.setAttribute( 'aria-checked',String(choice===button) )); status.textContent='';confirm.disabled=false;confirm.textContent='Move to Week Ahead & continue'; }); return button; })); } launcher.addEventListener('click',async()=>{ const target=currentTarget(); if(!target){announce('Start or resume a checkpointed Today item before rescheduling.');return;} closeActions();status.textContent='Loading Week Ahead…';confirm.disabled=true;dialog.showModal(); try{ const opened=await controller.open(target.identity);render(opened); estimate.value=opened.estimate_minutes||''; status.textContent=opened.estimate_minutes?'Choose a future day.':'Add an estimate, then choose a future day.'; schedule(()=>daysRoot.querySelector('button:not(:disabled)')?.focus()); }catch(error){status.textContent=error.message;} }); qs('#cancel-today-week-reschedule').addEventListener('click',close); dialog.addEventListener('cancel',event=>{event.preventDefault();close();}); confirm.addEventListener('click',async()=>{ if(!selectedDate)return; confirm.disabled=true;status.textContent='Moving Today into Week Ahead…'; try{ await controller.confirm(selectedDate,Number(estimate.value),{allowOverload}); await refresh();warm();dialog.close();announce('Moved to Week Ahead. Continuing Today.'); await continueToday(); }catch(error){ const overload=error.message.includes('Confirm overload');allowOverload=overload; status.textContent=error.message; confirm.textContent=overload?'Confirm overload & continue':'Move to Week Ahead & continue'; confirm.disabled=false; } }); return {controller,close}; } if (typeof module !== 'undefined' && module.exports) { module.exports = createTodayWeekReschedule; module.exports.mount = mountTodayWeekReschedule; }