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:
@@ -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 } : {}),
|
||||||
|
|||||||
@@ -1714,6 +1714,7 @@
|
|||||||
"requester": "مقدّم الطلب",
|
"requester": "مقدّم الطلب",
|
||||||
"org": "الجهة",
|
"org": "الجهة",
|
||||||
"phone": "الهاتف",
|
"phone": "الهاتف",
|
||||||
|
"attendeeCount": "عدد الحضور",
|
||||||
"publicRequest": "طلب عام",
|
"publicRequest": "طلب عام",
|
||||||
"roomLabel": "القاعة",
|
"roomLabel": "القاعة",
|
||||||
"titleLabel": "عنوان الحجز",
|
"titleLabel": "عنوان الحجز",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" && (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user