From 66bc96f97ff85db550991927e6975901724651d2 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Mon, 27 Apr 2026 12:05:40 +0000 Subject: [PATCH] Add executive meeting management system with role-based access Implement a new API endpoint and UI for managing executive meetings, including role-based access control and database schema changes for meeting data and attendees. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 079afca5-8c32-40ea-b7d1-8bf26358ff75 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/BRoHDEI Replit-Helium-Checkpoint-Created: true --- .../src/routes/executive-meetings.ts | 114 +++++ artifacts/api-server/src/routes/index.ts | 2 + artifacts/tx-os/public/opengraph.jpg | Bin 24623 -> 25865 bytes artifacts/tx-os/src/App.tsx | 2 + artifacts/tx-os/src/locales/ar.json | 31 ++ artifacts/tx-os/src/locales/en.json | 31 ++ .../tx-os/src/pages/executive-meetings.tsx | 418 ++++++++++++++++++ lib/db/src/schema/executive-meetings.ts | 191 ++++++++ lib/db/src/schema/index.ts | 1 + replit.md | 3 +- scripts/src/seed.ts | 265 +++++++++++ 11 files changed, 1057 insertions(+), 1 deletion(-) create mode 100644 artifacts/api-server/src/routes/executive-meetings.ts create mode 100644 artifacts/tx-os/src/pages/executive-meetings.tsx create mode 100644 lib/db/src/schema/executive-meetings.ts diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts new file mode 100644 index 00000000..16a4a0d6 --- /dev/null +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -0,0 +1,114 @@ +import { Router, type IRouter } from "express"; +import { eq, asc, inArray } from "drizzle-orm"; +import { db } from "@workspace/db"; +import { + executiveMeetingsTable, + executiveMeetingAttendeesTable, + rolesTable, +} from "@workspace/db"; +import { requireAuth, getEffectiveRoleIds } from "../middlewares/auth"; +import type { Request, Response, NextFunction } from "express"; + +const router: IRouter = Router(); + +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +const EXECUTIVE_ROLE_NAMES = [ + "admin", + "executive_ceo", + "executive_office_manager", + "executive_coord_lead", + "executive_coordinator", + "executive_viewer", +] as const; + +async function requireExecutiveAccess( + req: Request, + res: Response, + next: NextFunction, +): Promise { + if (!req.session.userId) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + const effectiveIds = await getEffectiveRoleIds(req.session.userId); + if (effectiveIds.length === 0) { + res.status(403).json({ error: "Forbidden" }); + return; + } + const allowed = await db + .select({ id: rolesTable.id }) + .from(rolesTable) + .where(inArray(rolesTable.name, EXECUTIVE_ROLE_NAMES as unknown as string[])); + const allowedIds = new Set(allowed.map((r) => r.id)); + const ok = effectiveIds.some((id) => allowedIds.has(id)); + if (!ok) { + res.status(403).json({ error: "Forbidden" }); + return; + } + next(); +} + +router.get( + "/executive-meetings", + requireAuth, + requireExecutiveAccess, + async (req, res): Promise => { + const dateRaw = typeof req.query.date === "string" ? req.query.date.trim() : ""; + if (dateRaw && !DATE_RE.test(dateRaw)) { + res.status(400).json({ error: "Invalid 'date' (expected YYYY-MM-DD)" }); + return; + } + const targetDate = dateRaw || new Date().toISOString().slice(0, 10); + + const meetings = await db + .select() + .from(executiveMeetingsTable) + .where(eq(executiveMeetingsTable.meetingDate, targetDate)) + .orderBy(asc(executiveMeetingsTable.dailyNumber)); + + if (meetings.length === 0) { + res.json({ date: targetDate, meetings: [] }); + return; + } + + const meetingIds = meetings.map((m) => m.id); + const attendees = await db + .select() + .from(executiveMeetingAttendeesTable) + .where(inArray(executiveMeetingAttendeesTable.meetingId, meetingIds)) + .orderBy( + asc(executiveMeetingAttendeesTable.meetingId), + asc(executiveMeetingAttendeesTable.sortOrder), + ); + + const byMeeting = new Map(); + for (const a of attendees) { + const arr = byMeeting.get(a.meetingId) ?? []; + arr.push(a); + byMeeting.set(a.meetingId, arr); + } + + const result = meetings.map((m) => ({ + ...m, + attendees: byMeeting.get(m.id) ?? [], + })); + + res.json({ date: targetDate, meetings: result }); + }, +); + +router.get( + "/executive-meetings/dates", + requireAuth, + requireExecutiveAccess, + async (_req, res): Promise => { + const rows = await db + .selectDistinct({ d: executiveMeetingsTable.meetingDate }) + .from(executiveMeetingsTable) + .orderBy(asc(executiveMeetingsTable.meetingDate)); + res.json({ dates: rows.map((r) => r.d) }); + }, +); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 7e9fe5c2..43a82980 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -14,6 +14,7 @@ import notesRouter from "./notes"; import groupsRouter from "./groups"; import rolesRouter from "./roles"; import auditRouter from "./audit"; +import executiveMeetingsRouter from "./executive-meetings"; const router: IRouter = Router(); @@ -32,5 +33,6 @@ router.use(notesRouter); router.use(groupsRouter); router.use(rolesRouter); router.use(auditRouter); +router.use(executiveMeetingsRouter); export default router; diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index d1100405f26b05144245080dc91af986cfeb0978..ec995ed15f1f6811adbae109a2e7e7b025b8b450 100644 GIT binary patch literal 25865 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

