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.
Every booking (public + internal) now gets a unique, NOT NULL, searchable
reference (RB-{year}-{pad4(id)}) 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 (NOT NULL + unique) + requesterPhone index; new
enums/types. Applied via drizzle push; existing rows backfilled.
- api-server: extended public booking schema + refine (org required when
external); reserveBookingId (nextval on the serial sequence) so the
reference is written in a single INSERT and can never be null; GET
/protocol/bookings accepts a `q` param filtering by reference OR phone
(server-side, indexed, case-insensitive).
- tx-os public form rewritten; admin card shows new fields; admin search
box now debounced and wired to the API `q` param (server-side filtering)
with an empty state.
- ar/en i18n keys under protocol.bookings.
Deploy safety / backfill:
- lib/db pre-push-cleanup (runs before every drizzle push, dev + deploy)
now backfills booking_reference for any pre-existing rows
(RB-{year(created_at)}-{pad4(id)}) BEFORE the NOT NULL + unique
constraint is enforced, so pushing the constraint on a populated table
can never fail. Idempotent: only updates NULL rows.
Verification:
- Public booking POST returns a reference; external-without-org rejected
(400); DB has zero null references after push. Re-running push logs
"No protocol_room_bookings booking_reference to backfill" (idempotent).
- Pre-existing push.ts / executive-meetings.ts typecheck errors are
unrelated to this task (files untouched).
This commit is contained in:
@@ -81,6 +81,48 @@ async function main(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// Backfill booking references BEFORE the schema push enforces the
|
||||
// NOT NULL + unique constraint on protocol_room_bookings.booking_reference.
|
||||
// Without this, pushing the constraint against a populated table (any
|
||||
// pre-existing booking predates the column) would fail. Reference format
|
||||
// mirrors makeBookingReference() in the API: RB-{year}-{pad4(id)}, which
|
||||
// is unique because id is unique. Idempotent: only touches NULL rows.
|
||||
if (await tableExists(client, "protocol_room_bookings")) {
|
||||
const hasColumn = await client.query(
|
||||
`SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'protocol_room_bookings'
|
||||
AND column_name = 'booking_reference'`,
|
||||
);
|
||||
if ((hasColumn.rowCount ?? 0) > 0) {
|
||||
const backfilled = await client.query(
|
||||
`UPDATE protocol_room_bookings
|
||||
SET booking_reference =
|
||||
'RB-' || EXTRACT(YEAR FROM created_at)::int
|
||||
|| '-' || LPAD(id::text, 4, '0')
|
||||
WHERE booking_reference IS NULL`,
|
||||
);
|
||||
const filled = backfilled.rowCount ?? 0;
|
||||
if (filled > 0) {
|
||||
console.log(
|
||||
`[pre-push] Backfilled ${filled} protocol_room_bookings booking_reference value(s).`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
"[pre-push] No protocol_room_bookings booking_reference to backfill.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[pre-push] protocol_room_bookings.booking_reference column not present yet — skipping backfill.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[pre-push] protocol_room_bookings table not present yet — skipping booking_reference backfill.",
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK").catch(() => {});
|
||||
|
||||
Reference in New Issue
Block a user