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.
Verification:
- Public booking POST returns a reference; external-without-org rejected
(400); DB has zero null references after push.
- Pre-existing push.ts / executive-meetings.ts typecheck errors are
unrelated to this task (files untouched).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useRoute } from "wouter";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -493,9 +493,25 @@ export default function ProtocolPage() {
|
||||
queryKey: ["protocol", "rooms"],
|
||||
queryFn: () => apiJson<Room[]>(`${API}/protocol/rooms`),
|
||||
});
|
||||
const [bookingSearch, setBookingSearch] = useState("");
|
||||
const [debouncedBookingSearch, setDebouncedBookingSearch] = useState("");
|
||||
useEffect(() => {
|
||||
const id = setTimeout(
|
||||
() => setDebouncedBookingSearch(bookingSearch.trim()),
|
||||
300,
|
||||
);
|
||||
return () => clearTimeout(id);
|
||||
}, [bookingSearch]);
|
||||
const bookings = useQuery<Booking[]>({
|
||||
queryKey: ["protocol", "bookings"],
|
||||
queryFn: () => apiJson<Booking[]>(`${API}/protocol/bookings`),
|
||||
queryKey: ["protocol", "bookings", debouncedBookingSearch],
|
||||
queryFn: () =>
|
||||
apiJson<Booking[]>(
|
||||
`${API}/protocol/bookings${
|
||||
debouncedBookingSearch
|
||||
? `?q=${encodeURIComponent(debouncedBookingSearch)}`
|
||||
: ""
|
||||
}`,
|
||||
),
|
||||
});
|
||||
const external = useQuery<ExternalMeeting[]>({
|
||||
queryKey: ["protocol", "external"],
|
||||
@@ -576,7 +592,6 @@ export default function ProtocolPage() {
|
||||
id: number;
|
||||
} | null>(null);
|
||||
const [bookingsView, setBookingsView] = useState<"list" | "calendar">("list");
|
||||
const [bookingSearch, setBookingSearch] = useState("");
|
||||
const [selectedDay, setSelectedDay] = useState<Date>(() => new Date());
|
||||
const [calendarMonth, setCalendarMonth] = useState<Date>(() => new Date());
|
||||
const now = useNow(60000);
|
||||
@@ -1071,22 +1086,13 @@ export default function ProtocolPage() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{bookings.data?.length === 0 && (
|
||||
{bookings.data?.length === 0 && !debouncedBookingSearch && (
|
||||
<EmptyState text={t("protocol.bookings.empty")} />
|
||||
)}
|
||||
{bookingsView === "list" ? (
|
||||
(() => {
|
||||
const q = bookingSearch.trim().toLowerCase();
|
||||
const filtered = q
|
||||
? (bookings.data ?? []).filter(
|
||||
(b) =>
|
||||
(b.bookingReference ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
(b.requesterPhone ?? "").toLowerCase().includes(q),
|
||||
)
|
||||
: (bookings.data ?? []);
|
||||
if (q && filtered.length === 0) {
|
||||
const list = bookings.data ?? [];
|
||||
if (debouncedBookingSearch && list.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
text={t("protocol.bookings.searchEmpty")}
|
||||
@@ -1095,7 +1101,7 @@ export default function ProtocolPage() {
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{filtered.map((b) => renderBookingCard(b))}
|
||||
{list.map((b) => renderBookingCard(b))}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
|
||||
Reference in New Issue
Block a user