115 lines
7.3 KiB
JavaScript
115 lines
7.3 KiB
JavaScript
function createSearchWeekPlan({week,claim,accept=item=>item,identity,maxItems=5}={}) {
|
|
let running=null;
|
|
const eligible=detail=>Boolean(detail&&detail.kind==='issue'&&detail.state==='open'&&
|
|
(detail.claimable||detail.assigned_to_me));
|
|
const loadDay=date=>{
|
|
const value=week.day(date),estimates=value.estimates||{};
|
|
return {...value,planned_minutes:(value.ids||[]).reduce((total,id)=>total+(Number(estimates[id])||0),0)};
|
|
};
|
|
async function preview(detail) {
|
|
if(!eligible(detail))return {eligible:false,days:[]};
|
|
await week.load();
|
|
const id=identity(detail),existing=week.placement(id);
|
|
return {eligible:true,existing,days:week.dates().map(item=>({...item,...loadDay(item.date)}))};
|
|
}
|
|
function plan(detail,options={}) {
|
|
if(running)return running;
|
|
const perform=async()=>{
|
|
if(!eligible(detail))return {status:'ineligible'};
|
|
const estimate=Number(options.estimate);
|
|
if(!Number.isFinite(estimate)||estimate<=0)return {status:'estimate-required'};
|
|
await week.load();
|
|
if(!week.dates().some(item=>item.date===options.date))return {status:'date-required'};
|
|
const originalId=identity(detail),existing=week.placement(originalId);
|
|
if(existing&&existing.date!==options.date&&!options.move)
|
|
return {status:'already-planned',date:existing.date,estimate:existing.estimate};
|
|
const destination=loadDay(options.date);
|
|
const alreadyThere=(destination.ids||[]).includes(originalId);
|
|
if(!alreadyThere&&(destination.ids||[]).length>=maxItems)return {status:'day-full',limit:maxItems};
|
|
const previous=alreadyThere?(Number(destination.estimates?.[originalId])||0):0;
|
|
const plannedMinutes=destination.planned_minutes-previous+estimate;
|
|
if(Number(destination.capacity_minutes)>0&&plannedMinutes>Number(destination.capacity_minutes)&&!options.confirmOverload)
|
|
return {status:'overload-confirmation-required',planned_minutes:plannedMinutes,
|
|
capacity_minutes:Number(destination.capacity_minutes)};
|
|
let item=detail,assigned=false;
|
|
if(detail.claimable){item=await claim(detail);assigned=true;}
|
|
item=accept(item)||item;
|
|
const id=identity(item);
|
|
if(!week.place(id,options.date,estimate,{move:Boolean(options.move)}))
|
|
return {status:assigned?'assigned-not-planned':'not-planned',item};
|
|
try {
|
|
await week.flush();
|
|
return {status:'planned',date:options.date,estimate,item};
|
|
} catch(_error) {
|
|
return {status:'planned-pending',date:options.date,estimate};
|
|
}
|
|
};
|
|
running=perform().finally(()=>{running=null;});
|
|
return running;
|
|
}
|
|
return {eligible,preview,plan,pending:()=>Boolean(running)};
|
|
}
|
|
function createSearchWeekPlanUI({planner,document,window,getDetail,onPlanned=()=>{},announce=()=>{},escapeHtml,escapeAttribute}={}) {
|
|
const qs=selector=>document.querySelector(selector);
|
|
let trigger=null,detail=null,preview=null,overload=false;
|
|
function close(navigate=false) {
|
|
if(navigate&&window.history.state?.searchWeekPlan){window.history.back();return;}
|
|
qs('#search-week-plan-sheet').hidden=true;
|
|
detail=null;preview=null;overload=false;trigger?.focus?.();trigger=null;
|
|
}
|
|
function selectDate(date) {
|
|
qs('#search-week-plan-days').querySelectorAll('[data-search-week-date]').forEach(button=>
|
|
button.setAttribute('aria-pressed',String(button.dataset.searchWeekDate===date)));
|
|
qs('#search-week-plan-days').dataset.selected=date;
|
|
overload=false;qs('#confirm-search-week-plan').textContent='Plan';
|
|
}
|
|
async function open(button) {
|
|
const selected=getDetail();
|
|
if(!selected||planner.pending())return false;
|
|
trigger=button;detail=selected;overload=false;
|
|
const sheet=qs('#search-week-plan-sheet'),status=qs('#search-week-plan-status');
|
|
sheet.hidden=false;status.textContent='Loading Week Ahead…';qs('#confirm-search-week-plan').disabled=true;
|
|
window.history.pushState({...window.history.state,searchWeekPlan:true},'',window.location.href);
|
|
qs('#search-week-plan-copy').textContent=detail.title||'Untitled issue';
|
|
try {
|
|
preview=await planner.preview(detail);
|
|
const existing=preview.existing;
|
|
qs('#search-week-plan-days').innerHTML=preview.days.map((day,index)=>
|
|
'<button type="button" data-search-week-date="'+escapeAttribute(day.date)+'" aria-pressed="'+String(index===0)+'"><strong>'+escapeHtml(day.label)+
|
|
'</strong><small>'+escapeHtml(day.planned_minutes+(day.capacity_minutes?' of '+day.capacity_minutes:'')+' min · '+day.ids.length+' planned')+'</small></button>').join('');
|
|
qs('#search-week-plan-days').querySelectorAll('[data-search-week-date]').forEach(dayButton=>
|
|
dayButton.addEventListener('click',()=>selectDate(dayButton.dataset.searchWeekDate)));
|
|
selectDate(existing?.date||preview.days[0].date);
|
|
qs('#search-week-plan-estimate').value=existing?.estimate||'';
|
|
status.textContent=existing?'Already planned for '+existing.date+'. Choose another day to move it.':'Choose a day and add an estimate.';
|
|
qs('#confirm-search-week-plan').disabled=false;qs('#search-week-plan-estimate').focus();return true;
|
|
} catch(error) {status.textContent=(error.message||'Week Ahead is unavailable.')+' Retry when connected.';return false;}
|
|
}
|
|
async function confirm() {
|
|
if(!detail||planner.pending())return;
|
|
const date=qs('#search-week-plan-days').dataset.selected,estimate=Number(qs('#search-week-plan-estimate').value);
|
|
const existing=preview?.existing,move=Boolean(existing&&existing.date!==date);
|
|
const button=qs('#confirm-search-week-plan'),status=qs('#search-week-plan-status');button.disabled=true;
|
|
const outcome=await planner.plan(detail,{date,estimate,move,confirmOverload:overload});
|
|
if(outcome.status==='overload-confirmation-required'){
|
|
overload=true;button.textContent='Confirm over capacity';
|
|
status.textContent=outcome.planned_minutes+' min exceeds '+outcome.capacity_minutes+' min capacity. Confirm to plan anyway.';
|
|
} else if(outcome.status==='estimate-required')status.textContent='Enter an estimate in minutes.';
|
|
else if(outcome.status==='day-full')status.textContent='That day already has '+outcome.limit+' items. Choose another day.';
|
|
else if(outcome.status==='already-planned')status.textContent='Already planned for '+outcome.date+'. Choose Move to change the day.';
|
|
else if(outcome.status==='assigned-not-planned')status.textContent='Assigned, not planned. Find it in My Work and retry.';
|
|
else if(outcome.status==='planned-pending'){announce('Planned for '+date+' · sync pending.');onPlanned(detail,true);close(true);}
|
|
else if(outcome.status==='planned'){announce('Planned for '+date+'.');onPlanned(detail,false);close(true);}
|
|
else status.textContent='Could not plan this issue. Retry.';
|
|
button.disabled=false;
|
|
}
|
|
qs('#plan-search-result').addEventListener('click',event=>open(event.currentTarget));
|
|
qs('#cancel-search-week-plan').addEventListener('click',()=>close(true));
|
|
qs('#confirm-search-week-plan').addEventListener('click',confirm);
|
|
window.addEventListener('popstate',()=>{if(!qs('#search-week-plan-sheet').hidden)close();});
|
|
document.addEventListener('keydown',event=>{
|
|
if(event.key==='Escape'&&!qs('#search-week-plan-sheet').hidden){event.preventDefault();close(true);}
|
|
});
|
|
return {open,close,confirm};
|
|
}
|
|
if(typeof module!=='undefined'&&module.exports){module.exports=createSearchWeekPlan;module.exports.UI=createSearchWeekPlanUI;} |