@voyantjs/bookings-ui 0.62.3 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,40 +18,37 @@ that spacing when a shell owns the page chrome.
18
18
 
19
19
  ## Components
20
20
 
21
- - `BookingsPage`, `BookingDetailPage`, `BookingWorkspacePage`
21
+ - `BookingsPage`, `BookingDetailPage`
22
22
  - `BookingList`, `BookingDialog`, `BookingCreateDialog`, `BookingCancellationDialog`, `StatusChangeDialog`
23
23
  - `TravelerList`, `TravelerDialog`, `BookingItemList`, `BookingGroupSection`
24
24
  - `BookingPaymentsSummary`, `BookingPaymentScheduleList`, `BookingGuaranteeList`
25
25
  - `SupplierStatusList`, `BookingActivityTimeline`, `BookingNotes`
26
26
 
27
- `BookingWorkspacePage` wraps the existing `BookingDetailPage` in a reusable
28
- operator workspace shell. It publishes cross-module navigation for booking,
29
- finance, legal, travelers, and activity work while keeping module-specific
30
- surfaces app-owned through typed slots:
27
+ `BookingDetailPage` is the canonical multi-tab booking surface (Overview,
28
+ Travelers, Payments, Suppliers, Documents, Activity, plus optional `Invoices`
29
+ and `Ledger` tabs). Templates inject template-owned cards via the `slots`
30
+ prop and wire router navigation through `onBack`, `onPersonOpen`,
31
+ `onOrganizationOpen`, `onCollectPayment`, and `onRecordPayment` callbacks:
31
32
 
32
33
  ```tsx
33
- <BookingWorkspacePage
34
+ <BookingDetailPage
34
35
  id={bookingId}
36
+ hideBreadcrumb
37
+ onBack={() => router.push("/bookings")}
38
+ onPersonOpen={(personId) => router.push(`/people/${personId}`)}
39
+ onCollectPayment={() => setCollectOpen(true)}
40
+ onRecordPayment={() => setRecordOpen(true)}
35
41
  slots={{
36
- actionBar: ({ booking }) => <button type="button">Assign {booking.bookingNumber}</button>,
37
- bookingTab: ({ booking }) => <BookingOverviewPanel booking={booking} />,
38
- financeSidebar: ({ bookingId }) => <FinanceStatusCard bookingId={bookingId} />,
39
- legalTab: ({ bookingId }) => <ContractChecklist bookingId={bookingId} />,
40
- travelersTabExtensions: ({ bulkActions }) => (
41
- <BatchTravelerTools selectedTravelerIds={bulkActions.selectedTravelerIds} />
42
- ),
43
- activityTab: ({ bookingId }) => <OperatorTimeline bookingId={bookingId} />,
42
+ header: (booking) => <WidgetSlot slot="booking.header" props={{ booking }} />,
43
+ overviewStart: () => <CatalogSourceCard bookingId={bookingId} />,
44
+ financeStart: () => <PendingPaymentSessions bookingId={bookingId} />,
45
+ invoicesTab: { content: (booking) => <InvoicesCard booking={booking} /> },
46
+ ledgerTab: { content: <ActionLedgerPanel bookingId={bookingId} /> },
47
+ documents: () => <DocumentsTable bookingId={bookingId} />,
44
48
  }}
45
49
  />
46
50
  ```
47
51
 
48
- Slot render functions receive the booking, active workspace section, section
49
- setter, and bulk-action state for traveler and finance selections. Use
50
- `bookingTab` when an app wants to use top-level finance, legal, traveler, or
51
- activity tabs without nesting the default `BookingDetailPage` tabs. When
52
- `bookingTab` is omitted, the workspace keeps mounting `BookingDetailPage`; use
53
- `bookingDetailSlots` to pass slots through to that default detail page.
54
-
55
52
  ## I18n
56
53
 
57
54
  Components render English by default. To localize them, wrap your UI in
@@ -1,7 +1,20 @@
1
1
  import { type BookingRecord } from "@voyantjs/bookings-react";
2
2
  import { type ReactNode } from "react";
3
+ /**
4
+ * Optional extra tab. When provided, the canonical layout renders a
5
+ * trigger and a content panel. Modeled after PersonDetailPage's
6
+ * commercial-tab slot shape. The trigger label falls back to the i18n
7
+ * default (`messages.bookingDetailPage.tabInvoices` etc.).
8
+ */
9
+ export interface BookingDetailTabSlot {
10
+ label?: string;
11
+ /** Receives the loaded booking so the slot can use sell amount, ids, etc. */
12
+ content: ReactNode | ((booking: BookingRecord) => ReactNode);
13
+ }
3
14
  export interface BookingDetailPageSlots {
15
+ /** Rendered between the title row and the summary card. */
4
16
  header?: (booking: BookingRecord) => ReactNode;
17
+ /** Rendered between the summary card and the tabs. */
5
18
  afterSummary?: (booking: BookingRecord) => ReactNode;
6
19
  overviewStart?: (booking: BookingRecord) => ReactNode;
7
20
  overviewEnd?: (booking: BookingRecord) => ReactNode;
@@ -10,18 +23,36 @@ export interface BookingDetailPageSlots {
10
23
  financeEnd?: (booking: BookingRecord) => ReactNode;
11
24
  documents?: (booking: BookingRecord) => ReactNode;
12
25
  activityEnd?: (booking: BookingRecord) => ReactNode;
26
+ /** Mounts a dedicated `Invoices` tab between Payments and Suppliers. */
27
+ invoicesTab?: BookingDetailTabSlot;
28
+ /** Mounts a dedicated `Ledger` tab at the far right. */
29
+ ledgerTab?: BookingDetailTabSlot;
13
30
  }
14
31
  export interface BookingDetailPageProps {
15
32
  id: string;
16
33
  className?: string;
17
34
  locale?: string;
35
+ /** When true, the inline `Bookings > #` breadcrumb is suppressed
36
+ * (use when the host shell already renders breadcrumbs). */
37
+ hideBreadcrumb?: boolean;
18
38
  onBack?: () => void;
19
39
  onPersonOpen?: (personId: string) => void;
20
40
  onOrganizationOpen?: (organizationId: string) => void;
41
+ /** Wired to a primary `Generate payment link` button on the Payments tab. */
21
42
  onCollectPayment?: (booking: BookingRecord) => void;
43
+ /** Wired to a secondary `Record payment` button on the Payments tab. */
44
+ onRecordPayment?: (booking: BookingRecord) => void;
22
45
  slots?: BookingDetailPageSlots;
23
46
  }
24
- export declare function BookingDetailPage({ id, className, locale, onBack, onPersonOpen, onOrganizationOpen, onCollectPayment, slots, }: BookingDetailPageProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBack, onPersonOpen, onOrganizationOpen, onCollectPayment, onRecordPayment, slots, }: BookingDetailPageProps): import("react/jsx-runtime").JSX.Element;
48
+ /**
49
+ * Billing/contact card for the Travelers tab. Snapshot fields on the
50
+ * booking row are the source of truth (they capture contact info at
51
+ * the time of booking). When they're empty — typically for bookings
52
+ * created via flows that don't snapshot — fall back to the linked CRM
53
+ * person / organization so the operator still sees who they're
54
+ * billing.
55
+ */
25
56
  export declare function BookingBillingContextCard({ booking }: {
26
57
  booking: BookingRecord;
27
58
  }): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"booking-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/booking-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AA2BjC,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAgBhD,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAC9C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACrD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACnD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAClD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACjD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,kBAAkB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,EAAE,EACF,SAAS,EACT,MAAM,EACN,MAAM,EACN,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,GACN,EAAE,sBAAsB,2CA4PxB;AAED,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CAmChF"}
