Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings without a (startTime, endTime) tuple are a normal state and now rotate freely alongside timed rows. Server (artifacts/api-server/src/routes/executive-meetings.ts): - Dropped the `no_time_window` early-return guard in /api/executive-meetings/rotate-content. - Sort + slot construction now tolerate null start times (NULLS LAST, tie-break by id) so a null tuple rotates as a real slot. - All other safeguards (cancelled_in_rotate, optimistic lock, renumberDayByStartTime, audit logging) remain intact. Client (artifacts/tx-os/src/pages/executive-meetings.tsx): - Removed missingTimeCount / dayRotatable memos, the dayRotatable prop wiring, dragBlockedByMissingTime + effectiveDragEnabled composition, the row's data-drag-blocked / native title / cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire DragBlockedTooltipButton component + its render site, and the no_time_window errorToast branch in rotateContent. Pruned now-dead Tooltip + AlertTriangle imports. i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block (tooltip + errorToast) from en.json and ar.json. Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs` and added `executive-meetings-rotate-allows-missing-times.spec.mjs`, which inserts 3 meetings (one with null times), verifies no drag-blocked warning button / aria-disabled, drags the top row to the bottom slot, asserts the rotate-content POST succeeds, and confirms exactly one row still has a null tuple after rotation. Verified: row-drag, row-quick-actions, and the new spec all pass (8/8). Architect code review PASSED.
This commit is contained in:
@@ -2454,19 +2454,11 @@ router.post(
|
||||
code: "cancelled_in_rotate",
|
||||
};
|
||||
}
|
||||
// Time window guard: every meeting in the rotation must already
|
||||
// have a (startTime, endTime) — rotation only makes sense when
|
||||
// every slot has a tuple to hand off.
|
||||
for (const r of lockedRows) {
|
||||
if (r.startTime == null || r.endTime == null) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: "Every meeting must have a scheduled time window to rotate",
|
||||
code: "no_time_window",
|
||||
};
|
||||
}
|
||||
}
|
||||
// #497: the previous `no_time_window` guard was removed —
|
||||
// meetings without a (startTime, endTime) tuple are a perfectly
|
||||
// normal state and the rotation algorithm below handles `null`
|
||||
// tuples just like real ones (a `null/null` slot rotates to its
|
||||
// new physical row alongside the timed slots).
|
||||
// Optimistic-lock check on every meeting in the rotation. Same
|
||||
// shape as swap-times so the client can render the same conflict
|
||||
// toast.
|
||||
@@ -2514,15 +2506,25 @@ router.post(
|
||||
}
|
||||
// Build the canonical chronological slot order. Sort by
|
||||
// (startTime, then id) so the slot[i] tuple is deterministic
|
||||
// even when two rows share a start time.
|
||||
// even when two rows share a start time. #497: null start times
|
||||
// sort LAST (after every timed row) and tie-break by id so the
|
||||
// ordering is stable across runs. This keeps the visual
|
||||
// chronological order matching what the client renders (timed
|
||||
// rows first in time order, untimed rows trailing by insertion).
|
||||
const chrono = lockedRows.slice().sort((a, b) => {
|
||||
if (a.startTime! < b.startTime!) return -1;
|
||||
if (a.startTime! > b.startTime!) return 1;
|
||||
const aNull = a.startTime == null;
|
||||
const bNull = b.startTime == null;
|
||||
if (aNull && !bNull) return 1;
|
||||
if (!aNull && bNull) return -1;
|
||||
if (!aNull && !bNull) {
|
||||
if (a.startTime! < b.startTime!) return -1;
|
||||
if (a.startTime! > b.startTime!) return 1;
|
||||
}
|
||||
return a.id - b.id;
|
||||
});
|
||||
const slots = chrono.map((r) => ({
|
||||
startTime: r.startTime!,
|
||||
endTime: r.endTime!,
|
||||
startTime: r.startTime,
|
||||
endTime: r.endTime,
|
||||
}));
|
||||
// Two-phase write so we don't transiently violate the per-day
|
||||
// unique index on dailyNumber: park every row on a temporary
|
||||
|
||||
Reference in New Issue
Block a user