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:
@@ -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),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user