@voyantjs/travel-composer-react 0.105.6 → 0.105.8

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.
@@ -0,0 +1,33 @@
1
+ import type { TripEnvelopeStatus } from "@voyantjs/travel-composer";
2
+ export declare const TRIP_STATUS_ALL = "__all__";
3
+ export type TripStatusFilter = TripEnvelopeStatus | typeof TRIP_STATUS_ALL;
4
+ export interface TripListFiltersPopoverProps {
5
+ open: boolean;
6
+ onOpenChange(open: boolean): void;
7
+ activeFilterCount: number;
8
+ status: TripStatusFilter;
9
+ onStatusChange(value: TripStatusFilter): void;
10
+ productId: string | null;
11
+ onProductIdChange(value: string | null): void;
12
+ accommodationId: string | null;
13
+ onAccommodationIdChange(value: string | null): void;
14
+ cruiseId: string | null;
15
+ onCruiseIdChange(value: string | null): void;
16
+ hasFlight: boolean;
17
+ onHasFlightChange(value: boolean): void;
18
+ totalMin: string;
19
+ onTotalMinChange(value: string): void;
20
+ totalMax: string;
21
+ onTotalMaxChange(value: string): void;
22
+ createdRange: {
23
+ from: string | null;
24
+ to: string | null;
25
+ } | null;
26
+ onCreatedRangeChange(value: {
27
+ from: string | null;
28
+ to: string | null;
29
+ } | null): void;
30
+ onFiltersChanged(): void;
31
+ }
32
+ export declare function TripListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, accommodationId, onAccommodationIdChange, cruiseId, onCruiseIdChange, hasFlight, onHasFlightChange, totalMin, onTotalMinChange, totalMax, onTotalMaxChange, createdRange, onCreatedRangeChange, onFiltersChanged, }: TripListFiltersPopoverProps): import("react/jsx-runtime").JSX.Element;
33
+ //# sourceMappingURL=trip-list-filters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trip-list-filters.d.ts","sourceRoot":"","sources":["../../src/admin/trip-list-filters.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAmBnE,eAAO,MAAM,eAAe,YAAY,CAAA;AACxC,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,GAAG,OAAO,eAAe,CAAA;AAgE1E,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,gBAAgB,CAAA;IACxB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAA;IAC7C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAC7C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IACnD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAC5C,SAAS,EAAE,OAAO,CAAA;IAClB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D,oBAAoB,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,GAAG,IAAI,CAAA;IACpF,gBAAgB,IAAI,IAAI,CAAA;CACzB;AAED,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,iBAAiB,EACjB,MAAM,EACN,cAAc,EACd,SAAS,EACT,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,QAAQ,EACR,gBAAgB,EAChB,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,gBAAgB,EAChB,QAAQ,EACR,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,GACjB,EAAE,2BAA2B,2CA6L7B"}
@@ -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 "@voyantjs/admin";
4
+ import { useCatalogSearch } from "@voyantjs/catalog-react";
5
+ import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
6
+ import { Badge } from "@voyantjs/ui/components/badge";
7
+ import { Button } from "@voyantjs/ui/components/button";
8
+ import { Checkbox } from "@voyantjs/ui/components/checkbox";
9
+ import { DateRangePicker } from "@voyantjs/ui/components/date-picker";
10
+ import { Input } from "@voyantjs/ui/components/input";
11
+ import { Label } from "@voyantjs/ui/components/label";
12
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
13
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/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 `travel-composer-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":"AAsBA,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,232 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useOperatorAdminMessages as useAdminMessages, useAdminNavigate } from "@voyantjs/admin";
5
+ import { formatMessage } from "@voyantjs/i18n";
6
+ import { Badge } from "@voyantjs/ui/components/badge";
7
+ import { Button } from "@voyantjs/ui/components/button";
8
+ import { Input } from "@voyantjs/ui/components/input";
9
+ import { Label } from "@voyantjs/ui/components/label";
10
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
11
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
12
+ import { ArrowDown, ArrowUp, ArrowUpDown, Plus, Search, X } from "lucide-react";
13
+ import * as React from "react";
14
+ import { useVoyantTravelComposerContext } from "../provider.js";
15
+ import { listTripsQueryOptions } from "../query-options.js";
16
+ import { componentTitleFor, readComponentSchedule, sortComponentsBySchedule, } from "./trip-component-display.js";
17
+ import { TRIP_STATUS_ALL, TripListFiltersPopover, } from "./trip-list-filters.js";
18
+ const PAGE_SIZE = 25;
19
+ const SKELETON_ROWS = 6;
20
+ const TABLE_COLUMN_COUNT = 7;
21
+ /**
22
+ * Initial list parameters mirrored by the `travel-composer-index`
23
+ * contribution's loader so the SSR-seeded cache entry and the page's first
24
+ * `useQuery` line up on the same key.
25
+ */
26
+ export const initialTripsListParams = {
27
+ limit: PAGE_SIZE,
28
+ offset: 0,
29
+ sortBy: "updatedAt",
30
+ sortDir: "desc",
31
+ };
32
+ const statusBadgeVariant = {
33
+ draft: "secondary",
34
+ priced: "secondary",
35
+ reserve_in_progress: "secondary",
36
+ reserved: "default",
37
+ checkout_started: "secondary",
38
+ booked: "default",
39
+ failed: "destructive",
40
+ cancelled: "destructive",
41
+ };
42
+ /**
43
+ * Packaged admin host for the trips list page (packaged-admin RFC Phase 3).
44
+ * Opening a trip (or composing a new one) resolves the `"trip.detail"`
45
+ * semantic destination (RFC §4.7); the page keeps its filter/sort/paging
46
+ * state locally (no URL search contract), so the host takes no props.
47
+ */
48
+ export function TripsHost() {
49
+ const messages = useAdminMessages().trips;
50
+ const listMessages = messages.list;
51
+ const navigateTo = useAdminNavigate();
52
+ const { baseUrl, fetcher } = useVoyantTravelComposerContext();
53
+ const client = React.useMemo(() => ({ baseUrl, fetcher }), [baseUrl, fetcher]);
54
+ const [search, setSearch] = React.useState("");
55
+ const [status, setStatus] = React.useState(TRIP_STATUS_ALL);
56
+ const [productId, setProductId] = React.useState(null);
57
+ const [accommodationId, setAccommodationId] = React.useState(null);
58
+ const [cruiseId, setCruiseId] = React.useState(null);
59
+ const [hasFlight, setHasFlight] = React.useState(false);
60
+ const [totalMin, setTotalMin] = React.useState("");
61
+ const [totalMax, setTotalMax] = React.useState("");
62
+ const [createdRange, setCreatedRange] = React.useState(null);
63
+ const [sortBy, setSortBy] = React.useState("updatedAt");
64
+ const [sortDir, setSortDir] = React.useState("desc");
65
+ const [offset, setOffset] = React.useState(0);
66
+ const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false);
67
+ const totalMinCents = parseAmountCents(totalMin);
68
+ const totalMaxCents = parseAmountCents(totalMax);
69
+ const params = React.useMemo(() => ({
70
+ limit: PAGE_SIZE,
71
+ offset,
72
+ sortBy,
73
+ sortDir,
74
+ ...(search.trim() ? { search: search.trim() } : {}),
75
+ ...(status !== TRIP_STATUS_ALL ? { status } : {}),
76
+ ...(productId ? { productId } : {}),
77
+ ...(accommodationId ? { accommodationId } : {}),
78
+ ...(cruiseId ? { cruiseId } : {}),
79
+ ...(hasFlight ? { hasFlight: true } : {}),
80
+ ...(totalMinCents !== null ? { totalMinCents } : {}),
81
+ ...(totalMaxCents !== null ? { totalMaxCents } : {}),
82
+ ...(createdRange?.from ? { createdFrom: createdRange.from } : {}),
83
+ ...(createdRange?.to ? { createdTo: createdRange.to } : {}),
84
+ }), [
85
+ offset,
86
+ sortBy,
87
+ sortDir,
88
+ search,
89
+ status,
90
+ productId,
91
+ accommodationId,
92
+ cruiseId,
93
+ hasFlight,
94
+ totalMinCents,
95
+ totalMaxCents,
96
+ createdRange,
97
+ ]);
98
+ const activeFilterCount = (status !== TRIP_STATUS_ALL ? 1 : 0) +
99
+ (productId ? 1 : 0) +
100
+ (accommodationId ? 1 : 0) +
101
+ (cruiseId ? 1 : 0) +
102
+ (hasFlight ? 1 : 0) +
103
+ (totalMin !== "" || totalMax !== "" ? 1 : 0) +
104
+ (createdRange?.from || createdRange?.to ? 1 : 0);
105
+ const { data, isPending, isFetching, isError } = useQuery(listTripsQueryOptions(client, params));
106
+ const trips = data?.data ?? [];
107
+ const total = data?.total ?? 0;
108
+ const page = Math.floor(offset / PAGE_SIZE) + 1;
109
+ const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
110
+ const showSkeleton = isPending || (isFetching && trips.length === 0);
111
+ const resetOffset = () => setOffset(0);
112
+ const handleSort = (field) => {
113
+ setOffset(0);
114
+ if (sortBy !== field) {
115
+ setSortBy(field);
116
+ setSortDir(field === "status" ? "asc" : "desc");
117
+ return;
118
+ }
119
+ setSortDir((prev) => (prev === "asc" ? "desc" : "asc"));
120
+ };
121
+ const hasActiveFilters = search.trim() !== "" || activeFilterCount > 0;
122
+ const clearFilters = () => {
123
+ setSearch("");
124
+ setStatus(TRIP_STATUS_ALL);
125
+ setProductId(null);
126
+ setAccommodationId(null);
127
+ setCruiseId(null);
128
+ setHasFlight(false);
129
+ setTotalMin("");
130
+ setTotalMax("");
131
+ setCreatedRange(null);
132
+ resetOffset();
133
+ };
134
+ 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) => {
135
+ setSearch(event.target.value);
136
+ resetOffset();
137
+ }, 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
138
+ ? listMessages.countEmpty
139
+ : formatMessage(listMessages.countRange, {
140
+ start: String(trips.length === 0 ? 0 : offset + 1),
141
+ end: String(offset + trips.length),
142
+ total: String(total),
143
+ }) }), _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, {
144
+ page: String(page),
145
+ pageCount: String(pageCount),
146
+ }) }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + PAGE_SIZE >= total, onClick: () => setOffset((prev) => prev + PAGE_SIZE), children: listMessages.next })] })] })] })] }));
147
+ }
148
+ function SortHeader({ label, field, sortBy, sortDir, onSort }) {
149
+ const active = sortBy === field;
150
+ const Icon = active ? (sortDir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
151
+ 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 })] }));
152
+ }
153
+ function TripTableSkeleton({ rows }) {
154
+ 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}`))) }));
155
+ }
156
+ function TripRow({ trip, messages, onOpen, }) {
157
+ const envelope = trip.envelope;
158
+ const activeComponents = sortComponentsBySchedule(trip.components.filter((component) => component.status !== "removed"));
159
+ const schedule = tripScheduleBounds(activeComponents);
160
+ 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) }) })] }));
161
+ }
162
+ function componentCountLabel(components, messages) {
163
+ const committedBookings = components.filter((component) => component.bookingId).length;
164
+ const externalRefs = components.filter((component) => component.orderId || component.paymentSessionId).length;
165
+ const parts = [
166
+ formatMessage(messages.list.componentCount, {
167
+ count: String(components.length),
168
+ label: components.length === 1 ? messages.list.componentSingular : messages.list.componentPlural,
169
+ }),
170
+ ];
171
+ if (committedBookings > 0) {
172
+ parts.push(formatMessage(messages.list.componentCount, {
173
+ count: String(committedBookings),
174
+ label: committedBookings === 1 ? messages.list.bookingSingular : messages.list.bookingPlural,
175
+ }));
176
+ }
177
+ if (externalRefs > 0) {
178
+ parts.push(`${externalRefs} ${messages.list.external}`);
179
+ }
180
+ return parts.join(" · ");
181
+ }
182
+ function TripComponentSummary({ components, messages, }) {
183
+ const visibleComponents = components.slice(0, 2);
184
+ if (visibleComponents.length === 0) {
185
+ return (_jsx("p", { className: "max-w-64 truncate text-muted-foreground text-xs", children: messages.list.noComponents }));
186
+ }
187
+ 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
188
+ ? ` +${components.length - visibleComponents.length}`
189
+ : ""] }));
190
+ }
191
+ function TripComponentName({ component }) {
192
+ return _jsx(_Fragment, { children: componentTitleFor(component) });
193
+ }
194
+ function tripScheduleBounds(components) {
195
+ let start = null;
196
+ let end = null;
197
+ for (const component of components) {
198
+ const schedule = readComponentSchedule(component);
199
+ if (schedule.start && (!start || new Date(schedule.start) < new Date(start))) {
200
+ start = schedule.start;
201
+ }
202
+ const candidateEnd = schedule.end ?? schedule.start;
203
+ if (candidateEnd && (!end || new Date(candidateEnd) > new Date(end))) {
204
+ end = candidateEnd;
205
+ }
206
+ }
207
+ return { start, end };
208
+ }
209
+ function parseAmountCents(value) {
210
+ if (value.trim() === "")
211
+ return null;
212
+ const parsed = Number.parseFloat(value);
213
+ if (!Number.isFinite(parsed) || parsed < 0)
214
+ return null;
215
+ return Math.round(parsed * 100);
216
+ }
217
+ function formatDate(value) {
218
+ if (!value)
219
+ return "-";
220
+ const date = value instanceof Date ? value : new Date(value);
221
+ if (Number.isNaN(date.getTime()))
222
+ return "-";
223
+ return date.toLocaleDateString();
224
+ }
225
+ function formatMoney(amountCents, currency) {
226
+ if (amountCents == null)
227
+ return "-";
228
+ return (amountCents / 100).toLocaleString(undefined, {
229
+ style: "currency",
230
+ currency: currency ?? "EUR",
231
+ });
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/travel-composer-react",
3
- "version": "0.105.6",
3
+ "version": "0.105.8",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,29 +34,85 @@
34
34
  "types": "./dist/query-keys.d.ts",
35
35
  "import": "./dist/query-keys.js",
36
36
  "default": "./dist/query-keys.js"
37
+ },
38
+ "./admin": {
39
+ "types": "./dist/admin/index.d.ts",
40
+ "import": "./dist/admin/index.js",
41
+ "default": "./dist/admin/index.js"
37
42
  }
