From 22e76a9f492bd52d6e7f96447b446790bfaafd83 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Fri, 1 May 2026 12:33:49 +0000 Subject: [PATCH] Task #273: 5-minute pre-meeting alert for Executive Meetings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Floating, draggable alert that appears on every Tx OS page when an Executive Meeting is within five minutes of starting. Schema - New table `executive_meeting_alert_state (meetingId, userId, dismissed, acknowledged, updatedAt)` with unique (meetingId,userId) in lib/db/src/schema/executive-meetings.ts. Apply via `pnpm --filter @workspace/db run push-force` per environment. API (artifacts/api-server/src/routes/executive-meetings.ts) - GET /executive-meetings/alert-state?date=YYYY-MM-DD - POST /executive-meetings/:id/alert-state (action: shown|acknowledged|dismissed) - race-safe: onConflictDoNothing upsert + conditional UPDATE … RETURNING. - POST /executive-meetings/:id/postpone-minutes - POST /executive-meetings/:id/reschedule - POST /executive-meetings/:id/cancel - all three lock the meeting row inside the tx with SELECT … FOR UPDATE, compute oldValue from the locked row, run conflict detection in the same tx snapshot, and write the audit row before commit; - cancel is idempotent (no duplicate audit if already cancelled); - postpone-minutes rejects ranges that would cross midnight (use reschedule for cross-day moves). - New helper `renumberDayByStartTime(tx, date)` runs inside each mutation tx and rewrites `daily_number` for every meeting on the affected date(s): active meetings 1..N by start_time, cancelled meetings pushed to the tail. Uses a negative-shift dance so the (meeting_date, daily_number) unique index never trips mid-update. Reschedule renumbers both the old and new date when the day moves. - detectMeetingConflicts now accepts a tx-like executor so the conflict scan reads the same DB snapshot as the UPDATE that just shifted the row. Frontend - New component artifacts/tx-os/src/components/executive-meetings/ upcoming-meeting-alert.tsx — globally mounted in App.tsx inside AuthProvider. Draggable with localStorage position persistence, RTL-aware, polls every 30 s, shows start–end window, postpone-by- minutes chips [5,10,15,30,45,60], full reschedule sub-form, and a cancel-meeting flow that requires an explicit confirm step before the destructive call fires. - Eligibility window is strict 0 < remainingMinutes <= 5 — the alert hides as soon as the meeting actually starts. - Primary action buttons: Done, Postpone (when canMutate), Dismiss (in addition to the X icon). - artifacts/tx-os/src/pages/executive-meetings.tsx schedule view now filters out `status === "cancelled"` from the displayed list, so a cancelled meeting disappears from today's view (still queryable via the API for archive/audit consumers). i18n - New `executiveMeetings.alert.*` keys in both en.json and ar.json (postpone, reschedule, cancel, cancel-confirm prompt, conflict warning, etc.). Postpone chips fire immediately - Each preset minute chip (5/10/15/30/45/60) now calls postponeBy(n) on a single click — no second Apply step. The manual minute input + Apply button remain for custom/fractional values. Viewport-resize clamping - A useEffect in UpcomingMeetingAlert listens for `resize` and `orientationchange` and clamps the floating panel's position back inside the current viewport, also re-clamps once on mount so a stale localStorage position from a wider viewport is corrected. Tests - artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs — 9 Playwright scenarios, all green: 1. Done acknowledges the alert 2. Postpone-10 shifts both start and end and clears the alert 3. Cancel-with-confirm marks the meeting cancelled 4. Dismiss (X) writes a dismissed audit row 5. Single chip click immediately shifts start/end by +10 minutes AND surfaces the conflict-warning toast 6. Re-clamps the panel back into the viewport when the window shrinks (seeds a stale right-edge localStorage position at 1400px, then resizes to 420px and asserts the bounding box) 7. Reschedule to a different day clears today's alert 8. Cancel removes the meeting from today's schedule and renumbers the survivor 9. Arabic locale renders the RTL alert with Arabic title Copy - "Dismiss alert" / "تجاهل التنبيه" wording on the dismiss control (and the toast that follows) to match the product spec. Docs - replit.md updated with the new table, routes, and migration step. Pre-existing tsc errors at lines 546, 662, and 2107 of executive-meetings.ts are unrelated to this task. --- .../api-server/src/routes/executive-meetings.ts | 5 ++++- .../executive-meetings-upcoming-alert.spec.mjs | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index dda64cff..936e8a3d 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -1000,7 +1000,10 @@ router.post( } if ( startMin + body.minutes >= 24 * 60 || - endMin + body.minutes > 24 * 60 + // Treat 24:00 (1440) the same as 00:00 the next day — both wrap + // the day boundary, so the end-time guard is `>=`, matching the + // start-time guard. + endMin + body.minutes >= 24 * 60 ) { return { ok: false, diff --git a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs index 9f82cd21..0fec6142 100644 --- a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs +++ b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs @@ -456,11 +456,17 @@ test("Upcoming-meeting alert: Cancel removes the meeting from today's schedule v .toBe(true); // Schedule view: cancelled meeting must disappear from today's list. + // Match both EN and AR variants of the seeded titles because the + // admin user's preferredLanguage may flip i18n away from the + // localStorage hint we set at start of test (the schedule cell shows + // titleAr when isRtl, titleEn otherwise). await page.goto("/executive-meetings"); - await expect(page.getByText(`${TEST_TAG} renum-B EN`)).toBeVisible({ - timeout: 10_000, - }); - await expect(page.getByText(`${TEST_TAG} renum-A EN`)).toBeHidden(); + await expect( + page.getByText(new RegExp(`${TEST_TAG} renum-B (EN|AR)`)), + ).toBeVisible({ timeout: 10_000 }); + await expect( + page.getByText(new RegExp(`${TEST_TAG} renum-A (EN|AR)`)), + ).toBeHidden(); }); test("Upcoming-meeting alert: Arabic locale renders the RTL alert with Arabic title", async ({