1
+ {"version":3,"file":"booking-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/booking-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AA+BjC,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAiBhD;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,OAAO,EAAE,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAC9C,sDAAsD;IACtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACrD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACnD,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACtD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACpD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IAClD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACjD,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,SAAS,CAAA;IACnD,wEAAwE;IACxE,WAAW,CAAC,EAAE,oBAAoB,CAAA;IAClC,wDAAwD;IACxD,SAAS,CAAC,EAAE,oBAAoB,CAAA;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;gEAC4D;IAC5D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,kBAAkB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IACnD,wEAAwE;IACxE,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAClD,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,EAAE,EACF,SAAS,EACT,MAAM,EACN,cAAc,EACd,MAAM,EACN,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,KAAK,GACN,EAAE,sBAAsB,2CA2RxB;AASD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,2CA8DhF"}
@@ -1,12 +1,14 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { bookingStatusBadgeVariant, useBooking, useBookingMutation, } from "@voyantjs/bookings-react";
4
- import { Badge, Button, Card, CardContent, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyantjs/ui/components";
4
+ import { useOrganization, usePerson } from "@voyantjs/crm-react";
5
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@voyantjs/ui/components";
5
6
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
6
- import { Ban, Calendar, ChevronRight, Mail, MapPin, MoreHorizontal, Pencil, Phone, RefreshCw, Trash2, Users, } from "lucide-react";
7
+ import { Ban, Calendar, ChevronRight, CreditCard, Mail, MapPin, MoreHorizontal, Pencil, Phone, RefreshCw, Trash2, Users, } from "lucide-react";
7
8
  import { useState } from "react";
8
9
  import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/index.js";
9
10
  import { BookingActivityTimeline } from "./booking-activity-timeline.js";
11
+ import { BookingBillingDialog } from "./booking-billing-dialog.js";
10
12
  import { BookingCancellationDialog } from "./booking-cancellation-dialog.js";
11
13
  import { BookingDialog } from "./booking-dialog.js";
12
14
  import { BookingGroupSection } from "./booking-group-section.js";
@@ -18,7 +20,7 @@ import { BookingPaymentsSummary } from "./booking-payments-summary.js";
18
20
  import { StatusChangeDialog } from "./status-change-dialog.js";
19
21
  import { SupplierStatusList } from "./supplier-status-list.js";
20
22
  import { TravelerList } from "./traveler-list.js";
21
- export function BookingDetailPage({ id, className, locale, onBack, onPersonOpen, onOrganizationOpen, onCollectPayment, slots, }) {
23
+ export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBack, onPersonOpen, onOrganizationOpen, onCollectPayment, onRecordPayment, slots, }) {
22
24
  const i18n = useBookingsUiI18nOrDefault();
23
25
  const messages = useBookingsUiMessagesOrDefault();
24
26
  const detailMessages = messages.bookingDetailPage;
@@ -39,18 +41,41 @@ export function BookingDetailPage({ id, className, locale, onBack, onPersonOpen,
39
41
  const sellHint = booking.priceOverride?.isManual
40
42
  ? `${detailMessages.summaryPriceOverride}: ${booking.priceOverride.reason}`
41
43
  : booking.sellCurrency;
42
- return (_jsxs("div", { "data-slot": "booking-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [onBack ? (_jsx("button", { type: "button", onClick: onBack, className: "transition-colors hover:text-foreground", children: detailMessages.breadcrumbBookings })) : (_jsx("span", { children: detailMessages.breadcrumbBookings })), _jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "font-normal text-foreground", children: booking.bookingNumber })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: booking.bookingNumber }), _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: getBookingStatusLabel(booking.status, messages.common.bookingStatusLabels) })] }), _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.editAction] }), _jsxs(DropdownMenuItem, { onClick: () => setStatusDialogOpen(true), children: [_jsx(RefreshCw, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.changeStatusAction] }), canCancel ? (_jsxs(DropdownMenuItem, { onClick: () => setCancelDialogOpen(true), children: [_jsx(Ban, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.cancelBookingAction] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", disabled: remove.isPending, onClick: async () => {
44
+ return (_jsxs("div", { "data-slot": "booking-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [hideBreadcrumb ? null : (_jsxs("div", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [onBack ? (_jsx("button", { type: "button", onClick: onBack, className: "transition-colors hover:text-foreground", children: detailMessages.breadcrumbBookings })) : (_jsx("span", { children: detailMessages.breadcrumbBookings })), _jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "font-normal text-foreground", children: booking.bookingNumber })] })), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: booking.bookingNumber }), _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: getBookingStatusLabel(booking.status, messages.common.bookingStatusLabels) })] }), _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.editAction] }), _jsxs(DropdownMenuItem, { onClick: () => setStatusDialogOpen(true), children: [_jsx(RefreshCw, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.changeStatusAction] }), canCancel ? (_jsxs(DropdownMenuItem, { onClick: () => setCancelDialogOpen(true), children: [_jsx(Ban, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.cancelBookingAction] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", disabled: remove.isPending, onClick: async () => {
43
45
  if (confirm(detailMessages.deleteConfirm)) {
44
46
  await remove.mutateAsync(id);
45
47
  onBack?.();
46
48
  }
47
49
  }, children: [_jsx(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }), detailMessages.deleteAction] })] })] }), slots?.header?.(booking), _jsx(Card, { children: _jsxs(CardContent, { className: "grid grid-cols-2 gap-6 py-6 sm:grid-cols-4", children: [_jsx(SummaryStat, { label: detailMessages.summarySell, value: formatAmount(booking.sellAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue), hint: sellHint }), _jsx(SummaryStat, { label: detailMessages.summaryCostMargin, value: formatAmount(booking.costAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue), hint: formatMargin(booking.marginPercent, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryDates, value: booking.startDate
48
50
  ? `${formatDate(booking.startDate, resolvedLocale, detailMessages.noValue)} - ${formatDate(booking.endDate, resolvedLocale, detailMessages.noValue)}`
49
- : detailMessages.tbd, icon: _jsx(Calendar, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(SummaryStat, { label: detailMessages.summaryTravelers, value: booking.pax != null ? String(booking.pax) : detailMessages.noValue, icon: _jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), booking.personId ? (_jsx(SummaryLink, { label: detailMessages.summaryPerson, id: booking.personId, onOpen: onPersonOpen })) : null, booking.organizationId ? (_jsx(SummaryLink, { label: detailMessages.summaryOrganization, id: booking.organizationId, onOpen: onOrganizationOpen })) : null, _jsx(SummaryStat, { label: detailMessages.summaryCreated, value: formatDate(booking.createdAt, resolvedLocale, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryUpdated, value: formatDate(booking.updatedAt, resolvedLocale, detailMessages.noValue) })] }) }), slots?.afterSummary?.(booking), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full justify-start", children: [_jsx(TabsTrigger, { value: "overview", children: detailMessages.tabOverview }), _jsx(TabsTrigger, { value: "travelers", children: detailMessages.tabTravelers }), _jsx(TabsTrigger, { value: "finance", children: detailMessages.tabFinance }), _jsx(TabsTrigger, { value: "suppliers", children: detailMessages.tabSuppliers }), _jsx(TabsTrigger, { value: "documents", children: detailMessages.tabDocuments }), _jsx(TabsTrigger, { value: "activity", children: detailMessages.tabActivity })] }), _jsxs(TabsContent, { value: "overview", className: "mt-4 flex flex-col gap-6", children: [slots?.overviewStart?.(booking), _jsx(BookingItemList, { bookingId: id }), _jsx(BookingGroupSection, { bookingId: id }), visibleInternalNotes(booking.internalNotes) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-5", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: detailMessages.internalNotesLabel }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: visibleInternalNotes(booking.internalNotes) })] }) })) : null, slots?.overviewEnd?.(booking)] }), _jsxs(TabsContent, { value: "travelers", className: "mt-4 flex flex-col gap-6", children: [slots?.travelersStart?.(booking), _jsx(BookingBillingContextCard, { booking: booking }), _jsx(TravelerList, { bookingId: id, autoReveal: true })] }), _jsxs(TabsContent, { value: "finance", className: "mt-4 flex flex-col gap-6", children: [onCollectPayment ? (_jsx("div", { className: "flex items-center justify-end", children: _jsx(Button, { onClick: () => onCollectPayment(booking), children: detailMessages.collectPaymentAction }) })) : null, slots?.financeStart?.(booking), _jsx(BookingPaymentsSummary, { bookingId: id, variant: "admin" }), _jsx(BookingPaymentScheduleList, { bookingId: id }), _jsx(BookingGuaranteeList, { bookingId: id }), slots?.financeEnd?.(booking)] }), _jsx(TabsContent, { value: "suppliers", className: "mt-4", children: _jsx(SupplierStatusList, { bookingId: id }) }), _jsx(TabsContent, { value: "documents", className: "mt-4 flex flex-col gap-4", children: slots?.documents ? (slots.documents(booking)) : (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: detailMessages.documentsSlotEmpty })) }), _jsxs(TabsContent, { value: "activity", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingActivityTimeline, { bookingId: id }), _jsx(BookingNotes, { bookingId: id }), slots?.activityEnd?.(booking)] })] }), _jsx(BookingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking }), _jsx(StatusChangeDialog, { open: statusDialogOpen, onOpenChange: setStatusDialogOpen, bookingId: id, currentStatus: booking.status }), _jsx(BookingCancellationDialog, { open: cancelDialogOpen, onOpenChange: setCancelDialogOpen, booking: booking })] }));
51
+ : detailMessages.tbd, icon: _jsx(Calendar, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(SummaryStat, { label: detailMessages.summaryTravelers, value: booking.pax != null ? String(booking.pax) : detailMessages.noValue, icon: _jsx(Users, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), booking.personId ? (_jsx(SummaryPersonLink, { label: detailMessages.summaryPerson, personId: booking.personId, onOpen: onPersonOpen })) : null, booking.organizationId ? (_jsx(SummaryOrganizationLink, { label: detailMessages.summaryOrganization, organizationId: booking.organizationId, onOpen: onOrganizationOpen })) : null, _jsx(SummaryStat, { label: detailMessages.summaryCreated, value: formatDate(booking.createdAt, resolvedLocale, detailMessages.noValue) }), _jsx(SummaryStat, { label: detailMessages.summaryUpdated, value: formatDate(booking.updatedAt, resolvedLocale, detailMessages.noValue) })] }) }), slots?.afterSummary?.(booking), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full justify-start", children: [_jsx(TabsTrigger, { value: "overview", children: detailMessages.tabOverview }), _jsx(TabsTrigger, { value: "travelers", children: detailMessages.tabTravelers }), _jsx(TabsTrigger, { value: "finance", children: detailMessages.tabFinance }), slots?.invoicesTab ? (_jsx(TabsTrigger, { value: "invoices", children: slots.invoicesTab.label ?? detailMessages.tabInvoices })) : null, _jsx(TabsTrigger, { value: "suppliers", children: detailMessages.tabSuppliers }), _jsx(TabsTrigger, { value: "documents", children: detailMessages.tabDocuments }), _jsx(TabsTrigger, { value: "activity", children: detailMessages.tabActivity }), slots?.ledgerTab ? (_jsx(TabsTrigger, { value: "ledger", children: slots.ledgerTab.label ?? detailMessages.tabLedger })) : null] }), _jsxs(TabsContent, { value: "overview", className: "mt-4 flex flex-col gap-6", children: [slots?.overviewStart?.(booking), _jsx(BookingItemList, { bookingId: id }), _jsx(BookingGroupSection, { bookingId: id }), visibleInternalNotes(booking.internalNotes) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-5", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: detailMessages.internalNotesLabel }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: visibleInternalNotes(booking.internalNotes) })] }) })) : null, slots?.overviewEnd?.(booking)] }), _jsxs(TabsContent, { value: "travelers", className: "mt-4 flex flex-col gap-6", children: [slots?.travelersStart?.(booking), _jsx(BookingBillingContextCard, { booking: booking }), _jsx(TravelerList, { bookingId: id, autoReveal: true })] }), _jsxs(TabsContent, { value: "finance", className: "mt-4 flex flex-col gap-6", children: [onCollectPayment || onRecordPayment ? (_jsxs("div", { className: "flex items-center justify-end gap-2", children: [onRecordPayment ? (_jsx(Button, { variant: "outline", onClick: () => onRecordPayment(booking), children: detailMessages.recordPaymentAction })) : null, onCollectPayment ? (_jsx(Button, { onClick: () => onCollectPayment(booking), children: detailMessages.collectPaymentAction })) : null] })) : null, slots?.financeStart?.(booking), _jsx(BookingPaymentsSummary, { bookingId: id, variant: "admin" }), _jsx(BookingPaymentScheduleList, { bookingId: id }), _jsx(BookingGuaranteeList, { bookingId: id }), slots?.financeEnd?.(booking)] }), slots?.invoicesTab ? (_jsx(TabsContent, { value: "invoices", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.invoicesTab.content, booking) })) : null, _jsx(TabsContent, { value: "suppliers", className: "mt-4", children: _jsx(SupplierStatusList, { bookingId: id }) }), _jsx(TabsContent, { value: "documents", className: "mt-4 flex flex-col gap-4", children: slots?.documents ? (slots.documents(booking)) : (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: detailMessages.documentsSlotEmpty })) }), _jsxs(TabsContent, { value: "activity", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingActivityTimeline, { bookingId: id }), _jsx(BookingNotes, { bookingId: id }), slots?.activityEnd?.(booking)] }), slots?.ledgerTab ? (_jsx(TabsContent, { value: "ledger", className: "mt-4 flex flex-col gap-6", children: renderTabSlot(slots.ledgerTab.content, booking) })) : null] }), _jsx(BookingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking }), _jsx(StatusChangeDialog, { open: statusDialogOpen, onOpenChange: setStatusDialogOpen, bookingId: id, currentStatus: booking.status }), _jsx(BookingCancellationDialog, { open: cancelDialogOpen, onOpenChange: setCancelDialogOpen, booking: booking })] }));
50
52
  }
