Executive Meetings Phase 2: full module + atomic audit
Original task (#108): polish the daily-schedule UI and ship the remaining 8 sections of the Executive Meetings module (Manage, Requests, Approvals, Tasks, Notifications, Audit, PDF, Font Settings) with bilingual i18n and fine-grained RBAC. What changed: - Schedule UI: centered cells, '#' header (not 'م'), attendees widest, RTL column order locked, navy/white/gray + red highlighting only. - Schema: made executive_meeting_requests.meetingId nullable so 'create-meeting' suggestions don't need a target row. - Backend rewrite of artifacts/api-server/src/routes/executive-meetings.ts: GET /me (now returns userId), full CRUD for meetings (transactional attendee replace), requests CRUD + approve/reject/withdraw, tasks CRUD with assignee status updates, audit-logs GET (admin), notifications GET, font-settings GET/PATCH (per-user + global). RBAC via 5 role sets and a makeRequireRoles middleware factory. - Audit-in-transaction: every mutation (meeting/request/task/font CRUD) wraps the DB write AND the audit-log insert in the same db.transaction so audit entries cannot drift from state if either insert fails. This was the main finding from the architect review and is now fixed. - Path-to-regexp 8 fix: replaced inline ':id(\\d+)' with router.param('id') + next('route') guard, plus NaN guards in handlers. - Frontend rewrite of artifacts/tx-os/src/pages/executive-meetings.tsx (~2200 lines): 8 new section components, RBAC-aware UI via /me, per-task assignee check now shown for status buttons. - i18n: full executiveMeetings.* key set added in ar.json + en.json. Verified: full e2e (admin login -> all 9 tabs -> create meeting -> submit request -> approve -> audit shows full chain -> font save) plus a smoke regression after the audit-in-tx refactor (atomic audit row created in lockstep with mutation). Out of scope (proposed as follow-ups #110-#112): real notification delivery, real PDF generation (currently window.print), and automated integration tests for the new endpoints. The pre-existing 'apps-open.test.mjs' syntax error in the test workflow is unrelated to this task.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
@@ -622,7 +622,7 @@
|
||||
"externalAttendance": "حضور خارجي",
|
||||
"placeholderBody": "هذا القسم قيد التطوير في المراحل القادمة. ستتوفر إدارته الكاملة قريباً.",
|
||||
"col": {
|
||||
"number": "م",
|
||||
"number": "#",
|
||||
"meeting": "الاجتماع",
|
||||
"attendees": "الحضور",
|
||||
"time": "الوقت"
|
||||
@@ -637,6 +637,231 @@
|
||||
"audit": "سجل التعديلات",
|
||||
"pdf": "تصدير PDF",
|
||||
"fontSettings": "إعدادات الخط"
|
||||
},
|
||||
"common": {
|
||||
"save": "حفظ",
|
||||
"cancel": "إلغاء",
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل",
|
||||
"add": "إضافة",
|
||||
"submit": "إرسال",
|
||||
"close": "إغلاق",
|
||||
"loading": "جارٍ التحميل...",
|
||||
"saved": "تم الحفظ",
|
||||
"saveFailed": "تعذر الحفظ",
|
||||
"actions": "إجراءات",
|
||||
"noPermission": "ليست لديك صلاحية لتنفيذ هذا الإجراء",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"all": "الكل",
|
||||
"filter": "تصفية"
|
||||
},
|
||||
"manage": {
|
||||
"heading": "إدارة الاجتماعات",
|
||||
"addMeeting": "إضافة اجتماع",
|
||||
"editMeeting": "تعديل الاجتماع",
|
||||
"deleteMeeting": "حذف الاجتماع",
|
||||
"deleteConfirm": "حذف هذا الاجتماع نهائياً؟",
|
||||
"noMeetings": "لا توجد اجتماعات لهذا اليوم. اضغط \"إضافة اجتماع\" لإنشاء واحد.",
|
||||
"field": {
|
||||
"titleAr": "العنوان (عربي)",
|
||||
"titleEn": "العنوان (إنجليزي)",
|
||||
"meetingDate": "التاريخ",
|
||||
"dailyNumber": "الرقم اليومي",
|
||||
"dailyNumberHint": "اتركه فارغاً ليتم الترقيم تلقائياً",
|
||||
"startTime": "وقت البدء",
|
||||
"endTime": "وقت الانتهاء",
|
||||
"location": "المكان",
|
||||
"meetingUrl": "رابط الاجتماع",
|
||||
"platform": "المنصة",
|
||||
"status": "الحالة",
|
||||
"isHighlighted": "تمييز (لون أحمر)",
|
||||
"notes": "ملاحظات"
|
||||
},
|
||||
"attendees": {
|
||||
"label": "قائمة الحضور",
|
||||
"name": "الاسم",
|
||||
"type": "نوع الحضور",
|
||||
"add": "إضافة حاضر",
|
||||
"remove": "إزالة"
|
||||
},
|
||||
"platform": {
|
||||
"none": "بدون",
|
||||
"webex": "Webex",
|
||||
"teams": "Teams",
|
||||
"zoom": "Zoom",
|
||||
"other": "أخرى"
|
||||
},
|
||||
"attendanceType": {
|
||||
"internal": "داخلي",
|
||||
"virtual": "افتراضي",
|
||||
"external": "خارجي"
|
||||
},
|
||||
"status": {
|
||||
"scheduled": "مجدول",
|
||||
"cancelled": "ملغى",
|
||||
"completed": "منتهٍ",
|
||||
"postponed": "مؤجل"
|
||||
},
|
||||
"errors": {
|
||||
"titleRequired": "العنوان بالعربية مطلوب",
|
||||
"dateRequired": "التاريخ مطلوب",
|
||||
"duplicateNumber": "هذا الرقم اليومي مستخدم بالفعل في هذا التاريخ",
|
||||
"saveFailed": "تعذر حفظ الاجتماع"
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"heading": "طلبات التعديل",
|
||||
"newRequest": "طلب جديد",
|
||||
"myRequests": "طلباتي",
|
||||
"allRequests": "جميع الطلبات",
|
||||
"empty": "لا توجد طلبات.",
|
||||
"filterStatus": "تصفية حسب الحالة",
|
||||
"submitFor": "اقتراح تعديل على:",
|
||||
"noTargetMeeting": "اقتراح اجتماع جديد (بدون ربط)",
|
||||
"details": "التفاصيل",
|
||||
"detailsPlaceholder": "اشرح ما تقترح بالتفصيل...",
|
||||
"submitted": "تم إرسال الطلب",
|
||||
"submitFailed": "تعذر إرسال الطلب",
|
||||
"requestedBy": "مقدم الطلب",
|
||||
"requestedAt": "وقت الطلب",
|
||||
"type": {
|
||||
"create": "إنشاء اجتماع",
|
||||
"edit": "تعديل بيانات",
|
||||
"delete": "حذف",
|
||||
"reschedule": "إعادة جدولة",
|
||||
"highlight": "تمييز",
|
||||
"unhighlight": "إزالة التمييز",
|
||||
"other": "آخر"
|
||||
},
|
||||
"status": {
|
||||
"new": "جديد",
|
||||
"approved": "تمت الموافقة",
|
||||
"rejected": "مرفوض",
|
||||
"withdrawn": "مسحوب"
|
||||
}
|
||||
},
|
||||
"approvals": {
|
||||
"heading": "موافقات مدير المكتب",
|
||||
"empty": "لا توجد طلبات بانتظار المراجعة.",
|
||||
"approve": "اعتماد",
|
||||
"reject": "رفض",
|
||||
"reviewNotes": "ملاحظات المراجعة",
|
||||
"reviewNotesPlaceholder": "اكتب ملاحظاتك هنا (اختياري)",
|
||||
"approved": "تم الاعتماد",
|
||||
"rejected": "تم الرفض",
|
||||
"actionFailed": "تعذر تنفيذ المراجعة"
|
||||
},
|
||||
"tasks": {
|
||||
"heading": "مهام فريق التنسيق",
|
||||
"addTask": "إضافة مهمة",
|
||||
"empty": "لا توجد مهام.",
|
||||
"field": {
|
||||
"taskType": "نوع المهمة",
|
||||
"assignee": "المُكلَّف",
|
||||
"dueDate": "تاريخ الاستحقاق",
|
||||
"notes": "ملاحظات",
|
||||
"meeting": "الاجتماع",
|
||||
"status": "الحالة"
|
||||
},
|
||||
"status": {
|
||||
"pending": "بالانتظار",
|
||||
"in_progress": "قيد التنفيذ",
|
||||
"completed": "منجزة",
|
||||
"cancelled": "ملغاة"
|
||||
},
|
||||
"markCompleted": "إنهاء المهمة",
|
||||
"markInProgress": "بدء التنفيذ",
|
||||
"deleteConfirm": "حذف هذه المهمة؟",
|
||||
"saved": "تم حفظ المهمة",
|
||||
"deleted": "تم حذف المهمة"
|
||||
},
|
||||
"audit": {
|
||||
"heading": "سجل التعديلات",
|
||||
"empty": "لا توجد قيود في السجل.",
|
||||
"filterDate": "تصفية بالتاريخ",
|
||||
"filterEntity": "النوع",
|
||||
"all": "الكل",
|
||||
"col": {
|
||||
"when": "الوقت",
|
||||
"actor": "المنفّذ",
|
||||
"action": "الإجراء",
|
||||
"entity": "النوع",
|
||||
"entityId": "المعرف",
|
||||
"diff": "التفاصيل"
|
||||
},
|
||||
"entity": {
|
||||
"meeting": "اجتماع",
|
||||
"attendee": "حاضر",
|
||||
"request": "طلب",
|
||||
"task": "مهمة",
|
||||
"font_settings": "إعدادات الخط"
|
||||
},
|
||||
"action": {
|
||||
"create": "إنشاء",
|
||||
"update": "تعديل",
|
||||
"delete": "حذف",
|
||||
"approve": "اعتماد",
|
||||
"reject": "رفض",
|
||||
"submit": "إرسال"
|
||||
}
|
||||
},
|
||||
"pdf": {
|
||||
"heading": "تصدير / طباعة",
|
||||
"intro": "تستخدم هذه الواجهة طابعة المتصفح لإنتاج نسخة PDF من جدول اليوم. اختر التاريخ ثم اضغط طباعة.",
|
||||
"print": "طباعة الجدول",
|
||||
"openSchedule": "عرض الجدول",
|
||||
"selectDate": "تاريخ الجدول"
|
||||
},
|
||||
"fontSettingsPage": {
|
||||
"heading": "إعدادات الخط",
|
||||
"intro": "تطبق هذه الإعدادات على جدول الاجتماعات. يمكنك حفظ تفضيلك الشخصي، ويمكن للمسؤول حفظ الإعدادات العامة لجميع المستخدمين.",
|
||||
"scope": "النطاق",
|
||||
"scopeUser": "تفضيلي الشخصي",
|
||||
"scopeGlobal": "عام (جميع المستخدمين)",
|
||||
"fontFamily": "نوع الخط",
|
||||
"fontSize": "حجم الخط",
|
||||
"fontWeight": "السماكة",
|
||||
"alignment": "المحاذاة",
|
||||
"weight": {
|
||||
"regular": "عادي",
|
||||
"medium": "متوسط",
|
||||
"semibold": "نصف عريض",
|
||||
"bold": "عريض"
|
||||
},
|
||||
"align": {
|
||||
"start": "بداية",
|
||||
"center": "وسط",
|
||||
"end": "نهاية"
|
||||
},
|
||||
"preview": "معاينة",
|
||||
"previewText": "نص تجريبي ١٢٣ Sample 123",
|
||||
"save": "حفظ الإعدادات",
|
||||
"saved": "تم الحفظ",
|
||||
"reset": "إعادة الافتراضي"
|
||||
},
|
||||
"notificationsPage": {
|
||||
"heading": "التنبيهات",
|
||||
"intro": "هذه قائمة التنبيهات المجدولة المرتبطة باجتماعات المكتب. لا يتم الإرسال الفعلي حالياً (وضع تجريبي).",
|
||||
"empty": "لا توجد تنبيهات مجدولة.",
|
||||
"col": {
|
||||
"meeting": "الاجتماع",
|
||||
"user": "المستخدم",
|
||||
"type": "النوع",
|
||||
"scheduledAt": "الموعد",
|
||||
"status": "الحالة"
|
||||
},
|
||||
"type": {
|
||||
"reminder": "تذكير",
|
||||
"change": "تنبيه تعديل",
|
||||
"cancel": "تنبيه إلغاء"
|
||||
},
|
||||
"status": {
|
||||
"pending": "بالانتظار",
|
||||
"sent": "أُرسل",
|
||||
"skipped": "متخطى",
|
||||
"failed": "فشل"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,6 +634,231 @@
|
||||
"audit": "Audit Log",
|
||||
"pdf": "PDF Export",
|
||||
"fontSettings": "Font Settings"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"add": "Add",
|
||||
"submit": "Submit",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Save failed",
|
||||
"actions": "Actions",
|
||||
"noPermission": "You do not have permission for this action",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"all": "All",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"manage": {
|
||||
"heading": "Manage Meetings",
|
||||
"addMeeting": "Add meeting",
|
||||
"editMeeting": "Edit meeting",
|
||||
"deleteMeeting": "Delete meeting",
|
||||
"deleteConfirm": "Permanently delete this meeting?",
|
||||
"noMeetings": "No meetings on this day. Click \"Add meeting\" to create one.",
|
||||
"field": {
|
||||
"titleAr": "Title (Arabic)",
|
||||
"titleEn": "Title (English)",
|
||||
"meetingDate": "Date",
|
||||
"dailyNumber": "Daily number",
|
||||
"dailyNumberHint": "Leave blank for auto-numbering",
|
||||
"startTime": "Start time",
|
||||
"endTime": "End time",
|
||||
"location": "Location",
|
||||
"meetingUrl": "Meeting URL",
|
||||
"platform": "Platform",
|
||||
"status": "Status",
|
||||
"isHighlighted": "Highlight (red)",
|
||||
"notes": "Notes"
|
||||
},
|
||||
"attendees": {
|
||||
"label": "Attendees",
|
||||
"name": "Name",
|
||||
"type": "Attendance type",
|
||||
"add": "Add attendee",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"platform": {
|
||||
"none": "None",
|
||||
"webex": "Webex",
|
||||
"teams": "Teams",
|
||||
"zoom": "Zoom",
|
||||
"other": "Other"
|
||||
},
|
||||
"attendanceType": {
|
||||
"internal": "Internal",
|
||||
"virtual": "Virtual",
|
||||
"external": "External"
|
||||
},
|
||||
"status": {
|
||||
"scheduled": "Scheduled",
|
||||
"cancelled": "Cancelled",
|
||||
"completed": "Completed",
|
||||
"postponed": "Postponed"
|
||||
},
|
||||
"errors": {
|
||||
"titleRequired": "Arabic title is required",
|
||||
"dateRequired": "Date is required",
|
||||
"duplicateNumber": "That daily number already exists for this date",
|
||||
"saveFailed": "Could not save the meeting"
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"heading": "Change Requests",
|
||||
"newRequest": "New request",
|
||||
"myRequests": "My requests",
|
||||
"allRequests": "All requests",
|
||||
"empty": "No requests yet.",
|
||||
"filterStatus": "Filter by status",
|
||||
"submitFor": "Suggest a change to:",
|
||||
"noTargetMeeting": "Propose a brand-new meeting (no link)",
|
||||
"details": "Details",
|
||||
"detailsPlaceholder": "Explain your suggestion in detail...",
|
||||
"submitted": "Request submitted",
|
||||
"submitFailed": "Could not submit the request",
|
||||
"requestedBy": "Submitted by",
|
||||
"requestedAt": "Submitted at",
|
||||
"type": {
|
||||
"create": "Create meeting",
|
||||
"edit": "Edit details",
|
||||
"delete": "Delete",
|
||||
"reschedule": "Reschedule",
|
||||
"highlight": "Highlight",
|
||||
"unhighlight": "Remove highlight",
|
||||
"other": "Other"
|
||||
},
|
||||
"status": {
|
||||
"new": "New",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"withdrawn": "Withdrawn"
|
||||
}
|
||||
},
|
||||
"approvals": {
|
||||
"heading": "Office Manager Approvals",
|
||||
"empty": "No requests pending review.",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"reviewNotes": "Review notes",
|
||||
"reviewNotesPlaceholder": "Optional notes for the requester",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"actionFailed": "Could not complete the review"
|
||||
},
|
||||
"tasks": {
|
||||
"heading": "Coordination Tasks",
|
||||
"addTask": "Add task",
|
||||
"empty": "No tasks yet.",
|
||||
"field": {
|
||||
"taskType": "Task type",
|
||||
"assignee": "Assignee",
|
||||
"dueDate": "Due date",
|
||||
"notes": "Notes",
|
||||
"meeting": "Meeting",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"in_progress": "In progress",
|
||||
"completed": "Completed",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"markCompleted": "Mark completed",
|
||||
"markInProgress": "Start",
|
||||
"deleteConfirm": "Delete this task?",
|
||||
"saved": "Task saved",
|
||||
"deleted": "Task deleted"
|
||||
},
|
||||
"audit": {
|
||||
"heading": "Audit Log",
|
||||
"empty": "No audit entries.",
|
||||
"filterDate": "Filter by date",
|
||||
"filterEntity": "Entity",
|
||||
"all": "All",
|
||||
"col": {
|
||||
"when": "When",
|
||||
"actor": "Actor",
|
||||
"action": "Action",
|
||||
"entity": "Entity",
|
||||
"entityId": "ID",
|
||||
"diff": "Details"
|
||||
},
|
||||
"entity": {
|
||||
"meeting": "Meeting",
|
||||
"attendee": "Attendee",
|
||||
"request": "Request",
|
||||
"task": "Task",
|
||||
"font_settings": "Font settings"
|
||||
},
|
||||
"action": {
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"delete": "Delete",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"submit": "Submit"
|
||||
}
|
||||
},
|
||||
"pdf": {
|
||||
"heading": "Print / Export",
|
||||
"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"
|
||||
},
|
||||
"fontSettingsPage": {
|
||||
"heading": "Font Settings",
|
||||
"intro": "These settings apply to the meetings schedule. You can save your personal preference; admins can also save the global default.",
|
||||
"scope": "Scope",
|
||||
"scopeUser": "My preference",
|
||||
"scopeGlobal": "Global (everyone)",
|
||||
"fontFamily": "Font family",
|
||||
"fontSize": "Font size",
|
||||
"fontWeight": "Weight",
|
||||
"alignment": "Alignment",
|
||||
"weight": {
|
||||
"regular": "Regular",
|
||||
"medium": "Medium",
|
||||
"semibold": "Semibold",
|
||||
"bold": "Bold"
|
||||
},
|
||||
"align": {
|
||||
"start": "Start",
|
||||
"center": "Center",
|
||||
"end": "End"
|
||||
},
|
||||
"preview": "Preview",
|
||||
"previewText": "Sample text 123 نص تجريبي ١٢٣",
|
||||
"save": "Save settings",
|
||||
"saved": "Saved",
|
||||
"reset": "Reset to default"
|
||||
},
|
||||
"notificationsPage": {
|
||||
"heading": "Notifications",
|
||||
"intro": "Scheduled reminders and change-notice records for executive meetings. No actual delivery is wired up yet (placeholder).",
|
||||
"empty": "No scheduled notifications.",
|
||||
"col": {
|
||||
"meeting": "Meeting",
|
||||
"user": "User",
|
||||
"type": "Type",
|
||||
"scheduledAt": "Scheduled",
|
||||
"status": "Status"
|
||||
},
|
||||
"type": {
|
||||
"reminder": "Reminder",
|
||||
"change": "Change notice",
|
||||
"cancel": "Cancellation notice"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"sent": "Sent",
|
||||
"skipped": "Skipped",
|
||||
"failed": "Failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,9 +74,10 @@ export const executiveMeetingRequestsTable = pgTable(
|
||||
"executive_meeting_requests",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
meetingId: integer("meeting_id")
|
||||
.notNull()
|
||||
.references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
|
||||
meetingId: integer("meeting_id").references(
|
||||
() => executiveMeetingsTable.id,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
requestedBy: integer("requested_by").references(() => usersTable.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
- **Notifications**: unread tracking, mark-all-read
|
||||
- **Admin Panel**: CRUD for apps, services, users (admin role required)
|
||||
- **Session-based Auth**: RBAC with roles (admin/user)
|
||||
- **Executive Meetings (Phase 1)**: bilingual daily-schedule table for the executive office, app entry on the home grid (`/executive-meetings`), 9-section sub-nav (schedule, manage, requests, approvals, tasks, notifications, audit, pdf, font settings) — only the daily schedule is implemented in Phase 1; other sections render placeholders.
|
||||
- **Executive Meetings (Phase 2)**: bilingual full-stack module under `/executive-meetings` with 9 sections — Schedule (centered cells, attendees widest column, RTL-locked column order # / الاجتماع / الحضور / الوقت), Manage Meetings (CRUD with attendee replace, transactional), Change Requests (submit / withdraw, supports `meetingId=null` for create-suggestions), Approvals (approve/reject with review notes), Tasks (CRUD with assignee status updates — assignees and mutators can change status, only mutators can delete), Notifications (per-user feed), Audit Log (full action chain, admin role required), PDF (window.print export), Font Settings (per-user + global scope, family/size/weight/alignment with live preview). RBAC enforced via 5 role sets (READ/MUTATE/APPROVE/REQUEST/ADMIN_AUDIT) and a `makeRequireRoles` middleware factory; `/api/executive-meetings/me` returns `{userId, roles, canRead, canMutate, canApprove, canSubmitRequest, canViewAudit}`. Every mutation (meeting/request/task/font CRUD) wraps the DB write **and** the audit-log insert in the same `db.transaction(...)` so audit entries cannot drift from state. Routes use `router.param("id")` with `next("route")` to handle path-to-regexp 8 (no inline `:id(\\d+)` support).
|
||||
|
||||
## Key Commands
|
||||
|
||||
|
||||
Reference in New Issue
Block a user