@voyantjs/bookings-ui 0.62.3 → 0.63.1
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 +18 -21
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +8 -2
- package/dist/components/booking-detail-page.d.ts +32 -1
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +53 -10
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +18 -5
- package/dist/components/booking-list-filters.d.ts +7 -1
- package/dist/components/booking-list-filters.d.ts.map +1 -1
- package/dist/components/booking-list-filters.js +44 -2
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +21 -7
- package/dist/components/option-units-stepper-section.d.ts +11 -1
- package/dist/components/option-units-stepper-section.d.ts.map +1 -1
- package/dist/components/option-units-stepper-section.js +41 -14
- package/dist/components/supplier-status-list.d.ts.map +1 -1
- package/dist/components/supplier-status-list.js +46 -9
- package/dist/i18n/en.d.ts +10 -19
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +13 -22
- package/dist/i18n/messages.d.ts +10 -8
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +20 -38
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +10 -19
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +13 -22
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/package.json +30 -30
- package/dist/components/booking-workspace-page.d.ts +0 -61
- package/dist/components/booking-workspace-page.d.ts.map +0 -1
- package/dist/components/booking-workspace-page.js +0 -113
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
|
|
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
|
-
`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAIA,OAAO,EAML,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AA0WjC,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,GACjB,EAAE,wBAAwB,2CAqB1B;AAED,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,gBAAgB,EAChB,OAAc,EACd,QAAQ,GACT,EAAE,sBAAsB,2CA+2BxB"}
|
|
@@ -93,6 +93,9 @@ function stripUnitSuffix(name) {
|
|
|
93
93
|
const idx = name.lastIndexOf(" - ");
|
|
94
94
|
return idx > 0 ? name.slice(0, idx) : name;
|
|
95
95
|
}
|
|
96
|
+
function isRoomUnit(unit) {
|
|
97
|
+
return unit.unitType === "room";
|
|
98
|
+
}
|
|
96
99
|
/**
|
|
97
100
|
* Any payment-schedule entry the operator has marked as already
|
|
98
101
|
* paid. Drives the smart-default booking status on submit — if money
|
|
@@ -383,6 +386,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
383
386
|
});
|
|
384
387
|
return optionSlots.length > 0 ? optionSlots : allOpenSlots;
|
|
385
388
|
}, [slotsData?.data, slotsFromIso, product.optionId, allOpenSlots]);
|
|
389
|
+
const selectedSlot = React.useMemo(() => slots.find((slot) => slot.id === slotId) ?? null, [slots, slotId]);
|
|
386
390
|
const setSelectedSlot = React.useCallback((nextSlotId) => {
|
|
387
391
|
const selectedSlot = nextSlotId ? allOpenSlots.find((slot) => slot.id === nextSlotId) : null;
|
|
388
392
|
if (selectedSlot?.optionId && selectedSlot.optionId !== product.optionId) {
|
|
@@ -427,7 +431,7 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
427
431
|
// (assignment, redistribution) keeps working — `redistributeByAge`
|
|
428
432
|
// moves the traveler to the matching age-banded unit at submit.
|
|
429
433
|
const roomUnitOptions = React.useMemo(() => {
|
|
430
|
-
const units = roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? []);
|
|
434
|
+
const units = (roomUnits.length > 0 ? roomUnits : (slotUnitAvailability.data?.data ?? [])).filter(isRoomUnit);
|
|
431
435
|
if (units.length === 0)
|
|
432
436
|
return [];
|
|
433
437
|
const optionGroups = new Map();
|
|
@@ -728,7 +732,9 @@ export function BookingCreateForm({ onCreated, defaultProductId, enabled = true,
|
|
|
728
732
|
const isSubmitting = createBookingMutation.isPending;
|
|
729
733
|
return (_jsxs("div", { className: "grid min-h-0 flex-1 gap-6 lg:grid-cols-12", children: [_jsxs("div", { className: "flex min-h-0 min-w-0 flex-col lg:col-span-8", children: [_jsxs("div", { className: "flex flex-1 flex-col gap-4 overflow-y-auto px-1 pb-2", children: [_jsx(ProductPickerSection, { value: product, onChange: setProduct, enabled: enabled, lockProduct: Boolean(defaultProductId), labels: {
|
|
730
734
|
optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
|
|
731
|
-
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: slots.find((s) => s.id === slotId) ?? null, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", clearable: true })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange: setRooms, productId: product.productId, slotId: slotId, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange,
|
|
735
|
+
}, showOptionPicker: false }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsx(AsyncCombobox, { value: slotId, onChange: (v) => setSelectedSlot(v), items: slots, selectedItem: slots.find((s) => s.id === slotId) ?? null, getKey: (slot) => slot.id, getLabel: (slot) => formatSlotLabel(slot), placeholder: messages.bookingCreateDialog.placeholders.departure, emptyText: messages.bookingCreateDialog.placeholders.departureEmpty, triggerClassName: "w-full", clearable: true })] })) : null, product.productId && slotId ? (_jsx(OptionUnitsStepperSection, { value: rooms, onChange: setRooms, productId: product.productId, slotId: slotId, optionId: product.optionId, enabled: enabled, onUnitsChange: handleRoomUnitsChange, slotHasFiniteCapacity: Boolean(selectedSlot) &&
|
|
736
|
+
!selectedSlot?.unlimited &&
|
|
737
|
+
typeof selectedSlot?.remainingPax === "number", labels: {
|
|
732
738
|
heading: messages.bookingCreateDialog.labels.roomsHeading,
|
|
733
739
|
noOption: messages.bookingCreateDialog.labels.roomsNoOption,
|
|
734
740
|
noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
|
|
@@ -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;
|
|
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 {
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
|
75
|
-
|
|
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":"
|
|
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
|
|
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, {
|
|
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({
|
|
77
|
+
function ItemDetailPanel({ item }) {
|
|
70
78
|
const messages = useBookingsUiMessagesOrDefault();
|
|
71
79
|
const { formatCurrency } = useBookingsUiI18nOrDefault();
|
|
72
80
|
const dateRange = formatItemDateRange(item);
|
|
73
|
-
return (
|
|
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":"
|
|
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-
|
|
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;
|
|
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"}
|