147 lines
6.3 KiB
JavaScript
147 lines
6.3 KiB
JavaScript
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;
|
|
}
|