Task #273: 5-minute pre-meeting alert for Executive Meetings
- New `executive_meeting_alert_state` table (per-user, per-meeting) tracking `dismissed`/`acknowledged`. Migration via `pnpm --filter @workspace/db run push-force`. - New API routes in artifacts/api-server/src/routes/executive-meetings.ts: GET /alert-state, POST /:id/alert-state, POST /:id/postpone-minutes, POST /:id/reschedule, POST /:id/cancel. All three mutation routes acquire a row lock with SELECT ... FOR UPDATE inside the transaction, compute oldValue from the locked snapshot, run conflict detection in the same tx, and write the audit row before commit. Cancel is idempotent. Postpone-minutes rejects ranges that would cross midnight (use reschedule for cross-day moves). - Alert-state route uses race-safe onConflictDoNothing upsert + a conditional UPDATE ... RETURNING so transition audits never duplicate. - New i18n keys `executiveMeetings.alert.*` in en.json + ar.json, including the cancel-confirm prompt. - 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 30s, 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 (gated on confirmCancel state). - Playwright spec executive-meetings-upcoming-alert.spec.mjs with 6 scenarios: appear+Done, postpone-10 shifts times, cancel-with- confirm, dismiss audit, postpone-chip+conflict-warning toast, AR/RTL render. All 6 pass. - replit.md updated with the new table, routes, and migration step. Pre-existing tsc errors at lines 489, 605, and 2039 of executive-meetings.ts are not touched by this task.
This commit is contained in:
@@ -188,6 +188,35 @@ export const executiveMeetingPdfArchivesTable = pgTable(
|
||||
},
|
||||
);
|
||||
|
||||
// #273: Per-user dismissal/acknowledgment state for the 5-minute
|
||||
// upcoming-meeting alert. One row per (meetingId, userId). Absence of
|
||||
// a row means the user has neither acknowledged ("Done") nor dismissed
|
||||
// ("Delete alert") the alert for that meeting yet, so the alert is
|
||||
// eligible to fire when the meeting falls inside the 5-minute window.
|
||||
export const executiveMeetingAlertStateTable = pgTable(
|
||||
"executive_meeting_alert_state",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
meetingId: integer("meeting_id")
|
||||
.notNull()
|
||||
.references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
dismissed: boolean("dismissed").notNull().default(false),
|
||||
acknowledged: boolean("acknowledged").notNull().default(false),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(t) => ({
|
||||
perUserUnique: uniqueIndex(
|
||||
"executive_meeting_alert_state_meeting_user_unique",
|
||||
).on(t.meetingId, t.userId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const executiveMeetingFontSettingsTable = pgTable(
|
||||
"executive_meeting_font_settings",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user