53
+ function renderTabSlot(content, booking) {
54
+ return typeof content === "function" ? content(booking) : content;
55
+ }
56
+ /**
57
+ * Billing/contact card for the Travelers tab. Snapshot fields on the
58
+ * booking row are the source of truth (they capture contact info at
59
+ * the time of booking). When they're empty — typically for bookings
60
+ * created via flows that don't snapshot — fall back to the linked CRM
61
+ * person / organization so the operator still sees who they're
62
+ * billing.
63
+ */
51
64
  export function BookingBillingContextCard({ booking }) {
52
65
  const messages = useBookingsUiMessagesOrDefault().bookingDetailPage;
53
- const payerName = [booking.contactFirstName, booking.contactLastName].filter(Boolean).join(" ");
66
+ const [editOpen, setEditOpen] = useState(false);
67
+ const person = usePerson(booking.personId ?? undefined, {
68
+ enabled: Boolean(booking.personId),
69
+ }).data;
70
+ const organization = useOrganization(booking.organizationId ?? undefined, {
71
+ enabled: Boolean(booking.organizationId) && !booking.personId,
72
+ }).data;
73
+ const payerName = [booking.contactFirstName, booking.contactLastName].filter(Boolean).join(" ") ||
74
+ (person ? [person.firstName, person.lastName].filter(Boolean).join(" ") : "") ||
75
+ organization?.name ||
76
+ "";
77
+ const email = booking.contactEmail ?? person?.email ?? null;
78
+ const phone = booking.contactPhone ?? person?.phone ?? null;
54
79
  const address = [
55
80
  booking.contactAddressLine1,
56
81
  booking.contactCity,
@@ -60,7 +85,7 @@ export function BookingBillingContextCard({ booking }) {
60
85
  ]
61
86
  .filter(Boolean)
62
87
  .join(", ");
63
- return (_jsx(Card, { children: _jsxs(CardContent, { className: "grid gap-4 py-5 md:grid-cols-4", children: [_jsx(BillingField, { label: messages.billingPayer, value: payerName || messages.noValue }), _jsx(BillingField, { label: messages.billingEmail, value: booking.contactEmail ?? messages.noValue, icon: _jsx(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(BillingField, { label: messages.billingPhone, value: booking.contactPhone ?? messages.noValue, icon: _jsx(Phone, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(BillingField, { label: messages.billingAddress, value: address || messages.noValue, icon: _jsx(MapPin, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] }) }));
88
+ return (_jsxs(_Fragment, { children: [_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between pb-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(CreditCard, { className: "h-4 w-4", "aria-hidden": "true" }), messages.billingPayer] }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": "true" }), messages.editAction] })] }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-4", children: [_jsx(BillingField, { label: messages.billingPayer, value: payerName || messages.noValue }), _jsx(BillingField, { label: messages.billingEmail, value: email ?? messages.noValue, icon: _jsx(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(BillingField, { label: messages.billingPhone, value: phone ?? messages.noValue, icon: _jsx(Phone, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(BillingField, { label: messages.billingAddress, value: address || messages.noValue, icon: _jsx(MapPin, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] })] }), _jsx(BookingBillingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking })] }));
64
89
  }
65
90
  function SummaryStat({ label, value, hint, icon, }) {
66
91
  return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [icon, label] }), _jsx("div", { className: "text-base font-semibold tabular-nums", children: value }), hint ? _jsx("div", { className: "text-xs text-muted-foreground", children: hint }) : null] }));
@@ -71,8 +96,19 @@ function BillingField({ label, value, icon }) {
71
96
  function ActionMenu({ children }) {
72
97
  return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: children })] }));
73
98
  }
74
- function SummaryLink({ label, id, onOpen, }) {
75
- return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(id), className: "truncate text-left font-mono text-primary text-xs hover:underline", children: id })) : (_jsx("span", { className: "truncate font-mono text-xs", children: id }))] }));
99
+ function SummaryPersonLink({ label, personId, onOpen, }) {
100
+ // Hydrate the CRM person so the header shows a human name with a
101
+ // link to the detail page, falling back to the raw id while the
102
+ // record is in flight (or when the person was hard-deleted).
103
+ const person = usePerson(personId).data;
104
+ const name = person ? [person.firstName, person.lastName].filter(Boolean).join(" ").trim() : "";
105
+ const display = name || personId;
106
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(personId), className: "truncate text-left text-sm font-medium text-primary hover:underline", children: display })) : (_jsx("span", { className: "truncate text-sm font-medium", children: display }))] }));
107
+ }
108
+ function SummaryOrganizationLink({ label, organizationId, onOpen, }) {
109
+ const organization = useOrganization(organizationId).data;
110
+ const display = organization?.name || organizationId;
111
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(organizationId), className: "truncate text-left text-sm font-medium text-primary hover:underline", children: display })) : (_jsx("span", { className: "truncate text-sm font-medium", children: display }))] }));
76
112
  }
