#262: remove Requests / Approvals / Tasks tabs from Executive Meetings

Full-stack removal of the three retired sections.

Backend
- routes/executive-meetings.ts: deleted /requests* + /tasks* handlers,
  REQUEST_ROLES / TASK_VIEW_ROLES / TASK_BROAD_VIEW_ROLES, the three
  capability flags from /me, retired imports, and dead schemas
  (detailsByType, request*Schema, taskCreateSchema, taskPatchSchema,
  dueAtSchema, dateOnly, timeHm). canApprove kept (FontSettings).
- lib/executive-meeting-notify.ts: types collapsed to ['meeting_created'].

Frontend
- pages/executive-meetings.tsx: deleted Requests/Approvals/Tasks
  sections, RequestListRow, retired SECTIONS entries, MeRoles type, and
  unused icon imports.
- hooks/use-notifications-socket.ts: dropped two retired invalidations.
- locales/{ar,en}.json: removed nav + section + 6 retired type keys.

Schema + DB
- lib/db/src/schema/executive-meetings.ts: tables/relations removed.
- scripts/cleanup-em-requests-tasks.sql: idempotent cleanup — orphan
  prefs / notifications / audit rows then DROP TABLE … CASCADE.
  Applied to dev DB; `db push` re-synced.

Tests
- Sequential `node --test --test-concurrency=1` → 226/226 pass.
- 3 pre-existing parallel-file pollution failures in the workflow
  runner are unrelated to #262 (verified by sequential run).