38
43
  },
39
44
  "peerDependencies": {
40
45
  "@tanstack/react-query": "^5.0.0",
46
+ "lucide-react": "^0.475.0",
41
47
  "react": "^19.0.0",
42
48
  "react-dom": "^19.0.0",
43
49
  "zod": "^4.0.0",
44
- "@voyantjs/travel-composer": "^0.105.6"
50
+ "@voyantjs/admin": "^0.110.0",
51
+ "@voyantjs/bookings-react": "^0.114.0",
52
+ "@voyantjs/catalog": "^0.112.0",
53
+ "@voyantjs/catalog-react": "^0.112.0",
54
+ "@voyantjs/crm-react": "^0.114.0",
55
+ "@voyantjs/finance": "^0.114.0",
56
+ "@voyantjs/flights": "^0.114.0",
57
+ "@voyantjs/flights-react": "^0.114.0",
58
+ "@voyantjs/travel-composer": "^0.105.8",
59
+ "@voyantjs/ui": "^0.106.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@voyantjs/admin": {
63
+ "optional": true
64
+ },
65
+ "@voyantjs/bookings-react": {
66
+ "optional": true
67
+ },
68
+ "@voyantjs/catalog": {
69
+ "optional": true
70
+ },
71
+ "@voyantjs/catalog-react": {
72
+ "optional": true
73
+ },
74
+ "@voyantjs/crm-react": {
75
+ "optional": true
76
+ },
77
+ "@voyantjs/finance": {
78
+ "optional": true
79
+ },
80
+ "@voyantjs/flights": {
81
+ "optional": true
82
+ },
83
+ "@voyantjs/flights-react": {
84
+ "optional": true
85
+ },
86
+ "@voyantjs/ui": {
87
+ "optional": true
88
+ }
45
89
  },
