stackchain-dashboard/frontend/create-and-start.js
timmy 5c9420ed9c
All checks were successful
CI / lint (pull_request) Successful in 1m48s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped
feat: require estimate before create and start (Closes #849)
2026-08-14 20:41:57 +00:00

85 lines
3.4 KiB
JavaScript

function createCreateAndStart({ todayWork, todaySync, refresh, warm, start, announce }) {
const completed = new Set();
function available() {
return todayWork.read().length < todayWork.limit;
}
function capacity(value) {
if (String(value ?? '').trim() === '') return { valid: false, reason: 'required' };
const minutes = Number(value);
if (!Number.isInteger(minutes) || minutes < 5 || minutes > 1440) {
return { valid: false, reason: 'range' };
}
const plan = todayWork.planning();
const capacityMinutes = plan.capacity_minutes;
if (!Number.isInteger(capacityMinutes)) {
return { valid: true, minutes, capacityMinutes: null, plannedMinutes: null,
remainingMinutes: null, projectedMinutes: null, fits: true };
}
const plannedMinutes = todayWork.read().reduce(
(total, identity) => total + (Number.isInteger(plan.estimates[identity]) ? plan.estimates[identity] : 0), 0
);
const remainingMinutes = capacityMinutes - plannedMinutes;
const projectedMinutes = remainingMinutes - minutes;
return { valid: true, minutes, capacityMinutes, plannedMinutes, remainingMinutes,
projectedMinutes, fits: projectedMinutes >= 0 };
}
function capacityMessage(result) {
if (!result.valid) return result.reason === 'required'
? 'Required only for Create & start.'
: 'Use a whole number from 5 to 1440 minutes.';
if (result.capacityMinutes === null) {
return result.minutes + ' min · Set a Today budget to see remaining time.';
}
return result.fits
? result.minutes + ' min · ' + result.projectedMinutes + ' min will remain in Today.'
: result.minutes + ' min · ' + Math.abs(result.projectedMinutes) + ' min over Today capacity.';
}
function complete(issue, estimateMinutes) {
const identity = todayWork.identity(issue);
if (!identity || completed.has(identity)) return 'exists';
const added = todayWork.add(issue);
if (added === 'full') {
announce('Created, but Today changed—remove an item, then add this issue.');
return 'full';
}
if (added !== 'added' && added !== 'exists') {
announce('Created, but Today could not be saved on this device.');
return 'unavailable';
}
let estimatedPlan = null;
if (Number.isInteger(estimateMinutes)) {
estimatedPlan = todayWork.planning();
estimatedPlan.estimates[identity] = estimateMinutes;
if (!todayWork.replacePlanning(estimatedPlan)) {
announce('Created and added to Today, but its estimate could not be saved on this device.');
return 'estimate-unavailable';
}
}
if (!todaySync.enqueue('add', identity)) {
announce('Created and saved to Today on this device, but account sync is unavailable.');
return 'sync-unavailable';
}
if (estimatedPlan) {
if (!todaySync.enqueueConfiguration(estimatedPlan.capacity_minutes, estimatedPlan.estimates)) {
announce('Created and estimated on this device, but Today plan sync is unavailable.');
return 'sync-unavailable';
}
}
completed.add(identity);
refresh();
todaySync.flush();
warm();
start(issue);
announce('Created, added to Today, and ready to work.');
return 'started';
}
return { available, capacity, capacityMessage, complete };
}
if (typeof module !== 'undefined' && module.exports) module.exports = createCreateAndStart;