oHRlEt3RT^^Zn?F^ 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 F{$F{PCX@gG literal 24623 zcmeFZ2Ut^C+bFt05j&#PNc%t$A%X~@*U>~!s-tXuXj3x*=0EnY zp!eOuxZkHA|IG&GUG1jN4nZB4{>_;Gr{*JfZQLX`d(^zmU}jM6=4Px`&+8~uro+qf7R zfa77X^FZ#6gr2rBypg1GwsZabF(K`f6UsFe11 z+uty`f5+|4-gE~+$!Kc}L5rCXbix#Z&a^_%(c61{p!he>CIGs4z;n5RpB>~3*+2r2 zCgcKPAaPK;0$qkAA(`C)=qhyRAk!hHgNF_=9b#rabol70qeqV%Im&jD<@hO1HZCqs zHVzIL?|FXMSs@+{4gt7;knn|zq8GXOFI~PQa{0W-MG^WW49v{TM-Lx8bM)vL5f}$d zxH~JP3T>P?=qJvYB9am!Ai{W&eVb3@Oa!ov0e{u2k5Vr2gj=6Jn~&ohmnq0vcr z|3T>1X@t-%L5)tl&@Fh_^c95Q`$9x$ORToTU32cXjAi?G-O>&drO0}Ck4;y+47Fk-r#K8kht6&9wUX35laf|%e=`&i6L`HVZ6?V^%i(0 z@WQPdob++cE=A~^-;HyO&1?(1ZZsB|-ivY4ClaJg4UuF72Emakf{UgTD+KAOtL+_s ztS^Ha5()}UH}pDX_zQDX$9tk&I@OvP*XaI1mwV?Fx)yw{(b%Xw_^m=C?|T2W?k zxNmofmhp9Dga&_2rG4Gbv&*m;wQG;7+?kHetb6W<2#Q;&jvJIJADfK0|4s2H&1Bih za7&U{Iql-Q&x2z3*I7r;|3mE`(?^+>U*%t%W(c%@^IA>K9;vQ|v@&~#K7rK8!*lqt z{w!V8-blMC+WyY*ym9JApHuhyd=y4M8r^A&IAw%H5|GahKf1|keT!K2ebS@S9OdDV54&uq2@v8fO*2zlIEvGJIYz-Dfz`8Fdgz%o zy1+{clMzpeNq490Ew-}*u0A~Ge3((V@s&vohm={Ev9n7N29Y7g-cB*imo)XrGZrn3 z&S|zSZUp#Xd^tMJy0}sF{OKaI8%-LFhmVRL-Fwy$#t=F;1^iebg?>1&{8J={;^Fhl zIU=jyhelPSz^V(quB^QnfS(orFtb$|;8&1}U6as{P7E&k5N_#z{!N%`PR*z5&EitW z3-Xo&=Pz~@iOXL|*y<_Xid&2=4TzlGp-yeEoZnQ8DwrJSKN-k;LA=1=(k}Gjjr`Z{ zmHZ7$6Z`dJomMM13j;D{hRKicDRuDm%AEv78(QpJ8vdl#xfqv^Uz)spDX}!G2}UlZ zlWnynk2iChX-fAS1`Ehto;=AC=bO5w+&nUd?4sQn`hLEV1)XOKf&6}BL0|1;Jf

    }%gr&Dth4VK$149_m4&u*P&Xq?)B(PzT{SniF; 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 + diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index d417c712..f11753d3 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -567,5 +567,36 @@ "deleteConfirm": "حذف هذه الملاحظة؟", "deleteLabelConfirm": "حذف هذا التصنيف؟", "editTitle": "تعديل ملاحظة" + }, + "executiveMeetings": { + "titleAr": "إدارة الاجتماعات التنفيذية", + "titleEn": "Executive Meetings Management", + "scheduleHeading": "قائمة بأسماء حضور الاجتماعات", + "date": "التاريخ:", + "exportPdf": "تصدير PDF", + "fontSettings": "إعدادات الخط", + "noMeetings": "لا توجد اجتماعات في هذا اليوم", + "virtual": "اتصال مرئي", + "virtualAttendance": "الحضور عبر الاتصال المرئي", + "internalAttendance": "الحضور الداخلي", + "externalAttendance": "حضور خارجي", + "placeholderBody": "هذا القسم قيد التطوير في المراحل القادمة. ستتوفر إدارته الكاملة قريباً.", + "col": { + "number": "م", + "meeting": "الاجتماع", + "attendees": "الحضور", + "time": "الوقت" + }, + "nav": { + "schedule": "جدول الاجتماعات اليومي", + "manage": "إدارة الاجتماعات", + "requests": "طلبات التعديل", + "approvals": "موافقات مدير المكتب", + "tasks": "مهام فريق التنسيق", + "notifications": "التنبيهات", + "audit": "سجل التعديلات", + "pdf": "تصدير PDF", + "fontSettings": "إعدادات الخط" + } } } diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index f6ddbc42..1d82b28f 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -564,5 +564,36 @@ "deleteConfirm": "Delete this note?", "deleteLabelConfirm": "Delete this label?", "editTitle": "Edit note" + }, + "executiveMeetings": { + "titleAr": "Executive Meetings Management", + "titleEn": "إدارة الاجتماعات التنفيذية", + "scheduleHeading": "Meeting Attendance List", + "date": "Date:", + "exportPdf": "Export PDF", + "fontSettings": "Font Settings", + "noMeetings": "No meetings scheduled for this day", + "virtual": "Virtual", + "virtualAttendance": "Virtual Attendance", + "internalAttendance": "Internal Attendance", + "externalAttendance": "External Attendance", + "placeholderBody": "This section is under development and will be available in upcoming phases.", + "col": { + "number": "#", + "meeting": "Meeting", + "attendees": "Attendees", + "time": "Time" + }, + "nav": { + "schedule": "Daily Schedule", + "manage": "Manage Meetings", + "requests": "Change Requests", + "approvals": "Office Manager Approvals", + "tasks": "Coordination Tasks", + "notifications": "Notifications", + "audit": "Audit Log", + "pdf": "PDF Export", + "fontSettings": "Font Settings" + } } } diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx new file mode 100644 index 00000000..1d4c0e73 --- /dev/null +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -0,0 +1,418 @@ +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useLocation } from "wouter"; +import { useQuery } from "@tanstack/react-query"; +import { + ArrowLeft, + ArrowRight, + CalendarClock, + FileDown, + Type, + Languages, + ListChecks, + ClipboardList, + CheckSquare, + ListTodo, + Bell, + ScrollText, + FileText, + Settings as SettingsIcon, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; + +type Attendee = { + id: number; + meetingId: number; + name: string; + title: string | null; + attendanceType: "internal" | "virtual" | "external"; + sortOrder: number; +}; + +type Meeting = { + id: number; + dailyNumber: number; + titleAr: string; + titleEn: string; + meetingDate: string; + startTime: string | null; + endTime: string | null; + location: string | null; + meetingUrl: string | null; + platform: "none" | "webex" | "teams" | "zoom" | "other"; + status: string; + isHighlighted: number; + notes: string | null; + attendees: Attendee[]; +}; + +type DayResponse = { + date: string; + meetings: Meeting[]; +}; + +const SECTIONS = [ + { key: "schedule", icon: CalendarClock }, + { key: "manage", icon: ListChecks }, + { key: "requests", icon: ClipboardList }, + { key: "approvals", icon: CheckSquare }, + { key: "tasks", icon: ListTodo }, + { key: "notifications", icon: Bell }, + { key: "audit", icon: ScrollText }, + { key: "pdf", icon: FileText }, + { key: "fontSettings", icon: SettingsIcon }, +] as const; + +type SectionKey = (typeof SECTIONS)[number]["key"]; + +function formatTime(t: string | null): string { + if (!t) return ""; + return t.slice(0, 5); +} + +function todayIso(): string { + return new Date().toISOString().slice(0, 10); +} + +export default function ExecutiveMeetingsPage() { + const { t, i18n } = useTranslation(); + const [, setLocation] = useLocation(); + const isRtl = i18n.language === "ar"; + const BackIcon = isRtl ? ArrowRight : ArrowLeft; + + const [section, setSection] = useState("schedule"); + const [date, setDate] = useState(todayIso()); + + const { data, isLoading } = useQuery({ + queryKey: ["/api/executive-meetings", date], + queryFn: async () => { + const res = await fetch(`/api/executive-meetings?date=${date}`, { + credentials: "include", + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return res.json(); + }, + }); + + const meetings = data?.meetings ?? []; + + return ( +
    + {/* Top header */} +
    +
    + + +
    +
    + +
    +
    +
    + {t("executiveMeetings.titleAr")} +
    +
    + {t("executiveMeetings.titleEn")} +
    +
    +
    + +
    + +
    + + + +
    +
    + + {/* Section tabs */} + +
    + +
    + {section === "schedule" && ( + + )} + {section !== "schedule" && ( + + )} +
    +
    + ); +} + +function ScheduleSection({ + meetings, + date, + onDateChange, + isLoading, + isRtl, + t, +}: { + meetings: Meeting[]; + date: string; + onDateChange: (d: string) => void; + isLoading: boolean; + isRtl: boolean; + t: (k: string) => string; +}) { + return ( +
    +
    +

    + {t("executiveMeetings.scheduleHeading")} +

    + +
    + +
    + + + + + + + + + + + {isLoading && ( + + + + )} + {!isLoading && meetings.length === 0 && ( + + + + )} + {meetings.map((m) => ( + + ))} + +
    + {t("executiveMeetings.col.number")} + + {t("executiveMeetings.col.meeting")} + + {t("executiveMeetings.col.attendees")} + + {t("executiveMeetings.col.time")} +
    + {t("common.loading")} +
    + {t("executiveMeetings.noMeetings")} +
    +
    +
    + ); +} + +function MeetingRow({ + meeting, + isRtl, + t, +}: { + meeting: Meeting; + isRtl: boolean; + t: (k: string) => string; +}) { + const title = isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr; + const isCancelled = meeting.status === "cancelled"; + const highlight = meeting.isHighlighted === 1; + + const numberCellCls = + "border border-gray-300 px-2 py-3 text-center align-middle font-semibold " + + (highlight || isCancelled + ? "bg-red-600 text-white" + : "bg-white text-[#0B1E3F]"); + + const virtual = meeting.attendees.filter((a) => a.attendanceType === "virtual"); + const internal = meeting.attendees.filter((a) => a.attendanceType === "internal"); + const external = meeting.attendees.filter((a) => a.attendanceType === "external"); + const hasSplit = virtual.length > 0 && (internal.length > 0 || external.length > 0); + + const platformLabel: Record = { + none: "", + webex: "Webex", + teams: "Teams", + zoom: "Zoom", + other: t("executiveMeetings.virtual"), + }; + + return ( + + {meeting.dailyNumber} + + +
    {title}
    + {meeting.location && ( +
    {meeting.location}
    + )} + + + + {hasSplit ? ( +
    + {virtual.length > 0 && ( + + )} + {internal.length > 0 && ( + + )} + {external.length > 0 && ( + + )} +
    + ) : meeting.platform !== "none" && virtual.length > 0 ? ( + + ) : ( + + )} + + + +
    +
    {formatTime(meeting.startTime)}
    +
    +
    {formatTime(meeting.endTime)}
    +
    + + + ); +} + +function AttendeeGroup({ label, items }: { label: string; items: Attendee[] }) { + return ( +
    +
    — {label} —
    + +
    + ); +} + +function AttendeeFlow({ items }: { items: Attendee[] }) { + if (items.length === 0) { + return ; + } + return ( +
      + {items.map((a, idx) => ( +
    • + {idx + 1}- + {a.name} +
    • + ))} +
    + ); +} + +function PlaceholderSection({ + sectionKey, + t, +}: { + sectionKey: SectionKey; + t: (k: string) => string; +}) { + return ( +
    +
    + +
    +

    + {t(`executiveMeetings.nav.${sectionKey}`)} +

    +

    + {t("executiveMeetings.placeholderBody")} +

    +
    + ); +} diff --git a/lib/db/src/schema/executive-meetings.ts b/lib/db/src/schema/executive-meetings.ts new file mode 100644 index 00000000..2b680672 --- /dev/null +++ b/lib/db/src/schema/executive-meetings.ts @@ -0,0 +1,191 @@ +import { + pgTable, + serial, + integer, + varchar, + text, + timestamp, + date, + time, + jsonb, + index, + uniqueIndex, +} from "drizzle-orm/pg-core"; +import { usersTable } from "./users"; + +export const executiveMeetingsTable = pgTable( + "executive_meetings", + { + id: serial("id").primaryKey(), + dailyNumber: integer("daily_number").notNull(), + titleAr: varchar("title_ar", { length: 300 }).notNull(), + titleEn: varchar("title_en", { length: 300 }).notNull().default(""), + meetingDate: date("meeting_date").notNull(), + startTime: time("start_time", { withTimezone: false }), + endTime: time("end_time", { withTimezone: false }), + location: varchar("location", { length: 300 }), + meetingUrl: text("meeting_url"), + platform: varchar("platform", { length: 32 }).notNull().default("none"), + status: varchar("status", { length: 32 }).notNull().default("scheduled"), + isHighlighted: integer("is_highlighted").notNull().default(0), + notes: text("notes"), + attachments: jsonb("attachments").default([]), + createdBy: integer("created_by").references(() => usersTable.id, { + onDelete: "set null", + }), + updatedBy: integer("updated_by").references(() => usersTable.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, + (t) => ({ + dateIdx: index("executive_meetings_date_idx").on(t.meetingDate), + dateNumberUnique: uniqueIndex("executive_meetings_date_number_unique").on( + t.meetingDate, + t.dailyNumber, + ), + }), +); + +export const executiveMeetingAttendeesTable = pgTable( + "executive_meeting_attendees", + { + id: serial("id").primaryKey(), + meetingId: integer("meeting_id") + .notNull() + .references(() => executiveMeetingsTable.id, { onDelete: "cascade" }), + name: varchar("name", { length: 200 }).notNull(), + title: varchar("title", { length: 200 }), + attendanceType: varchar("attendance_type", { length: 32 }) + .notNull() + .default("internal"), + sortOrder: integer("sort_order").notNull().default(0), + }, + (t) => ({ + meetingIdx: index("executive_meeting_attendees_meeting_idx").on(t.meetingId), + }), +); + +export const executiveMeetingRequestsTable = pgTable( + "executive_meeting_requests", + { + id: serial("id").primaryKey(), + meetingId: integer("meeting_id") + .notNull() + .references(() => executiveMeetingsTable.id, { onDelete: "cascade" }), + requestedBy: integer("requested_by").references(() => usersTable.id, { + onDelete: "set null", + }), + requestType: varchar("request_type", { length: 64 }).notNull(), + requestDetails: jsonb("request_details"), + status: varchar("status", { length: 32 }).notNull().default("new"), + reviewedBy: integer("reviewed_by").references(() => usersTable.id, { + onDelete: "set null", + }), + reviewDecision: varchar("review_decision", { length: 32 }), + reviewNotes: text("review_notes"), + assignedTo: integer("assigned_to").references(() => usersTable.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, +); + +export const executiveMeetingTasksTable = pgTable("executive_meeting_tasks", { + id: serial("id").primaryKey(), + requestId: integer("request_id").references(() => executiveMeetingRequestsTable.id, { + onDelete: "cascade", + }), + meetingId: integer("meeting_id").references(() => executiveMeetingsTable.id, { + onDelete: "cascade", + }), + assignedTo: integer("assigned_to").references(() => usersTable.id, { + onDelete: "set null", + }), + taskType: varchar("task_type", { length: 64 }).notNull(), + status: varchar("status", { length: 32 }).notNull().default("pending"), + dueAt: timestamp("due_at", { withTimezone: true }), + completedAt: timestamp("completed_at", { withTimezone: true }), + notes: text("notes"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), +}); + +export const executiveMeetingNotificationsTable = pgTable( + "executive_meeting_notifications", + { + id: serial("id").primaryKey(), + meetingId: integer("meeting_id").references(() => executiveMeetingsTable.id, { + onDelete: "cascade", + }), + userId: integer("user_id").references(() => usersTable.id, { onDelete: "cascade" }), + notificationType: varchar("notification_type", { length: 64 }).notNull(), + scheduledAt: timestamp("scheduled_at", { withTimezone: true }), + sentAt: timestamp("sent_at", { withTimezone: true }), + status: varchar("status", { length: 32 }).notNull().default("pending"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, +); + +export const executiveMeetingAuditLogsTable = pgTable( + "executive_meeting_audit_logs", + { + id: serial("id").primaryKey(), + action: varchar("action", { length: 100 }).notNull(), + entityType: varchar("entity_type", { length: 50 }).notNull(), + entityId: integer("entity_id"), + oldValue: jsonb("old_value"), + newValue: jsonb("new_value"), + performedBy: integer("performed_by").references(() => usersTable.id, { + onDelete: "set null", + }), + performedAt: timestamp("performed_at", { withTimezone: true }).notNull().defaultNow(), + }, +); + +export const executiveMeetingPdfArchivesTable = pgTable( + "executive_meeting_pdf_archives", + { + id: serial("id").primaryKey(), + archiveDate: date("archive_date").notNull(), + filePath: text("file_path").notNull(), + version: integer("version").notNull().default(1), + generatedBy: integer("generated_by").references(() => usersTable.id, { + onDelete: "set null", + }), + generatedAt: timestamp("generated_at", { withTimezone: true }).notNull().defaultNow(), + }, +); + +export const executiveMeetingFontSettingsTable = pgTable( + "executive_meeting_font_settings", + { + id: serial("id").primaryKey(), + scope: varchar("scope", { length: 16 }).notNull().default("global"), + userId: integer("user_id").references(() => usersTable.id, { onDelete: "cascade" }), + fontFamily: varchar("font_family", { length: 100 }).notNull().default("system"), + fontSize: integer("font_size").notNull().default(14), + fontWeight: varchar("font_weight", { length: 16 }).notNull().default("regular"), + alignment: varchar("alignment", { length: 16 }).notNull().default("start"), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()), + }, +); + +export type ExecutiveMeeting = typeof executiveMeetingsTable.$inferSelect; +export type ExecutiveMeetingAttendee = typeof executiveMeetingAttendeesTable.$inferSelect; +export type ExecutiveMeetingRequest = typeof executiveMeetingRequestsTable.$inferSelect; +export type ExecutiveMeetingTask = typeof executiveMeetingTasksTable.$inferSelect; diff --git a/lib/db/src/schema/index.ts b/lib/db/src/schema/index.ts index 19f1ac11..b2595ba5 100644 --- a/lib/db/src/schema/index.ts +++ b/lib/db/src/schema/index.ts @@ -12,3 +12,4 @@ export * from "./password-reset-tokens"; export * from "./notes"; export * from "./groups"; export * from "./audit-logs"; +export * from "./executive-meetings"; diff --git a/replit.md b/replit.md index ff2ebdbf..c956daed 100644 --- a/replit.md +++ b/replit.md @@ -40,6 +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. ## Key Commands @@ -56,7 +57,7 @@ ## Database Tables -`users`, `roles`, `permissions`, `user_roles`, `role_permissions`, `apps`, `app_permissions`, `service_categories`, `services`, `conversations`, `conversation_participants`, `messages`, `message_reads`, `notifications`, `user_sessions` +`users`, `roles`, `permissions`, `user_roles`, `role_permissions`, `apps`, `app_permissions`, `service_categories`, `services`, `conversations`, `conversation_participants`, `messages`, `message_reads`, `notifications`, `user_sessions`, `audit_logs`, `executive_meetings`, `executive_meeting_attendees`, `executive_meeting_requests`, `executive_meeting_tasks`, `executive_meeting_notifications`, `executive_meeting_audit_logs`, `executive_meeting_pdf_archives`, `executive_meeting_font_settings` ## Important Notes diff --git a/scripts/src/seed.ts b/scripts/src/seed.ts index 1f85905d..e6b699ad 100644 --- a/scripts/src/seed.ts +++ b/scripts/src/seed.ts @@ -13,6 +13,8 @@ import { userGroupsTable, groupAppsTable, groupRolesTable, + executiveMeetingsTable, + executiveMeetingAttendeesTable, } from "@workspace/db"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; @@ -232,6 +234,19 @@ async function main() { isSystem: false, sortOrder: 6, }, + { + slug: "executive-meetings", + nameAr: "إدارة الاجتماعات التنفيذية", + nameEn: "Executive Meetings", + descriptionAr: "جدولة اجتماعات المكتب التنفيذي ومتابعتها", + descriptionEn: "Schedule and track executive office meetings", + iconName: "CalendarClock", + route: "/executive-meetings", + color: "#0B1E3F", + isActive: true, + isSystem: false, + sortOrder: 7, + }, ]; const insertedApps = await db.insert(appsTable).values(apps).onConflictDoNothing().returning(); @@ -436,6 +451,256 @@ async function main() { } console.log("Groups created and existing users mapped"); + // Executive meetings: roles + const execRoles = [ + { + name: "executive_ceo", + descriptionAr: "الرئيس التنفيذي", + descriptionEn: "Chief Executive Officer", + isSystem: 1, + }, + { + name: "executive_office_manager", + descriptionAr: "مدير المكتب التنفيذي", + descriptionEn: "Executive Office Manager", + isSystem: 1, + }, + { + name: "executive_coord_lead", + descriptionAr: "رئيس التنسيق والتوثيق", + descriptionEn: "Coordination Lead", + isSystem: 1, + }, + { + name: "executive_coordinator", + descriptionAr: "منسق المكتب التنفيذي", + descriptionEn: "Executive Coordinator", + isSystem: 1, + }, + { + name: "executive_viewer", + descriptionAr: "مشاهد الاجتماعات التنفيذية", + descriptionEn: "Executive Meetings Viewer", + isSystem: 1, + }, + ]; + await db.insert(rolesTable).values(execRoles).onConflictDoNothing(); + console.log("Executive Meetings roles created"); + + // Executive meetings: sample data for today (idempotent per-date) + const today = new Date().toISOString().slice(0, 10); + const existingForToday = await db + .select({ id: executiveMeetingsTable.id }) + .from(executiveMeetingsTable) + .where(eq(executiveMeetingsTable.meetingDate, today)); + if (existingForToday.length === 0) { + { + const sampleMeetings: Array<{ + meeting: { + dailyNumber: number; + titleAr: string; + titleEn: string; + meetingDate: string; + startTime: string; + endTime: string; + platform: "none" | "webex" | "teams" | "zoom" | "other"; + isHighlighted: number; + status: string; + }; + attendees: Array<{ + name: string; + attendanceType: "internal" | "virtual" | "external"; + }>; + }> = [ + { + meeting: { + dailyNumber: 1, + titleAr: "اجتماع افتتاحي للمكتب التنفيذي", + titleEn: "Executive Office Opening Meeting", + meetingDate: today, + startTime: "10:00", + endTime: "10:30", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "أ. سعد العتيبي", attendanceType: "internal" }, + { name: "أ. خالد الشهري", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 2, + titleAr: "اجتماع بشأن خطة التحول الرقمي", + titleEn: "Digital Transformation Plan Meeting", + meetingDate: today, + startTime: "11:00", + endTime: "11:30", + platform: "webex", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "د. محمد الزهراني", attendanceType: "virtual" }, + { name: "أ. عبدالعزيز القحطاني", attendanceType: "virtual" }, + { name: "أ. ناصر الدوسري", attendanceType: "virtual" }, + { name: "م. فهد المالكي", attendanceType: "virtual" }, + ], + }, + { + meeting: { + dailyNumber: 3, + titleAr: "اجتماع تحضيري لاجتماع مجلس الإدارة", + titleEn: "Board Meeting Preparation", + meetingDate: today, + startTime: "11:40", + endTime: "12:10", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "أ. تركي السبيعي", attendanceType: "internal" }, + { name: "أ. ماجد الحربي", attendanceType: "internal" }, + { name: "أ. عبدالله البقمي", attendanceType: "internal" }, + { name: "أ. سلطان الغامدي", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 4, + titleAr: "اجتماع بشأن عرض الخطة الخاصة بالقطاعين", + titleEn: "Two-Sector Plan Presentation", + meetingDate: today, + startTime: "13:00", + endTime: "13:30", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "أ. بدر العنزي", attendanceType: "internal" }, + { name: "أ. هاني المطيري", attendanceType: "internal" }, + { name: "أ. وليد الرشيدي", attendanceType: "internal" }, + { name: "أ. يوسف الجهني", attendanceType: "internal" }, + { name: "أ. عبدالرحمن الشمري", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 5, + titleAr: "اجتماع مع أ. عبدالله بشأن المتابعة", + titleEn: "Follow-up Meeting with Mr. Abdullah", + meetingDate: today, + startTime: "13:30", + endTime: "13:50", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [{ name: "أ. عبدالله الزهراني", attendanceType: "internal" }], + }, + { + meeting: { + dailyNumber: 6, + titleAr: "اجتماع دوري بشأن مواضيع مكتب الإدارة", + titleEn: "Periodic Office Topics Meeting", + meetingDate: today, + startTime: "14:00", + endTime: "14:30", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "أ. مازن الفيفي", attendanceType: "internal" }, + { name: "أ. سامي العسيري", attendanceType: "internal" }, + { name: "أ. أحمد البلوي", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 7, + titleAr: "اجتماع بشأن إطار اعتماد البرامج", + titleEn: "Program Accreditation Framework", + meetingDate: today, + startTime: "14:40", + endTime: "15:10", + platform: "none", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "د. عبدالمجيد الحارثي", attendanceType: "internal" }, + { name: "د. منصور القرني", attendanceType: "internal" }, + { name: "أ. فيصل الزهراني", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 8, + titleAr: "اجتماع بشأن استراتيجية الموارد البشرية", + titleEn: "HR Strategy Meeting", + meetingDate: today, + startTime: "15:30", + endTime: "16:30", + platform: "webex", + isHighlighted: 1, + status: "scheduled", + }, + attendees: [ + { name: "د. سعود الشهراني", attendanceType: "virtual" }, + { name: "أ. عبدالإله الحربي", attendanceType: "virtual" }, + { name: "أ. خالد العنزي", attendanceType: "virtual" }, + { name: "أ. أحمد المالكي", attendanceType: "internal" }, + { name: "أ. ناصر القحطاني", attendanceType: "internal" }, + { name: "أ. سعيد الغامدي", attendanceType: "internal" }, + { name: "أ. علي الزهراني", attendanceType: "internal" }, + { name: "أ. محمد البقمي", attendanceType: "internal" }, + ], + }, + { + meeting: { + dailyNumber: 9, + titleAr: "اجتماع بشأن متابعة المشاريع المشتركة", + titleEn: "Joint Projects Follow-up", + meetingDate: today, + startTime: "16:40", + endTime: "17:10", + platform: "webex", + isHighlighted: 0, + status: "scheduled", + }, + attendees: [ + { name: "أ. يزيد العتيبي", attendanceType: "virtual" }, + { name: "أ. تركي السهلي", attendanceType: "virtual" }, + { name: "أ. عبدالله الزيد", attendanceType: "internal" }, + ], + }, + ]; + + for (const { meeting, attendees } of sampleMeetings) { + const [inserted] = await db + .insert(executiveMeetingsTable) + .values(meeting) + .returning(); + if (!inserted) continue; + await db.insert(executiveMeetingAttendeesTable).values( + attendees.map((a, idx) => ({ + meetingId: inserted.id, + name: a.name, + attendanceType: a.attendanceType, + sortOrder: idx, + })), + ); + } + console.log("Executive Meetings sample data created for today"); + } + } else { + console.log("Executive Meetings sample data already exists for today, skipping"); + } + console.log("\n✅ Seeding complete!"); console.log("\nDemo accounts:"); console.log(" Admin: username=admin, password=admin123");