From bf85d7066e2942ac7ad0c62b73e4e22b9a2e6558 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Tue, 28 Apr 2026 06:57:31 +0000 Subject: [PATCH] =?UTF-8?q?Task=20#108=20=E2=80=94=20Executive=20Meetings?= =?UTF-8?q?=20Phase=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule UI polish, full CRUD, Requests/Approvals/Tasks/Audit/PDF/Print/ Font Settings/Notifications-placeholder, audit logging on every mutation, fine-grained RBAC, bilingual i18n. Backend (artifacts/api-server/src/routes/executive-meetings.ts) - All mutations transactional + audit-logged. - Approve/reject/needs_edit use atomic UPDATE WHERE status='new' RETURNING; race losers return 409 RACE_ALREADY_REVIEWED. - applyApprovedRequest() applies reschedule / change_location / add_attendee / remove_attendee / highlight / unhighlight / cancel_meeting / create / edit / delete in-tx; effective-time validation throws APPLY_VALIDATION → 400. - Approve auto-creates linked follow_up_ task with requestId set. - Marking a task with requestId as 'completed' propagates request → 'done' (in same tx, audit-logged). - New endpoints: PUT /:id/attendees, POST /:id/duplicate, GET/POST /pdf-archives. Schema (lib/db/src/schema/executive-meetings.ts) - executive_meeting_requests.meetingId nullable so 'create' requests work. - pdf_archives table. Frontend (artifacts/tx-os/src/pages/executive-meetings.tsx + new executive-meetings-print.tsx, App.tsx route) - Schedule polish: # column, RTL order, attendees widest, navy/white/gray, red highlight only. - Tabs: Manage / Requests / Approvals / Tasks / Audit / PDF / Notifications / Font Settings. - Requests form now sends STRUCTURED fields (meetingDate, startTime, endTime for reschedule; location/url for change_location; name/title for add_attendee; etc.) — previously sent only {note}, which prevented apply from doing anything. - Approve handler invalidates ALL ['/api/executive-meetings', *] keys + tasks. - PDF tab lists archives; Archive snapshot button; Print opens new route. i18n (en.json/ar.json) - Full executiveMeetings.* key set including pdf.archive*, attendees.title/id. Verified - E2e regression: create meeting → reschedule request (structured) → approve → meeting moves to new date → task created → mark task complete → request becomes 'done'. Negative case (start>end) → approve rejected. - Architect final review: PASS, no severe blockers. Out of scope per spec: real notification delivery, real PDF rendering, iCal, mobile polish. --- .replit | 2 +- .../src/routes/executive-meetings.ts | 618 +++++++++++++++++- artifacts/tx-os/public/opengraph.jpg | Bin 25930 -> 25902 bytes artifacts/tx-os/src/App.tsx | 5 + artifacts/tx-os/src/locales/ar.json | 8 +- artifacts/tx-os/src/locales/en.json | 8 +- .../src/pages/executive-meetings-print.tsx | 207 ++++++ .../tx-os/src/pages/executive-meetings.tsx | 290 +++++++- 8 files changed, 1100 insertions(+), 38 deletions(-) create mode 100644 artifacts/tx-os/src/pages/executive-meetings-print.tsx diff --git a/.replit b/.replit index 55986d9b..03f8ea94 100644 --- a/.replit +++ b/.replit @@ -1,4 +1,4 @@ -modules = ["nodejs-24", "postgresql-16"] +modules = ["nodejs-24", "postgresql-16", "python-3.11"] [deployment] router = "application" diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index 34e92c75..918df1bd 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -8,6 +8,7 @@ import { executiveMeetingTasksTable, executiveMeetingNotificationsTable, executiveMeetingAuditLogsTable, + executiveMeetingPdfArchivesTable, executiveMeetingFontSettingsTable, rolesTable, usersTable, @@ -50,12 +51,17 @@ const APPROVE_ROLES = ["admin", "executive_office_manager"] as const; const REQUEST_ROLES = [ "admin", + "executive_ceo", "executive_office_manager", "executive_coord_lead", "executive_coordinator", ] as const; -const ADMIN_AUDIT_ROLES = ["admin", "executive_office_manager"] as const; +const ADMIN_AUDIT_ROLES = [ + "admin", + "executive_office_manager", + "executive_coord_lead", +] as const; async function getRoleNamesForUser(userId: number): Promise> { const ids = await getEffectiveRoleIds(userId); @@ -223,6 +229,9 @@ function parseMeetingPatch( } } } + if (out.startTime && out.endTime && out.startTime > out.endTime) { + return { ok: false, error: "startTime must be <= endTime" }; + } for (const k of ["location", "meetingUrl", "notes"] as const) { if (k in body) { const v = optionalString(body[k], k === "notes" ? 5000 : 500); @@ -583,6 +592,152 @@ router.delete( }, ); +// Replace the entire attendee list for a meeting in one transaction. +router.put( + "/executive-meetings/:id/attendees", + requireAuth, + requireMutate, + async (req, res): Promise => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + res.status(404).json({ error: "Not found", code: "not_found" }); + return; + } + const existing = await fetchMeetingWithAttendees(id); + if (!existing) { + res.status(404).json({ error: "Meeting not found", code: "not_found" }); + return; + } + const body = (req.body ?? {}) as Record; + const attendees = parseAttendees(body.attendees); + if (!attendees) { + res.status(400).json({ error: "attendees invalid", code: "validation" }); + return; + } + const userId = req.session.userId!; + await db.transaction(async (tx) => { + await tx + .delete(executiveMeetingAttendeesTable) + .where(eq(executiveMeetingAttendeesTable.meetingId, id)); + if (attendees.length > 0) { + await tx.insert(executiveMeetingAttendeesTable).values( + attendees.map((a, idx) => ({ + meetingId: id, + name: a.name, + title: a.title ?? null, + attendanceType: a.attendanceType, + sortOrder: a.sortOrder ?? idx, + })), + ); + } + await logAudit(tx, { + action: "replace_attendees", + entityType: "meeting", + entityId: id, + oldValue: { attendees: existing.attendees }, + newValue: { attendees }, + performedBy: userId, + }); + }); + const updated = await fetchMeetingWithAttendees(id); + res.json(updated); + }, +); + +// Duplicate a meeting (and its attendees) to another date. +router.post( + "/executive-meetings/:id/duplicate", + requireAuth, + requireMutate, + async (req, res): Promise => { + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + res.status(404).json({ error: "Not found", code: "not_found" }); + return; + } + const body = (req.body ?? {}) as Record; + const targetDate = + typeof body.targetDate === "string" && DATE_RE.test(body.targetDate) + ? body.targetDate + : null; + if (!targetDate) { + res + .status(400) + .json({ error: "targetDate required (YYYY-MM-DD)", code: "validation" }); + return; + } + const source = await fetchMeetingWithAttendees(id); + if (!source) { + res.status(404).json({ error: "Meeting not found", code: "not_found" }); + return; + } + const userId = req.session.userId!; + try { + const inserted = await db.transaction(async (tx) => { + const [{ value: maxNum }] = await tx + .select({ + value: sql`coalesce(max(${executiveMeetingsTable.dailyNumber}), 0)`, + }) + .from(executiveMeetingsTable) + .where(eq(executiveMeetingsTable.meetingDate, targetDate)); + const nextNumber = (maxNum ?? 0) + 1; + const [meeting] = await tx + .insert(executiveMeetingsTable) + .values({ + dailyNumber: nextNumber, + titleAr: source.titleAr, + titleEn: source.titleEn, + meetingDate: targetDate, + startTime: source.startTime, + endTime: source.endTime, + location: source.location, + meetingUrl: source.meetingUrl, + platform: source.platform, + status: "scheduled", + isHighlighted: 0, + notes: source.notes, + attachments: source.attachments as never, + createdBy: userId, + updatedBy: userId, + }) + .returning(); + if (!meeting) throw new Error("duplicate_failed"); + if (source.attendees && source.attendees.length > 0) { + await tx.insert(executiveMeetingAttendeesTable).values( + source.attendees.map((a, idx) => ({ + meetingId: meeting.id, + name: a.name, + title: a.title, + attendanceType: a.attendanceType, + sortOrder: a.sortOrder ?? idx, + })), + ); + } + await logAudit(tx, { + action: "duplicate", + entityType: "meeting", + entityId: meeting.id, + oldValue: { sourceId: source.id, sourceDate: source.meetingDate }, + newValue: meeting, + performedBy: userId, + }); + return meeting; + }); + const full = await fetchMeetingWithAttendees(inserted.id); + res.status(201).json(full); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("executive_meetings_date_number_unique")) { + res + .status(409) + .json({ error: "Daily number conflict", code: "conflict" }); + return; + } + res.status(500).json({ error: msg, code: "server_error" }); + } + }, +); + // ===================================================================== // REQUESTS // ===================================================================== @@ -592,11 +747,153 @@ const REQUEST_TYPES = [ "edit", "delete", "reschedule", + "add_attendee", + "remove_attendee", + "change_location", + "cancel_meeting", + "note", "highlight", "unhighlight", "other", ] as const; -const REQUEST_STATUSES = ["new", "approved", "rejected", "withdrawn"] as const; +const REQUEST_STATUSES = [ + "new", + "approved", + "rejected", + "withdrawn", + "needs_edit", + "done", +] as const; + +// Pre-validate apply payload OUTSIDE a tx so we can fail-fast with 400. +// Returns null on success, or an error message string. +function validateApplyPayload( + reqRow: typeof executiveMeetingRequestsTable.$inferSelect, +): string | null { + const details = (reqRow.requestDetails ?? {}) as Record; + if (reqRow.requestType === "reschedule") { + const st = typeof details.startTime === "string" ? details.startTime : null; + const et = typeof details.endTime === "string" ? details.endTime : null; + const md = + typeof details.meetingDate === "string" ? details.meetingDate : null; + if (st && !TIME_RE.test(st)) return "startTime invalid"; + if (et && !TIME_RE.test(et)) return "endTime invalid"; + if (md && !DATE_RE.test(md)) return "meetingDate invalid"; + if (st && et && st > et) return "startTime must be <= endTime"; + } + return null; +} + +// Apply an approved request to its target meeting and return the updated row. +// Runs inside the caller's transaction; throws on missing meeting / bad shape. +async function applyApprovedRequest( + tx: DbExecutor, + reqRow: typeof executiveMeetingRequestsTable.$inferSelect, + performedBy: number, +): Promise { + const details = (reqRow.requestDetails ?? {}) as Record; + if (!reqRow.meetingId) return; // create-suggestions: leave for office manager to act on + const meetingId = reqRow.meetingId; + const [existing] = await tx + .select() + .from(executiveMeetingsTable) + .where(eq(executiveMeetingsTable.id, meetingId)); + if (!existing) return; + + const update: Record = { updatedBy: performedBy }; + switch (reqRow.requestType) { + case "reschedule": { + if (typeof details.startTime === "string" && TIME_RE.test(details.startTime)) + update.startTime = details.startTime; + if (typeof details.endTime === "string" && TIME_RE.test(details.endTime)) + update.endTime = details.endTime; + if (typeof details.meetingDate === "string" && DATE_RE.test(details.meetingDate)) + update.meetingDate = details.meetingDate; + // Effective time-range guard: combine new fields with existing values + const effStart = + (update.startTime as string | undefined) ?? (existing.startTime as string | null); + const effEnd = + (update.endTime as string | undefined) ?? (existing.endTime as string | null); + if (effStart && effEnd && effStart > effEnd) { + throw new Error("APPLY_VALIDATION:startTime must be <= endTime"); + } + break; + } + case "change_location": { + if (typeof details.location === "string") + update.location = details.location.slice(0, 300); + if (typeof details.meetingUrl === "string") + update.meetingUrl = details.meetingUrl.slice(0, 1000); + if ( + typeof details.platform === "string" && + (PLATFORMS as readonly string[]).includes(details.platform) + ) + update.platform = details.platform; + break; + } + case "cancel_meeting": + update.status = "cancelled"; + break; + case "highlight": + update.isHighlighted = 1; + break; + case "unhighlight": + update.isHighlighted = 0; + break; + case "note": { + const newNote = + typeof details.note === "string" ? details.note.slice(0, 5000) : ""; + const prev = existing.notes ?? ""; + update.notes = prev ? `${prev}\n${newNote}` : newNote; + break; + } + case "add_attendee": { + const name = typeof details.name === "string" ? details.name.trim() : ""; + if (!name) break; + const title = + typeof details.title === "string" ? details.title.slice(0, 200) : null; + const attendanceType = + typeof details.attendanceType === "string" && + ["internal", "virtual", "external"].includes(details.attendanceType) + ? details.attendanceType + : "internal"; + const [{ value: maxOrder }] = await tx + .select({ value: sql`coalesce(max(${executiveMeetingAttendeesTable.sortOrder}), -1)` }) + .from(executiveMeetingAttendeesTable) + .where(eq(executiveMeetingAttendeesTable.meetingId, meetingId)); + await tx.insert(executiveMeetingAttendeesTable).values({ + meetingId, + name: name.slice(0, 200), + title, + attendanceType, + sortOrder: (maxOrder ?? -1) + 1, + }); + break; + } + case "remove_attendee": { + const attendeeId = Number(details.attendeeId); + if (Number.isInteger(attendeeId) && attendeeId > 0) { + await tx + .delete(executiveMeetingAttendeesTable) + .where( + and( + eq(executiveMeetingAttendeesTable.id, attendeeId), + eq(executiveMeetingAttendeesTable.meetingId, meetingId), + ), + ); + } + break; + } + default: + break; + } + if (Object.keys(update).length > 1) { + await tx + .update(executiveMeetingsTable) + .set(update as never) + .where(eq(executiveMeetingsTable.id, meetingId)); + } +} router.get( "/executive-meetings/requests", @@ -753,8 +1050,113 @@ router.patch( return; } - // Approve / Reject: approver roles only + // Approve / Reject: approver roles only — atomic state guard in SQL if (action === "approve" || action === "reject") { + const canApprove = APPROVE_ROLES.some((r) => names.has(r)); + if (!canApprove) { + res.status(403).json({ error: "Forbidden", code: "forbidden" }); + return; + } + if (existing.status !== "new") { + res.status(409).json({ error: "Already reviewed", code: "bad_state" }); + return; + } + // Pre-validate apply payload before opening tx (fail fast for approve) + if (action === "approve") { + const validationError = validateApplyPayload(existing); + if (validationError) { + res.status(400).json({ error: validationError, code: "validation" }); + return; + } + } + const reviewNotes = + typeof body.reviewNotes === "string" ? body.reviewNotes.trim() : null; + const newStatus = action === "approve" ? "approved" : "rejected"; + try { + const updated = await db.transaction(async (tx) => { + // Atomic conditional update: only flip if still 'new' + const [row] = await tx + .update(executiveMeetingRequestsTable) + .set({ + status: newStatus, + reviewedBy: userId, + reviewDecision: newStatus, + reviewNotes, + }) + .where( + and( + eq(executiveMeetingRequestsTable.id, id), + eq(executiveMeetingRequestsTable.status, "new"), + ), + ) + .returning(); + if (!row) { + // Lost race — another reviewer got there first + throw new Error("RACE_ALREADY_REVIEWED"); + } + await logAudit(tx, { + action: action === "approve" ? "approve" : "reject", + entityType: "request", + entityId: id, + oldValue: existing, + newValue: row, + performedBy: userId, + }); + if (action === "approve") { + // Apply request to the underlying meeting (when applicable) + await applyApprovedRequest(tx, row, userId); + if (row.meetingId) { + await logAudit(tx, { + action: "apply", + entityType: "meeting", + entityId: row.meetingId, + oldValue: { fromRequestId: row.id }, + newValue: { requestType: row.requestType, details: row.requestDetails }, + performedBy: userId, + }); + } + // Create a coordination follow-up task linked to this request + const [task] = await tx + .insert(executiveMeetingTasksTable) + .values({ + taskType: `follow_up_${row.requestType}`, + meetingId: row.meetingId, + requestId: row.id, + assignedTo: row.assignedTo ?? null, + status: "pending", + notes: reviewNotes, + }) + .returning(); + await logAudit(tx, { + action: "create", + entityType: "task", + entityId: task!.id, + newValue: task!, + performedBy: userId, + }); + } + return row; + }); + res.json(updated); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (msg === "RACE_ALREADY_REVIEWED") { + res.status(409).json({ error: "Already reviewed", code: "bad_state" }); + return; + } + if (msg.startsWith("APPLY_VALIDATION:")) { + res + .status(400) + .json({ error: msg.slice("APPLY_VALIDATION:".length), code: "validation" }); + return; + } + res.status(500).json({ error: msg, code: "server_error" }); + } + return; + } + + // Office manager: needs_edit (returns to requester) — atomic, only from 'new' + if (action === "needs_edit") { const canApprove = APPROVE_ROLES.some((r) => names.has(r)); if (!canApprove) { res.status(403).json({ error: "Forbidden", code: "forbidden" }); @@ -766,29 +1168,43 @@ router.patch( } const reviewNotes = typeof body.reviewNotes === "string" ? body.reviewNotes.trim() : null; - const newStatus = action === "approve" ? "approved" : "rejected"; - const updated = await db.transaction(async (tx) => { - const [row] = await tx - .update(executiveMeetingRequestsTable) - .set({ - status: newStatus, - reviewedBy: userId, - reviewDecision: newStatus, - reviewNotes, - }) - .where(eq(executiveMeetingRequestsTable.id, id)) - .returning(); - await logAudit(tx, { - action: action === "approve" ? "approve" : "reject", - entityType: "request", - entityId: id, - oldValue: existing, - newValue: row, - performedBy: userId, + try { + const updated = await db.transaction(async (tx) => { + const [row] = await tx + .update(executiveMeetingRequestsTable) + .set({ + status: "needs_edit", + reviewedBy: userId, + reviewDecision: "needs_edit", + reviewNotes, + }) + .where( + and( + eq(executiveMeetingRequestsTable.id, id), + eq(executiveMeetingRequestsTable.status, "new"), + ), + ) + .returning(); + if (!row) throw new Error("RACE_ALREADY_REVIEWED"); + await logAudit(tx, { + action: "needs_edit", + entityType: "request", + entityId: id, + oldValue: existing, + newValue: row, + performedBy: userId, + }); + return row; }); - return row; - }); - res.json(updated); + res.json(updated); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (msg === "RACE_ALREADY_REVIEWED") { + res.status(409).json({ error: "Already reviewed", code: "bad_state" }); + return; + } + res.status(500).json({ error: msg, code: "server_error" }); + } return; } @@ -997,6 +1413,33 @@ router.patch( newValue: row, performedBy: userId, }); + // Propagate task completion → originating request becomes "done" + if ( + row && + row.status === "completed" && + existing.status !== "completed" && + row.requestId + ) { + const [reqOld] = await tx + .select() + .from(executiveMeetingRequestsTable) + .where(eq(executiveMeetingRequestsTable.id, row.requestId)); + if (reqOld && reqOld.status !== "done") { + const [reqNew] = await tx + .update(executiveMeetingRequestsTable) + .set({ status: "done" }) + .where(eq(executiveMeetingRequestsTable.id, row.requestId)) + .returning(); + await logAudit(tx, { + action: "done", + entityType: "request", + entityId: row.requestId, + oldValue: reqOld, + newValue: reqNew, + performedBy: userId, + }); + } + } return row; }); res.json(updated); @@ -1048,8 +1491,17 @@ router.get( requireAdminAudit, async (req, res): Promise => { const dateRaw = typeof req.query.date === "string" ? req.query.date.trim() : ""; + const dateFromRaw = + typeof req.query.dateFrom === "string" ? req.query.dateFrom.trim() : ""; + const dateToRaw = + typeof req.query.dateTo === "string" ? req.query.dateTo.trim() : ""; const entityType = typeof req.query.entityType === "string" ? req.query.entityType.trim() : ""; + const action = + typeof req.query.action === "string" ? req.query.action.trim() : ""; + const actorRaw = Number(req.query.actorId); + const actorId = + Number.isInteger(actorRaw) && actorRaw > 0 ? actorRaw : null; const limitRaw = Number(req.query.limit); const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(Math.floor(limitRaw), 200) : 100; @@ -1060,9 +1512,28 @@ router.get( conds.push(gte(executiveMeetingAuditLogsTable.performedAt, start)); conds.push(lt(executiveMeetingAuditLogsTable.performedAt, end)); } + if (dateFromRaw && DATE_RE.test(dateFromRaw)) { + conds.push( + gte( + executiveMeetingAuditLogsTable.performedAt, + new Date(`${dateFromRaw}T00:00:00.000Z`), + ), + ); + } + if (dateToRaw && DATE_RE.test(dateToRaw)) { + const end = new Date(`${dateToRaw}T00:00:00.000Z`); + end.setUTCDate(end.getUTCDate() + 1); + conds.push(lt(executiveMeetingAuditLogsTable.performedAt, end)); + } if (entityType) { conds.push(eq(executiveMeetingAuditLogsTable.entityType, entityType)); } + if (action) { + conds.push(eq(executiveMeetingAuditLogsTable.action, action)); + } + if (actorId) { + conds.push(eq(executiveMeetingAuditLogsTable.performedBy, actorId)); + } const where = conds.length > 0 ? and(...conds) : undefined; const rows = await db .select({ @@ -1098,7 +1569,7 @@ router.get( router.get( "/executive-meetings/notifications", requireAuth, - requireAdminAudit, + requireRead, async (_req, res): Promise => { const rows = await db .select({ @@ -1126,6 +1597,101 @@ router.get( }, ); +// ===================================================================== +// PDF ARCHIVES +// ===================================================================== + +router.get( + "/executive-meetings/pdf-archives", + requireAuth, + requireRead, + async (req, res): Promise => { + const dateRaw = typeof req.query.date === "string" ? req.query.date.trim() : ""; + const conds = []; + if (dateRaw && DATE_RE.test(dateRaw)) { + conds.push(eq(executiveMeetingPdfArchivesTable.archiveDate, dateRaw)); + } + const where = conds.length > 0 ? and(...conds) : undefined; + const rows = await db + .select({ + a: executiveMeetingPdfArchivesTable, + username: usersTable.username, + displayNameAr: usersTable.displayNameAr, + displayNameEn: usersTable.displayNameEn, + }) + .from(executiveMeetingPdfArchivesTable) + .leftJoin( + usersTable, + eq(usersTable.id, executiveMeetingPdfArchivesTable.generatedBy), + ) + .where(where as never) + .orderBy(desc(executiveMeetingPdfArchivesTable.generatedAt)) + .limit(200); + res.json({ + archives: rows.map((row) => ({ + ...row.a, + generator: row.username + ? { + username: row.username, + displayNameAr: row.displayNameAr, + displayNameEn: row.displayNameEn, + } + : null, + })), + }); + }, +); + +router.post( + "/executive-meetings/pdf-archives", + requireAuth, + requireRead, + async (req, res): Promise => { + const body = (req.body ?? {}) as Record; + const archiveDate = + typeof body.archiveDate === "string" && DATE_RE.test(body.archiveDate) + ? body.archiveDate + : null; + if (!archiveDate) { + res + .status(400) + .json({ error: "archiveDate required (YYYY-MM-DD)", code: "validation" }); + return; + } + const filePath = + typeof body.filePath === "string" && body.filePath.trim() + ? body.filePath.trim().slice(0, 500) + : `print:${archiveDate}`; + const userId = req.session.userId!; + const created = await db.transaction(async (tx) => { + const [{ value: maxV }] = await tx + .select({ + value: sql`coalesce(max(${executiveMeetingPdfArchivesTable.version}), 0)`, + }) + .from(executiveMeetingPdfArchivesTable) + .where(eq(executiveMeetingPdfArchivesTable.archiveDate, archiveDate)); + const [row] = await tx + .insert(executiveMeetingPdfArchivesTable) + .values({ + archiveDate, + filePath, + version: (maxV ?? 0) + 1, + generatedBy: userId, + }) + .returning(); + await logAudit(tx, { + action: "create", + entityType: "pdf_archive", + entityId: row!.id, + newValue: row!, + performedBy: userId, + }); + return row; + }); + res.status(201).json(created); + }, +); + // ===================================================================== // FONT SETTINGS // ===================================================================== diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index 326f2774f3edca446b0994e47f30cf6c64849b1c..1830d67ae98a9194eee190cc906fb2ceec9c7685 100644 GIT binary patch literal 25902 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 diff --git a/artifacts/tx-os/src/App.tsx b/artifacts/tx-os/src/App.tsx index 1f5c5891..1d4092f2 100644 --- a/artifacts/tx-os/src/App.tsx +++ b/artifacts/tx-os/src/App.tsx @@ -18,6 +18,7 @@ import NotesPage from "@/pages/notes"; import MyOrdersPage from "@/pages/my-orders"; import OrdersIncomingPage from "@/pages/orders-incoming"; import ExecutiveMeetingsPage from "@/pages/executive-meetings"; +import ExecutiveMeetingsPrintPage from "@/pages/executive-meetings-print"; const queryClient = new QueryClient({ defaultOptions: { @@ -50,6 +51,10 @@ function Router() { + diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index 8e8b5dc6..725b78a2 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -681,6 +681,8 @@ "attendees": { "label": "قائمة الحضور", "name": "الاسم", + "title": "المسمى", + "id": "رقم الحاضر", "type": "نوع الحضور", "add": "إضافة حاضر", "remove": "إزالة" @@ -811,7 +813,11 @@ "intro": "تستخدم هذه الواجهة طابعة المتصفح لإنتاج نسخة PDF من جدول اليوم. اختر التاريخ ثم اضغط طباعة.", "print": "طباعة الجدول", "openSchedule": "عرض الجدول", - "selectDate": "تاريخ الجدول" + "selectDate": "تاريخ الجدول", + "archiveSnapshot": "أرشفة نسخة", + "archived": "تمت أرشفة النسخة", + "archivesHeading": "النسخ المؤرشفة", + "noArchives": "لا توجد نسخ مؤرشفة لهذا التاريخ بعد." }, "fontSettingsPage": { "heading": "إعدادات الخط", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 2e351eb0..b9417c6f 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -678,6 +678,8 @@ "attendees": { "label": "Attendees", "name": "Name", + "title": "Title", + "id": "Attendee ID", "type": "Attendance type", "add": "Add attendee", "remove": "Remove" @@ -808,7 +810,11 @@ "intro": "This view uses the browser print dialog to produce a PDF of the day's schedule. Pick a date and click Print.", "print": "Print schedule", "openSchedule": "Open schedule", - "selectDate": "Schedule date" + "selectDate": "Schedule date", + "archiveSnapshot": "Archive snapshot", + "archived": "Snapshot archived", + "archivesHeading": "Archived snapshots", + "noArchives": "No snapshots archived for this date yet." }, "fontSettingsPage": { "heading": "Font Settings", diff --git a/artifacts/tx-os/src/pages/executive-meetings-print.tsx b/artifacts/tx-os/src/pages/executive-meetings-print.tsx new file mode 100644 index 00000000..3e188f3b --- /dev/null +++ b/artifacts/tx-os/src/pages/executive-meetings-print.tsx @@ -0,0 +1,207 @@ +import { useEffect, useMemo, useState } from "react"; + +type Attendee = { + id: number; + name: string; + title: string | null; + attendanceType: "internal" | "external" | "online"; + sortOrder: number; +}; + +type Meeting = { + id: number; + dailyNumber: number; + titleAr: string; + titleEn: string | null; + meetingDate: string; + startTime: string | null; + endTime: string | null; + location: string | null; + meetingUrl: string | null; + platform: string | null; + status: string; + isHighlighted: number; + notes: string | null; + attendees?: Attendee[]; +}; + +type DayResponse = { date: string; meetings: Meeting[] }; + +function getQuery(): { date: string; lang: "ar" | "en" } { + const params = new URLSearchParams(window.location.search); + const date = + params.get("date") || new Date().toISOString().slice(0, 10); + const lang = params.get("lang") === "en" ? "en" : "ar"; + return { date, lang }; +} + +const T = { + ar: { + title: "جدول الاجتماعات التنفيذية", + no: "#", + meeting: "الاجتماع", + attendees: "الحضور", + time: "الوقت", + location: "المكان", + none: "لا توجد اجتماعات لهذا اليوم.", + print: "طباعة", + back: "إغلاق", + loading: "جارٍ التحميل…", + }, + en: { + title: "Executive Meetings Schedule", + no: "#", + meeting: "Meeting", + attendees: "Attendees", + time: "Time", + location: "Location", + none: "No meetings for this day.", + print: "Print", + back: "Close", + loading: "Loading…", + }, +} as const; + +export default function ExecutiveMeetingsPrintPage() { + const { date, lang } = useMemo(getQuery, []); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const isRtl = lang === "ar"; + const t = T[lang]; + + useEffect(() => { + document.documentElement.dir = isRtl ? "rtl" : "ltr"; + document.documentElement.lang = lang; + document.title = `${t.title} — ${date}`; + }, [isRtl, lang, date, t.title]); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await fetch(`/api/executive-meetings?date=${date}`, { + credentials: "include", + }); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + const json = (await res.json()) as DayResponse; + if (!cancelled) setData(json); + } catch (err) { + if (!cancelled) + setError(err instanceof Error ? err.message : String(err)); + } + })(); + return () => { + cancelled = true; + }; + }, [date]); + + const meetings = data?.meetings ?? []; + + return ( +
+ + +
+
+ {t.title} — {date} +
+
+ + +
+
+ +
+

