@voyantjs/bookings-ui 0.62.2 → 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 +18 -21
- 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/supplier-status-list.d.ts.map +1 -1
- package/dist/components/supplier-status-list.js +46 -9
- package/dist/i18n/en.d.ts +9 -19
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +12 -22
- package/dist/i18n/messages.d.ts +9 -8
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +18 -38
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +9 -19
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +12 -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,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"}
|
|
@@ -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 =
|
|
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:
|
|
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
|
-
|
|
162
|
-
|
|
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,
|
|
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"}
|