import config from "@payload-config"; import { getPayload } from "payload"; import type { Where } from "payload"; import { headers as nextHeaders } from "next/headers"; import Link from "next/link"; import { isPayloadUser } from "@/utils/access-control/isPayloadUser"; import { hasPermission } from "@/utils/access-control/hasPermission"; import { LedgerRow, NoticeState, PageShell, STATUS_TONE_CLASSES, statusToneFor, type ReadoutCell, } from "@/components/frontend/operations"; import type { SurfaceUser } from "@/lib/operations/surface"; import { listReservationOptions } from "@/lib/operations/reservationOptions"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { CancelButton } from "@/components/frontend/operations/reservations/ReservationCancelButton"; import { CreateReservationDialog } from "@/components/frontend/operations/reservations/CreateReservationDialog"; const RESERVATION_PAGE_SIZE = 20; export interface ReservationRow { id: number; reservationKey: string; operationId: string; status: string; expiresAt: string | null; } export interface ReservationFilters { status?: string; operationId?: string; } export interface ReservationListData { rows: ReservationRow[]; /** True when the viewer holds operation-reservations:read. False -> empty rows. */ capability: boolean; page: number; totalDocs: number; totalPages: number; } type PayloadType = Awaited>; /** * List of allocation reservations with status/operationId filters and * pagination. Runs through collection access control (default access + the * explicit session user, never overrideAccess). Gated behind * operation-reservations:read: a viewer lacking that permission gets an empty * result (capability false) so the page can render the permission notice * instead of a list that looks like "no data". */ export async function getReservationList( payload: PayloadType, user: SurfaceUser, filters: ReservationFilters, requestedPage = 1, ): Promise { const canSee = await hasPermission(payload, user, "operation-reservations:read"); if (!canSee) { return { rows: [], capability: false, page: 1, totalDocs: 0, totalPages: 0 }; } const conditions: Where[] = []; if (filters.status) conditions.push({ status: { equals: filters.status } }); if (filters.operationId) conditions.push({ operationId: { equals: filters.operationId } }); const where: Where | undefined = conditions.length > 0 ? { and: conditions } : undefined; const found = await payload.find({ collection: "operation-reservations", where, sort: "-createdAt", page: Math.max(1, requestedPage), limit: RESERVATION_PAGE_SIZE, depth: 0, user: user ?? undefined, }); const rows: ReservationRow[] = found.docs.map((doc) => { const r = doc as unknown as ReservationRow; return { id: r.id, reservationKey: r.reservationKey, operationId: r.operationId, status: r.status, expiresAt: r.expiresAt ?? null, }; }); return { rows, capability: true, page: found.page ?? Math.max(1, requestedPage), totalDocs: found.totalDocs ?? found.docs.length, totalPages: found.totalPages ?? 1, }; } function reservationHref(filters: ReservationFilters, page: number): string { const params = new URLSearchParams(); if (filters.status) params.set("status", filters.status); if (filters.operationId) params.set("operationId", filters.operationId); if (page > 1) params.set("page", String(page)); const qs = params.toString(); return qs ? `/operations/reservations?${qs}` : "/operations/reservations"; } const STATUS_OPTIONS: { value: string; label: string }[] = [ { value: "", label: "All" }, { value: "reserved", label: "Reserved" }, { value: "settled", label: "Settled" }, { value: "cancelled", label: "Cancelled" }, { value: "expired", label: "Expired" }, { value: "no-show", label: "No-Show" }, ]; export const metadata = { title: "Reservations: Polaris Task Force", }; export default async function ReservationsPage({ searchParams, }: { searchParams: Promise; }) { const params = await searchParams; const payload = await getPayload({ config }); const { user: authUser } = await payload.auth({ headers: await nextHeaders(), canSetHeaders: false, }); const user = isPayloadUser(authUser) ? authUser : null; const surfaceUser: SurfaceUser = user ? { id: user.id } : null; const page = Number.parseInt(params.page ?? "1", 10); const filters: ReservationFilters = { status: params.status || undefined, operationId: params.operationId || undefined, }; const data = await getReservationList(payload, surfaceUser, filters, Number.isNaN(page) ? 1 : page); // The cancel control is server-gated: only viewers holding // operation-reservations:update may render it, and only on reserved rows. const canCancel = data.capability && user !== null ? await hasPermission(payload, user, "operation-reservations:update") : false; // Server-gated create dialog: only operation-reservations:create viewers open it; // logistics qualification gates the vehicle/cargo/budget sections inside. const canCreate = user ? await hasPermission(payload, user, "operation-reservations:create") : false; const canManageLogistics = user ? await hasLogisticsQualification(payload, user) : false; const options = canCreate && user ? await listReservationOptions(payload, user) : null; return (

Every allocation of people, vehicles, cargo, and treasury budget against an operation, with its settlement outcome. Reservations are earmarks only; the settlement record on a row captures what was consumed or returned.

Clear
{canCreate && options ? ( ) : null} {data.capability === false ? ( ) : data.rows.length === 0 ? ( ) : (
{data.rows.map((row) => { const cells: ReadoutCell[] = [ { label: "Operation", value: row.operationId }, { label: "Status", value: row.status }, { label: "Expires", value: row.expiresAt ?? "n/a" }, ]; return (
{row.reservationKey} {row.status} {canCancel && row.status === "reserved" ? (
) : null}
); })}
Page {data.page} of {Math.max(1, data.totalPages)} ({data.totalDocs} reservations) {data.page > 1 && ( Prev )} {data.page < data.totalPages && ( Next )}
)}
); }