Task #670: revamp public room-booking form (protocol)

Public booking form now captures department, meeting type (internal/
external), meeting purpose, and a repeatable "key attendees" table
(name + position + side). External meetings require the entity name
(reuses requesterOrg). "عدد الحضور" relabeled to "العدد المتوقع للحضور".
Added a static red disclaimer note about possible cancellation.

Each booking now gets an auto-generated searchable reference
(RB-{year}-{pad4}) shown on the success screen; staff can search the
admin bookings list by reference OR phone.

Changes:
- lib/db schema: department, meetingType, purpose, keyAttendees (jsonb),
  bookingReference (unique) + requesterPhone index; new enums/types.
  Applied via drizzle push; backfilled existing 4 rows.
- api-server: extended public booking schema + refine (org required when
  external), makeBookingReference, public/internal POST generate+return
  bookingReference.
- tx-os public form rewritten; admin card shows new fields + client-side
  search with empty state.
- ar/en i18n keys under protocol.bookings.

Notes:
- Pre-existing push.ts typecheck errors are unrelated to this task.
- Architect review: PASS, no blocking issues.
This commit is contained in:
Riyadh
2026-07-07 12:26:19 +00:00
parent e167b3ef45
commit 3ff1338c7e
6 changed files with 524 additions and 73 deletions
+43 -2
View File
@@ -8,6 +8,7 @@ import {
jsonb,
boolean,
index,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod/v4";
@@ -35,6 +36,25 @@ export const PROTOCOL_BOOKING_STATUSES = [
export type ProtocolBookingStatus =
(typeof PROTOCOL_BOOKING_STATUSES)[number];
// A room booking is either an internal meeting or a meeting with an external
// entity. When external, the requester's organization (requesterOrg / اسم
// الجهة) is required.
export const PROTOCOL_MEETING_TYPES = ["internal", "external"] as const;
export type ProtocolMeetingType = (typeof PROTOCOL_MEETING_TYPES)[number];
// Which "side" a key attendee belongs to: from the authority (من الهيئة) or
// from outside it (خارج الهيئة).
export const PROTOCOL_ATTENDEE_SIDES = ["internal", "external"] as const;
export type ProtocolAttendeeSide = (typeof PROTOCOL_ATTENDEE_SIDES)[number];
// A notable attendee ("أبرز الحضور") captured on the booking request. Stored
// as a JSON array on the booking row.
export type ProtocolKeyAttendee = {
name: string;
position: string;
side: ProtocolAttendeeSide;
};
export const PROTOCOL_ISSUE_STATUSES = [
"pending",
"approved",
@@ -94,9 +114,26 @@ export const protocolRoomBookingsTable = pgTable(
// 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.
// Expected number of attendees (العدد المتوقع للحضور). Optional; supplied
// by the guest on a public request or by staff on an internal booking.
attendeeCount: integer("attendee_count"),
// The requester's department / administration (الإدارة). Optional.
department: varchar("department", { length: 300 }),
// Whether the meeting is internal or with an external entity. When
// "external", requesterOrg (اسم الجهة) is required. Defaults to internal.
meetingType: varchar("meeting_type", { length: 20 })
.notNull()
.default("internal"),
// Free-text purpose of the meeting (الغرض من الاجتماع). Optional.
purpose: text("purpose"),
// Notable attendees (أبرز الحضور): array of { name, position, side }.
keyAttendees: jsonb("key_attendees")
.$type<ProtocolKeyAttendee[]>()
.notNull()
.default([]),
// Human-friendly, unique booking reference (رقم الحجز) shown to the
// requester on success and searchable by staff. Generated on insert.
bookingReference: varchar("booking_reference", { length: 40 }),
// 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
@@ -128,6 +165,10 @@ export const protocolRoomBookingsTable = pgTable(
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),
referenceIdx: uniqueIndex("protocol_bookings_reference_idx").on(
t.bookingReference,
),
phoneIdx: index("protocol_bookings_phone_idx").on(t.requesterPhone),
}),
);