46
90
  "devDependencies": {
47
91
  "@tanstack/react-query": "^5.100.11",
48
92
  "@types/react": "^19.2.14",
49
93
  "@types/react-dom": "^19.2.3",
94
+ "lucide-react": "^0.475.0",
50
95
  "react": "^19.2.4",
51
96
  "react-dom": "^19.2.4",
52
97
  "typescript": "^6.0.2",
53
98
  "vitest": "^4.1.2",
54
99
  "zod": "^4.3.6",
100
+ "@voyantjs/admin": "^0.110.0",
101
+ "@voyantjs/bookings-react": "^0.114.0",
102
+ "@voyantjs/catalog": "^0.112.0",
103
+ "@voyantjs/catalog-react": "^0.112.0",
104
+ "@voyantjs/crm-react": "^0.114.0",
105
+ "@voyantjs/finance": "^0.114.0",
106
+ "@voyantjs/flights": "^0.114.0",
107
+ "@voyantjs/flights-react": "^0.114.0",
108
+ "@voyantjs/i18n": "^0.106.0",
55
109
  "@voyantjs/react": "^0.104.1",
56
- "@voyantjs/travel-composer": "^0.105.6",
110
+ "@voyantjs/travel-composer": "^0.105.8",
111
+ "@voyantjs/ui": "^0.106.0",
57
112
  "@voyantjs/voyant-typescript-config": "^0.1.0"
58
113
  },
59
114
  "dependencies": {
115
+ "@voyantjs/i18n": "^0.106.0",
60
116
  "@voyantjs/react": "^0.104.1"
61
117
  },
62
118
  "files": [