77
113
  function getBookingStatusLabel(status, labels) {
78
114
  return labels[status] ?? status;
@@ -100,6 +136,13 @@ function formatDate(iso, locale, empty) {
100
136
  year: "numeric",
101
137
  });
102
138
  }
139
+ /**
140
+ * Strip internal markers (`__contract_acceptance__:`,
141
+ * `__payment_policy_source__:`) before rendering — those are
142
+ * server-side relays, not human-readable notes. Their canonical home
143
+ * is on the contract signature row / schedule history; surfacing them
144
+ * here just confuses operators with raw JSON.
145
+ */
103
146
  function visibleInternalNotes(notes) {
104
147
  if (!notes)
105
148
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"booking-item-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-list.tsx"],"names":[],"mappings":"AAwBA,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,2CAqLlE"}
1
+ {"version":3,"file":"booking-item-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-list.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,2CAwMlE"}
@@ -6,7 +6,6 @@ import { Calendar, ChevronDown, ChevronRight, Package, Pencil, Plus, Trash2 } fr
6
6
  import * as React from "react";
7
7
  import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
8
8
  import { BookingItemDialog } from "./booking-item-dialog.js";
9
- import { BookingItemTravelers } from "./booking-item-travelers.js";
10
9
  const statusVariant = {
11
10
  draft: "outline",
12
11
  on_hold: "secondary",
@@ -34,7 +33,16 @@ export function BookingItemList({ bookingId }) {
34
33
  setExpandedItemId(isExpanded ? null : item.id);
35
34
  }, className: "text-muted-foreground hover:text-foreground", "aria-label": isExpanded
36
35
  ? messages.bookingItemList.actions.collapseItem
37
- : messages.bookingItemList.actions.expandItem, children: isExpanded ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5" })) }) }), _jsx("td", { className: "p-2 font-medium", children: item.title }), _jsx("td", { className: "p-2", children: messages.bookingItemDialog.itemTypeLabels[item.itemType] }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[item.status] ?? "secondary", children: messages.bookingItemDialog.itemStatusLabels[item.status] }) }), _jsx("td", { className: "p-2 text-right font-mono", children: item.quantity }), _jsx("td", { className: "p-2 text-right font-mono", children: item.totalSellAmountCents == null
36
+ : messages.bookingItemList.actions.expandItem, children: isExpanded ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5" })) }) }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "font-medium", children: item.productNameSnapshot ?? item.title }), (() => {
37
+ const subtitleParts = [
38
+ item.optionNameSnapshot,
39
+ item.unitNameSnapshot ??
40
+ (item.productNameSnapshot ? item.title : null),
41
+ ].filter((part) => Boolean(part));
42
+ if (subtitleParts.length === 0)
43
+ return null;
44
+ return (_jsx("span", { className: "text-muted-foreground text-xs", children: subtitleParts.join(" · ") }));
45
+ })()] }) }), _jsx("td", { className: "p-2", children: messages.bookingItemDialog.itemTypeLabels[item.itemType] }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[item.status] ?? "secondary", children: messages.bookingItemDialog.itemStatusLabels[item.status] }) }), _jsx("td", { className: "p-2 text-right font-mono", children: item.quantity }), _jsx("td", { className: "p-2 text-right font-mono", children: item.totalSellAmountCents == null
38
46
  ? messages.bookingItemList.values.totalUnavailable
39
47
  : formatCurrency(item.totalSellAmountCents / 100, item.sellCurrency) }), _jsx("td", { className: "p-2 text-right font-mono text-muted-foreground", children: item.totalCostAmountCents == null || !item.costCurrency
40
48
  ? messages.bookingItemList.values.costUnavailable
@@ -48,7 +56,7 @@ export function BookingItemList({ bookingId }) {
48
56
  if (confirm(messages.bookingItemList.actions.deleteConfirm)) {
49
57
  remove.mutate(item.id);
50
58
  }
51
- }, className: "text-muted-foreground hover:text-destructive", "aria-label": messages.bookingItemList.actions.deleteItem, children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }), isExpanded && (_jsxs("tr", { className: "border-b last:border-b-0 bg-muted/10", children: [_jsx("td", {}), _jsx("td", { colSpan: 8, className: "p-3", children: _jsx(ItemDetailPanel, { bookingId: bookingId, item: item }) })] }))] }, item.id));
59
+ }, className: "text-muted-foreground hover:text-destructive", "aria-label": messages.bookingItemList.actions.deleteItem, children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }), isExpanded && (_jsxs("tr", { className: "border-b last:border-b-0 bg-muted/10", children: [_jsx("td", {}), _jsx("td", { colSpan: 8, className: "p-3", children: _jsx(ItemDetailPanel, { item: item }) })] }))] }, item.id));
52
60
  }) })] }) })) }), _jsx(BookingItemDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
53
61
  setDialogOpen(nextOpen);
54
62
  if (!nextOpen) {
@@ -66,11 +74,11 @@ export function BookingItemList({ bookingId }) {
66
74
  * per-item travelers list. Compact two-column layout on wide
67
75
  * screens, stacks on narrow ones.
68
76
  */
69
- function ItemDetailPanel({ bookingId, item, }) {
77
+ function ItemDetailPanel({ item }) {
70
78
  const messages = useBookingsUiMessagesOrDefault();
71
79
  const { formatCurrency } = useBookingsUiI18nOrDefault();
72
80
  const dateRange = formatItemDateRange(item);
73
- return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(DetailBlock, { label: messages.bookingItemList.detail.description, children: item.description ? (_jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.description })) : (_jsx("p", { className: "text-muted-foreground text-xs italic", children: messages.bookingItemList.detail.noDescription })) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(DetailBlock, { label: messages.bookingItemList.detail.dates, children: _jsxs("div", { className: "flex items-baseline gap-1.5 text-sm", children: [_jsx(Calendar, { className: "h-3.5 w-3.5 self-center text-muted-foreground" }), dateRange ?? (_jsx("span", { className: "text-muted-foreground text-xs", children: messages.bookingItemList.values.serviceDateUnavailable }))] }) }), _jsx(DetailBlock, { label: messages.bookingItemList.detail.cost, children: item.totalCostAmountCents != null && item.costCurrency ? (_jsxs("div", { className: "text-sm", children: [_jsx("span", { className: "font-mono", children: formatCurrency(item.totalCostAmountCents / 100, item.costCurrency) }), item.unitCostAmountCents != null && item.quantity > 1 ? (_jsxs("span", { className: "ml-1.5 text-muted-foreground text-xs", children: ["(", formatCurrency(item.unitCostAmountCents / 100, item.costCurrency), " \u00D7", " ", item.quantity, ")"] })) : null] })) : (_jsx("span", { className: "text-muted-foreground text-xs", children: messages.bookingItemList.values.costUnavailable })) })] })] }), _jsx(BookingItemTravelers, { bookingId: bookingId, itemId: item.id })] }));
81
+ return (_jsx("div", { className: "space-y-3", children: _jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(DetailBlock, { label: messages.bookingItemList.detail.description, children: item.description ? (_jsx("p", { className: "whitespace-pre-wrap text-sm", children: item.description })) : (_jsx("p", { className: "text-muted-foreground text-xs italic", children: messages.bookingItemList.detail.noDescription })) }), _jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(DetailBlock, { label: messages.bookingItemList.detail.dates, children: _jsxs("div", { className: "flex items-baseline gap-1.5 text-sm", children: [_jsx(Calendar, { className: "h-3.5 w-3.5 self-center text-muted-foreground" }), dateRange ?? (_jsx("span", { className: "text-muted-foreground text-xs", children: messages.bookingItemList.values.serviceDateUnavailable }))] }) }), _jsx(DetailBlock, { label: messages.bookingItemList.detail.cost, children: item.totalCostAmountCents != null && item.costCurrency ? (_jsxs("div", { className: "text-sm", children: [_jsx("span", { className: "font-mono", children: formatCurrency(item.totalCostAmountCents / 100, item.costCurrency) }), item.unitCostAmountCents != null && item.quantity > 1 ? (_jsxs("span", { className: "ml-1.5 text-muted-foreground text-xs", children: ["(", formatCurrency(item.unitCostAmountCents / 100, item.costCurrency), " \u00D7", " ", item.quantity, ")"] })) : null] })) : (_jsx("span", { className: "text-muted-foreground text-xs", children: messages.bookingItemList.values.costUnavailable })) })] })] }) }));
74
82
  }
