feat: protect Today from urgent deadlines (Closes #721)
This commit is contained in:
1 parent
0d29b29bef
commit
fcc9ead33e
8 files changed
+195
-3
No files matched your search
@@ -253,7 +253,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.agenda-replan-actions button { min-height:44px; width:100%; }
|
||||
.agenda-replan-actions label { grid-column:1/-1; }
|
||||
.agenda-replan-actions input { box-sizing:border-box; min-height:44px; width:100%; max-width:100%; }
|
||||
.protect-today { margin:10px 0; padding:12px; border:1px solid #2f6f9f; border-radius:12px; background:#0d2136; display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||
.protect-today p { margin:4px 0; }
|
||||
.protect-today button { min-height:44px; flex:0 0 auto; }
|
||||
@media(max-width:360px) { .agenda-replan-launch { align-items:stretch; flex-direction:column; } .agenda-replan-actions { grid-template-columns:1fr; } .agenda-replan-actions label { grid-column:auto; } }
|
||||
@media(max-width:430px) { .protect-today { align-items:stretch; flex-direction:column; } .protect-today button { width:100%; } }
|
||||
.my-work-card-main { display:block; width:100%; color:var(--text); text-align:left; font:inherit; background:transparent; border:0; padding:0; }
|
||||
.my-work-card-main.review-trigger { width:100%; text-align:left; font:inherit; }
|
||||
.my-work-card:hover { border-color:var(--accent); }
|
||||
|
||||
+27
-2
@@ -1533,6 +1533,7 @@
|
||||
}
|
||||
|
||||
let planTodayTrigger = null;
|
||||
let pendingProtectToday = null;
|
||||
function formatPlanMinutes(minutes) {
|
||||
if (!Number.isInteger(minutes)) return 'Not set';
|
||||
const absolute = Math.abs(minutes);
|
||||
@@ -1972,7 +1973,7 @@
|
||||
return;
|
||||
}
|
||||
if (trigger) planTodayTrigger = trigger;
|
||||
qs('#plan-today-title').textContent = rolloverReviewPlan ? 'New day review' : 'Plan Today';
|
||||
qs('#plan-today-title').textContent = pendingProtectToday ? 'Protect Today' : (rolloverReviewPlan ? 'New day review' : 'Plan Today');
|
||||
if (actualMinutes) pendingPlanActualMinutes = actualMinutes;
|
||||
if (navigate) {
|
||||
taskOverlayHistory.open('plan-today');
|
||||
@@ -1980,15 +1981,33 @@
|
||||
}
|
||||
const recommendations = actualMinutes || pendingPlanActualMinutes || todayRecapView.pendingReplan()?.actual_minutes;
|
||||
pendingPlanActualMinutes = null;
|
||||
planToday.open(todayMyWork, activeMyWork, todayWork.planning(), recommendations);
|
||||
const protectProposal = pendingProtectToday;
|
||||
const selected = protectProposal?.selected || todayMyWork;
|
||||
planToday.open(selected, activeMyWork, todayWork.planning(), recommendations);
|
||||
pendingProtectToday = null;
|
||||
qs('#discard-recap-replan').hidden = !todayRecapView.pendingReplan();
|
||||
qs('#plan-today-error').textContent = '';
|
||||
qs('#plan-today-sheet').hidden = false;
|
||||
document.body.classList.add('task-overlay-open');
|
||||
renderPlanToday();
|
||||
if (protectProposal) qs('#plan-today-build-status').textContent = protectProposal.summary +
|
||||
(protectProposal.displaced.length ? '. Displaced work remains unchanged until you save.' : '. Review estimates and capacity before saving.');
|
||||
qs('#cancel-plan-today').focus();
|
||||
}
|
||||
|
||||
qs('#protect-today').addEventListener('click', event => {
|
||||
const proposal = protectToday.propose({
|
||||
agenda:agendaMyWork(activeMyWork), today:todayMyWork,
|
||||
identity:item => todayWork.identity(item), limit:todayWork.limit,
|
||||
});
|
||||
if (!proposal.protected.length) {
|
||||
qs('#protect-today-status').textContent = 'No overdue or due-today work needs protection.';
|
||||
return;
|
||||
}
|
||||
pendingProtectToday = proposal;
|
||||
openPlanToday(event.currentTarget);
|
||||
});
|
||||
|
||||
qs('#discard-recap-replan').addEventListener('click', () => {
|
||||
todayRecapView.discardReplan();
|
||||
planToday.open(todayMyWork, activeMyWork, todayWork.planning());
|
||||
@@ -2515,6 +2534,12 @@
|
||||
agendaMyWork(activeMyWork) :
|
||||
filterMyWork(activeMyWork, selectedWorkFilter, selectedWorkMilestone);
|
||||
const overdue = queueItems.filter(item => item.agenda_group === 'Overdue');
|
||||
const urgentAgenda = selectedWorkFilter === 'agenda' ? queueItems.filter(item =>
|
||||
item.agenda_group === 'Overdue' || item.agenda_group === 'Today') : [];
|
||||
const protectPanel = qs('#protect-today-panel');
|
||||
protectPanel.hidden = selectedWorkFilter !== 'agenda' || urgentAgenda.length === 0;
|
||||
qs('#protect-today-status').textContent = urgentAgenda.length ?
|
||||
urgentAgenda.length + ' urgent ' + (urgentAgenda.length === 1 ? 'deadline is' : 'deadlines are') + ' ready to reconcile with Today.' : '';
|
||||
const replanPanel = qs('#agenda-replan');
|
||||
replanPanel.hidden = selectedWorkFilter !== 'agenda' || overdue.length === 0;
|
||||
if (!agendaReplan?.snapshot().active) {
|
||||
|
||||
@@ -221,6 +221,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="protect-today" id="protect-today-panel" aria-labelledby="protect-today-heading" hidden>
|
||||
<div>
|
||||
<strong id="protect-today-heading">Deadlines need a plan</strong>
|
||||
<p class="small muted">Put overdue and due-today work first, then review what fits.</p>
|
||||
<div class="small" id="protect-today-status" role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
<button id="protect-today" type="button">Protect Today</button>
|
||||
</section>
|
||||
<div class="my-work-list" id="my-work-list"></div>
|
||||
<div class="small" id="work-page-status" aria-live="polite"></div>
|
||||
<button class="load-more-work" id="load-more-work" type="button" hidden>Load older work</button>
|
||||
@@ -1003,6 +1011,7 @@
|
||||
<script src="static/offline-today.js"></script>
|
||||
<script src="static/my-work.js"></script>
|
||||
<script src="static/agenda-replan.js"></script>
|
||||
<script src="static/protect-today.js"></script>
|
||||
<script src="static/notification-undo.js"></script>
|
||||
<script src="static/card-planning.js"></script>
|
||||
<script src="static/work-selection.js"></script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
function createProtectToday() {
|
||||
function propose({ agenda = [], today = [], identity, limit = 5 }) {
|
||||
const key = item => String(identity?.(item) || '');
|
||||
const urgent = [];
|
||||
const seen = new Set();
|
||||
for (const item of [...agenda, ...today]) {
|
||||
const id = key(item);
|
||||
const eligible = item?.kind === 'issue' && item.is_assigned === true && item.state !== 'closed' &&
|
||||
['Overdue', 'Today'].includes(item.agenda_group);
|
||||
if (!eligible || !id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
urgent.push(item);
|
||||
}
|
||||
urgent.sort((left, right) =>
|
||||
String(left.due_date || '').localeCompare(String(right.due_date || '')) ||
|
||||
String(left.repository || '').localeCompare(String(right.repository || '')) ||
|
||||
Number(left.number || 0) - Number(right.number || 0)
|
||||
);
|
||||
if (!urgent.length) return {
|
||||
selected:[], protected:[], displaced:[],
|
||||
summary:'No overdue or due-today work needs protection.',
|
||||
};
|
||||
const existing = [];
|
||||
for (const item of today) {
|
||||
const id = key(item);
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
existing.push(item);
|
||||
}
|
||||
const combined = [...urgent, ...existing];
|
||||
const selected = combined.slice(0, limit);
|
||||
const selectedIds = new Set(selected.map(key));
|
||||
const displaced = today.filter(item => !selectedIds.has(key(item)) && !urgent.some(candidate => key(candidate) === key(item)));
|
||||
return {
|
||||
selected,
|
||||
protected:urgent,
|
||||
displaced,
|
||||
summary:urgent.length + ' urgent ' + (urgent.length === 1 ? 'item' : 'items') + ' protected · ' +
|
||||
displaced.length + (displaced.length ? ' displaced for review' : ' displaced'),
|
||||
};
|
||||
}
|
||||
return { propose };
|
||||
}
|
||||
|
||||
const protectToday = createProtectToday();
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = protectToday;
|
||||
@@ -33,6 +33,7 @@ const SHELL = [
|
||||
BASE + 'static/offline-today.js',
|
||||
BASE + 'static/my-work.js',
|
||||
BASE + 'static/agenda-replan.js',
|
||||
BASE + 'static/protect-today.js',
|
||||
BASE + 'static/notification-undo.js',
|
||||
BASE + 'static/card-planning.js',
|
||||
BASE + 'static/work-selection.js',
|
||||
|
||||
Reference in new issue
Block a user