- Pre-existing tsc warnings at routes L509/625/L1594 untouched.
This commit is contained in:
riyadhafraa
2026-05-01 08:28:11 +00:00
parent 21c935064d
commit 389e8b785c
10 changed files with 8 additions and 99 deletions
@@ -37,11 +37,15 @@ WHERE notification_type IN (
'task_completed'
);
-- 3. Audit logs that targeted request/task entities.
-- 3. Global notifications (bell feed) tied to retired request/task events.
DELETE FROM notifications
WHERE related_type IN ('executive_meeting_request', 'executive_meeting_task');
-- 4. Audit logs that targeted request/task entities.
DELETE FROM executive_meeting_audit_logs
WHERE entity_type IN ('request', 'task');
-- 4. Drop the retired tables themselves. CASCADE removes any FK indices
-- 5. Drop the retired tables themselves. CASCADE removes any FK indices
-- that referenced them; nothing in the surviving schema depends on
-- these two tables.
DROP TABLE IF EXISTS executive_meeting_tasks CASCADE;
@@ -21,9 +21,6 @@ import { logger } from "./logger";
* iterates this list to render one row per type. New event types must be
* appended here so users can opt in/out of them.
*/
// #262: collapsed to a single event after Requests/Approvals/Tasks were
// removed. Old types (request_*, task_*) are scrubbed from the prefs and
// notifications tables by cleanup-em-requests-tasks.sql.
export const EXECUTIVE_MEETING_NOTIFICATION_TYPES = [
"meeting_created",
] as const;
@@ -17,8 +17,6 @@ import { db } from "@workspace/db";
import {
executiveMeetingsTable,
executiveMeetingAttendeesTable,
// #262: executiveMeetingRequestsTable + executiveMeetingTasksTable
// removed alongside the Requests / Approvals / Tasks tabs.
executiveMeetingNotificationsTable,
executiveMeetingNotificationPrefsTable,
executiveMeetingAuditLogsTable,
@@ -87,8 +85,6 @@ const MERGE_COLUMN_INDEX: Record<(typeof MERGE_COLUMNS)[number], number> = {
attendees: 2,
time: 3,
};
// #262: REQUEST_TYPES, REQUEST_REVIEW_STATUSES, TASK_STATUSES removed
// alongside the Requests / Approvals / Tasks tabs.
const FONT_FAMILIES = [
"system",
"Cairo",
@@ -112,8 +108,6 @@ const ADMIN_AUDIT_ROLES = [
"executive_office_manager",
"executive_coord_lead",
] as const;
// #262: REQUEST_ROLES, TASK_VIEW_ROLES, TASK_BROAD_VIEW_ROLES removed
// alongside the Requests / Approvals / Tasks tabs.
const READ_ROLES = [
"admin",
"executive_ceo",
@@ -158,15 +152,11 @@ function makeRequireRoles(allowed: ReadonlyArray<string>) {
const requireMutate = makeRequireRoles(MUTATE_ROLES);
const requireApprove = makeRequireRoles(APPROVE_ROLES);
const requireAdminAudit = makeRequireRoles(ADMIN_AUDIT_ROLES);
// #262: requireRequest + requireTaskView removed with their handlers.
// ---------- Zod schemas ----------
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD");
// #262: kept after the Requests/Approvals/Tasks block was removed; the
// audit + meetings query handlers still need it to validate ?date=… and
// ?dateFrom/dateTo= inputs.
const DATE_RE_LOCAL = /^\d{4}-\d{2}-\d{2}$/;
const timeSchema = z
.string()
@@ -284,13 +274,6 @@ const duplicateSchema = z.object({
// frontend and server share the same contract for POST /reorder.
const reorderSchema = ExecutiveMeetingsReorderBody;
// #262: dateOnly + timeHm helpers removed; only the request payload
// schemas referenced them.
// #262: detailsByType, requestPayloadSchemas, requestCreateSchema,
// requestNestedCreateSchema, requestReviewSchema, dueAtSchema,
// taskCreateSchema, taskPatchSchema removed alongside the
// Requests / Approvals / Tasks handlers.
const pdfArchiveCreateSchema = z.object({
archiveDate: dateSchema,
filePath: z.string().trim().max(500).optional(),
@@ -452,9 +435,6 @@ router.get(
roles: Array.from(names),
canRead: READ_ROLES.some((r) => names.has(r)),
canMutate: MUTATE_ROLES.some((r) => names.has(r)),
// #262: canApprove now only gates the global font-settings write
// path; canSubmitRequest, canViewTasks, canViewAllTasks were
// removed with the Requests/Approvals/Tasks tabs.
canApprove: APPROVE_ROLES.some((r) => names.has(r)),
canViewAudit: ADMIN_AUDIT_ROLES.some((r) => names.has(r)),
});
@@ -15,7 +15,6 @@ const TEST_PASSWORD = "TestPass123!";
const pool = new pg.Pool({ connectionString: DATABASE_URL });
// #262: requestIds + taskIds dropped with their tables.
const created = {
userIds: [],
groupIds: [],
@@ -270,8 +269,6 @@ after(async () => {
/* ignore */
}
}
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
// removed alongside the tables themselves.
if (created.meetingIds.length > 0) {
await pool.query(
`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`,
@@ -292,8 +289,6 @@ after(async () => {
);
}
if (created.userIds.length > 0) {
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
// removed alongside the tables themselves.
await pool.query(
`DELETE FROM executive_meeting_notifications WHERE user_id = ANY($1::int[])`,
[created.userIds],
@@ -502,12 +497,6 @@ test("meeting_created: fan-out excludes actor, dedupes direct+group, writes both
expectNoSocketEventFor(coord2Sock, "meeting_created");
});
// #262: removed test block previously at L522-622 (Requests/Tasks event type).
// #262: removed test block previously at L623-626 (Requests/Tasks event type).
// #262: removed test block previously at L627-630 (Requests/Tasks event type).
// #262: removed test block previously at L631-634 (Requests/Tasks event type).
// #262: removed test block previously at L635-674 (Requests/Tasks event type).
// #262: removed test block previously at L675-754 (Requests/Tasks event type).
// #238: opt-out coverage for filterRecipientsByNotificationPref. The HTTP
// surface gives us black-box access — set a pref row, trigger the event,
@@ -633,7 +622,6 @@ test("pref opt-out: missing pref row defaults to ON (user receives delivery)", a
});
});
// #262: removed test block previously at L855-900 (Requests/Tasks event type).
test("pref opt-out: email=false does NOT affect the in-app channel", async () => {
// Channel independence — muting email must leave in-app delivery alone.
await setPref(approver1.id, "meeting_created", { inApp: true, email: false });
@@ -14,7 +14,6 @@ const TEST_PASSWORD = "TestPass123!";
const pool = new pg.Pool({ connectionString: DATABASE_URL });
// #262: requestIds + taskIds dropped with their tables.
const created = {
userIds: [],
meetingIds: [],
@@ -102,8 +101,6 @@ before(async () => {
});
after(async () => {
// #262: executive_meeting_tasks + executive_meeting_requests cleanup
// removed alongside the tables themselves.
if (created.meetingIds.length > 0) {
await pool.query(`DELETE FROM executive_meeting_attendees WHERE meeting_id = ANY($1::int[])`, [
created.meetingIds,
@@ -138,9 +135,6 @@ const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
.toISOString()
.slice(0, 10);
// #262: canSubmitRequest, canViewTasks, canViewAllTasks dropped from /me
// when the Requests/Approvals/Tasks tabs were removed. The surviving
// flags are canRead, canMutate, canApprove, canViewAudit.
test("GET /me exposes the surviving capability flags", async () => {
const res = await api(adminCookie, "GET", "/api/executive-meetings/me");
assert.equal(res.status, 200);
@@ -595,11 +589,6 @@ test("Sanitization: duplicate path re-sanitizes location/meetingUrl/notes and pr
);
});
// #262: removed test block previously at L609-657 (Requests/Tasks).
// #262: removed test block previously at L658-679 (Requests/Tasks).
// #262: removed test block previously at L680-717 (Requests/Tasks).
// #262: removed test block previously at L718-760 (Requests/Tasks).
// #262: removed test block previously at L761-782 (Requests/Tasks).
test("Audit logs: admin can list, plain coordinator gets 403", async () => {
const ok = await api(
adminCookie,
@@ -1123,7 +1112,6 @@ test("Reorder: rejects an incomplete-day request (orderedIds missing some)", asy
assert.equal(body.code, "incomplete_day");
});
// #262: removed test block previously at L1306-1339 (Requests/Tasks).
test("Reorder: rejects ids that do not all belong to meetingDate", async () => {
const sameDay = await api(adminCookie, "POST", "/api/executive-meetings", {
titleAr: "اليوم", titleEn: "Today", meetingDate: today,
@@ -1209,9 +1197,6 @@ test("Meeting CRUD permissions: coordinator forbidden, lead allowed, admin allow
"executive_coord_lead must be able to PATCH meetings");
});
// #262: removed test block previously at L1425-1463 (Requests/Tasks).
// #262: removed test block previously at L1464-1506 (Requests/Tasks).
// #262: removed test block previously at L1507-1551 (Requests/Tasks).
test("Font settings: PUT then GET returns the user-scoped row roundtrip", async () => {
const put = await api(adminCookie, "PUT",
"/api/executive-meetings/font-settings", {
@@ -1266,8 +1251,6 @@ test("router.param: non-numeric :id returns 404 across endpoints (no crash)", as
["GET", "/api/executive-meetings/-1"],
["POST", "/api/executive-meetings/abc/duplicate"],
["PUT", "/api/executive-meetings/abc/attendees"],
// #262: /requests + /tasks routes were retired with the
// Requests/Approvals/Tasks tabs.
];
for (const [method, path] of cases) {
const res = await api(adminCookie, method, path,
@@ -1422,8 +1405,6 @@ test("Notification prefs: PUT rejects unknown notificationType with 400", async
assert.equal(bad.status, 400);
});
// #262: removed test block previously at L1427-1471 (covered by meeting_created tests in executive-meetings-notifications.test.mjs).
// #262: removed test block previously at L1472-1524 (covered by meeting_created tests in executive-meetings-notifications.test.mjs).
test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to defaults", async () => {
// #236: clicking "Restore defaults" in the UI hits DELETE. After it
// returns, every event type must read default-on regardless of what
@@ -1432,9 +1413,6 @@ test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to de
const fresh = await createUser("em_pref_restore", "executive_office_manager");
const cookie = await login(fresh.username, TEST_PASSWORD);
// #262: only `meeting_created` survives, so the "two different event
// types" half of the original test collapsed to a single row covering
// both channels muted at once. The DELETE must still clear it.
const put = await api(cookie, "PUT",
"/api/executive-meetings/notification-prefs",
{
@@ -1477,8 +1455,6 @@ test("Notification prefs: DELETE wipes the user's pref rows so GET reverts to de
// And actual fan-out must reach the user again — a regression in the
// DELETE handler that left rows behind would silently re-mute them.
// #262: trigger via POST /api/executive-meetings (the only event type
// that still fans out).
const beforeCount = await pool.query(
`SELECT COUNT(*)::int AS n
FROM executive_meeting_notifications
@@ -90,8 +90,6 @@ export function useNotificationsSocket() {
queryClient.invalidateQueries({
queryKey: ["/api/executive-meetings/notifications"],
});
// #262: requests/tasks query invalidations dropped when those
// sub-resources were removed from the API.
});
socket.on(
+1 -7
View File
@@ -1280,13 +1280,7 @@
"reminder": "تذكير",
"change": "تنبيه تعديل",
"cancel": "تنبيه إلغاء",
"meeting_created": "اجتماع جديد",
"request_submitted": "طلب بانتظار المراجعة",
"request_approved": "تم اعتماد الطلب",
"request_rejected": "تم رفض الطلب",
"request_needs_edit": "الطلب يحتاج تعديلاً",
"task_assigned": "إسناد مهمة",
"task_completed": "اكتمال مهمة"
"meeting_created": "اجتماع جديد"
},
"status": {
"pending": "بالانتظار",
+1 -7
View File
@@ -1181,13 +1181,7 @@
"reminder": "Reminder",
"change": "Change notice",
"cancel": "Cancellation notice",
"meeting_created": "Meeting created",
"request_submitted": "Request awaiting review",
"request_approved": "Request approved",
"request_rejected": "Request rejected",
"request_needs_edit": "Request needs edits",
"task_assigned": "Task assigned",
"task_completed": "Task completed"
"meeting_created": "Meeting created"
},
"status": {
"pending": "Pending",
@@ -18,8 +18,6 @@ import {
Type,
Languages,
ListChecks,
// #262: ClipboardList, CheckSquare, ListTodo removed with the
// Requests / Approvals / Tasks tabs.
Bell,
ScrollText,
FileText,
@@ -146,11 +144,6 @@ type Meeting = {
type DayResponse = { date: string; meetings: Meeting[] };
// #262: MeRoles collapsed to the same shape as MeCapabilities (only one
// type definition is needed now that the Requests/Approvals/Tasks tabs
// are gone). RequestRow / TaskRow types removed alongside the sections
// that consumed them.
type AuditEntry = {
id: number;
action: string;
@@ -224,8 +217,6 @@ function formatBytesForDisplay(bytes: number): string {
return `${value < 10 && unitIdx > 0 ? value.toFixed(1) : Math.round(value)} ${units[unitIdx]}`;
}
// #262: removed `requests`, `approvals`, `tasks` entries (and their
// icons) when the corresponding sections were retired.
const SECTIONS = [
{ key: "schedule", icon: CalendarClock },
{ key: "manage", icon: ListChecks },
@@ -237,9 +228,6 @@ const SECTIONS = [
type SectionKey = (typeof SECTIONS)[number]["key"];
// #262: canSubmitRequest, canViewTasks, canViewAllTasks dropped from /me;
// canApprove is kept because the global font-settings write path still
// gates on it.
type MeCapabilities = {
userId: number;
roles: string[];
@@ -656,7 +644,6 @@ export default function ExecutiveMeetingsPage() {
{section === "manage" && me && (
<ManageSection date={date} onDateChange={setDate} canMutate={me.canMutate} isRtl={isRtl} t={t} />
)}
{/* #262: Requests / Approvals / Tasks section blocks removed. */}
{section === "notifications" && me && (
<NotificationsSection
canView={me.canRead}
-9
View File
@@ -95,11 +95,6 @@ export const executiveMeetingAttendeesTable = pgTable(
}),
);
// #262: removed executiveMeetingRequestsTable + executiveMeetingTasksTable.
// The Requests / Approvals / Tasks tabs were retired from the page; the
// underlying DB tables are dropped by
// artifacts/api-server/scripts/cleanup-em-requests-tasks.sql.
export const executiveMeetingNotificationsTable = pgTable(
"executive_meeting_notifications",
{
@@ -130,10 +125,6 @@ export const executiveMeetingNotificationsTable = pgTable(
* notificationType mirrors the keys passed to
* `recordExecutiveMeetingNotifications` / `sendExecutiveMeetingEmail`:
* - meeting_created
*
* #262: collapsed to a single event type after Requests/Approvals/Tasks
* were removed. Orphan rows for the old types are deleted by
* cleanup-em-requests-tasks.sql.
*/
export const executiveMeetingNotificationPrefsTable = pgTable(
"executive_meeting_notification_prefs",