80 lines
3.8 KiB
JavaScript
80 lines
3.8 KiB
JavaScript
function createTomorrowPlan({fetchJson,localDate,timeZone}={}) {
|
|
let plan={revision:0,ids:[],capacity_minutes:null,estimates:{}};
|
|
const state=()=>({...plan,ids:[...plan.ids],estimates:{...plan.estimates}});
|
|
function adopt(value) {
|
|
if (!Number.isInteger(value?.revision)||!Array.isArray(value?.ids)) return false;
|
|
plan={revision:value.revision,ids:[...value.ids],capacity_minutes:value.capacity_minutes??null,
|
|
estimates:{...(value.estimates||{})},...(value.plan_date?{plan_date:value.plan_date,timezone:value.timezone||null}:{})};
|
|
return state();
|
|
}
|
|
function nextLocalDate() {
|
|
const [y,m,d]=localDate().split('-').map(Number);
|
|
return new Date(Date.UTC(y,m-1,d+1)).toISOString().slice(0,10);
|
|
}
|
|
async function load(){return adopt(await fetchJson('api/v1/tomorrow'));}
|
|
async function save(value) {
|
|
const body={base_revision:plan.revision,ids:[...(value.ids||[])],
|
|
capacity_minutes:value.capacity_minutes??null,estimates:{...(value.estimates||{})},
|
|
plan_date:nextLocalDate(),timezone:timeZone()};
|
|
return adopt(await fetchJson('api/v1/tomorrow',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));
|
|
}
|
|
async function promote(today_revision) {
|
|
if (!plan.plan_date||plan.plan_date>localDate()||!plan.ids.length) return false;
|
|
const promotion_id=`tomorrow-${plan.plan_date}-r${plan.revision}`;
|
|
return fetchJson('api/v1/tomorrow/promote',{method:'POST',headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({promotion_id,tomorrow_revision:plan.revision,today_revision})});
|
|
}
|
|
function millisecondsUntilNextDay() {
|
|
const now=Date.now();
|
|
const zone=timeZone();
|
|
let formatter;
|
|
try {
|
|
formatter=new Intl.DateTimeFormat('en-CA',{timeZone:zone,year:'numeric',month:'2-digit',day:'2-digit',
|
|
hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23'});
|
|
} catch (_error) {
|
|
const next=new Date(now);
|
|
next.setHours(24,0,0,250);
|
|
return Math.max(1,next.getTime()-now);
|
|
}
|
|
const zoned=value=>Object.fromEntries(formatter.formatToParts(new Date(value))
|
|
.filter(part=>part.type!=='literal').map(part=>[part.type,Number(part.value)]));
|
|
const current=zoned(now);
|
|
const targetWall=Date.UTC(current.year,current.month-1,current.day+1);
|
|
let candidate=targetWall;
|
|
for(let attempt=0;attempt<2;attempt+=1){
|
|
const parts=zoned(candidate);
|
|
const offset=Date.UTC(parts.year,parts.month-1,parts.day,parts.hour,parts.minute,parts.second)-candidate;
|
|
candidate=targetWall-offset;
|
|
}
|
|
return Math.max(1,candidate-now+250);
|
|
}
|
|
function summary(value=plan) {
|
|
const ids=Array.isArray(value?.ids)?value.ids:[];
|
|
if(!ids.length) return 'Nothing planned';
|
|
const label=`${ids.length} planned`;
|
|
const estimates=value.estimates||{};
|
|
const estimated=ids.reduce((total,id)=>total+(Number(estimates[id])||0),0);
|
|
const capacity=Number(value.capacity_minutes)||0;
|
|
return estimated&&capacity?`${label} · ${estimated} of ${capacity} min`:label;
|
|
}
|
|
function startLifecycle({windowObject,documentObject,check,setTimer=setTimeout,clearTimer=clearTimeout,
|
|
nextDelay=millisecondsUntilNextDay}={}) {
|
|
let flight=null;
|
|
let timer=null;
|
|
const schedule=()=>{
|
|
if(timer!==null) clearTimer(timer);
|
|
timer=setTimer(run,nextDelay());
|
|
};
|
|
function run(){
|
|
if(!flight) flight=Promise.resolve().then(check).finally(()=>{flight=null;schedule();});
|
|
return flight;
|
|
}
|
|
windowObject?.addEventListener?.('online',run);
|
|
documentObject?.addEventListener?.('visibilitychange',()=>documentObject.hidden?false:run());
|
|
run();
|
|
return {run,stop(){if(timer!==null)clearTimer(timer);timer=null;}};
|
|
}
|
|
return {adopt,load,save,promote,state,summary,nextLocalDate,startLifecycle};
|
|
}
|
|
if(typeof module!=='undefined'&&module.exports)module.exports=createTomorrowPlan;
|