@voyant-travel/trips-react 0.110.2
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/LICENSE +201 -0
- package/README.md +30 -0
- package/dist/admin/admin-trips-page-controls.d.ts +28 -0
- package/dist/admin/admin-trips-page-controls.d.ts.map +1 -0
- package/dist/admin/admin-trips-page-controls.js +28 -0
- package/dist/admin/admin-trips-page-model.d.ts +87 -0
- package/dist/admin/admin-trips-page-model.d.ts.map +1 -0
- package/dist/admin/admin-trips-page-model.js +457 -0
- package/dist/admin/admin-trips-page.d.ts +6 -0
- package/dist/admin/admin-trips-page.d.ts.map +1 -0
- package/dist/admin/admin-trips-page.js +322 -0
- package/dist/admin/admin-trips-panels.d.ts +11 -0
- package/dist/admin/admin-trips-panels.d.ts.map +1 -0
- package/dist/admin/admin-trips-panels.js +11 -0
- package/dist/admin/index.d.ts +63 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +119 -0
- package/dist/admin/pages/trip-detail-page.d.ts +10 -0
- package/dist/admin/pages/trip-detail-page.d.ts.map +1 -0
- package/dist/admin/pages/trip-detail-page.js +12 -0
- package/dist/admin/trip-component-display.d.ts +10 -0
- package/dist/admin/trip-component-display.d.ts.map +1 -0
- package/dist/admin/trip-component-display.js +137 -0
- package/dist/admin/trip-detail-host.d.ts +12 -0
- package/dist/admin/trip-detail-host.d.ts.map +1 -0
- package/dist/admin/trip-detail-host.js +37 -0
- package/dist/admin/trip-detail-record-model.d.ts +30 -0
- package/dist/admin/trip-detail-record-model.d.ts.map +1 -0
- package/dist/admin/trip-detail-record-model.js +56 -0
- package/dist/admin/trip-detail-record.d.ts +47 -0
- package/dist/admin/trip-detail-record.d.ts.map +1 -0
- package/dist/admin/trip-detail-record.js +170 -0
- package/dist/admin/trip-list-filters.d.ts +33 -0
- package/dist/admin/trip-list-filters.d.ts.map +1 -0
- package/dist/admin/trip-list-filters.js +94 -0
- package/dist/admin/trips-host.d.ts +15 -0
- package/dist/admin/trips-host.d.ts.map +1 -0
- package/dist/admin/trips-host.js +233 -0
- package/dist/admin/trips-panels/catalog-configurator.d.ts +34 -0
- package/dist/admin/trips-panels/catalog-configurator.d.ts.map +1 -0
- package/dist/admin/trips-panels/catalog-configurator.js +200 -0
- package/dist/admin/trips-panels/catalog-options.d.ts +58 -0
- package/dist/admin/trips-panels/catalog-options.d.ts.map +1 -0
- package/dist/admin/trips-panels/catalog-options.js +124 -0
- package/dist/admin/trips-panels/committed-component-card.d.ts +27 -0
- package/dist/admin/trips-panels/committed-component-card.d.ts.map +1 -0
- package/dist/admin/trips-panels/committed-component-card.js +44 -0
- package/dist/admin/trips-panels/display.d.ts +51 -0
- package/dist/admin/trips-panels/display.d.ts.map +1 -0
- package/dist/admin/trips-panels/display.js +336 -0
- package/dist/admin/trips-panels/flight-configurator.d.ts +34 -0
- package/dist/admin/trips-panels/flight-configurator.d.ts.map +1 -0
- package/dist/admin/trips-panels/flight-configurator.js +208 -0
- package/dist/admin/trips-panels/manual-configurators.d.ts +16 -0
- package/dist/admin/trips-panels/manual-configurators.d.ts.map +1 -0
- package/dist/admin/trips-panels/manual-configurators.js +41 -0
- package/dist/admin/trips-panels/pending-component-card.d.ts +16 -0
- package/dist/admin/trips-panels/pending-component-card.d.ts.map +1 -0
- package/dist/admin/trips-panels/pending-component-card.js +29 -0
- package/dist/admin/trips-panels/shared.d.ts +122 -0
- package/dist/admin/trips-panels/shared.d.ts.map +1 -0
- package/dist/admin/trips-panels/shared.js +152 -0
- package/dist/admin/trips-panels/travelers-section.d.ts +53 -0
- package/dist/admin/trips-panels/travelers-section.d.ts.map +1 -0
- package/dist/admin/trips-panels/travelers-section.js +183 -0
- package/dist/admin/trips-panels/trip-preview-rail.d.ts +52 -0
- package/dist/admin/trips-panels/trip-preview-rail.d.ts.map +1 -0
- package/dist/admin/trips-panels/trip-preview-rail.js +122 -0
- package/dist/cache.d.ts +9 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +21 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +51 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/use-price-trip.d.ts +3 -0
- package/dist/hooks/use-price-trip.d.ts.map +1 -0
- package/dist/hooks/use-price-trip.js +17 -0
- package/dist/hooks/use-reserve-trip.d.ts +3 -0
- package/dist/hooks/use-reserve-trip.d.ts.map +1 -0
- package/dist/hooks/use-reserve-trip.js +17 -0
- package/dist/hooks/use-trip-checkout.d.ts +3 -0
- package/dist/hooks/use-trip-checkout.d.ts.map +1 -0
- package/dist/hooks/use-trip-checkout.js +17 -0
- package/dist/hooks/use-trip-components.d.ts +40 -0
- package/dist/hooks/use-trip-components.d.ts.map +1 -0
- package/dist/hooks/use-trip-components.js +13 -0
- package/dist/hooks/use-trip.d.ts +5 -0
- package/dist/hooks/use-trip.d.ts.map +1 -0
- package/dist/hooks/use-trip.js +13 -0
- package/dist/hooks/use-trips.d.ts +6 -0
- package/dist/hooks/use-trips.d.ts.map +1 -0
- package/dist/hooks/use-trips.js +12 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/operations.d.ts +212 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +92 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +1 -0
- package/dist/query-keys.d.ts +10 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +9 -0
- package/dist/query-options.d.ts +167 -0
- package/dist/query-options.d.ts.map +1 -0
- package/dist/query-options.js +22 -0
- package/dist/schemas.d.ts +69 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +16 -0
- package/package.json +133 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useOperatorAdminMessages as useAdminMessages } from "@voyant-travel/admin";
|
|
4
|
+
import { useCatalogSearch } from "@voyant-travel/catalog-react";
|
|
5
|
+
import { AsyncCombobox } from "@voyant-travel/ui/components/async-combobox";
|
|
6
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
7
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
8
|
+
import { Checkbox } from "@voyant-travel/ui/components/checkbox";
|
|
9
|
+
import { DateRangePicker } from "@voyant-travel/ui/components/date-picker";
|
|
10
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
11
|
+
import { Label } from "@voyant-travel/ui/components/label";
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@voyant-travel/ui/components/popover";
|
|
13
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyant-travel/ui/components/select";
|
|
14
|
+
import { ListFilter } from "lucide-react";
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
export const TRIP_STATUS_ALL = "__all__";
|
|
17
|
+
function catalogHitLabel(hit) {
|
|
18
|
+
return (catalogHitStringField(hit, "name") ??
|
|
19
|
+
catalogHitStringField(hit, "title") ??
|
|
20
|
+
catalogHitStringField(hit, "hotel.name") ??
|
|
21
|
+
hit.id);
|
|
22
|
+
}
|
|
23
|
+
function catalogHitStringField(hit, field) {
|
|
24
|
+
const value = hit.document.fields[field];
|
|
25
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
26
|
+
}
|
|
27
|
+
function CatalogFilterCombobox({ label, value, onValueChange, items, selectedItem, onSelectedItemChange, onSearchChange, placeholder, emptyText, onFiltersChanged, }) {
|
|
28
|
+
const id = `trips-filter-${label.toLowerCase()}`;
|
|
29
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, children: label }), _jsx(AsyncCombobox, { value: value, onChange: (nextValue) => {
|
|
30
|
+
onValueChange(nextValue);
|
|
31
|
+
const match = nextValue ? items.find((hit) => hit.id === nextValue) : null;
|
|
32
|
+
onSelectedItemChange(match ?? null);
|
|
33
|
+
onFiltersChanged();
|
|
34
|
+
}, items: items, selectedItem: selectedItem, getKey: (hit) => hit.id, getLabel: catalogHitLabel, getSecondary: (hit) => catalogHitStringField(hit, "source.kind") ?? undefined, onSearchChange: onSearchChange, placeholder: placeholder, emptyText: emptyText })] }));
|
|
35
|
+
}
|
|
36
|
+
export function TripListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, accommodationId, onAccommodationIdChange, cruiseId, onCruiseIdChange, hasFlight, onHasFlightChange, totalMin, onTotalMinChange, totalMax, onTotalMaxChange, createdRange, onCreatedRangeChange, onFiltersChanged, }) {
|
|
37
|
+
const messages = useAdminMessages().trips;
|
|
38
|
+
const filterMessages = messages.filters;
|
|
39
|
+
const statusOptions = [
|
|
40
|
+
{ value: TRIP_STATUS_ALL, label: filterMessages.allStatuses },
|
|
41
|
+
{ value: "draft", label: messages.statuses.draft },
|
|
42
|
+
{ value: "priced", label: messages.statuses.priced },
|
|
43
|
+
{ value: "reserve_in_progress", label: messages.statuses.reserve_in_progress },
|
|
44
|
+
{ value: "reserved", label: messages.statuses.reserved },
|
|
45
|
+
{ value: "checkout_started", label: messages.statuses.checkout_started },
|
|
46
|
+
{ value: "booked", label: messages.statuses.booked },
|
|
47
|
+
{ value: "failed", label: messages.statuses.failed },
|
|
48
|
+
{ value: "cancelled", label: messages.statuses.cancelled },
|
|
49
|
+
];
|
|
50
|
+
const [selectedProduct, setSelectedProduct] = React.useState(null);
|
|
51
|
+
const [productSearch, setProductSearch] = React.useState("");
|
|
52
|
+
const [selectedStay, setSelectedStay] = React.useState(null);
|
|
53
|
+
const [staySearch, setStaySearch] = React.useState("");
|
|
54
|
+
const [selectedCruise, setSelectedCruise] = React.useState(null);
|
|
55
|
+
const [cruiseSearch, setCruiseSearch] = React.useState("");
|
|
56
|
+
const productsQuery = useCatalogSearch({
|
|
57
|
+
vertical: "products",
|
|
58
|
+
query: productSearch,
|
|
59
|
+
mode: "keyword",
|
|
60
|
+
pagination: { limit: 20 },
|
|
61
|
+
});
|
|
62
|
+
const productHits = productsQuery.data?.hits ?? [];
|
|
63
|
+
const staysQuery = useCatalogSearch({
|
|
64
|
+
vertical: "accommodations",
|
|
65
|
+
query: staySearch,
|
|
66
|
+
mode: "keyword",
|
|
67
|
+
pagination: { limit: 20 },
|
|
68
|
+
});
|
|
69
|
+
const stayHits = staysQuery.data?.hits ?? [];
|
|
70
|
+
const cruisesQuery = useCatalogSearch({
|
|
71
|
+
vertical: "cruises",
|
|
72
|
+
query: cruiseSearch,
|
|
73
|
+
mode: "keyword",
|
|
74
|
+
pagination: { limit: 20 },
|
|
75
|
+
});
|
|
76
|
+
const cruiseHits = cruisesQuery.data?.hits ?? [];
|
|
77
|
+
const markChanged = () => onFiltersChanged();
|
|
78
|
+
return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4" }), filterMessages.trigger, activeFilterCount > 0 ? (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount })) : null] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "trips-filter-status", children: filterMessages.status }), _jsxs(Select, { value: status, onValueChange: (value) => {
|
|
79
|
+
onStatusChange((value ?? TRIP_STATUS_ALL));
|
|
80
|
+
markChanged();
|
|
81
|
+
}, children: [_jsx(SelectTrigger, { id: "trips-filter-status", className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: statusOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] }), _jsx(CatalogFilterCombobox, { label: filterMessages.products, value: productId, onValueChange: onProductIdChange, items: productHits, selectedItem: selectedProduct, onSelectedItemChange: setSelectedProduct, onSearchChange: setProductSearch, placeholder: filterMessages.anyProduct, emptyText: filterMessages.noProducts, onFiltersChanged: markChanged }), _jsx(CatalogFilterCombobox, { label: filterMessages.stays, value: accommodationId, onValueChange: onAccommodationIdChange, items: stayHits, selectedItem: selectedStay, onSelectedItemChange: setSelectedStay, onSearchChange: setStaySearch, placeholder: filterMessages.anyStay, emptyText: filterMessages.noStays, onFiltersChanged: markChanged }), _jsx(CatalogFilterCombobox, { label: filterMessages.cruises, value: cruiseId, onValueChange: onCruiseIdChange, items: cruiseHits, selectedItem: selectedCruise, onSelectedItemChange: setSelectedCruise, onSearchChange: setCruiseSearch, placeholder: filterMessages.anyCruise, emptyText: filterMessages.noCruises, onFiltersChanged: markChanged }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "trips-filter-flight", checked: hasFlight, onCheckedChange: (checked) => {
|
|
82
|
+
onHasFlightChange(checked === true);
|
|
83
|
+
markChanged();
|
|
84
|
+
} }), _jsx(Label, { htmlFor: "trips-filter-flight", className: "font-normal", children: filterMessages.hasFlight })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: filterMessages.total }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, step: "0.01", placeholder: filterMessages.min, value: totalMin, onChange: (event) => {
|
|
85
|
+
onTotalMinChange(event.target.value);
|
|
86
|
+
markChanged();
|
|
87
|
+
}, className: "w-full", "aria-label": filterMessages.totalMinAria }), _jsx("span", { className: "text-muted-foreground", children: "-" }), _jsx(Input, { type: "number", min: 0, step: "0.01", placeholder: filterMessages.max, value: totalMax, onChange: (event) => {
|
|
88
|
+
onTotalMaxChange(event.target.value);
|
|
89
|
+
markChanged();
|
|
90
|
+
}, className: "w-full", "aria-label": filterMessages.totalMaxAria })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: filterMessages.createdAt }), _jsx(DateRangePicker, { value: createdRange, onChange: (value) => {
|
|
91
|
+
onCreatedRangeChange(value);
|
|
92
|
+
markChanged();
|
|
93
|
+
}, placeholder: filterMessages.anyDate, clearable: true, className: "w-full" })] })] }) })] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ListTripsParams } from "../operations.js";
|
|
2
|
+
/**
|
|
3
|
+
* Initial list parameters mirrored by the `trips-index`
|
|
4
|
+
* contribution's loader so the SSR-seeded cache entry and the page's first
|
|
5
|
+
* `useQuery` line up on the same key.
|
|
6
|
+
*/
|
|
7
|
+
export declare const initialTripsListParams: ListTripsParams;
|
|
8
|
+
/**
|
|
9
|
+
* Packaged admin host for the trips list page (packaged-admin RFC Phase 3).
|
|
10
|
+
* Opening a trip (or composing a new one) resolves the `"trip.detail"`
|
|
11
|
+
* semantic destination (RFC §4.7); the page keeps its filter/sort/paging
|
|
12
|
+
* state locally (no URL search contract), so the host takes no props.
|
|
13
|
+
*/
|
|
14
|
+
export declare function TripsHost(): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=trips-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trips-host.d.ts","sourceRoot":"","sources":["../../src/admin/trips-host.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAoBvD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,eAKpC,CAAA;AAaD;;;;;GAKG;AACH,wBAAgB,SAAS,4CA8RxB"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: trips-react; existing UI surface stays co-located until a dedicated split preserves behavior and tests.
|
|
2
|
+
"use client";
|
|
3
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { useOperatorAdminMessages as useAdminMessages, useAdminNavigate, } from "@voyant-travel/admin";
|
|
6
|
+
import { formatMessage } from "@voyant-travel/i18n";
|
|
7
|
+
import { Badge } from "@voyant-travel/ui/components/badge";
|
|
8
|
+
import { Button } from "@voyant-travel/ui/components/button";
|
|
9
|
+
import { Input } from "@voyant-travel/ui/components/input";
|
|
10
|
+
import { Label } from "@voyant-travel/ui/components/label";
|
|
11
|
+
import { Skeleton } from "@voyant-travel/ui/components/skeleton";
|
|
12
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyant-travel/ui/components/table";
|
|
13
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, Plus, Search, X } from "lucide-react";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { useVoyantTripsContext } from "../provider.js";
|
|
16
|
+
import { listTripsQueryOptions } from "../query-options.js";
|
|
17
|
+
import { componentTitleFor, readComponentSchedule, sortComponentsBySchedule, } from "./trip-component-display.js";
|
|
18
|
+
import { TRIP_STATUS_ALL, TripListFiltersPopover, } from "./trip-list-filters.js";
|
|
19
|
+
const PAGE_SIZE = 25;
|
|
20
|
+
const SKELETON_ROWS = 6;
|
|
21
|
+
const TABLE_COLUMN_COUNT = 7;
|
|
22
|
+
/**
|
|
23
|
+
* Initial list parameters mirrored by the `trips-index`
|
|
24
|
+
* contribution's loader so the SSR-seeded cache entry and the page's first
|
|
25
|
+
* `useQuery` line up on the same key.
|
|
26
|
+
*/
|
|
27
|
+
export const initialTripsListParams = {
|
|
28
|
+
limit: PAGE_SIZE,
|
|
29
|
+
offset: 0,
|
|
30
|
+
sortBy: "updatedAt",
|
|
31
|
+
sortDir: "desc",
|
|
32
|
+
};
|
|
33
|
+
const statusBadgeVariant = {
|
|
34
|
+
draft: "secondary",
|
|
35
|
+
priced: "secondary",
|
|
36
|
+
reserve_in_progress: "secondary",
|
|
37
|
+
reserved: "default",
|
|
38
|
+
checkout_started: "secondary",
|
|
39
|
+
booked: "default",
|
|
40
|
+
failed: "destructive",
|
|
41
|
+
cancelled: "destructive",
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Packaged admin host for the trips list page (packaged-admin RFC Phase 3).
|
|
45
|
+
* Opening a trip (or composing a new one) resolves the `"trip.detail"`
|
|
46
|
+
* semantic destination (RFC §4.7); the page keeps its filter/sort/paging
|
|
47
|
+
* state locally (no URL search contract), so the host takes no props.
|
|
48
|
+
*/
|
|
49
|
+
export function TripsHost() {
|
|
50
|
+
const messages = useAdminMessages().trips;
|
|
51
|
+
const listMessages = messages.list;
|
|
52
|
+
const navigateTo = useAdminNavigate();
|
|
53
|
+
const { baseUrl, fetcher } = useVoyantTripsContext();
|
|
54
|
+
const client = React.useMemo(() => ({ baseUrl, fetcher }), [baseUrl, fetcher]);
|
|
55
|
+
const [search, setSearch] = React.useState("");
|
|
56
|
+
const [status, setStatus] = React.useState(TRIP_STATUS_ALL);
|
|
57
|
+
const [productId, setProductId] = React.useState(null);
|
|
58
|
+
const [accommodationId, setAccommodationId] = React.useState(null);
|
|
59
|
+
const [cruiseId, setCruiseId] = React.useState(null);
|
|
60
|
+
const [hasFlight, setHasFlight] = React.useState(false);
|
|
61
|
+
const [totalMin, setTotalMin] = React.useState("");
|
|
62
|
+
const [totalMax, setTotalMax] = React.useState("");
|
|
63
|
+
const [createdRange, setCreatedRange] = React.useState(null);
|
|
64
|
+
const [sortBy, setSortBy] = React.useState("updatedAt");
|
|
65
|
+
const [sortDir, setSortDir] = React.useState("desc");
|
|
66
|
+
const [offset, setOffset] = React.useState(0);
|
|
67
|
+
const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false);
|
|
68
|
+
const totalMinCents = parseAmountCents(totalMin);
|
|
69
|
+
const totalMaxCents = parseAmountCents(totalMax);
|
|
70
|
+
const params = React.useMemo(() => ({
|
|
71
|
+
limit: PAGE_SIZE,
|
|
72
|
+
offset,
|
|
73
|
+
sortBy,
|
|
74
|
+
sortDir,
|
|
75
|
+
...(search.trim() ? { search: search.trim() } : {}),
|
|
76
|
+
...(status !== TRIP_STATUS_ALL ? { status } : {}),
|
|
77
|
+
...(productId ? { productId } : {}),
|
|
78
|
+
...(accommodationId ? { accommodationId } : {}),
|
|
79
|
+
...(cruiseId ? { cruiseId } : {}),
|
|
80
|
+
...(hasFlight ? { hasFlight: true } : {}),
|
|
81
|
+
...(totalMinCents !== null ? { totalMinCents } : {}),
|
|
82
|
+
...(totalMaxCents !== null ? { totalMaxCents } : {}),
|
|
83
|
+
...(createdRange?.from ? { createdFrom: createdRange.from } : {}),
|
|
84
|
+
...(createdRange?.to ? { createdTo: createdRange.to } : {}),
|
|
85
|
+
}), [
|
|
86
|
+
offset,
|
|
87
|
+
sortBy,
|
|
88
|
+
sortDir,
|
|
89
|
+
search,
|
|
90
|
+
status,
|
|
91
|
+
productId,
|
|
92
|
+
accommodationId,
|
|
93
|
+
cruiseId,
|
|
94
|
+
hasFlight,
|
|
95
|
+
totalMinCents,
|
|
96
|
+
totalMaxCents,
|
|
97
|
+
createdRange,
|
|
98
|
+
]);
|
|
99
|
+
const activeFilterCount = (status !== TRIP_STATUS_ALL ? 1 : 0) +
|
|
100
|
+
(productId ? 1 : 0) +
|
|
101
|
+
(accommodationId ? 1 : 0) +
|
|
102
|
+
(cruiseId ? 1 : 0) +
|
|
103
|
+
(hasFlight ? 1 : 0) +
|
|
104
|
+
(totalMin !== "" || totalMax !== "" ? 1 : 0) +
|
|
105
|
+
(createdRange?.from || createdRange?.to ? 1 : 0);
|
|
106
|
+
const { data, isPending, isFetching, isError } = useQuery(listTripsQueryOptions(client, params));
|
|
107
|
+
const trips = data?.data ?? [];
|
|
108
|
+
const total = data?.total ?? 0;
|
|
109
|
+
const page = Math.floor(offset / PAGE_SIZE) + 1;
|
|
110
|
+
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
111
|
+
const showSkeleton = isPending || (isFetching && trips.length === 0);
|
|
112
|
+
const resetOffset = () => setOffset(0);
|
|
113
|
+
const handleSort = (field) => {
|
|
114
|
+
setOffset(0);
|
|
115
|
+
if (sortBy !== field) {
|
|
116
|
+
setSortBy(field);
|
|
117
|
+
setSortDir(field === "status" ? "asc" : "desc");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setSortDir((prev) => (prev === "asc" ? "desc" : "asc"));
|
|
121
|
+
};
|
|
122
|
+
const hasActiveFilters = search.trim() !== "" || activeFilterCount > 0;
|
|
123
|
+
const clearFilters = () => {
|
|
124
|
+
setSearch("");
|
|
125
|
+
setStatus(TRIP_STATUS_ALL);
|
|
126
|
+
setProductId(null);
|
|
127
|
+
setAccommodationId(null);
|
|
128
|
+
setCruiseId(null);
|
|
129
|
+
setHasFlight(false);
|
|
130
|
+
setTotalMin("");
|
|
131
|
+
setTotalMax("");
|
|
132
|
+
setCreatedRange(null);
|
|
133
|
+
resetOffset();
|
|
134
|
+
};
|
|
135
|
+
return (_jsxs("main", { className: "flex flex-col gap-6 p-6", children: [_jsx("div", { className: "flex flex-col gap-4 md:flex-row md:items-end md:justify-between", children: _jsxs("div", { children: [_jsx("h1", { className: "font-bold text-2xl tracking-tight", children: listMessages.title }), _jsx("p", { className: "text-muted-foreground text-sm", children: listMessages.description })] }) }), _jsxs("div", { 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: "trips-search", className: "sr-only", children: listMessages.searchLabel }), _jsx(Search, { className: "-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" }), _jsx(Input, { id: "trips-search", placeholder: listMessages.searchPlaceholder, value: search, onChange: (event) => {
|
|
136
|
+
setSearch(event.target.value);
|
|
137
|
+
resetOffset();
|
|
138
|
+
}, className: "pl-9" })] }), _jsx(TripListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: setStatus, productId: productId, onProductIdChange: setProductId, accommodationId: accommodationId, onAccommodationIdChange: setAccommodationId, cruiseId: cruiseId, onCruiseIdChange: setCruiseId, hasFlight: hasFlight, onHasFlightChange: setHasFlight, totalMin: totalMin, onTotalMinChange: setTotalMin, totalMax: totalMax, onTotalMaxChange: setTotalMax, createdRange: createdRange, onCreatedRangeChange: setCreatedRange, onFiltersChanged: resetOffset }), hasActiveFilters ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), listMessages.clearFilters] })) : null, _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => navigateTo("trip.detail", { tripId: "new" }), children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), listMessages.newTrip] }) })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: listMessages.columns.start }), _jsx(TableHead, { children: listMessages.columns.end }), _jsx(TableHead, { children: _jsx(SortHeader, { label: listMessages.columns.status, field: "status", sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: listMessages.columns.components }), _jsx(TableHead, { children: _jsx(SortHeader, { label: listMessages.columns.total, field: "total", sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: listMessages.columns.created, field: "createdAt", sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: listMessages.columns.updated, field: "updatedAt", sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(TripTableSkeleton, { rows: SKELETON_ROWS })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-destructive text-sm", children: listMessages.loadFailed }) })) : trips.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-muted-foreground text-sm", children: hasActiveFilters ? listMessages.emptyFiltered : listMessages.empty }) })) : (trips.map((trip) => (_jsx(TripRow, { trip: trip, messages: messages, onOpen: () => navigateTo("trip.detail", { tripId: trip.envelope.id }) }, trip.envelope.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-muted-foreground text-sm", children: [_jsx("span", { children: total === 0
|
|
139
|
+
? listMessages.countEmpty
|
|
140
|
+
: formatMessage(listMessages.countRange, {
|
|
141
|
+
start: String(trips.length === 0 ? 0 : offset + 1),
|
|
142
|
+
end: String(offset + trips.length),
|
|
143
|
+
total: String(total),
|
|
144
|
+
}) }), _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 - PAGE_SIZE)), children: listMessages.previous }), _jsx("span", { children: formatMessage(listMessages.pageOf, {
|
|
145
|
+
page: String(page),
|
|
146
|
+
pageCount: String(pageCount),
|
|
147
|
+
}) }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + PAGE_SIZE >= total, onClick: () => setOffset((prev) => prev + PAGE_SIZE), children: listMessages.next })] })] })] })] }));
|
|
148
|
+
}
|
|
149
|
+
function SortHeader({ label, field, sortBy, sortDir, onSort }) {
|
|
150
|
+
const active = sortBy === field;
|
|
151
|
+
const Icon = active ? (sortDir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
|
|
152
|
+
return (_jsxs("button", { type: "button", onClick: () => onSort(field), className: "-ml-2 inline-flex h-8 items-center gap-1 rounded-sm px-2 hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("span", { children: label }), _jsx(Icon, { className: `size-3.5 ${active ? "text-foreground" : "text-muted-foreground/60"}`, "aria-hidden": true })] }));
|
|
153
|
+
}
|
|
154
|
+
function TripTableSkeleton({ rows }) {
|
|
155
|
+
return (_jsx(_Fragment, { children: Array.from({ length: rows }).map((_, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-20 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-48" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) })] }, `skeleton-${idx}`))) }));
|
|
156
|
+
}
|
|
157
|
+
function TripRow({ trip, messages, onOpen, }) {
|
|
158
|
+
const envelope = trip.envelope;
|
|
159
|
+
const activeComponents = sortComponentsBySchedule(trip.components.filter((component) => component.status !== "removed"));
|
|
160
|
+
const schedule = tripScheduleBounds(activeComponents);
|
|
161
|
+
return (_jsxs(TableRow, { className: "cursor-pointer", onClick: onOpen, children: [_jsx(TableCell, { children: _jsx("span", { className: "whitespace-nowrap text-sm", children: formatDate(schedule.start) }) }), _jsx(TableCell, { children: _jsx("span", { className: "whitespace-nowrap text-sm", children: formatDate(schedule.end) }) }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusBadgeVariant[envelope.status], children: messages.statuses[envelope.status] }) }), _jsx(TableCell, { children: _jsxs("div", { className: "min-w-0", children: [_jsx("p", { children: componentCountLabel(activeComponents, messages) }), _jsx(TripComponentSummary, { components: activeComponents, messages: messages })] }) }), _jsx(TableCell, { children: formatMoney(envelope.aggregateTotalAmountCents, envelope.aggregateCurrency) }), _jsx(TableCell, { children: _jsx("span", { className: "whitespace-nowrap text-muted-foreground text-sm", children: formatDate(envelope.createdAt) }) }), _jsx(TableCell, { children: _jsx("span", { className: "whitespace-nowrap text-muted-foreground text-sm", children: formatDate(envelope.updatedAt) }) })] }));
|
|
162
|
+
}
|
|
163
|
+
function componentCountLabel(components, messages) {
|
|
164
|
+
const committedBookings = components.filter((component) => component.bookingId).length;
|
|
165
|
+
const externalRefs = components.filter((component) => component.orderId || component.paymentSessionId).length;
|
|
166
|
+
const parts = [
|
|
167
|
+
formatMessage(messages.list.componentCount, {
|
|
168
|
+
count: String(components.length),
|
|
169
|
+
label: components.length === 1 ? messages.list.componentSingular : messages.list.componentPlural,
|
|
170
|
+
}),
|
|
171
|
+
];
|
|
172
|
+
if (committedBookings > 0) {
|
|
173
|
+
parts.push(formatMessage(messages.list.componentCount, {
|
|
174
|
+
count: String(committedBookings),
|
|
175
|
+
label: committedBookings === 1 ? messages.list.bookingSingular : messages.list.bookingPlural,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
if (externalRefs > 0) {
|
|
179
|
+
parts.push(`${externalRefs} ${messages.list.external}`);
|
|
180
|
+
}
|
|
181
|
+
return parts.join(" · ");
|
|
182
|
+
}
|
|
183
|
+
function TripComponentSummary({ components, messages, }) {
|
|
184
|
+
const visibleComponents = components.slice(0, 2);
|
|
185
|
+
if (visibleComponents.length === 0) {
|
|
186
|
+
return (_jsx("p", { className: "max-w-64 truncate text-muted-foreground text-xs", children: messages.list.noComponents }));
|
|
187
|
+
}
|
|
188
|
+
return (_jsxs("p", { className: "max-w-64 truncate text-muted-foreground text-xs", children: [visibleComponents.map((component, index) => (_jsxs("span", { children: [index > 0 ? ", " : "", _jsx(TripComponentName, { component: component })] }, component.id))), components.length > visibleComponents.length
|
|
189
|
+
? ` +${components.length - visibleComponents.length}`
|
|
190
|
+
: ""] }));
|
|
191
|
+
}
|
|
192
|
+
function TripComponentName({ component }) {
|
|
193
|
+
return _jsx(_Fragment, { children: componentTitleFor(component) });
|
|
194
|
+
}
|
|
195
|
+
function tripScheduleBounds(components) {
|
|
196
|
+
let start = null;
|
|
197
|
+
let end = null;
|
|
198
|
+
for (const component of components) {
|
|
199
|
+
const schedule = readComponentSchedule(component);
|
|
200
|
+
if (schedule.start && (!start || new Date(schedule.start) < new Date(start))) {
|
|
201
|
+
start = schedule.start;
|
|
202
|
+
}
|
|
203
|
+
const candidateEnd = schedule.end ?? schedule.start;
|
|
204
|
+
if (candidateEnd && (!end || new Date(candidateEnd) > new Date(end))) {
|
|
205
|
+
end = candidateEnd;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { start, end };
|
|
209
|
+
}
|
|
210
|
+
function parseAmountCents(value) {
|
|
211
|
+
if (value.trim() === "")
|
|
212
|
+
return null;
|
|
213
|
+
const parsed = Number.parseFloat(value);
|
|
214
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
215
|
+
return null;
|
|
216
|
+
return Math.round(parsed * 100);
|
|
217
|
+
}
|
|
218
|
+
function formatDate(value) {
|
|
219
|
+
if (!value)
|
|
220
|
+
return "-";
|
|
221
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
222
|
+
if (Number.isNaN(date.getTime()))
|
|
223
|
+
return "-";
|
|
224
|
+
return date.toLocaleDateString();
|
|
225
|
+
}
|
|
226
|
+
function formatMoney(amountCents, currency) {
|
|
227
|
+
if (amountCents == null)
|
|
228
|
+
return "-";
|
|
229
|
+
return (amountCents / 100).toLocaleString(undefined, {
|
|
230
|
+
style: "currency",
|
|
231
|
+
currency: currency ?? "EUR",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Draft } from "@voyant-travel/bookings-react/journey";
|
|
2
|
+
import { type AvailabilitySlot, type CatalogVertical, type PendingComponent } from "./shared.js";
|
|
3
|
+
export declare function CatalogConfigurator({ pending, onChange, paxAdult, }: {
|
|
4
|
+
pending: Extract<PendingComponent, {
|
|
5
|
+
kind: "product" | "stay";
|
|
6
|
+
}>;
|
|
7
|
+
onChange(next: PendingComponent): void;
|
|
8
|
+
paxAdult: number;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function createCatalogBookingDraft({ vertical, entityId, sourceKind, sourceConnectionId, sourceRef, paxAdult, startsAt, endsAt, }: {
|
|
11
|
+
vertical: CatalogVertical;
|
|
12
|
+
entityId: string;
|
|
13
|
+
sourceKind: string;
|
|
14
|
+
sourceConnectionId: string | null;
|
|
15
|
+
sourceRef: string | null;
|
|
16
|
+
paxAdult: number;
|
|
17
|
+
startsAt: string;
|
|
18
|
+
endsAt: string;
|
|
19
|
+
}): Draft;
|
|
20
|
+
export declare function useProductDepartures(productId: string | null): import("@tanstack/react-query").UseQueryResult<{
|
|
21
|
+
rows: AvailabilitySlot[];
|
|
22
|
+
}, Error>;
|
|
23
|
+
export declare function ProductDeparturePicker({ slots, isLoading, isError, value, disabled, onChange, }: {
|
|
24
|
+
slots: ReadonlyArray<AvailabilitySlot>;
|
|
25
|
+
isLoading: boolean;
|
|
26
|
+
isError: boolean;
|
|
27
|
+
value: string;
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
onChange(slotId: string): void;
|
|
30
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
export declare function updateDraftSchedule(draft: Draft, startsAt: string, endsAt: string): Draft;
|
|
32
|
+
export declare function updateDraftDeparture(draft: Draft, slot: AvailabilitySlot): Draft;
|
|
33
|
+
export declare function updateDraftPax(draft: Draft, paxAdult: number): Draft;
|
|
34
|
+
//# sourceMappingURL=catalog-configurator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog-configurator.d.ts","sourceRoot":"","sources":["../../../src/admin/trips-panels/catalog-configurator.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAAc,MAAM,uCAAuC,CAAA;AAU9E,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EAQpB,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAA;AAEpB,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,OAAO,CAAC,gBAAgB,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAAA;KAAE,CAAC,CAAA;IAChE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACtC,QAAQ,EAAE,MAAM,CAAA;CACjB,2CA0KA;AAED,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,kBAAkB,EAClB,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,eAAe,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,KAAK,CAmBR;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;UAItB,gBAAgB,EAAE;UAWxD;AAED,wBAAgB,sBAAsB,CAAC,EACrC,KAAK,EACL,SAAS,EACT,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAA;IACtC,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B,2CAuCA;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAmBzF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,GAAG,KAAK,CAShF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAWpE"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useOperatorAdminMessages as useAdminMessages } from "@voyant-travel/admin";
|
|
5
|
+
import { emptyDraft } from "@voyant-travel/bookings-react/journey";
|
|
6
|
+
import { useCatalogSearch } from "@voyant-travel/catalog-react";
|
|
7
|
+
import { useBookingQuote } from "@voyant-travel/catalog-react/booking-engine";
|
|
8
|
+
import { AsyncCombobox } from "@voyant-travel/ui/components/async-combobox";
|
|
9
|
+
import { DateTimeField } from "@voyant-travel/ui/components/date-time-field";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { useVoyantTripsContext } from "../../provider.js";
|
|
12
|
+
import { CatalogComponentOptions } from "./catalog-options.js";
|
|
13
|
+
import { formatDepartureLabel } from "./display.js";
|
|
14
|
+
import { catalogHitLabel, catalogHitSourceConnectionId, catalogHitSourceKind, catalogHitSourceRef, catalogHitStringField, catalogHitThumbnailUrl, Field, verticalForKind, } from "./shared.js";
|
|
15
|
+
export function CatalogConfigurator({ pending, onChange, paxAdult, }) {
|
|
16
|
+
const t = useAdminMessages().trips.adminComposer.panels;
|
|
17
|
+
const vertical = verticalForKind(pending.kind);
|
|
18
|
+
const [catalogSearch, setCatalogSearch] = React.useState("");
|
|
19
|
+
const [selectedCatalogHit, setSelectedCatalogHit] = React.useState(null);
|
|
20
|
+
const catalogQuery = useCatalogSearch({
|
|
21
|
+
vertical,
|
|
22
|
+
query: catalogSearch,
|
|
23
|
+
mode: "keyword",
|
|
24
|
+
pagination: { limit: 20 },
|
|
25
|
+
enabled: true,
|
|
26
|
+
});
|
|
27
|
+
const catalogHits = React.useMemo(() => {
|
|
28
|
+
return catalogQuery.data?.hits ?? [];
|
|
29
|
+
}, [catalogQuery.data?.hits]);
|
|
30
|
+
const selectedCatalog = catalogHits.find((hit) => hit.id === pending.catalogEntityId) ?? selectedCatalogHit;
|
|
31
|
+
const departureQuery = useProductDepartures(pending.kind === "product" ? pending.catalogEntityId : null);
|
|
32
|
+
const selectedDeparture = React.useMemo(() => departureQuery.data?.rows.find((slot) => slot.id === pending.bookingDraft?.configure.departureSlotId) ?? null, [departureQuery.data?.rows, pending.bookingDraft?.configure.departureSlotId]);
|
|
33
|
+
const quoteDraft = pending.bookingDraft && (pending.bookingDraft.configure.pax?.adult ?? 0) !== paxAdult
|
|
34
|
+
? updateDraftPax(pending.bookingDraft, paxAdult)
|
|
35
|
+
: pending.bookingDraft;
|
|
36
|
+
const quote = useBookingQuote({
|
|
37
|
+
surface: "admin",
|
|
38
|
+
draft: quoteDraft,
|
|
39
|
+
scope: { locale: "en-GB", audience: "staff", market: "default" },
|
|
40
|
+
enabled: Boolean(pending.bookingDraft),
|
|
41
|
+
});
|
|
42
|
+
const shape = quote.data?.shape ?? null;
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (!pending.bookingDraft)
|
|
45
|
+
return;
|
|
46
|
+
const currentAdult = pending.bookingDraft.configure.pax?.adult ?? 0;
|
|
47
|
+
if (currentAdult === paxAdult)
|
|
48
|
+
return;
|
|
49
|
+
onChange({ ...pending, bookingDraft: updateDraftPax(pending.bookingDraft, paxAdult) });
|
|
50
|
+
}, [onChange, paxAdult, pending]);
|
|
51
|
+
const fieldLabel = pending.kind === "stay" ? t.catalogSearch.hotelLabel : t.catalogSearch.productLabel;
|
|
52
|
+
const placeholder = pending.kind === "stay"
|
|
53
|
+
? t.catalogSearch.hotelSearchPlaceholder
|
|
54
|
+
: t.catalogSearch.productSearchPlaceholder;
|
|
55
|
+
const emptyText = pending.kind === "stay" ? t.catalogSearch.hotelEmpty : t.catalogSearch.productEmpty;
|
|
56
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Field, { label: fieldLabel, children: _jsx(AsyncCombobox, { value: pending.catalogEntityId, onChange: (value) => {
|
|
57
|
+
const hit = value ? catalogHits.find((item) => item.id === value) : null;
|
|
58
|
+
setSelectedCatalogHit(hit ?? null);
|
|
59
|
+
const nextEntityId = value;
|
|
60
|
+
const nextSourceKind = hit ? catalogHitSourceKind(hit) : null;
|
|
61
|
+
const nextSourceConnectionId = hit ? catalogHitSourceConnectionId(hit) : null;
|
|
62
|
+
const nextSourceRef = hit ? catalogHitSourceRef(hit) : null;
|
|
63
|
+
onChange({
|
|
64
|
+
...pending,
|
|
65
|
+
catalogEntityId: nextEntityId,
|
|
66
|
+
catalogEntityName: hit ? catalogHitLabel(hit) : null,
|
|
67
|
+
catalogSourceKind: nextSourceKind,
|
|
68
|
+
catalogSourceConnectionId: nextSourceConnectionId,
|
|
69
|
+
catalogSourceRef: nextSourceRef,
|
|
70
|
+
catalogThumbnailUrl: hit ? catalogHitThumbnailUrl(hit) : null,
|
|
71
|
+
bookingDraft: nextEntityId && nextSourceKind
|
|
72
|
+
? createCatalogBookingDraft({
|
|
73
|
+
vertical,
|
|
74
|
+
entityId: nextEntityId,
|
|
75
|
+
sourceKind: nextSourceKind,
|
|
76
|
+
sourceConnectionId: nextSourceConnectionId,
|
|
77
|
+
sourceRef: nextSourceRef,
|
|
78
|
+
paxAdult,
|
|
79
|
+
startsAt: pending.startsAt,
|
|
80
|
+
endsAt: pending.endsAt,
|
|
81
|
+
})
|
|
82
|
+
: null,
|
|
83
|
+
});
|
|
84
|
+
}, items: catalogHits, selectedItem: selectedCatalog, getKey: (hit) => hit.id, getLabel: catalogHitLabel, getSecondary: (hit) => catalogHitStringField(hit, "source.kind") ?? undefined, onSearchChange: setCatalogSearch, placeholder: placeholder, emptyText: emptyText, triggerClassName: "w-full", clearable: true }) }), pending.kind === "product" ? (_jsx(ProductDeparturePicker, { slots: departureQuery.data?.rows ?? [], isLoading: departureQuery.isLoading, isError: departureQuery.isError, value: selectedDeparture?.id ?? "", disabled: !pending.catalogEntityId, onChange: (slotId) => {
|
|
85
|
+
const slot = departureQuery.data?.rows.find((candidate) => candidate.id === slotId);
|
|
86
|
+
if (!slot)
|
|
87
|
+
return;
|
|
88
|
+
onChange({
|
|
89
|
+
...pending,
|
|
90
|
+
startsAt: slot.startsAt,
|
|
91
|
+
endsAt: slot.endsAt ?? "",
|
|
92
|
+
bookingDraft: pending.bookingDraft
|
|
93
|
+
? updateDraftDeparture(pending.bookingDraft, slot)
|
|
94
|
+
: null,
|
|
95
|
+
});
|
|
96
|
+
} })) : (_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: t.fromLabel, children: _jsx(DateTimeField, { value: pending.startsAt, onChange: (value) => {
|
|
97
|
+
const startsAt = value ?? "";
|
|
98
|
+
onChange({
|
|
99
|
+
...pending,
|
|
100
|
+
startsAt,
|
|
101
|
+
bookingDraft: pending.bookingDraft
|
|
102
|
+
? updateDraftSchedule(pending.bookingDraft, startsAt, pending.endsAt)
|
|
103
|
+
: null,
|
|
104
|
+
});
|
|
105
|
+
} }) }), _jsx(Field, { label: t.toLabel, children: _jsx(DateTimeField, { value: pending.endsAt, onChange: (value) => {
|
|
106
|
+
const endsAt = value ?? "";
|
|
107
|
+
onChange({
|
|
108
|
+
...pending,
|
|
109
|
+
endsAt,
|
|
110
|
+
bookingDraft: pending.bookingDraft
|
|
111
|
+
? updateDraftSchedule(pending.bookingDraft, pending.startsAt, endsAt)
|
|
112
|
+
: null,
|
|
113
|
+
});
|
|
114
|
+
} }) })] })), shape ? (_jsx(CatalogComponentOptions, { draft: pending.bookingDraft, shape: shape, onDraftChange: (bookingDraft) => onChange({ ...pending, bookingDraft }) })) : pending.bookingDraft && quote.isQuoting ? (_jsx("p", { className: "text-muted-foreground text-sm", children: t.loadingOptions })) : null] }));
|
|
115
|
+
}
|
|
116
|
+
export function createCatalogBookingDraft({ vertical, entityId, sourceKind, sourceConnectionId, sourceRef, paxAdult, startsAt, endsAt, }) {
|
|
117
|
+
const draft = emptyDraft({
|
|
118
|
+
module: vertical,
|
|
119
|
+
id: entityId,
|
|
120
|
+
sourceKind,
|
|
121
|
+
...(sourceConnectionId ? { sourceConnectionId } : {}),
|
|
122
|
+
...(sourceRef ? { sourceRef } : {}),
|
|
123
|
+
}, { buyerType: "B2C" });
|
|
124
|
+
return updateDraftSchedule({
|
|
125
|
+
...draft,
|
|
126
|
+
configure: { ...draft.configure, pax: { adult: paxAdult } },
|
|
127
|
+
}, startsAt, endsAt);
|
|
128
|
+
}
|
|
129
|
+
export function useProductDepartures(productId) {
|
|
130
|
+
const { baseUrl, fetcher } = useVoyantTripsContext();
|
|
131
|
+
return useQuery({
|
|
132
|
+
queryKey: ["admin-trips-product-departures", productId],
|
|
133
|
+
queryFn: async () => {
|
|
134
|
+
if (!productId)
|
|
135
|
+
return { rows: [] };
|
|
136
|
+
const res = await fetcher(`${baseUrl}/v1/admin/catalog/slots?entityModule=products&entityId=${encodeURIComponent(productId)}`);
|
|
137
|
+
if (!res.ok)
|
|
138
|
+
throw new Error(`Departures request failed: ${res.status}`);
|
|
139
|
+
return res.json();
|
|
140
|
+
},
|
|
141
|
+
enabled: Boolean(productId),
|
|
142
|
+
staleTime: 30_000,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export function ProductDeparturePicker({ slots, isLoading, isError, value, disabled, onChange, }) {
|
|
146
|
+
const t = useAdminMessages().trips.adminComposer.panels;
|
|
147
|
+
const [search, setSearch] = React.useState("");
|
|
148
|
+
const filteredSlots = React.useMemo(() => {
|
|
149
|
+
const trimmed = search.trim().toLowerCase();
|
|
150
|
+
if (!trimmed)
|
|
151
|
+
return slots;
|
|
152
|
+
return slots.filter((slot) => formatDepartureLabel(slot).toLowerCase().includes(trimmed));
|
|
153
|
+
}, [search, slots]);
|
|
154
|
+
const selectedSlot = slots.find((slot) => slot.id === value) ?? null;
|
|
155
|
+
return (_jsx(Field, { label: t.departureLabel, children: isLoading ? (_jsx("div", { className: "h-10 rounded-md border bg-muted/40" })) : isError ? (_jsx("p", { className: "text-destructive text-sm", children: t.departuresUnavailable })) : slots.length === 0 && !disabled ? (_jsx("p", { className: "text-muted-foreground text-sm", children: t.noUpcomingDepartures })) : (_jsx(AsyncCombobox, { value: value || null, onChange: (slotId) => {
|
|
156
|
+
if (slotId)
|
|
157
|
+
onChange(slotId);
|
|
158
|
+
}, items: filteredSlots, selectedItem: selectedSlot, getKey: (slot) => slot.id, getLabel: formatDepartureLabel, getSecondary: (slot) => slot.timezone, onSearchChange: setSearch, placeholder: t.searchDeparturesPlaceholder, emptyText: t.noDeparturesFound, triggerClassName: "w-full", disabled: disabled || slots.length === 0, clearable: false })) }));
|
|
159
|
+
}
|
|
160
|
+
export function updateDraftSchedule(draft, startsAt, endsAt) {
|
|
161
|
+
const departureDate = startsAt ? startsAt.slice(0, 10) : undefined;
|
|
162
|
+
const departureTime = startsAt?.includes("T") ? startsAt.slice(11, 16) : undefined;
|
|
163
|
+
const dateRange = startsAt && endsAt
|
|
164
|
+
? {
|
|
165
|
+
checkIn: startsAt.slice(0, 10),
|
|
166
|
+
checkOut: endsAt.slice(0, 10),
|
|
167
|
+
}
|
|
168
|
+
: undefined;
|
|
169
|
+
return {
|
|
170
|
+
...draft,
|
|
171
|
+
configure: {
|
|
172
|
+
...draft.configure,
|
|
173
|
+
departureDate,
|
|
174
|
+
departureTime,
|
|
175
|
+
dateRange,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function updateDraftDeparture(draft, slot) {
|
|
180
|
+
const scheduledDraft = updateDraftSchedule(draft, slot.startsAt, slot.endsAt ?? "");
|
|
181
|
+
return {
|
|
182
|
+
...scheduledDraft,
|
|
183
|
+
configure: {
|
|
184
|
+
...scheduledDraft.configure,
|
|
185
|
+
departureSlotId: slot.id,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function updateDraftPax(draft, paxAdult) {
|
|
190
|
+
return {
|
|
191
|
+
...draft,
|
|
192
|
+
configure: {
|
|
193
|
+
...draft.configure,
|
|
194
|
+
pax: {
|
|
195
|
+
...draft.configure.pax,
|
|
196
|
+
adult: paxAdult,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|