Add attendee count field to booking requests and display it

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
This commit is contained in:
Replit Agent
2026-07-06 11:33:58 +00:00
parent 62c38a508f
commit 2a194de12c
6 changed files with 38 additions and 0 deletions
@@ -161,6 +161,7 @@ const bookingCreateSchema = z
roomId: z.number().int().positive(), roomId: z.number().int().positive(),
title: z.string().trim().min(1).max(500), title: z.string().trim().min(1).max(500),
requesterName: z.string().trim().max(300).nullable().optional(), requesterName: z.string().trim().max(300).nullable().optional(),
attendeeCount: z.number().int().positive().max(100000).nullable().optional(),
startsAt: isoDateTime, startsAt: isoDateTime,
endsAt: isoDateTime, endsAt: isoDateTime,
notes: z.string().trim().max(5000).nullable().optional(), notes: z.string().trim().max(5000).nullable().optional(),
@@ -175,6 +176,7 @@ const bookingPatchSchema = z
roomId: z.number().int().positive().optional(), roomId: z.number().int().positive().optional(),
title: z.string().trim().min(1).max(500).optional(), title: z.string().trim().min(1).max(500).optional(),
requesterName: z.string().trim().max(300).nullable().optional(), requesterName: z.string().trim().max(300).nullable().optional(),
attendeeCount: z.number().int().positive().max(100000).nullable().optional(),
startsAt: isoDateTime.optional(), startsAt: isoDateTime.optional(),
endsAt: isoDateTime.optional(), endsAt: isoDateTime.optional(),
notes: z.string().trim().max(5000).nullable().optional(), notes: z.string().trim().max(5000).nullable().optional(),
@@ -191,6 +193,7 @@ const publicBookingRequestSchema = z
requesterName: z.string().trim().min(1).max(300), requesterName: z.string().trim().min(1).max(300),
requesterPhone: z.string().trim().min(3).max(40), requesterPhone: z.string().trim().min(3).max(40),
requesterOrg: z.string().trim().max(300).nullable().optional(), requesterOrg: z.string().trim().max(300).nullable().optional(),
attendeeCount: z.number().int().positive().max(100000).nullable().optional(),
startsAt: isoDateTime, startsAt: isoDateTime,
endsAt: isoDateTime, endsAt: isoDateTime,
notes: z.string().trim().max(5000).nullable().optional(), notes: z.string().trim().max(5000).nullable().optional(),
@@ -458,6 +461,7 @@ router.post(
requesterName: body.requesterName, requesterName: body.requesterName,
requesterPhone: body.requesterPhone, requesterPhone: body.requesterPhone,
requesterOrg: body.requesterOrg ?? null, requesterOrg: body.requesterOrg ?? null,
attendeeCount: body.attendeeCount ?? null,
startsAt, startsAt,
endsAt, endsAt,
status: "pending", status: "pending",
@@ -687,6 +691,7 @@ router.post(
roomId: body.roomId, roomId: body.roomId,
title: body.title, title: body.title,
requesterName: body.requesterName ?? null, requesterName: body.requesterName ?? null,
attendeeCount: body.attendeeCount ?? null,
startsAt, startsAt,
endsAt, endsAt,
status: canApprove ? "approved" : "pending", status: canApprove ? "approved" : "pending",
@@ -773,6 +778,9 @@ router.patch(
...(body.requesterName !== undefined ...(body.requesterName !== undefined
? { requesterName: body.requesterName } ? { requesterName: body.requesterName }
: {}), : {}),
...(body.attendeeCount !== undefined
? { attendeeCount: body.attendeeCount }
: {}),
startsAt, startsAt,
endsAt, endsAt,
...(body.notes !== undefined ? { notes: body.notes } : {}), ...(body.notes !== undefined ? { notes: body.notes } : {}),
+1
View File
@@ -1714,6 +1714,7 @@
"requester": "مقدّم الطلب", "requester": "مقدّم الطلب",
"org": "الجهة", "org": "الجهة",
"phone": "الهاتف", "phone": "الهاتف",
"attendeeCount": "عدد الحضور",
"publicRequest": "طلب عام", "publicRequest": "طلب عام",
"roomLabel": "القاعة", "roomLabel": "القاعة",
"titleLabel": "عنوان الحجز", "titleLabel": "عنوان الحجز",
+1
View File
@@ -1587,6 +1587,7 @@
"requester": "Requester", "requester": "Requester",
"org": "Organization", "org": "Organization",
"phone": "Phone", "phone": "Phone",
"attendeeCount": "Attendees",
"publicRequest": "Public request", "publicRequest": "Public request",
"roomLabel": "Room", "roomLabel": "Room",
"titleLabel": "Booking title", "titleLabel": "Booking title",
@@ -22,6 +22,7 @@ type FormState = {
title: string; title: string;
requesterPhone: string; requesterPhone: string;
requesterOrg: string; requesterOrg: string;
attendeeCount: string;
roomId: string; roomId: string;
startsAt: string; startsAt: string;
endsAt: string; endsAt: string;
@@ -34,6 +35,7 @@ const EMPTY: FormState = {
title: "", title: "",
requesterPhone: "", requesterPhone: "",
requesterOrg: "", requesterOrg: "",
attendeeCount: "",
roomId: "", roomId: "",
startsAt: "", startsAt: "",
endsAt: "", endsAt: "",
@@ -105,6 +107,10 @@ export default function ProtocolRequestPage() {
requesterName: form.requesterName.trim(), requesterName: form.requesterName.trim(),
requesterPhone: form.requesterPhone.trim(), requesterPhone: form.requesterPhone.trim(),
requesterOrg: form.requesterOrg.trim() || null, requesterOrg: form.requesterOrg.trim() || null,
attendeeCount:
form.attendeeCount.trim() === ""
? null
: Number(form.attendeeCount),
startsAt: localToIso(form.startsAt), startsAt: localToIso(form.startsAt),
endsAt: localToIso(form.endsAt), endsAt: localToIso(form.endsAt),
notes: form.notes.trim() || null, notes: form.notes.trim() || null,
@@ -249,6 +255,19 @@ export default function ProtocolRequestPage() {
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="attendeeCount">عدد الحضور</Label>
<Input
id="attendeeCount"
type="number"
inputMode="numeric"
min={1}
value={form.attendeeCount}
onChange={(e) => set("attendeeCount", e.target.value)}
placeholder="العدد المتوقع للحضور (اختياري)"
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="room"> <Label htmlFor="room">
القاعة <span className="text-rose-500">*</span> القاعة <span className="text-rose-500">*</span>
+6
View File
@@ -86,6 +86,7 @@ type Booking = {
requesterName: string | null; requesterName: string | null;
requesterPhone: string | null; requesterPhone: string | null;
requesterOrg: string | null; requesterOrg: string | null;
attendeeCount: number | null;
source: "internal" | "public"; source: "internal" | "public";
startsAt: string; startsAt: string;
endsAt: string; endsAt: string;
@@ -497,6 +498,11 @@ export default function ProtocolPage() {
<span dir="ltr">{b.requesterPhone}</span> <span dir="ltr">{b.requesterPhone}</span>
</div> </div>
)} )}
{b.attendeeCount != null && (
<div className="text-xs text-slate-400">
{t("protocol.bookings.attendeeCount")}: {b.attendeeCount}
</div>
)}
</div> </div>
<div className="flex flex-col items-end gap-1 shrink-0"> <div className="flex flex-col items-end gap-1 shrink-0">
{b.source === "public" && ( {b.source === "public" && (
+3
View File
@@ -94,6 +94,9 @@ export const protocolRoomBookingsTable = pgTable(
// The requester's organization / entity (الجهة). Optional even for // The requester's organization / entity (الجهة). Optional even for
// public requests; always null for internal bookings unless set. // public requests; always null for internal bookings unless set.
requesterOrg: varchar("requester_org", { length: 300 }), 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 // Provenance marker. "internal" for bookings created by authenticated
// protocol staff through the app; "public" for guest requests coming in // protocol staff through the app; "public" for guest requests coming in
// via the no-login booking-request link. Defaults to "internal" so all // via the no-login booking-request link. Defaults to "internal" so all