@voyantjs/travel-composer-react 0.105.7 → 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.
- package/dist/admin/admin-trip-composer-page.d.ts +6 -0
- package/dist/admin/admin-trip-composer-page.d.ts.map +1 -0
- package/dist/admin/admin-trip-composer-page.js +793 -0
- package/dist/admin/admin-trip-composer-panels.d.ts +180 -0
- package/dist/admin/admin-trip-composer-panels.d.ts.map +1 -0
- package/dist/admin/admin-trip-composer-panels.js +1372 -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 +257 -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 +232 -0
- package/package.json +59 -3
|
@@ -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.
|
|
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/
|
|
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.
|
|
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": [
|