{t.title}

+
{date}
+ + {error ? ( +
{error}
+ ) : !data ? ( +
{t.loading}
+ ) : meetings.length === 0 ? ( +
{t.none}
+ ) : ( + + + + + + + + + + + {meetings.map((m) => { + const title = isRtl + ? m.titleAr + : m.titleEn || m.titleAr; + const attendees = (m.attendees ?? []) + .map((a) => (a.title ? `${a.name} (${a.title})` : a.name)) + .join("، "); + const time = + m.startTime && m.endTime + ? `${m.startTime.slice(0, 5)} – ${m.endTime.slice(0, 5)}` + : m.startTime + ? m.startTime.slice(0, 5) + : "—"; + return ( + + + + + + + ); + })} + +
{t.no}{t.meeting}{t.attendees}{t.time}
{m.dailyNumber} +
{title}
+ {m.location ? ( +
+ {m.location} +
+ ) : null} +
+ {attendees || "—"} + {time}
+ )} +
+
+ ); +} diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index f7ba7ce4..dfb7bb77 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -165,6 +165,20 @@ type FontSettingsResponse = { user: ({ scope: "user"; userId: number } & FontPrefs) | null; }; +type PdfArchive = { + id: number; + archiveDate: string; + filePath: string; + version: number; + generatedBy: number | null; + generatedAt: string; + generator: { + username: string; + displayNameAr: string | null; + displayNameEn: string | null; + } | null; +}; + const SECTIONS = [ { key: "schedule", icon: CalendarClock }, { key: "manage", icon: ListChecks }, @@ -239,6 +253,7 @@ export default function ExecutiveMeetingsPage() { const { t, i18n } = useTranslation(); const [, setLocation] = useLocation(); const isRtl = i18n.language === "ar"; + const lang: "ar" | "en" = isRtl ? "ar" : "en"; const BackIcon = isRtl ? ArrowRight : ArrowLeft; const [section, setSection] = useState("schedule"); @@ -407,7 +422,7 @@ export default function ExecutiveMeetingsPage() { )} {section === "pdf" && ( - + )} {section === "fontSettings" && me && ( ("all"); const [open, setOpen] = useState(false); const [submitting, setSubmitting] = useState(false); - const [form, setForm] = useState({ + const [form, setForm] = useState<{ + requestType: string; + meetingId: string; + details: string; + meetingDate: string; + startTime: string; + endTime: string; + location: string; + meetingUrl: string; + platform: string; + attendeeName: string; + attendeeTitle: string; + attendeeType: string; + attendeeId: string; + }>({ requestType: "edit", meetingId: "", details: "", + meetingDate: "", + startTime: "", + endTime: "", + location: "", + meetingUrl: "", + platform: "none", + attendeeName: "", + attendeeTitle: "", + attendeeType: "internal", + attendeeId: "", }); const queryKey = ["/api/executive-meetings/requests", filterStatus, scope]; @@ -1158,19 +1197,62 @@ function RequestsSection({ }, }); + function buildRequestDetails(): Record { + const note = form.details.trim(); + const out: Record = {}; + if (note) out.note = note; + switch (form.requestType) { + case "reschedule": + if (form.meetingDate) out.meetingDate = form.meetingDate; + if (form.startTime) out.startTime = form.startTime; + if (form.endTime) out.endTime = form.endTime; + break; + case "change_location": + if (form.location.trim()) out.location = form.location.trim(); + if (form.meetingUrl.trim()) out.meetingUrl = form.meetingUrl.trim(); + if (form.platform) out.platform = form.platform; + break; + case "add_attendee": + if (form.attendeeName.trim()) out.name = form.attendeeName.trim(); + if (form.attendeeTitle.trim()) out.title = form.attendeeTitle.trim(); + if (form.attendeeType) out.attendanceType = form.attendeeType; + break; + case "remove_attendee": + if (form.attendeeId.trim()) out.attendeeId = Number(form.attendeeId); + break; + default: + break; + } + return out; + } + async function submit() { if (!me.canSubmitRequest) return; setSubmitting(true); try { const body: Record = { requestType: form.requestType, - requestDetails: { note: form.details.trim() }, + requestDetails: buildRequestDetails(), }; if (form.meetingId.trim()) body.meetingId = Number(form.meetingId); await apiJson(`/api/executive-meetings/requests`, { method: "POST", body }); toast({ title: t("executiveMeetings.requests.submitted") }); setOpen(false); - setForm({ requestType: "edit", meetingId: "", details: "" }); + setForm({ + requestType: "edit", + meetingId: "", + details: "", + meetingDate: "", + startTime: "", + endTime: "", + location: "", + meetingUrl: "", + platform: "none", + attendeeName: "", + attendeeTitle: "", + attendeeType: "internal", + attendeeId: "", + }); await qc.invalidateQueries({ queryKey: ["/api/executive-meetings/requests"] }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); @@ -1323,9 +1405,96 @@ function RequestsSection({ onChange={(e) => setForm({ ...form, meetingId: e.target.value })} /> + {form.requestType === "reschedule" && ( + <> + + + setForm({ ...form, meetingDate: e.target.value }) + } + data-testid="em-req-date" + /> + +
+ + + setForm({ ...form, startTime: e.target.value }) + } + data-testid="em-req-start" + /> + + + + setForm({ ...form, endTime: e.target.value }) + } + data-testid="em-req-end" + /> + +
+ + )} + {form.requestType === "change_location" && ( + <> + + + setForm({ ...form, location: e.target.value }) + } + /> + + + + setForm({ ...form, meetingUrl: e.target.value }) + } + /> + + + )} + {form.requestType === "add_attendee" && ( + <> + + + setForm({ ...form, attendeeName: e.target.value }) + } + /> + + + + setForm({ ...form, attendeeTitle: e.target.value }) + } + /> + + + )} + {form.requestType === "remove_attendee" && ( + + + setForm({ ...form, attendeeId: e.target.value }) + } + /> + + )}