75
83
  function DetailBlock({ label, children, }) {
76
84
  return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-muted-foreground text-xs uppercase tracking-wide", children: label }), _jsx("div", { children: children })] }));
@@ -87,6 +95,11 @@ function DetailBlock({ label, children, }) {
87
95
  * whatever the consumer's locale is.
88
96
  */
89
97
  function formatItemDateRange(item) {
98
+ // Pre-rendered departure label wins — it carries timezone + any
99
+ // start-point context the operator entered at booking time. The
100
+ // structured fields are still rendered as fallback for legacy rows.
101
+ if (item.departureLabelSnapshot)
102
+ return item.departureLabelSnapshot;
90
103
  const start = item.startsAt ? new Date(item.startsAt) : null;
91
104
  const end = item.endsAt ? new Date(item.endsAt) : null;
92
105
  if (start && Number.isFinite(start.getTime())) {
@@ -9,6 +9,12 @@ export interface BookingListFiltersPopoverProps {
9
9
  onProductIdChange: (productId: string | null) => void;
10
10
  optionId: string | null;
11
11
  onOptionIdChange: (optionId: string | null) => void;
12
+ /**
13
+ * Filter to bookings on a specific departure (availability slot).
14
+ * Picker is only populated when a product is selected.
15
+ */
16
+ availabilitySlotId: string | null;
17
+ onAvailabilitySlotIdChange: (availabilitySlotId: string | null) => void;
12
18
  supplierId: string | null;
13
19
  onSupplierIdChange: (supplierId: string | null) => void;
14
20
  productCategoryId: string | null;
@@ -31,5 +37,5 @@ export interface BookingListFiltersPopoverProps {
31
37
  onPaxMaxChange: (paxMax: string) => void;
32
38
  onFiltersChanged: () => void;
33
39
  }
34
- export declare function BookingListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, optionId, onOptionIdChange, supplierId, onSupplierIdChange, productCategoryId, onProductCategoryIdChange, personId, onPersonIdChange, organizationId, onOrganizationIdChange, dateRange, onDateRangeChange, paxMin, onPaxMinChange, paxMax, onPaxMaxChange, onFiltersChanged, }: BookingListFiltersPopoverProps): import("react/jsx-runtime").JSX.Element;
40
+ export declare function BookingListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, optionId, onOptionIdChange, availabilitySlotId, onAvailabilitySlotIdChange, supplierId, onSupplierIdChange, productCategoryId, onProductCategoryIdChange, personId, onPersonIdChange, organizationId, onOrganizationIdChange, dateRange, onDateRangeChange, paxMin, onPaxMinChange, paxMax, onPaxMaxChange, onFiltersChanged, }: BookingListFiltersPopoverProps): import("react/jsx-runtime").JSX.Element;
35
41
  //# sourceMappingURL=booking-list-filters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"booking-list-filters.d.ts","sourceRoot":"","sources":["../../src/components/booking-list-filters.tsx"],"names":[],"mappings":"AAgCA,eAAO,MAAM,kBAAkB,YAAY,CAAA;AAE3C,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACrD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,yBAAyB,EAAE,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,sBAAsB,EAAE,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,iBAAiB,EAAE,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAA;IACzF,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED,wBAAgB,yBAAyB,CAAC,EACxC,IAAI,EACJ,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,EACzB,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,SAAS,EACT,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,MAAM,EACN,cAAc,EACd,gBAAgB,GACjB,EAAE,8BAA8B,2CAiShC"}
1
+ {"version":3,"file":"booking-list-filters.d.ts","sourceRoot":"","sources":["../../src/components/booking-list-filters.tsx"],"names":[],"mappings":"AAkCA,eAAO,MAAM,kBAAkB,YAAY,CAAA;AAE3C,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACrD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD;;;OAGG;IACH,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,0BAA0B,EAAE,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvD,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,yBAAyB,EAAE,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACrE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,sBAAsB,EAAE,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,iBAAiB,EAAE,CAAC,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAA;IACzF,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,gBAAgB,EAAE,MAAM,IAAI,CAAA;CAC7B;AAED,wBAAgB,yBAAyB,CAAC,EACxC,IAAI,EACJ,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EAChB,kBAAkB,EAClB,0BAA0B,EAC1B,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,EACzB,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,sBAAsB,EACtB,SAAS,EACT,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,MAAM,EACN,cAAc,EACd,gBAAgB,GACjB,EAAE,8BAA8B,2CAqUhC"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useSlots } from "@voyantjs/availability-react";
3
4
  import { bookingStatuses } from "@voyantjs/bookings-react";
4
5
  import { useOrganizations, usePeople } from "@voyantjs/crm-react";
5
6
  import { useProductCategories, useProductOptions, useProducts } from "@voyantjs/products-react";
@@ -16,13 +17,14 @@ import { ListFilter } from "lucide-react";
16
17
  import * as React from "react";
17
18
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
18
19
  export const BOOKING_STATUS_ALL = "__all__";
19
- export function BookingListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, optionId, onOptionIdChange, supplierId, onSupplierIdChange, productCategoryId, onProductCategoryIdChange, personId, onPersonIdChange, organizationId, onOrganizationIdChange, dateRange, onDateRangeChange, paxMin, onPaxMinChange, paxMax, onPaxMaxChange, onFiltersChanged, }) {
20
+ export function BookingListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, optionId, onOptionIdChange, availabilitySlotId, onAvailabilitySlotIdChange, supplierId, onSupplierIdChange, productCategoryId, onProductCategoryIdChange, personId, onPersonIdChange, organizationId, onOrganizationIdChange, dateRange, onDateRangeChange, paxMin, onPaxMinChange, paxMax, onPaxMaxChange, onFiltersChanged, }) {
20
21
  const messages = useBookingsUiMessagesOrDefault();
21
22
  const filterMessages = messages.bookingList.filters;
22
23
  const statusLabels = messages.common.bookingStatusLabels;
23
24
  const [selectedProduct, setSelectedProduct] = React.useState(null);
24
25
  const [productSearch, setProductSearch] = React.useState("");
25
26
  const [selectedOption, setSelectedOption] = React.useState(null);
27
+ const [selectedSlot, setSelectedSlot] = React.useState(null);
26
28
  const [selectedSupplier, setSelectedSupplier] = React.useState(null);
27
29
  const [supplierSearch, setSupplierSearch] = React.useState("");
28
30
  const [selectedProductCategory, setSelectedProductCategory] = React.useState(null);
@@ -43,6 +45,15 @@ export function BookingListFiltersPopover({ open, onOpenChange, activeFilterCoun
43
45
  enabled: productId !== null,
44
46
  });
45
47
  const productOptions = optionsData?.data ?? [];
48
+ // Departure picker is product-scoped. The list endpoint orders
49
+ // results most-recent-first; capping at 50 keeps the dropdown
50
+ // workable while still surfacing the active season's slots.
51
+ const { data: slotsData } = useSlots({
52
+ productId: productId ?? undefined,
53
+ limit: 50,
54
+ enabled: productId !== null,
55
+ });
56
+ const slots = slotsData?.data ?? [];
46
57
  const { data: suppliersData } = useSuppliers({
47
58
  search: supplierSearch || undefined,
48
59
  limit: 20,
@@ -91,7 +102,17 @@ export function BookingListFiltersPopover({ open, onOpenChange, activeFilterCoun
91
102
  setSelectedOption(match);
92
103
  }
93
104
  markChanged();
94
- }, items: productOptions, selectedItem: selectedOption, getKey: (option) => option.id, getLabel: (option) => option.name, getSecondary: (option) => option.code ?? undefined, placeholder: filterMessages.option, emptyText: productId ? filterMessages.optionEmpty : filterMessages.optionNeedsProduct, disabled: productId === null })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-category", children: filterMessages.categoryLabel }), _jsx(AsyncCombobox, { value: productCategoryId, onChange: (value) => {
105
+ }, items: productOptions, selectedItem: selectedOption, getKey: (option) => option.id, getLabel: (option) => option.name, getSecondary: (option) => option.code ?? undefined, placeholder: filterMessages.option, emptyText: productId ? filterMessages.optionEmpty : filterMessages.optionNeedsProduct, disabled: productId === null })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-departure", children: filterMessages.departureLabel }), _jsx(AsyncCombobox, { value: availabilitySlotId, onChange: (value) => {
106
+ onAvailabilitySlotIdChange(value);
107
+ if (!value)
108
+ setSelectedSlot(null);
109
+ else {
110
+ const match = slots.find((slot) => slot.id === value);
111
+ if (match)
112
+ setSelectedSlot(match);
113
+ }
114
+ markChanged();
115
+ }, items: slots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), getSecondary: (slot) => slot.status, placeholder: filterMessages.departure, emptyText: productId ? filterMessages.departureEmpty : filterMessages.departureNeedsProduct, disabled: productId === null })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-category", children: filterMessages.categoryLabel }), _jsx(AsyncCombobox, { value: productCategoryId, onChange: (value) => {
95
116
  onProductCategoryIdChange(value);
