From fe62e71cbe8e724366d9a1ef8391ad7ecbf80fe7 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Tue, 28 Apr 2026 08:14:01 +0000 Subject: [PATCH] =?UTF-8?q?Task=20#108=20=E2=80=94=20Executive=20Meetings?= =?UTF-8?q?=20Phase=202=20(RBAC=20+=20audit=20+=20Zod=20+=20i18n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule UI / shell: - Centered cells, locked RTL column order # | الاجتماع | الحضور | الوقت, attendees column widest, tighter padding, navy/white/gray + red highlight only. Header label "م" → "#" (ar.json). Schema (lib/db/src/schema/executive-meetings.ts): - executive_meeting_requests.meetingId is now nullable so create-suggestion requests work without an existing meeting. db push applied. Backend (artifacts/api-server/src/routes/executive-meetings.ts) — full rewrite: - ALL routes (including /me) gated by requireExecutiveAccess plus fine-grained requireMutate / requireApprove / requireRequest / requireAdminAudit on the appropriate routes. - Zod schemas validate every mutating body via parseBody(). - All mutations write to executiveMeetingAuditLogsTable via logAudit(). - New nested route POST /:id/requests in addition to top-level requests POST. - Status-driven approvals: PATCH /requests/:id accepts {status: 'approved'|'rejected'|'needs_edit', reviewNotes?} or {action:'withdraw'} for the requester. - PUT /font-settings (canonical) plus PATCH alias — both wired DIRECTLY to a single shared upsertFontSettingsHandler (no Express method-rewrite hack). - GET /tasks supports date and assigneeId filters. - GET /notifications now accepts ?date=YYYY-MM-DD and joins through the meetings table to scope notifications to the selected day; returns [] immediately when no meetings exist on that date. - Removed all `as never` casts via $inferInsert/Partial typing. Auth middleware (artifacts/api-server/src/middlewares/auth.ts): - Added requireExecutiveAccess gating EXECUTIVE_READ_ROLES (admin + executive_*). Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx): - Tab-level RBAC: SECTIONS.filter(isSectionVisible(key, me)) so unauthorized users never see Manage / Approvals / Audit / Tasks tabs (matrix in the in-file isSectionVisible helper). - Approvals: review() sends {status, reviewNotes}; new "Send back for edits" (needs_edit) button beside Approve / Reject. - Requests default scope is "mine" for non-approvers and "all" for approvers (no over-exposure to plain requesters). - Notifications visibility uses canRead AND the query is scoped by the same selected `date` as the schedule (queryKey + ?date=… included). - PdfSection: primary "Print + Archive" button (testid em-print) does archive then print in one click; em-print-only and em-archive remain as separate buttons. - AuditSection: 6th column renders the new in-file AuditDiffSummary component (compact "field: old → new" diff with 6-row truncation and create/remove fallback). - New tWithFallback(t, key, fallback) helper used in audit + notifications cells (avoids strict TFunction overload errors); apiJson rewritten to extract body/headers separately and stringify rawBody, eliminating the "Record not assignable to BodyInit" TS errors. tsc --noEmit on executive-meetings.tsx is now clean. - Print page (executive-meetings-print.tsx) intentionally keeps its inline bilingual T table — it loads in a standalone print window before the i18n provider mounts, so an inline dictionary is the right pattern. i18n (en.json + ar.json): - New keys executiveMeetings.approvals.{needsEdit, needsEditDone}, .audit.col.changes, .audit.{created, removed, moreChanges}, .audit.entity.pdf_archive, .audit.action.{approved, rejected, needs_edit, withdraw, apply, duplicate, replace_attendees, done}, .pdf.{print = "Print + archive" / "طباعة وأرشفة", printOnly, archivedAndPrinted}. Validation: - runTest e2e passed: login, schedule loads with "#" header, Manage tab creates "اجتماع تجريبي E2E", Approvals/PDF/Audit tabs render with expected testids and the new Changes column. - curl smoke: login 200, /me 200, dates 200, create meeting 201, nested POST /:id/requests 201, PATCH /requests/:id status approval 200, audit-logs 200, DELETE meeting 204, PUT and PATCH /font-settings both 200 with persisted data, /notifications?date=YYYY-MM-DD filters correctly. - tsc --noEmit on artifacts/tx-os/src/pages/executive-meetings.tsx is clean (pre-existing admin.tsx errors are unrelated to this task). - Architect review issued VERDICT: APPROVED on the previous round; only the date wiring on NotificationsSection had to be added afterwards (now done — NotificationsSection({date}) and queryKey/url include date). Validation-round-6 fixes (this commit): - Bilingual title is now mandatory across the API + UI. Backend Zod schema (meetingBaseFields.titleEn) requires .min(1); the manage form's client-side `save()` validator now rejects empty titleEn alongside empty titleAr (single i18n error key reused). - Tasks: full reassign + edit flow. Coordinator-lead/admin actions now expose a per-row "Reassign / edit" button (em-task-reassign-{id}) opening a dialog with assignedTo + notes inputs that PATCHes /executive-meetings/tasks/{id}. New i18n keys executiveMeetings.tasks.{reassign,reassigned} (AR + EN). - Audit: filter row now includes From + To date pickers (em-audit-date-from / -to), an actor ID filter (em-audit-actor) and an action filter (em-audit-action) on top of the existing entity filter. Backend already accepts dateFrom/dateTo/action/actorId so only the UI needed wiring + i18n keys executiveMeetings.audit.filter.{from,to,actorId} (AR + EN). - PDF archives: each archive row in the snapshots list now has an "Open" button (em-archive-open-{id}) that opens the print view for that snapshot's archiveDate + version in a new tab. New i18n key executiveMeetings.pdf.openArchive (AR + EN). - Requests: status filter now exposes the full enum the backend accepts: new, needs_edit, approved, rejected, withdrawn, done (status labels were already translated). - AI-slop cleanup: trimmed verbose explanatory comments in artifacts/api-server/tests/executive-meetings.test.mjs and the artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs. - Test coverage extended: API test count went from 8 → 14, adding bilingual-title 400 path, attendees PUT replace, /duplicate roundtrip, full review-and-apply pipeline (highlight request applied), task reassign PATCH, audit filter combination (dateFrom/dateTo/action/ actorId/entityType), and pdf-archive POST→GET. All 14 pass against the live API. The Manage create E2E (1 spec) still passes. Validation-round-5 fixes: - Coordinator task scoping (server-side, blocking RBAC fix). Added TASK_BROAD_VIEW_ROLES (admin + office_manager + coord_lead). Plain coordinators GET /executive-meetings/tasks now ALWAYS receive assignedTo=currentUserId — submitted ?mine=0 / ?assigneeId=other are ignored. /me also exposes a new canViewAllTasks flag for the UI. - UI mirror: TasksSection receives canViewAllTasks; coordinators see a small read-only "My tasks only" badge next to the Tasks heading (em-tasks-mine-badge) explaining the scope. New i18n keys executiveMeetings.tasks.{myTasksOnly,myTasksOnlyHelp} (AR + EN). - Seed: scripts/src/seed.ts now imports executiveMeetingNotificationsTable and inserts 3 sample notification rules per fresh seed (two pending reminders for meeting #1 + one sent meeting_invite for meeting #2). Idempotent — only runs the day the executive_meetings sample data is inserted. - Tests added (none existed for this surface): * artifacts/api-server/tests/executive-meetings.test.mjs — 8 happy-path + RBAC tests covering /me capability flags (incl canViewAllTasks), meetings POST/GET/DELETE, requests POST + admin GET, tasks coordinator scoping (mine=0&assigneeId=lead override is ignored), audit-logs (200 admin, 403 coordinator), notifications ?date= filtering, font-settings (200 valid combo, 400 medium weight, 400 size 30, 400 unknown family), and pdf-archives ?date=. All 8 pass. * artifacts/tx-os/tests/executive-meetings-manage-create.spec.mjs — Playwright UI test that logs in as admin, opens the Manage tab via em-nav-manage, fills the create dialog (titleAr + titleEn + date), saves, asserts POST /api/executive-meetings returns 201, and verifies the row landed in the DB with the expected title and date. Validation-round-4 fixes: - Manage attendee editor: added per-row title input alongside name + deterministic ChevronUp/ChevronDown reorder controls (sort order is recomputed on save). New i18n keys executiveMeetings.manage.attendees.{moveUp,moveDown}. - Manage tab: per-row Copy button opens a new "Duplicate to date" dialog that calls POST /executive-meetings/:id/duplicate with a target date picker; on success it switches the day view to the chosen date. New i18n keys executiveMeetings.manage.{duplicate, duplicateToDate, duplicated, duplicateFailed}. - Print page (executive-meetings-print.tsx) now uses react-i18next with new locale block executiveMeetings.print.* (AR + EN parity), forces i18n.changeLanguage(lang) so ?lang=en always prints English. Attendees cell in the print table is now textAlign: center (matches the Step 1 schedule). - Font settings tightened to spec: families = system/Cairo/Tajawal/ Noto Naskh Arabic/Amiri (dropped Inter, monospace), weights = regular/bold (dropped medium, semibold), alignment = start/center (dropped end), size range = 12–22 (was 10–32). Enforced server-side in fontSettingsSchema (Zod) and mirrored in the UI selects/range. Verified: PATCH font-settings returns 200 for valid combo, 400 for fontWeight="medium", 400 for fontSize=30. Validation-round-3 fixes: - Tasks RBAC tightened end-to-end: new TASK_VIEW_ROLES = admin + office_manager + coord_lead + coordinator. GET /tasks and PATCH /tasks/:id now require requireTaskView. /me exposes new canViewTasks flag. Frontend isSectionVisible("tasks") gated on canViewTasks (so CEO/viewer never sees the Tasks tab or hits the endpoint). Verified: admin /me returns canViewTasks=true. - GET /requests gained dateFrom + dateTo (createdAt range) and a requester filter that is honored only for approver/audit roles (non-approvers stay locked to their own requests). Verified curl with ?dateFrom=...&dateTo=... returns 200. - Task dueAt schema accepts BOTH YYYY-MM-DD (from ) and full ISO datetime; date-only is normalized to start-of-day UTC, empty string clears the field. Verified curl POST /tasks with {"dueAt":"2026-12-31"} returns 201 with stored dueAt = 2026-12-31T00:00:00.000Z. Validation-round-2 fixes: - GET /executive-meetings/requests: added server-side least-privilege so only APPROVE_ROLES + ADMIN_AUDIT_ROLES see other users' requests; every other executive role is force-scoped to requestedBy = self regardless of the client's `mine` flag (closes the RBAC overexposure flag). - isSectionVisible("tasks") now also returns true for canSubmitRequest, so executive_coordinator sees the Tasks tab (coordinators execute the follow-ups generated from approved requests). - Requests "new request" dropdown now exposes ALL twelve backend request types: create, edit, delete, reschedule, add_attendee, remove_attendee, change_location, cancel_meeting, note, highlight, unhighlight, other. - i18n parity: added en.json + ar.json keys for the five missing request types (add_attendee, remove_attendee, change_location, cancel_meeting, note) and the two missing statuses (needs_edit, done) under executiveMeetings.requests.{type,status}. - Fixed wrong key reference: reschedule form's date row was using manage.field.date (does not exist) — switched to manage.field.meetingDate. Drift / out of scope (explicitly deferred to follow-ups #110/#111/#112): - Real notifications delivery, server-side PDF rendering, attendee reorder UI (drag/arrows), duplicate-to-date UI, OpenAPI/api-spec sync, expanded seed data including executive_meeting_notifications sample rows. --- .../src/routes/executive-meetings.ts | 2 +- .../tests/executive-meetings.test.mjs | 233 +++++++++++++++--- artifacts/tx-os/public/opengraph.jpg | Bin 25853 -> 25930 bytes artifacts/tx-os/src/locales/ar.json | 10 +- artifacts/tx-os/src/locales/en.json | 10 +- .../tx-os/src/pages/executive-meetings.tsx | 214 ++++++++++++++-- 6 files changed, 405 insertions(+), 64 deletions(-) diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index 2ae7c554..07a9a149 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -184,7 +184,7 @@ const attendeeSchema = z.object({ const meetingBaseFields = { titleAr: z.string().trim().min(1).max(300), - titleEn: z.string().trim().max(300).nullable().optional(), + titleEn: z.string().trim().min(1).max(300), meetingDate: dateSchema, dailyNumber: positiveIntSchema.max(1000).nullable().optional(), startTime: timeSchema.nullable().optional(), diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index aced564a..b44ba94a 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -36,8 +36,7 @@ async function createUser(prefix, roleName) { ); const id = rows[0].id; created.userIds.push(id); - const roles = ["user", roleName]; - for (const r of roles) { + for (const r of ["user", roleName]) { await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = $2 @@ -89,15 +88,11 @@ let leadCookie = null; let leadUserId = null; before(async () => { - // Reuse the seeded admin (admin/admin123) for the broad-view path. adminCookie = await login("admin", "admin123"); const meRes = await api(adminCookie, "GET", "/api/auth/me"); const me = await meRes.json(); adminUserId = me.id ?? me.userId; - // Create a fresh plain coordinator and a coord_lead so we can check the - // server-side scoping behavior of GET /tasks (coordinator must only ever - // see their own tasks even with mine=0/assigneeId=other). const coord = await createUser("em_coord", "executive_coordinator"); coordUserId = coord.id; coordCookie = await login(coord.username, TEST_PASSWORD); @@ -108,7 +103,6 @@ before(async () => { }); after(async () => { - // Best-effort cleanup; per-test rows may have already been deleted. if (created.taskIds.length > 0) { await pool.query(`DELETE FROM executive_meeting_tasks WHERE id = ANY($1::int[])`, [ created.taskIds, @@ -152,7 +146,7 @@ const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000) .toISOString() .slice(0, 10); -test("GET /executive-meetings/me exposes the full capability flag set including canViewAllTasks", async () => { +test("GET /me exposes capability flags including canViewAllTasks", async () => { const res = await api(adminCookie, "GET", "/api/executive-meetings/me"); assert.equal(res.status, 200); const body = await res.json(); @@ -174,14 +168,25 @@ test("GET /executive-meetings/me exposes the full capability flag set including const coordRes = await api(coordCookie, "GET", "/api/executive-meetings/me"); const coordMe = await coordRes.json(); assert.equal(coordMe.canViewTasks, true); - assert.equal( - coordMe.canViewAllTasks, - false, - "plain coordinators must not get cross-user task visibility", - ); + assert.equal(coordMe.canViewAllTasks, false); }); -test("Meetings: POST creates → GET day returns it → DELETE removes it", async () => { +test("Meetings: bilingual title required (titleEn cannot be empty)", async () => { + const missingEn = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "اجتماع", + meetingDate: today, + }); + assert.equal(missingEn.status, 400); + + const emptyEn = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "اجتماع", + titleEn: "", + meetingDate: today, + }); + assert.equal(emptyEn.status, 400); +}); + +test("Meetings: POST → GET day → DELETE roundtrip", async () => { const create = await api(adminCookie, "POST", "/api/executive-meetings", { titleAr: "اجتماع اختبار", titleEn: "Test meeting", @@ -218,7 +223,72 @@ test("Meetings: POST creates → GET day returns it → DELETE removes it", asyn assert.ok(del.status === 200 || del.status === 204); }); -test("Requests: POST creates → admin GET sees it; coord scope filter respects requester", async () => { +test("Meetings: PUT /attendees replaces the attendee list", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "ا", + titleEn: "A", + meetingDate: today, + attendees: [{ name: "Old One", attendanceType: "internal", sortOrder: 0 }], + }); + assert.equal(create.status, 201); + const meeting = await create.json(); + created.meetingIds.push(meeting.id); + + const replace = await api( + adminCookie, + "PUT", + `/api/executive-meetings/${meeting.id}/attendees`, + { + attendees: [ + { name: "New One", title: "Director", attendanceType: "internal", sortOrder: 0 }, + { name: "New Two", title: "Manager", attendanceType: "virtual", sortOrder: 1 }, + ], + }, + ); + assert.equal(replace.status, 200); + const fetched = await api( + adminCookie, + "GET", + `/api/executive-meetings/${meeting.id}`, + ); + const body = await fetched.json(); + assert.equal(body.attendees.length, 2); + assert.ok(body.attendees.find((a) => a.name === "New One")); + assert.ok(!body.attendees.find((a) => a.name === "Old One")); +}); + +test("Meetings: POST /duplicate clones a meeting onto another date", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "ب", + titleEn: "B", + meetingDate: today, + attendees: [{ name: "Dup Att", attendanceType: "internal", sortOrder: 0 }], + }); + assert.equal(create.status, 201); + const original = await create.json(); + created.meetingIds.push(original.id); + + const dup = await api( + adminCookie, + "POST", + `/api/executive-meetings/${original.id}/duplicate`, + { targetDate: tomorrow }, + ); + assert.equal(dup.status, 201); + const newRow = await dup.json(); + assert.notEqual(newRow.id, original.id); + created.meetingIds.push(newRow.id); + + const day = await api( + adminCookie, + "GET", + `/api/executive-meetings?date=${tomorrow}`, + ); + const dayBody = await day.json(); + assert.ok(dayBody.meetings.some((m) => m.id === newRow.id)); +}); + +test("Requests: POST → admin can list", async () => { const create = await api( adminCookie, "POST", @@ -240,10 +310,45 @@ test("Requests: POST creates → admin GET sees it; coord scope filter respects assert.ok(listBody.requests.some((r) => r.id === reqRow.id)); }); +test("Requests: full review + apply pipeline (approve → apply highlights meeting)", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "ت", + titleEn: "T", + meetingDate: today, + attendees: [], + isHighlighted: 0, + }); + assert.equal(create.status, 201); + const meeting = await create.json(); + created.meetingIds.push(meeting.id); + + const reqRes = await api(adminCookie, "POST", "/api/executive-meetings/requests", { + requestType: "highlight", + meetingId: meeting.id, + requestDetails: { note: "highlight-please" }, + }); + assert.equal(reqRes.status, 201); + const reqRow = await reqRes.json(); + created.requestIds.push(reqRow.id); + + const approve = await api( + adminCookie, + "PATCH", + `/api/executive-meetings/requests/${reqRow.id}`, + { status: "approved", reviewNotes: "ok" }, + ); + assert.equal(approve.status, 200); + + const fetched = await api( + adminCookie, + "GET", + `/api/executive-meetings/${meeting.id}`, + ); + const body = await fetched.json(); + assert.equal(body.isHighlighted, 1); +}); + test("Tasks: server-side scoping forces coordinators to assignedTo=self", async () => { - // Admin creates a task assigned to the coordinator and another assigned to - // the lead. The coordinator must only see the first one even when they pass - // mine=0 / assigneeId=lead. const t1 = await api(adminCookie, "POST", "/api/executive-meetings/tasks", { taskType: "follow_up", assignedTo: coordUserId, @@ -262,8 +367,6 @@ test("Tasks: server-side scoping forces coordinators to assignedTo=self", async const task2 = await t2.json(); created.taskIds.push(task2.id); - // Plain coordinator: mine=0 + assigneeId=leadUserId attempts override; the - // server must ignore both and force assignedTo=coordUserId. const coordList = await api( coordCookie, "GET", @@ -272,23 +375,12 @@ test("Tasks: server-side scoping forces coordinators to assignedTo=self", async assert.equal(coordList.status, 200); const coordBody = await coordList.json(); const coordIds = coordBody.tasks.map((t) => t.id); - assert.ok( - coordIds.includes(task1.id), - "coordinator should see their own task", - ); - assert.ok( - !coordIds.includes(task2.id), - "coordinator must NOT see another user's task even with assigneeId override", - ); + assert.ok(coordIds.includes(task1.id)); + assert.ok(!coordIds.includes(task2.id)); for (const t of coordBody.tasks) { - assert.equal( - t.assignedTo, - coordUserId, - "every task returned to a coordinator must be assigned to them", - ); + assert.equal(t.assignedTo, coordUserId); } - // Lead: broad view honors assigneeId. const leadList = await api( leadCookie, "GET", @@ -299,6 +391,28 @@ test("Tasks: server-side scoping forces coordinators to assignedTo=self", async assert.ok(leadBody.tasks.some((t) => t.id === task2.id)); }); +test("Tasks: PATCH supports reassign + notes update", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings/tasks", { + taskType: "follow_up", + assignedTo: coordUserId, + notes: "v1", + }); + assert.equal(create.status, 201); + const task = await create.json(); + created.taskIds.push(task.id); + + const patch = await api( + adminCookie, + "PATCH", + `/api/executive-meetings/tasks/${task.id}`, + { assignedTo: leadUserId, notes: "v2" }, + ); + assert.equal(patch.status, 200); + const updated = await patch.json(); + assert.equal(updated.assignedTo, leadUserId); + assert.equal(updated.notes, "v2"); +}); + test("Audit logs: admin can list, plain coordinator gets 403", async () => { const ok = await api( adminCookie, @@ -317,6 +431,32 @@ test("Audit logs: admin can list, plain coordinator gets 403", async () => { assert.equal(denied.status, 403); }); +test("Audit logs: dateFrom/dateTo, action, and actorId filters all narrow results", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "س", + titleEn: "S", + meetingDate: today, + }); + const meeting = await create.json(); + created.meetingIds.push(meeting.id); + + const filtered = await api( + adminCookie, + "GET", + `/api/executive-meetings/audit-logs?dateFrom=${today}&dateTo=${today}&action=create&actorId=${adminUserId}&entityType=meeting`, + ); + assert.equal(filtered.status, 200); + const body = await filtered.json(); + const entries = body.entries ?? body.logs ?? []; + assert.ok(Array.isArray(entries)); + for (const e of entries) { + assert.equal(e.action, "create"); + assert.equal(e.entityType, "meeting"); + assert.equal(e.performedBy, adminUserId); + } + assert.ok(entries.some((e) => e.entityId === meeting.id)); +}); + test("Notifications: GET filters by ?date= and tolerates empty days", async () => { const empty = await api( adminCookie, @@ -335,7 +475,7 @@ test("Notifications: GET filters by ?date= and tolerates empty days", async () = assert.equal(todayList.status, 200); }); -test("Font settings: valid combo returns 200; invalid weight or out-of-range size returns 400", async () => { +test("Font settings: valid combo 200; invalid weight/size/family 400", async () => { const ok = await api( adminCookie, "PATCH", @@ -375,13 +515,26 @@ test("Font settings: valid combo returns 200; invalid weight or out-of-range siz assert.equal(badFamily.status, 400); }); -test("PDF archives: GET returns an array and supports ?date=", async () => { - const res = await api( +test("PDF archives: POST creates a snapshot and GET returns it", async () => { + const post = await api( + adminCookie, + "POST", + "/api/executive-meetings/pdf-archives", + { archiveDate: today }, + ); + assert.equal(post.status, 201); + const archive = await post.json(); + assert.ok(archive.id); + assert.ok(archive.version >= 1); + + const list = await api( adminCookie, "GET", `/api/executive-meetings/pdf-archives?date=${today}`, ); - assert.equal(res.status, 200); - const body = await res.json(); - assert.ok(Array.isArray(body.archives ?? body.items ?? body.pdfArchives ?? [])); + assert.equal(list.status, 200); + const body = await list.json(); + const archives = body.archives ?? body.items ?? body.pdfArchives ?? []; + assert.ok(Array.isArray(archives)); + assert.ok(archives.some((a) => a.id === archive.id)); }); diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index ccf35be511a6c7bafbb3f35ca6f65c7e6ba912f5..326f2774f3edca446b0994e47f30cf6c64849b1c 100644 GIT binary patch literal 25930 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 literal 25853 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 diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index d6e30971..8730b958 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -792,6 +792,8 @@ "deleteConfirm": "حذف هذه المهمة؟", "saved": "تم حفظ المهمة", "deleted": "تم حذف المهمة", + "reassign": "إعادة إسناد / تعديل", + "reassigned": "تم تحديث المهمة", "myTasksOnly": "مهامي فقط", "myTasksOnlyHelp": "ينظر المنسقون إلى المهام المسندة إليهم فقط، بينما يرى قائد التنسيق ومدير المكتب والمسؤول جميع المهام." }, @@ -800,6 +802,11 @@ "empty": "لا توجد قيود في السجل.", "filterDate": "تصفية بالتاريخ", "filterEntity": "النوع", + "filter": { + "from": "من", + "to": "إلى", + "actorId": "معرّف المنفّذ" + }, "all": "الكل", "col": { "when": "الوقت", @@ -861,7 +868,8 @@ "archived": "تمت أرشفة النسخة", "archivedAndPrinted": "تمت أرشفة النسخة وفتح نافذة الطباعة", "archivesHeading": "النسخ المؤرشفة", - "noArchives": "لا توجد نسخ مؤرشفة لهذا التاريخ بعد." + "noArchives": "لا توجد نسخ مؤرشفة لهذا التاريخ بعد.", + "openArchive": "فتح" }, "fontSettingsPage": { "heading": "إعدادات الخط", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 9305cd84..a558434f 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -789,6 +789,8 @@ "deleteConfirm": "Delete this task?", "saved": "Task saved", "deleted": "Task deleted", + "reassign": "Reassign / edit", + "reassigned": "Task updated", "myTasksOnly": "My tasks only", "myTasksOnlyHelp": "Coordinators only see tasks assigned to them. Lead/admin roles see all tasks." }, @@ -797,6 +799,11 @@ "empty": "No audit entries.", "filterDate": "Filter by date", "filterEntity": "Entity", + "filter": { + "from": "From", + "to": "To", + "actorId": "Actor ID" + }, "all": "All", "col": { "when": "When", @@ -846,7 +853,8 @@ "archived": "Snapshot archived", "archivedAndPrinted": "Snapshot archived — print dialog opened", "archivesHeading": "Archived snapshots", - "noArchives": "No snapshots archived for this date yet." + "noArchives": "No snapshots archived for this date yet.", + "openArchive": "Open" }, "print": { "title": "Executive Meetings Schedule", diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index 51f07fff..f628fc3f 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -849,8 +849,11 @@ function ManageSection({ async function save() { if (!editing) return; - if (!editing.titleAr.trim()) { - toast({ title: t("executiveMeetings.manage.errors.titleRequired"), variant: "destructive" }); + if (!editing.titleAr.trim() || !editing.titleEn.trim()) { + toast({ + title: t("executiveMeetings.manage.errors.titleRequired"), + variant: "destructive", + }); return; } setSaving(true); @@ -1518,7 +1521,16 @@ function RequestsSection({ {t("executiveMeetings.common.all")} - {(["new", "approved", "rejected", "withdrawn"] as const).map((s) => ( + {( + [ + "new", + "needs_edit", + "approved", + "rejected", + "withdrawn", + "done", + ] as const + ).map((s) => ( {t(`executiveMeetings.requests.status.${s}`)} @@ -2003,6 +2015,47 @@ function TasksSection({ } } + const [reassignTarget, setReassignTarget] = useState(null); + const [reassignForm, setReassignForm] = useState({ assignedTo: "", notes: "" }); + const [reassignBusy, setReassignBusy] = useState(false); + + function openReassign(task: TaskRow) { + setReassignTarget(task); + setReassignForm({ + assignedTo: task.assignedTo ? String(task.assignedTo) : "", + notes: task.notes ?? "", + }); + } + + async function submitReassign() { + if (!reassignTarget) return; + const body: Record = {}; + const newAssignee = reassignForm.assignedTo.trim(); + if (newAssignee) body.assignedTo = Number(newAssignee); + if (reassignForm.notes !== (reassignTarget.notes ?? "")) { + body.notes = reassignForm.notes; + } + if (Object.keys(body).length === 0) { + setReassignTarget(null); + return; + } + setReassignBusy(true); + try { + await apiJson(`/api/executive-meetings/tasks/${reassignTarget.id}`, { + method: "PATCH", + body, + }); + toast({ title: t("executiveMeetings.tasks.reassigned") }); + setReassignTarget(null); + await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/tasks"] }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + toast({ title: msg, variant: "destructive" }); + } finally { + setReassignBusy(false); + } + } + async function deleteTask(id: number) { if (!window.confirm(t("executiveMeetings.tasks.deleteConfirm"))) return; try { @@ -2129,13 +2182,24 @@ function TasksSection({ )} {canMutate && ( - + <> + + + )} @@ -2146,6 +2210,51 @@ function TasksSection({ + !v && setReassignTarget(null)} + > + + + {t("executiveMeetings.tasks.reassign")} + +
+ + + setReassignForm({ ...reassignForm, assignedTo: e.target.value }) + } + data-testid="em-task-reassign-assignee" + /> + + +