2a194de12c
Updates API schemas and database to include attendee count for bookings, modifies the public request form to accept this new optional field, and displays the attendee count on the booking details. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9c655ed7-866c-454d-b646-e458df854c65 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/OsTuUKE Replit-Helium-Checkpoint-Created: true
310 lines
12 KiB
TypeScript
310 lines
12 KiB
TypeScript
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 }),
|
|
// Expected number of attendees (عدد الحضور). Optional; supplied by the
|
|
// guest on a public request or by staff on an internal booking.
|
|
attendeeCount: integer("attendee_count"),
|
|
// 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<typeof insertProtocolRoomSchema>;
|
|
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<typeof insertProtocolGiftSchema>;
|
|
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;
|