96
117
  if (!value)
97
118
  setSelectedProductCategory(null);
@@ -146,3 +167,24 @@ function formatPersonName(person) {
146
167
  const name = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
147
168
  return name || person.email || person.id;
148
169
  }
170
+ /**
171
+ * Human-friendly departure label. Renders the local date + start time
172
+ * in the slot's own timezone so the operator sees what the customer
173
+ * sees, not whatever the admin's browser locale converts it to.
174
+ */
175
+ function formatSlotLabel(slot) {
176
+ try {
177
+ const formatter = new Intl.DateTimeFormat(undefined, {
178
+ day: "numeric",
179
+ month: "short",
180
+ year: "numeric",
181
+ hour: "2-digit",
182
+ minute: "2-digit",
183
+ timeZone: slot.timezone,
184
+ });
185
+ return formatter.format(new Date(slot.startsAt));
186
+ }
187
+ catch {
188
+ return slot.startsAt;
189
+ }
190
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"booking-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-list.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,0BAA0B,CAAA;AAejC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAClD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAChC;AAgBD,wBAAgB,WAAW,CAAC,EAC1B,QAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,GACd,GAAE,gBAAqB,2CAwVvB"}
1
+ {"version":3,"file":"booking-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-list.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,0BAA0B,CAAA;AAejC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAClD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAChC;AAiBD,wBAAgB,WAAW,CAAC,EAC1B,QAAa,EACb,eAAe,EACf,eAAe,EACf,aAAa,GACd,GAAE,gBAAqB,2CA8WvB"}
@@ -19,9 +19,10 @@ const SORTABLE_COLUMNS = {
19
19
  pax: "pax",
20
20
  startDate: "startDate",
21
21
  endDate: "endDate",
22
+ createdAt: "createdAt",
22
23
  };
