import { pgTable, serial, integer, varchar, text, timestamp, jsonb, boolean, index, } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod/v4"; import { usersTable } from "./users"; // --------------------------------------------------------------------------- // Public Relations & Protocol module ("العلاقات العامة والمراسم"). // // A completely standalone module. Every table is prefixed `protocol_` and has // no foreign keys into the Executive Meetings tables, so the two modules stay // fully independent. Only `usersTable` (the shared account table) is // referenced, and always with `on delete set null` so removing a user never // cascades into protocol history. // --------------------------------------------------------------------------- // Status values for room bookings and gift issuances. The overlap / // conflict-prevention rule only considers "pending" and "approved" rows as // occupying a room; "rejected" and "cancelled" free the slot again. export const PROTOCOL_BOOKING_STATUSES = [ "pending", "approved", "rejected", "cancelled", ] as const; export type ProtocolBookingStatus = (typeof PROTOCOL_BOOKING_STATUSES)[number]; export const PROTOCOL_ISSUE_STATUSES = [ "pending", "approved", "rejected", "issued", ] as const; export type ProtocolIssueStatus = (typeof PROTOCOL_ISSUE_STATUSES)[number]; // Gift catalogue kinds: a regular gift or a commemorative shield (درع). export const PROTOCOL_GIFT_KINDS = ["gift", "shield"] as const; export type ProtocolGiftKind = (typeof PROTOCOL_GIFT_KINDS)[number]; // --------------------------------------------------------------------------- // Rooms // --------------------------------------------------------------------------- export const protocolRoomsTable = pgTable( "protocol_rooms", { id: serial("id").primaryKey(), nameAr: varchar("name_ar", { length: 200 }).notNull(), nameEn: varchar("name_en", { length: 200 }).notNull().default(""), capacity: integer("capacity"), location: varchar("location", { length: 300 }), isActive: boolean("is_active").notNull().default(true), sortOrder: integer("sort_order").notNull().default(0), createdBy: integer("created_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) => ({ activeIdx: index("protocol_rooms_active_idx").on(t.isActive), }), ); // --------------------------------------------------------------------------- // Room bookings // --------------------------------------------------------------------------- export const protocolRoomBookingsTable = pgTable( "protocol_room_bookings", { id: serial("id").primaryKey(), roomId: integer("room_id") .notNull() .references(() => protocolRoomsTable.id, { onDelete: "cascade" }), title: varchar("title", { length: 500 }).notNull(), requesterName: varchar("requester_name", { length: 300 }), // Guest-supplied contact phone. Only populated for public // (self-service) booking requests; internal bookings leave it null. requesterPhone: varchar("requester_phone", { length: 40 }), // The requester's organization / entity (الجهة). Optional even for // public requests; always null for internal bookings unless set. requesterOrg: varchar("requester_org", { length: 300 }), // Provenance marker. "internal" for bookings created by authenticated // protocol staff through the app; "public" for guest requests coming in // via the no-login booking-request link. Defaults to "internal" so all // pre-existing rows are treated as internal. source: varchar("source", { length: 20 }).notNull().default("internal"), // Full timestamps so bookings can span any part of a day. The overlap // rule compares [startsAt, endsAt) intervals per room. startsAt: timestamp("starts_at", { withTimezone: true }).notNull(), endsAt: timestamp("ends_at", { withTimezone: true }).notNull(), status: varchar("status", { length: 20 }).notNull().default("pending"), notes: text("notes"), createdBy: integer("created_by").references(() => usersTable.id, { onDelete: "set null", }), approvedBy: integer("approved_by").references(() => usersTable.id, { onDelete: "set null", }), approvedAt: timestamp("approved_at", { withTimezone: true }), rejectionReason: text("rejection_reason"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow() .$onUpdate(() => new Date()), }, (t) => ({ roomIdx: index("protocol_bookings_room_idx").on(t.roomId), startIdx: index("protocol_bookings_start_idx").on(t.startsAt), statusIdx: index("protocol_bookings_status_idx").on(t.status), }), ); // --------------------------------------------------------------------------- // External protocol meetings (اجتماعات ومراسم خارجية) — independent from the // Executive Meetings module's is_external flag. // --------------------------------------------------------------------------- export const protocolExternalMeetingsTable = pgTable( "protocol_external_meetings", { id: serial("id").primaryKey(), title: varchar("title", { length: 500 }).notNull(), // The visiting party / external delegation. partyName: varchar("party_name", { length: 300 }), location: varchar("location", { length: 300 }), startsAt: timestamp("starts_at", { withTimezone: true }).notNull(), endsAt: timestamp("ends_at", { withTimezone: true }), // Lifecycle: pending -> approved | rejected, then optionally // completed | cancelled. Mirrors the booking / gift-issue approval flow. status: varchar("status", { length: 20 }).notNull().default("pending"), notes: text("notes"), createdBy: integer("created_by").references(() => usersTable.id, { onDelete: "set null", }), approvedBy: integer("approved_by").references(() => usersTable.id, { onDelete: "set null", }), approvedAt: timestamp("approved_at", { withTimezone: true }), rejectionReason: text("rejection_reason"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow() .$onUpdate(() => new Date()), }, (t) => ({ startIdx: index("protocol_ext_meetings_start_idx").on(t.startsAt), statusIdx: index("protocol_ext_meetings_status_idx").on(t.status), }), ); // --------------------------------------------------------------------------- // Gifts & shields catalogue // --------------------------------------------------------------------------- export const protocolGiftsTable = pgTable( "protocol_gifts", { id: serial("id").primaryKey(), nameAr: varchar("name_ar", { length: 300 }).notNull(), nameEn: varchar("name_en", { length: 300 }).notNull().default(""), kind: varchar("kind", { length: 20 }).notNull().default("gift"), descriptionAr: text("description_ar"), descriptionEn: text("description_en"), imageUrl: varchar("image_url", { length: 500 }), // Running stock count. Issuing an approved gift decrements this. quantityInStock: integer("quantity_in_stock").notNull().default(0), isActive: boolean("is_active").notNull().default(true), createdBy: integer("created_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) => ({ kindIdx: index("protocol_gifts_kind_idx").on(t.kind), }), ); // --------------------------------------------------------------------------- // Gift issuances (صرف الهدايا/الدروع) // --------------------------------------------------------------------------- export const protocolGiftIssuesTable = pgTable( "protocol_gift_issues", { id: serial("id").primaryKey(), giftId: integer("gift_id") .notNull() .references(() => protocolGiftsTable.id, { onDelete: "restrict" }), recipientName: varchar("recipient_name", { length: 300 }).notNull(), occasion: varchar("occasion", { length: 300 }), quantity: integer("quantity").notNull().default(1), issuedAt: timestamp("issued_at", { withTimezone: true }), status: varchar("status", { length: 20 }).notNull().default("pending"), notes: text("notes"), createdBy: integer("created_by").references(() => usersTable.id, { onDelete: "set null", }), approvedBy: integer("approved_by").references(() => usersTable.id, { onDelete: "set null", }), approvedAt: timestamp("approved_at", { withTimezone: true }), rejectionReason: text("rejection_reason"), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() .defaultNow() .$onUpdate(() => new Date()), }, (t) => ({ giftIdx: index("protocol_gift_issues_gift_idx").on(t.giftId), statusIdx: index("protocol_gift_issues_status_idx").on(t.status), }), ); // --------------------------------------------------------------------------- // Audit log (scoped to the protocol module only) // --------------------------------------------------------------------------- export const protocolAuditLogsTable = pgTable( "protocol_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(), }, (t) => ({ entityIdx: index("protocol_audit_entity_idx").on(t.entityType, t.entityId), performedAtIdx: index("protocol_audit_performed_at_idx").on(t.performedAt), }), ); // --------------------------------------------------------------------------- // Insert schemas / row types // --------------------------------------------------------------------------- export const insertProtocolRoomSchema = createInsertSchema( protocolRoomsTable, ).omit({ id: true, createdAt: true, updatedAt: true }); export type InsertProtocolRoom = z.infer; export type ProtocolRoom = typeof protocolRoomsTable.$inferSelect; export const insertProtocolRoomBookingSchema = createInsertSchema( protocolRoomBookingsTable, ).omit({ id: true, createdAt: true, updatedAt: true }); export type InsertProtocolRoomBooking = z.infer< typeof insertProtocolRoomBookingSchema >; export type ProtocolRoomBooking = typeof protocolRoomBookingsTable.$inferSelect; export const insertProtocolExternalMeetingSchema = createInsertSchema( protocolExternalMeetingsTable, ).omit({ id: true, createdAt: true, updatedAt: true }); export type InsertProtocolExternalMeeting = z.infer< typeof insertProtocolExternalMeetingSchema >; export type ProtocolExternalMeeting = typeof protocolExternalMeetingsTable.$inferSelect; export const insertProtocolGiftSchema = createInsertSchema( protocolGiftsTable, ).omit({ id: true, createdAt: true, updatedAt: true }); export type InsertProtocolGift = z.infer; export type ProtocolGift = typeof protocolGiftsTable.$inferSelect; export const insertProtocolGiftIssueSchema = createInsertSchema( protocolGiftIssuesTable, ).omit({ id: true, createdAt: true, updatedAt: true }); export type InsertProtocolGiftIssue = z.infer< typeof insertProtocolGiftIssueSchema >; export type ProtocolGiftIssue = typeof protocolGiftIssuesTable.$inferSelect; export type ProtocolAuditLog = typeof protocolAuditLogsTable.$inferSelect;