Add Public Relations & Protocol module (Task #649)
New, fully separate app module "العلاقات العامة والمراسم / Public Relations and Protocol". Does not touch executive-meetings; all new tables are prefixed protocol_ and are additive-only, routes under /protocol. Includes: - 6 protocol_ tables (rooms, room bookings, external meetings, gifts, gift issues, audit log) in lib/db. - API routes + role-scoped middleware (protocol_super_admin, pr_manager, officer, requester, viewer) with requireProtocolAccess gating. - Room-booking conflict prevention (overlap rule newStart<existingEnd AND newEnd>existingStart vs pending/approved) with AR error message. - Gifts/shields (دروع) registry + issuance with stock tracking. - Bilingual AR/EN frontend page (tabs + dialogs) at /protocol, app tile, i18n keys, and seed data (roles, permission, default rooms). Concurrency/security hardening from code review: - All role guards now validate users.isActive (inactive-user bypass fix). - Per-room SELECT ... FOR UPDATE lock serializes booking check-then-write. - Approve paths re-read inside the transaction and use state-conditional updates (WHERE ... AND status='pending' RETURNING). - Gift issuance claims the issue conditionally, then does an atomic conditional stock decrement; failure rolls back the claim. Verified: drizzle push (6 tables), seed, frontend + api-server typecheck (only pre-existing errors in untouched files), unauth gating returns 401, conflict predicate confirmed. Architect review PASS.
This commit is contained in:
@@ -27,6 +27,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 ProtocolPage from "@/pages/protocol";
|
||||
import EmbeddedAppPage from "@/pages/embedded-app";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -73,6 +74,7 @@ function Router() {
|
||||
<Route path="/my-orders" component={MyOrdersPage} />
|
||||
<Route path="/orders/incoming" component={OrdersIncomingPage} />
|
||||
<Route path="/meetings" component={ExecutiveMeetingsPage} />
|
||||
<Route path="/protocol" component={ProtocolPage} />
|
||||
{/* Back-compat redirect: the page used to live at
|
||||
/executive-meetings. Stored PDFs, email links, and bookmarks
|
||||
still point there, so catch the old path (and any deep
|
||||
|
||||
@@ -971,7 +971,9 @@
|
||||
"appName": "نظام Tx",
|
||||
"previous": "السابق",
|
||||
"next": "التالي",
|
||||
"empty": "(فارغ)"
|
||||
"empty": "(فارغ)",
|
||||
"back": "رجوع",
|
||||
"confirmDelete": "تأكيد الحذف"
|
||||
},
|
||||
"embeddedFrame": {
|
||||
"title": "تطبيق مدمج",
|
||||
@@ -1656,5 +1658,111 @@
|
||||
"saved": "تم الحفظ",
|
||||
"reset": "إعادة الافتراضي"
|
||||
}
|
||||
},
|
||||
"protocol": {
|
||||
"title": "العلاقات العامة والمراسم",
|
||||
"notes": "ملاحظات",
|
||||
"statusLabel": "الحالة",
|
||||
"confirmReject": "هل أنت متأكد من رفض هذا الطلب؟",
|
||||
"confirmDelete": "لا يمكن التراجع عن هذا الإجراء.",
|
||||
"tabs": {
|
||||
"dashboard": "لوحة المعلومات",
|
||||
"bookings": "حجوزات القاعات",
|
||||
"external": "اللقاءات الخارجية",
|
||||
"gifts": "الهدايا والدروع",
|
||||
"issues": "طلبات الصرف",
|
||||
"reports": "التقارير",
|
||||
"audit": "سجل النشاط"
|
||||
},
|
||||
"status": {
|
||||
"pending": "قيد الانتظار",
|
||||
"approved": "معتمد",
|
||||
"rejected": "مرفوض",
|
||||
"cancelled": "ملغى",
|
||||
"issued": "تم الصرف",
|
||||
"scheduled": "مجدول",
|
||||
"completed": "منجز"
|
||||
},
|
||||
"giftKind": {
|
||||
"gift": "هدية",
|
||||
"shield": "درع"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "اعتماد",
|
||||
"reject": "رفض",
|
||||
"cancel": "إلغاء",
|
||||
"approveIssue": "اعتماد وصرف"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "تعذّر إتمام العملية."
|
||||
},
|
||||
"dashboard": {
|
||||
"activeRooms": "القاعات المتاحة",
|
||||
"todayBookings": "حجوزات اليوم",
|
||||
"pendingBookings": "حجوزات بانتظار الاعتماد",
|
||||
"upcomingExternal": "لقاءات خارجية قادمة",
|
||||
"giftStock": "إجمالي مخزون الهدايا",
|
||||
"pendingIssues": "طلبات صرف معلّقة"
|
||||
},
|
||||
"bookings": {
|
||||
"new": "حجز جديد",
|
||||
"edit": "تعديل الحجز",
|
||||
"empty": "لا توجد حجوزات.",
|
||||
"requester": "مقدّم الطلب",
|
||||
"roomLabel": "القاعة",
|
||||
"titleLabel": "عنوان الحجز",
|
||||
"startsAt": "من",
|
||||
"endsAt": "إلى"
|
||||
},
|
||||
"external": {
|
||||
"new": "لقاء جديد",
|
||||
"edit": "تعديل اللقاء",
|
||||
"empty": "لا توجد لقاءات خارجية.",
|
||||
"titleLabel": "عنوان اللقاء",
|
||||
"party": "الجهة",
|
||||
"location": "المكان",
|
||||
"startsAt": "التاريخ والوقت"
|
||||
},
|
||||
"gifts": {
|
||||
"new": "صنف جديد",
|
||||
"edit": "تعديل الصنف",
|
||||
"empty": "لا توجد أصناف.",
|
||||
"stock": "المخزون",
|
||||
"nameAr": "الاسم (عربي)",
|
||||
"nameEn": "الاسم (إنجليزي)",
|
||||
"kind": "النوع"
|
||||
},
|
||||
"rooms": {
|
||||
"new": "قاعة جديدة",
|
||||
"edit": "تعديل القاعة",
|
||||
"title": "القاعات",
|
||||
"inactive": "غير مفعّلة",
|
||||
"nameAr": "الاسم (عربي)",
|
||||
"nameEn": "الاسم (إنجليزي)",
|
||||
"capacity": "السعة",
|
||||
"location": "الموقع"
|
||||
},
|
||||
"issues": {
|
||||
"new": "طلب صرف",
|
||||
"empty": "لا توجد طلبات صرف.",
|
||||
"recipient": "المستفيد",
|
||||
"gift": "الصنف",
|
||||
"occasion": "المناسبة",
|
||||
"quantity": "الكمية"
|
||||
},
|
||||
"reports": {
|
||||
"byRoom": "الحجوزات حسب القاعة",
|
||||
"byGift": "الصرف حسب الصنف",
|
||||
"room": "القاعة",
|
||||
"gift": "الصنف",
|
||||
"total": "الإجمالي",
|
||||
"requests": "الطلبات",
|
||||
"issued": "المصروفة",
|
||||
"issuedQty": "الكمية المصروفة",
|
||||
"empty": "لا توجد بيانات."
|
||||
},
|
||||
"audit": {
|
||||
"empty": "لا يوجد نشاط."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,7 +883,9 @@
|
||||
"appName": "Tx OS",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"empty": "(empty)"
|
||||
"empty": "(empty)",
|
||||
"back": "Back",
|
||||
"confirmDelete": "Confirm delete"
|
||||
},
|
||||
"embeddedFrame": {
|
||||
"title": "Embedded app",
|
||||
@@ -1529,5 +1531,111 @@
|
||||
"saved": "Saved",
|
||||
"reset": "Reset to default"
|
||||
}
|
||||
},
|
||||
"protocol": {
|
||||
"title": "Public Relations & Protocol",
|
||||
"notes": "Notes",
|
||||
"statusLabel": "Status",
|
||||
"confirmReject": "Are you sure you want to reject this request?",
|
||||
"confirmDelete": "This action cannot be undone.",
|
||||
"tabs": {
|
||||
"dashboard": "Dashboard",
|
||||
"bookings": "Room Bookings",
|
||||
"external": "External Meetings",
|
||||
"gifts": "Gifts & Shields",
|
||||
"issues": "Issuance Requests",
|
||||
"reports": "Reports",
|
||||
"audit": "Activity Log"
|
||||
},
|
||||
"status": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"cancelled": "Cancelled",
|
||||
"issued": "Issued",
|
||||
"scheduled": "Scheduled",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"giftKind": {
|
||||
"gift": "Gift",
|
||||
"shield": "Shield"
|
||||
},
|
||||
"actions": {
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"cancel": "Cancel",
|
||||
"approveIssue": "Approve & issue"
|
||||
},
|
||||
"errors": {
|
||||
"generic": "The operation could not be completed."
|
||||
},
|
||||
"dashboard": {
|
||||
"activeRooms": "Active rooms",
|
||||
"todayBookings": "Today's bookings",
|
||||
"pendingBookings": "Bookings awaiting approval",
|
||||
"upcomingExternal": "Upcoming external meetings",
|
||||
"giftStock": "Total gift stock",
|
||||
"pendingIssues": "Pending issuance requests"
|
||||
},
|
||||
"bookings": {
|
||||
"new": "New booking",
|
||||
"edit": "Edit booking",
|
||||
"empty": "No bookings yet.",
|
||||
"requester": "Requester",
|
||||
"roomLabel": "Room",
|
||||
"titleLabel": "Booking title",
|
||||
"startsAt": "From",
|
||||
"endsAt": "To"
|
||||
},
|
||||
"external": {
|
||||
"new": "New meeting",
|
||||
"edit": "Edit meeting",
|
||||
"empty": "No external meetings yet.",
|
||||
"titleLabel": "Meeting title",
|
||||
"party": "Party",
|
||||
"location": "Location",
|
||||
"startsAt": "Date & time"
|
||||
},
|
||||
"gifts": {
|
||||
"new": "New item",
|
||||
"edit": "Edit item",
|
||||
"empty": "No items yet.",
|
||||
"stock": "Stock",
|
||||
"nameAr": "Name (Arabic)",
|
||||
"nameEn": "Name (English)",
|
||||
"kind": "Kind"
|
||||
},
|
||||
"rooms": {
|
||||
"new": "New room",
|
||||
"edit": "Edit room",
|
||||
"title": "Rooms",
|
||||
"inactive": "inactive",
|
||||
"nameAr": "Name (Arabic)",
|
||||
"nameEn": "Name (English)",
|
||||
"capacity": "Capacity",
|
||||
"location": "Location"
|
||||
},
|
||||
"issues": {
|
||||
"new": "Issuance request",
|
||||
"empty": "No issuance requests yet.",
|
||||
"recipient": "Recipient",
|
||||
"gift": "Item",
|
||||
"occasion": "Occasion",
|
||||
"quantity": "Quantity"
|
||||
},
|
||||
"reports": {
|
||||
"byRoom": "Bookings by room",
|
||||
"byGift": "Issuance by item",
|
||||
"room": "Room",
|
||||
"gift": "Item",
|
||||
"total": "Total",
|
||||
"requests": "Requests",
|
||||
"issued": "Issued",
|
||||
"issuedQty": "Issued qty",
|
||||
"empty": "No data."
|
||||
},
|
||||
"audit": {
|
||||
"empty": "No activity yet."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user