23
24
  const SKELETON_ROW_COUNT = 6;
24
- const TABLE_COLUMN_COUNT = 7;
25
+ const TABLE_COLUMN_COUNT = 9;
25
26
  export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, headerActions, } = {}) {
26
27
  const [search, setSearch] = React.useState("");
27
28
  const [status, setStatus] = React.useState(BOOKING_STATUS_ALL);
@@ -31,6 +32,7 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
31
32
  const [productCategoryId, setProductCategoryId] = React.useState(null);
32
33
  const [personId, setPersonId] = React.useState(null);
33
34
  const [organizationId, setOrganizationId] = React.useState(null);
35
+ const [availabilitySlotId, setAvailabilitySlotId] = React.useState(null);
34
36
  const [dateRange, setDateRange] = React.useState(null);
35
37
  const [paxMin, setPaxMin] = React.useState("");
36
38
  const [paxMax, setPaxMax] = React.useState("");
@@ -49,6 +51,7 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
49
51
  status: status === BOOKING_STATUS_ALL ? undefined : status,
50
52
  productId: productId ?? undefined,
51
53
  optionId: optionId ?? undefined,
54
+ availabilitySlotId: availabilitySlotId ?? undefined,
52
55
  supplierId: supplierId ?? undefined,
53
56
  productCategoryId: productCategoryId ?? undefined,
54
57
  personId: personId ?? undefined,
@@ -93,6 +96,7 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
93
96
  const activeFilterCount = (status !== BOOKING_STATUS_ALL ? 1 : 0) +
94
97
  (productId !== null ? 1 : 0) +
95
98
  (optionId !== null ? 1 : 0) +
99
+ (availabilitySlotId !== null ? 1 : 0) +
96
100
  (supplierId !== null ? 1 : 0) +
97
101
  (productCategoryId !== null ? 1 : 0) +
98
102
  (personId !== null ? 1 : 0) +
@@ -105,6 +109,7 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
105
109
  setStatus(BOOKING_STATUS_ALL);
106
110
  setProductId(null);
107
111
  setOptionId(null);
112
+ setAvailabilitySlotId(null);
108
113
  setSupplierId(null);
109
114
  setProductCategoryId(null);
110
115
  setPersonId(null);
@@ -120,19 +125,23 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
120
125
  return (_jsxs("div", { "data-slot": "booking-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [_jsx(Label, { htmlFor: "bookings-search", className: "sr-only", children: messages.bookingList.searchPlaceholder }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { id: "bookings-search", placeholder: messages.bookingList.searchPlaceholder, value: search, onChange: (event) => {
121
126
  setSearch(event.target.value);
122
127
  resetOffset();
123
- }, className: "pl-9" })] }), _jsx(BookingListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: setStatus, productId: productId, onProductIdChange: setProductId, optionId: optionId, onOptionIdChange: setOptionId, supplierId: supplierId, onSupplierIdChange: setSupplierId, productCategoryId: productCategoryId, onProductCategoryIdChange: setProductCategoryId, personId: personId, onPersonIdChange: setPersonId, organizationId: organizationId, onOrganizationIdChange: setOrganizationId, dateRange: dateRange, onDateRangeChange: setDateRange, paxMin: paxMin, onPaxMinChange: setPaxMin, paxMax: paxMax, onPaxMaxChange: setPaxMax, onFiltersChanged: resetOffset }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), filterMessages.clear] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [headerActions, _jsxs(Button, { onClick: () => {
128
+ }, className: "pl-9" })] }), _jsx(BookingListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: setStatus, productId: productId, onProductIdChange: (next) => {
129
+ setProductId(next);
130
+ // Slot picker is product-scoped; clear when the product changes.
131
+ setAvailabilitySlotId(null);
132
+ }, optionId: optionId, onOptionIdChange: setOptionId, availabilitySlotId: availabilitySlotId, onAvailabilitySlotIdChange: setAvailabilitySlotId, supplierId: supplierId, onSupplierIdChange: setSupplierId, productCategoryId: productCategoryId, onProductCategoryIdChange: setProductCategoryId, personId: personId, onPersonIdChange: setPersonId, organizationId: organizationId, onOrganizationIdChange: setOrganizationId, dateRange: dateRange, onDateRangeChange: setDateRange, paxMin: paxMin, onPaxMinChange: setPaxMin, paxMax: paxMax, onPaxMaxChange: setPaxMax, onFiltersChanged: resetOffset }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), filterMessages.clear] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [headerActions, _jsxs(Button, { onClick: () => {
124
133
  if (onCreateBooking) {
125
134
  onCreateBooking();
126
135
  return;
127
136
  }
128
137
  setEditing(undefined);
129
138
  setDialogOpen(true);
130
- }, children: [_jsx(Plus, { className: "mr-2 size-4" }), messages.bookingList.newBooking] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.bookingNumber, field: SORTABLE_COLUMNS.bookingNumber, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: columnMessages.whatBooked }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.status, field: SORTABLE_COLUMNS.status, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.sellAmount, field: SORTABLE_COLUMNS.sellAmount, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.pax, field: SORTABLE_COLUMNS.pax, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.startDate, field: SORTABLE_COLUMNS.startDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.endDate, field: SORTABLE_COLUMNS.endDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(BookingTableSkeleton, { rows: SKELETON_ROW_COUNT })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: messages.bookingList.loadingError }) })) : bookings.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: messages.bookingList.empty }) })) : (bookings.map((booking) => (_jsxs(TableRow, { onClick: () => handleSelect(booking), className: "cursor-pointer", children: [_jsx(TableCell, { className: "font-medium", children: booking.bookingNumber }), _jsx(TableCell, { children: formatBookingItems(booking, messages.bookingList.itemsMore) }), _jsx(TableCell, { children: _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: statusLabels[booking.status] }) }), _jsx(TableCell, { children: booking.sellAmountCents == null
139
+ }, children: [_jsx(Plus, { className: "mr-2 size-4" }), messages.bookingList.newBooking] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.bookingNumber, field: SORTABLE_COLUMNS.bookingNumber, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: columnMessages.whatBooked }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.status, field: SORTABLE_COLUMNS.status, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.sellAmount, field: SORTABLE_COLUMNS.sellAmount, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.pax, field: SORTABLE_COLUMNS.pax, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.startDate, field: SORTABLE_COLUMNS.startDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.endDate, field: SORTABLE_COLUMNS.endDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: columnMessages.lead }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.createdAt, field: SORTABLE_COLUMNS.createdAt, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(BookingTableSkeleton, { rows: SKELETON_ROW_COUNT })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: messages.bookingList.loadingError }) })) : bookings.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: messages.bookingList.empty }) })) : (bookings.map((booking) => (_jsxs(TableRow, { onClick: () => handleSelect(booking), className: "cursor-pointer", children: [_jsx(TableCell, { className: "font-medium", children: booking.bookingNumber }), _jsx(TableCell, { children: formatBookingItems(booking, messages.bookingList.itemsMore) }), _jsx(TableCell, { children: _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: statusLabels[booking.status] }) }), _jsx(TableCell, { children: booking.sellAmountCents == null
131
140
  ? "—"
132
141
  : `${formatNumber(booking.sellAmountCents / 100, {
133
142
  minimumFractionDigits: 2,
134
143
  maximumFractionDigits: 2,
135
- })} ${booking.sellCurrency}` }), _jsx(TableCell, { children: booking.pax ?? "—" }), _jsx(TableCell, { children: formatBookingDateTime(booking.startsAt ?? booking.startDate, formatDateTime) }), _jsx(TableCell, { children: formatBookingDateTime(booking.endsAt ?? booking.endDate, formatDateTime) })] }, booking.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(messages.bookingList.showingSummary, {
144
+ })} ${booking.sellCurrency}` }), _jsx(TableCell, { children: booking.pax ?? "—" }), _jsx(TableCell, { children: formatBookingDateTime(booking.startsAt ?? booking.startDate, formatDateTime) }), _jsx(TableCell, { children: formatBookingDateTime(booking.endsAt ?? booking.endDate, formatDateTime) }), _jsx(TableCell, { children: formatLead(booking) }), _jsx(TableCell, { children: formatBookingDateTime(booking.createdAt, formatDateTime) })] }, booking.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(messages.bookingList.showingSummary, {
136
145
  count: bookings.length,
137
146
  total,
138
147
  }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((prev) => Math.max(0, prev - pageSize)), children: messages.bookingList.previousPage }), _jsx("span", { children: formatMessage(messages.bookingList.pageSummary, {
@@ -158,9 +167,8 @@ function formatBookingItems(booking, moreTemplate) {
158
167
  if (!first)
159
168
  return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
160
169
  const label = first.productName ?? first.title;
161
- if (rest.length === 0)
162
- return label;
163
- return (_jsxs("span", { children: [label, _jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: formatMessage(moreTemplate, { count: rest.length }) })] }));
170
+ const moreSuffix = rest.length === 0 ? "" : ` ${formatMessage(moreTemplate, { count: rest.length })}`;
171
+ return (_jsxs("div", { className: "max-w-[320px] truncate", title: `${label}${moreSuffix}`, children: [label, rest.length > 0 ? (_jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: formatMessage(moreTemplate, { count: rest.length }) })) : null] }));
164
172
  }
165
173
  function formatBookingDateTime(value, formatDateTime) {
166
174
  if (!value)
@@ -170,3 +178,9 @@ function formatBookingDateTime(value, formatDateTime) {
170
178
  }
171
179
  return formatDateTime(value);
172
180
  }
181
+ function formatLead(booking) {
182
+ const name = [booking.contactFirstName, booking.contactLastName].filter(Boolean).join(" ").trim();
183
+ if (name)
184
+ return name;
185
+ return booking.contactEmail ?? "—";
186
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"supplier-status-list.d.ts","sourceRoot":"","sources":["../../src/components/supplier-status-list.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AASD,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,2CA+GxE"}
1
+ {"version":3,"file":"supplier-status-list.d.ts","sourceRoot":"","sources":["../../src/components/supplier-status-list.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AASD,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,2CAyIxE"}