@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,63 @@
|
|
|
1
|
+
import { type AdminExtension, type NavItem } from "@voyantjs/admin";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic destinations the travel-composer admin surfaces navigate to
|
|
4
|
+
* (packaged-admin RFC §4.7). The trips list opens the trip detail page, the
|
|
5
|
+
* detail page links back to the list and across to the booking/person
|
|
6
|
+
* pages — instead of importing a host route tree they resolve these keys
|
|
7
|
+
* through `useAdminHref`/`useAdminNavigate` from `@voyantjs/admin`. Hosts
|
|
8
|
+
* register one resolver per key (`satisfies AdminDestinationResolvers`).
|
|
9
|
+
* Keys shared with other domains (`booking.detail`, `person.detail`) come
|
|
10
|
+
* from the bookings-react augmentation bound above.
|
|
11
|
+
*/
|
|
12
|
+
declare module "@voyantjs/admin" {
|
|
13
|
+
interface AdminDestinations {
|
|
14
|
+
/** The trips list page. */
|
|
15
|
+
"trip.list": Record<string, never>;
|
|
16
|
+
/** A trip's detail page; the `"new"` pseudo-id opens the composer. */
|
|
17
|
+
"trip.detail": {
|
|
18
|
+
tripId: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export type { AdminTripComposerPageProps } from "./admin-trip-composer-page.js";
|
|
23
|
+
export type { TripListFiltersPopoverProps, TripStatusFilter } from "./trip-list-filters.js";
|
|
24
|
+
export interface CreateTravelComposerAdminExtensionOptions {
|
|
25
|
+
/** Mount path of the trips pages inside the admin workspace. Default `/trips`. */
|
|
26
|
+
basePath?: string;
|
|
27
|
+
/** Localized nav/page labels. Defaults are the English operator nav labels. */
|
|
28
|
+
labels?: {
|
|
29
|
+
trips?: string;
|
|
30
|
+
allTrips?: string;
|
|
31
|
+
newTrip?: string;
|
|
32
|
+
};
|
|
33
|
+
/** Nav icon — icon choice stays with the host (e.g. lucide `Route`). */
|
|
34
|
+
icon?: NavItem["icon"];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The travel-composer admin contribution (packaged-admin RFC Phase 3,
|
|
38
|
+
* `@voyantjs/<domain>-react/admin` convention).
|
|
39
|
+
*
|
|
40
|
+
* NAVIGATION: package-delivered. Trips is NOT part of the BASE operator
|
|
41
|
+
* navigation (`createOperatorAdminNavigation` in `@voyantjs/admin`), so the
|
|
42
|
+
* extension contributes the Trips group itself — spliced in right after
|
|
43
|
+
* Bookings (both belong to the booking lifecycle) via `insertAfter`, with
|
|
44
|
+
* All trips / New trip sub-items. The icon stays a host choice.
|
|
45
|
+
*
|
|
46
|
+
* ROUTES: contributions carry the FULL route implementation (packaged-admin
|
|
47
|
+
* RFC §4.2/§4.8) — lazy `page` module loaders, data loaders fed by the
|
|
48
|
+
* host-supplied {@link AdminRouteLoaderContext} (QueryClient + runtime +
|
|
49
|
+
* params), per-route SSR mode. Hosts bind them into their code-assembled
|
|
50
|
+
* admin route tree; no per-route host files needed. The pages stay
|
|
51
|
+
* code-split because each contribution's `page` dynamically imports the
|
|
52
|
+
* specific host/page module — never this barrel — and the trip composer
|
|
53
|
+
* itself mounts through a nested `React.lazy`, so its heavy panel stack
|
|
54
|
+
* (flights search, catalog browse, booking journey) only fetches when an
|
|
55
|
+
* operator actually composes/edits a trip. The list keeps its filter/sort/
|
|
56
|
+
* paging state local (no URL search contracts), and every cross-route link
|
|
57
|
+
* resolves through the semantic destinations declared above — no app RPC
|
|
58
|
+
* client, no host route tree.
|
|
59
|
+
*
|
|
60
|
+
* WIDGETS: none contributed and no slots exposed yet.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createTravelComposerAdminExtension(options?: CreateTravelComposerAdminExtensionOptions): AdminExtension;
|
|
63
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/admin/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAKnB,KAAK,OAAO,EACb,MAAM,iBAAiB,CAAA;AAcxB;;;;;;;;;GASG;AACH,OAAO,QAAQ,iBAAiB,CAAC;IAC/B,UAAU,iBAAiB;QACzB,2BAA2B;QAC3B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;QAClC,sEAAsE;QACtE,aAAa,EAAE;YAAE,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAClC;CACF;AAWD,YAAY,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAA;AAC/E,YAAY,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAE3F,MAAM,WAAW,yCAAyC;IACxD,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,GAAE,yCAA8C,GACtD,cAAc,CAiFhB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { adminRoutePageModule, defineAdminExtension, } from "@voyantjs/admin";
|
|
2
|
+
// Lean static only: the client module (fetcher). Query options resolve via
|
|
3
|
+
// dynamic import inside the loaders so the travel-composer data layer
|
|
4
|
+
// (client + response schemas) stays out of the workspace-chrome chunk that
|
|
5
|
+
// evaluates this factory.
|
|
6
|
+
import { defaultFetcher } from "../client.js";
|
|
7
|
+
/**
|
|
8
|
+
* The travel-composer admin contribution (packaged-admin RFC Phase 3,
|
|
9
|
+
* `@voyantjs/<domain>-react/admin` convention).
|
|
10
|
+
*
|
|
11
|
+
* NAVIGATION: package-delivered. Trips is NOT part of the BASE operator
|
|
12
|
+
* navigation (`createOperatorAdminNavigation` in `@voyantjs/admin`), so the
|
|
13
|
+
* extension contributes the Trips group itself — spliced in right after
|
|
14
|
+
* Bookings (both belong to the booking lifecycle) via `insertAfter`, with
|
|
15
|
+
* All trips / New trip sub-items. The icon stays a host choice.
|
|
16
|
+
*
|
|
17
|
+
* ROUTES: contributions carry the FULL route implementation (packaged-admin
|
|
18
|
+
* RFC §4.2/§4.8) — lazy `page` module loaders, data loaders fed by the
|
|
19
|
+
* host-supplied {@link AdminRouteLoaderContext} (QueryClient + runtime +
|
|
20
|
+
* params), per-route SSR mode. Hosts bind them into their code-assembled
|
|
21
|
+
* admin route tree; no per-route host files needed. The pages stay
|
|
22
|
+
* code-split because each contribution's `page` dynamically imports the
|
|
23
|
+
* specific host/page module — never this barrel — and the trip composer
|
|
24
|
+
* itself mounts through a nested `React.lazy`, so its heavy panel stack
|
|
25
|
+
* (flights search, catalog browse, booking journey) only fetches when an
|
|
26
|
+
* operator actually composes/edits a trip. The list keeps its filter/sort/
|
|
27
|
+
* paging state local (no URL search contracts), and every cross-route link
|
|
28
|
+
* resolves through the semantic destinations declared above — no app RPC
|
|
29
|
+
* client, no host route tree.
|
|
30
|
+
*
|
|
31
|
+
* WIDGETS: none contributed and no slots exposed yet.
|
|
32
|
+
*/
|
|
33
|
+
export function createTravelComposerAdminExtension(options = {}) {
|
|
34
|
+
const { basePath = "/trips", labels = {}, icon } = options;
|
|
35
|
+
const { trips = "Trips", allTrips = "All trips", newTrip = "New trip" } = labels;
|
|
36
|
+
return defineAdminExtension({
|
|
37
|
+
id: "travel-composer",
|
|
38
|
+
navigation: [
|
|
39
|
+
{
|
|
40
|
+
// Splice Trips in right after Bookings — both belong to the booking
|
|
41
|
+
// lifecycle. `insertAfter` keeps the contribution shape; the resolver
|
|
42
|
+
// splices in place rather than appending at the end.
|
|
43
|
+
insertAfter: "bookings",
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
id: "travel-composer",
|
|
47
|
+
title: trips,
|
|
48
|
+
url: basePath,
|
|
49
|
+
icon,
|
|
50
|
+
items: [
|
|
51
|
+
{
|
|
52
|
+
id: "travel-composer-list",
|
|
53
|
+
title: allTrips,
|
|
54
|
+
url: basePath,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "travel-composer-new",
|
|
58
|
+
title: newTrip,
|
|
59
|
+
url: `${basePath}/new`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
routes: [
|
|
67
|
+
{
|
|
68
|
+
id: "travel-composer-index",
|
|
69
|
+
path: basePath,
|
|
70
|
+
title: trips,
|
|
71
|
+
// Route-backed destination (RFC §4.7 endgame): the key resolves by
|
|
72
|
+
// pure path interpolation of this route, so the host's resolver is
|
|
73
|
+
// generated (`voyant admin generate --destinations`).
|
|
74
|
+
destination: "trip.list",
|
|
75
|
+
ssr: "data-only",
|
|
76
|
+
page: () => import("./trips-host.js").then((module) => adminRoutePageModule(module.TripsHost)),
|
|
77
|
+
// Dynamic import on purpose: the query options pull the
|
|
78
|
+
// travel-composer data layer (client + response schemas), and a
|
|
79
|
+
// static import here would pin it into the workspace-chrome chunk
|
|
80
|
+
// that evaluates this factory. The host module carries the matching
|
|
81
|
+
// initial params so the seeded cache entry lines up with the page's
|
|
82
|
+
// first query.
|
|
83
|
+
loader: async ({ queryClient, runtime }) => {
|
|
84
|
+
const [{ listTripsQueryOptions }, { initialTripsListParams }] = await Promise.all([
|
|
85
|
+
import("../query-options.js"),
|
|
86
|
+
import("./trips-host.js"),
|
|
87
|
+
]);
|
|
88
|
+
return queryClient.ensureQueryData(listTripsQueryOptions(loaderClient(runtime), initialTripsListParams));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "travel-composer-detail",
|
|
93
|
+
path: `${basePath}/$id`,
|
|
94
|
+
title: trips,
|
|
95
|
+
destination: "trip.detail",
|
|
96
|
+
destinationParams: { id: "tripId" },
|
|
97
|
+
ssr: "data-only",
|
|
98
|
+
page: () => import("./pages/trip-detail-page.js"),
|
|
99
|
+
// The `"new"` pseudo-id mounts the composer with no trip to fetch.
|
|
100
|
+
// Dynamic import on purpose — see the trips index loader above.
|
|
101
|
+
loader: async ({ queryClient, runtime, params }) => {
|
|
102
|
+
const id = params.id;
|
|
103
|
+
if (!id || id === "new")
|
|
104
|
+
return;
|
|
105
|
+
const { getTripQueryOptions } = await import("../query-options.js");
|
|
106
|
+
return queryClient.ensureQueryData(getTripQueryOptions(loaderClient(runtime), id));
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Bridge the host-supplied {@link AdminRouteRuntime} (optional fetcher) to
|
|
114
|
+
* the required-fetcher client contract the travel-composer query options
|
|
115
|
+
* take — SSR loaders run with the host runtime's cookie-forwarding fetcher.
|
|
116
|
+
*/
|
|
117
|
+
function loaderClient(runtime) {
|
|
118
|
+
return { baseUrl: runtime.baseUrl, fetcher: runtime.fetcher ?? defaultFetcher };
|
|
119
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AdminRoutePageProps } from "@voyantjs/admin";
|
|
2
|
+
/**
|
|
3
|
+
* Param-taking page for the `travel-composer-detail` contribution: reads the
|
|
4
|
+
* trip envelope id off {@link AdminRoutePageProps} and binds it onto the
|
|
5
|
+
* packaged host. Resolved lazily through the contribution's `page` loader so
|
|
6
|
+
* the detail page (and the composer behind its Edit mode) lands in its own
|
|
7
|
+
* chunk.
|
|
8
|
+
*/
|
|
9
|
+
export default function TripDetailPage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=trip-detail-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trip-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/trip-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAErE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { TripDetailHost } from "../trip-detail-host.js";
|
|
3
|
+
/**
|
|
4
|
+
* Param-taking page for the `travel-composer-detail` contribution: reads the
|
|
5
|
+
* trip envelope id off {@link AdminRoutePageProps} and binds it onto the
|
|
6
|
+
* packaged host. Resolved lazily through the contribution's `page` loader so
|
|
7
|
+
* the detail page (and the composer behind its Edit mode) lands in its own
|
|
8
|
+
* chunk.
|
|
9
|
+
*/
|
|
10
|
+
export default function TripDetailPage({ params }) {
|
|
11
|
+
return _jsx(TripDetailHost, { id: params.id ?? "" });
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TripComponent } from "@voyantjs/travel-composer";
|
|
2
|
+
export declare function readComponentSchedule(component: TripComponent): {
|
|
3
|
+
start: string | null;
|
|
4
|
+
end: string | null;
|
|
5
|
+
};
|
|
6
|
+
export declare function formatScheduleLabel(component: TripComponent): string | null;
|
|
7
|
+
export declare function sortComponentsBySchedule(components: TripComponent[]): TripComponent[];
|
|
8
|
+
export declare function componentTitleFor(component: TripComponent, resolvedEntityName?: string | null): string;
|
|
9
|
+
export declare function componentReferenceLabelFor(component: TripComponent): string;
|
|
10
|
+
//# sourceMappingURL=trip-component-display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trip-component-display.d.ts","sourceRoot":"","sources":["../../src/admin/trip-component-display.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE9D,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,aAAa,GAAG;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CACnB,CA8BA;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAM3E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAcrF;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,aAAa,EACxB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GACjC,MAAM,CAgER;AAED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAW3E"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export function readComponentSchedule(component) {
|
|
2
|
+
const md = component.metadata;
|
|
3
|
+
if (md?.scheduledStartsAt) {
|
|
4
|
+
return { start: md.scheduledStartsAt, end: md.scheduledEndsAt ?? null };
|
|
5
|
+
}
|
|
6
|
+
const dateRange = md?.bookingDraftV1?.configure?.dateRange;
|
|
7
|
+
if (dateRange?.checkIn) {
|
|
8
|
+
return { start: dateRange.checkIn, end: dateRange.checkOut ?? null };
|
|
9
|
+
}
|
|
10
|
+
const flight = md?.flightDraft;
|
|
11
|
+
if (flight?.departDate) {
|
|
12
|
+
return { start: flight.departDate, end: flight.returnDate ?? null };
|
|
13
|
+
}
|
|
14
|
+
const cruise = md?.cruiseDraft;
|
|
15
|
+
if (cruise?.embarkationDate) {
|
|
16
|
+
return { start: cruise.embarkationDate, end: null };
|
|
17
|
+
}
|
|
18
|
+
return { start: null, end: null };
|
|
19
|
+
}
|
|
20
|
+
export function formatScheduleLabel(component) {
|
|
21
|
+
const { start, end } = readComponentSchedule(component);
|
|
22
|
+
if (!start)
|
|
23
|
+
return null;
|
|
24
|
+
const startLabel = formatDateTime(start);
|
|
25
|
+
if (!end || end === start)
|
|
26
|
+
return startLabel;
|
|
27
|
+
return `${startLabel} → ${formatDateTime(end)}`;
|
|
28
|
+
}
|
|
29
|
+
export function sortComponentsBySchedule(components) {
|
|
30
|
+
return [...components].sort((a, b) => {
|
|
31
|
+
const aStart = readComponentSchedule(a).start;
|
|
32
|
+
const bStart = readComponentSchedule(b).start;
|
|
33
|
+
const aMs = aStart ? new Date(aStart).getTime() : Number.NaN;
|
|
34
|
+
const bMs = bStart ? new Date(bStart).getTime() : Number.NaN;
|
|
35
|
+
const aMissing = Number.isNaN(aMs);
|
|
36
|
+
const bMissing = Number.isNaN(bMs);
|
|
37
|
+
if (aMissing && bMissing)
|
|
38
|
+
return a.sequence - b.sequence;
|
|
39
|
+
if (aMissing)
|
|
40
|
+
return 1;
|
|
41
|
+
if (bMissing)
|
|
42
|
+
return -1;
|
|
43
|
+
if (aMs !== bMs)
|
|
44
|
+
return aMs - bMs;
|
|
45
|
+
return a.sequence - b.sequence;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function componentTitleFor(component, resolvedEntityName) {
|
|
49
|
+
const metadata = component.metadata;
|
|
50
|
+
const entityName = cleanDisplayLabel(resolvedEntityName);
|
|
51
|
+
if (entityName)
|
|
52
|
+
return entityName;
|
|
53
|
+
const catalogName = cleanDisplayLabel(metadata?.catalogItem?.name);
|
|
54
|
+
if (catalogName)
|
|
55
|
+
return catalogName;
|
|
56
|
+
if (component.kind === "flight_placeholder" || component.kind === "flight_order") {
|
|
57
|
+
const origin = cleanDisplayLabel(metadata?.flightDraft?.origin);
|
|
58
|
+
const destination = cleanDisplayLabel(metadata?.flightDraft?.destination);
|
|
59
|
+
if (origin && destination)
|
|
60
|
+
return `${origin} → ${destination}`;
|
|
61
|
+
}
|
|
62
|
+
if (component.entityModule === "cruises") {
|
|
63
|
+
const cabin = cleanDisplayLabel(metadata?.cruiseDraft?.cabin);
|
|
64
|
+
if (cabin)
|
|
65
|
+
return `Cabin ${cabin}`;
|
|
66
|
+
}
|
|
67
|
+
if (component.entityModule === "accommodations") {
|
|
68
|
+
const accommodationName = cleanDisplayLabel(metadata?.accommodation?.propertyName) ??
|
|
69
|
+
cleanDisplayLabel(metadata?.accommodation?.name) ??
|
|
70
|
+
cleanDisplayLabel(metadata?.accommodation?.roomTypeName);
|
|
71
|
+
if (accommodationName)
|
|
72
|
+
return accommodationName;
|
|
73
|
+
}
|
|
74
|
+
if (metadata?.cruiseDraft) {
|
|
75
|
+
const cabin = cleanDisplayLabel(metadata.cruiseDraft.cabin);
|
|
76
|
+
if (cabin)
|
|
77
|
+
return `Cabin ${cabin}`;
|
|
78
|
+
}
|
|
79
|
+
if (component.kind === "manual_placeholder") {
|
|
80
|
+
const serviceName = cleanDisplayLabel(metadata?.manualService?.name);
|
|
81
|
+
if (serviceName)
|
|
82
|
+
return serviceName;
|
|
83
|
+
const title = cleanDisplayLabel(component.title);
|
|
84
|
+
if (title)
|
|
85
|
+
return title;
|
|
86
|
+
const description = cleanDisplayLabel(component.description);
|
|
87
|
+
if (description)
|
|
88
|
+
return description;
|
|
89
|
+
}
|
|
90
|
+
return componentReferenceLabelFor(component);
|
|
91
|
+
}
|
|
92
|
+
export function componentReferenceLabelFor(component) {
|
|
93
|
+
const reference = component.providerRef ??
|
|
94
|
+
component.supplierRef ??
|
|
95
|
+
component.bookingId ??
|
|
96
|
+
component.orderId ??
|
|
97
|
+
component.paymentSessionId ??
|
|
98
|
+
component.sourceRef ??
|
|
99
|
+
component.entityId ??
|
|
100
|
+
component.id;
|
|
101
|
+
return reference.length > 18 ? reference.slice(0, 18) : reference;
|
|
102
|
+
}
|
|
103
|
+
function cleanDisplayLabel(value) {
|
|
104
|
+
const trimmed = value?.trim();
|
|
105
|
+
if (!trimmed)
|
|
106
|
+
return null;
|
|
107
|
+
const normalized = trimmed.toLowerCase();
|
|
108
|
+
if (normalized === "untitled trip" ||
|
|
109
|
+
normalized === "untitled component" ||
|
|
110
|
+
normalized === "flight placeholder" ||
|
|
111
|
+
normalized.startsWith("flight placeholder ") ||
|
|
112
|
+
normalized === "manual placeholder" ||
|
|
113
|
+
normalized === "cruise" ||
|
|
114
|
+
normalized === "cruise placeholder" ||
|
|
115
|
+
normalized === "catalog booking" ||
|
|
116
|
+
normalized === "product booking" ||
|
|
117
|
+
normalized === "stay booking" ||
|
|
118
|
+
normalized === "cruise booking" ||
|
|
119
|
+
normalized === "external order" ||
|
|
120
|
+
normalized === "flight order" ||
|
|
121
|
+
/^component \d+$/.test(normalized)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return trimmed;
|
|
125
|
+
}
|
|
126
|
+
function formatDateTime(iso) {
|
|
127
|
+
const parsed = new Date(iso);
|
|
128
|
+
if (Number.isNaN(parsed.getTime()))
|
|
129
|
+
return iso;
|
|
130
|
+
const hasTime = iso.includes("T") || iso.includes(" ");
|
|
131
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
132
|
+
year: "numeric",
|
|
133
|
+
month: "short",
|
|
134
|
+
day: "numeric",
|
|
135
|
+
...(hasTime ? { hour: "2-digit", minute: "2-digit" } : {}),
|
|
136
|
+
}).format(parsed);
|
|
137
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaged admin host for the trip detail page (packaged-admin RFC
|
|
3
|
+
* Phase 3). The `"new"` pseudo-id mounts the admin trip composer directly;
|
|
4
|
+
* existing trips render the read-only record page (seeded by the
|
|
5
|
+
* `travel-composer-detail` contribution's loader) with an Edit toggle into
|
|
6
|
+
* the composer. Cross-route links (bookings, CRM people, the trips list)
|
|
7
|
+
* resolve through semantic destinations (RFC §4.7).
|
|
8
|
+
*/
|
|
9
|
+
export declare function TripDetailHost({ id }: {
|
|
10
|
+
id: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
//# sourceMappingURL=trip-detail-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trip-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/trip-detail-host.tsx"],"names":[],"mappings":"AAuDA;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,kDA0BpD"}
|
|
@@ -0,0 +1,257 @@
|
|
|
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 { useAdminHref, useOperatorAdminMessages as useAdminMessages, useAdminNavigate, } from "@voyantjs/admin";
|
|
5
|
+
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
6
|
+
import { buildPaymentLinkUrl } from "@voyantjs/finance/payment-link";
|
|
7
|
+
import { formatMessage } from "@voyantjs/i18n";
|
|
8
|
+
import { Badge } from "@voyantjs/ui/components/badge";
|
|
9
|
+
import { Button } from "@voyantjs/ui/components/button";
|
|
10
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components/card";
|
|
11
|
+
import { Separator } from "@voyantjs/ui/components/separator";
|
|
12
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
13
|
+
import { ArrowLeft, BedDouble, CalendarClock, Check, Copy, CreditCard, ExternalLink, Pencil, Plane, Route as RouteIcon, Ship, Users, } from "lucide-react";
|
|
14
|
+
import { lazy, Suspense, useState } from "react";
|
|
15
|
+
import { useVoyantTravelComposerContext } from "../provider.js";
|
|
16
|
+
import { getTripQueryOptions } from "../query-options.js";
|
|
17
|
+
import { componentReferenceLabelFor, componentTitleFor, formatScheduleLabel, sortComponentsBySchedule, } from "./trip-component-display.js";
|
|
18
|
+
const AdminTripComposerPage = lazy(() => import("./admin-trip-composer-page.js").then((module) => ({
|
|
19
|
+
default: module.AdminTripComposerPage,
|
|
20
|
+
})));
|
|
21
|
+
/**
|
|
22
|
+
* Packaged admin host for the trip detail page (packaged-admin RFC
|
|
23
|
+
* Phase 3). The `"new"` pseudo-id mounts the admin trip composer directly;
|
|
24
|
+
* existing trips render the read-only record page (seeded by the
|
|
25
|
+
* `travel-composer-detail` contribution's loader) with an Edit toggle into
|
|
26
|
+
* the composer. Cross-route links (bookings, CRM people, the trips list)
|
|
27
|
+
* resolve through semantic destinations (RFC §4.7).
|
|
28
|
+
*/
|
|
29
|
+
export function TripDetailHost({ id }) {
|
|
30
|
+
const [mode, setMode] = useState("record");
|
|
31
|
+
const { baseUrl, fetcher } = useVoyantTravelComposerContext();
|
|
32
|
+
const isNew = id === "new";
|
|
33
|
+
const tripQuery = useQuery({
|
|
34
|
+
...getTripQueryOptions({ baseUrl, fetcher }, id),
|
|
35
|
+
enabled: !isNew,
|
|
36
|
+
});
|
|
37
|
+
const trip = isNew ? null : (tripQuery.data ?? null);
|
|
38
|
+
if (!trip) {
|
|
39
|
+
if (!isNew && tripQuery.isPending)
|
|
40
|
+
return null;
|
|
41
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(AdminTripComposerPage, { initialTrip: null }) }));
|
|
42
|
+
}
|
|
43
|
+
if (mode === "edit") {
|
|
44
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(AdminTripComposerPage, { initialTrip: trip }) }));
|
|
45
|
+
}
|
|
46
|
+
return _jsx(TripRecordPage, { trip: trip, onEdit: () => setMode("edit") });
|
|
47
|
+
}
|
|
48
|
+
function TripRecordPage({ trip, onEdit }) {
|
|
49
|
+
const messages = useAdminMessages().trips;
|
|
50
|
+
const detailMessages = messages.detail;
|
|
51
|
+
const navigateTo = useAdminNavigate();
|
|
52
|
+
const { baseUrl } = useVoyantTravelComposerContext();
|
|
53
|
+
const [copiedPaymentLink, setCopiedPaymentLink] = useState(false);
|
|
54
|
+
const envelope = trip.envelope;
|
|
55
|
+
const activeComponents = sortComponentsBySchedule(trip.components.filter((component) => component.status !== "removed"));
|
|
56
|
+
const bookedComponents = activeComponents.filter((component) => component.bookingId).length;
|
|
57
|
+
const externalRefs = activeComponents.filter((component) => component.orderId || component.paymentSessionId).length;
|
|
58
|
+
const scheduleSummary = tripScheduleLabel(activeComponents);
|
|
59
|
+
return (_jsxs("main", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex flex-col gap-4 md:flex-row md:items-start md:justify-between", children: [_jsxs("div", { className: "space-y-3", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigateTo("trip.list", {}), children: [_jsx(ArrowLeft, { className: "size-4", "aria-hidden": "true" }), detailMessages.breadcrumb] }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("h1", { className: "font-bold text-2xl tracking-tight", children: detailMessages.title }), _jsx(Badge, { variant: envelope.status === "failed" ? "destructive" : "secondary", children: messages.statuses[envelope.status] })] }), envelope.description ? (_jsx("p", { className: "max-w-3xl text-muted-foreground text-sm", children: envelope.description })) : null, _jsx("p", { className: "text-muted-foreground text-xs", children: scheduleSummary
|
|
60
|
+
? `${scheduleSummary} · ${activeComponents.length} ${activeComponents.length === 1
|
|
61
|
+
? messages.list.componentSingular
|
|
62
|
+
: messages.list.componentPlural} · ${envelope.id}`
|
|
63
|
+
: `${activeComponents.length} ${activeComponents.length === 1
|
|
64
|
+
? messages.list.componentSingular
|
|
65
|
+
: messages.list.componentPlural} · ${envelope.id}` })] })] }), _jsxs(Button, { onClick: onEdit, children: [_jsx(Pencil, { className: "size-4", "aria-hidden": "true" }), detailMessages.editTrip] })] }), _jsxs("section", { className: "grid gap-4 md:grid-cols-4", children: [_jsx(SummaryCard, { label: detailMessages.summary.total, value: formatMoney(envelope.aggregateTotalAmountCents, envelope.aggregateCurrency), icon: CreditCard }), _jsx(SummaryCard, { label: detailMessages.summary.components, value: String(activeComponents.length), icon: RouteIcon }), _jsx(SummaryCard, { label: detailMessages.summary.bookings, value: String(bookedComponents), icon: CalendarClock }), _jsx(SummaryCard, { label: detailMessages.summary.externalRefs, value: String(externalRefs), icon: ExternalLink })] }), _jsxs("section", { className: "grid gap-4 lg:grid-cols-2", children: [_jsx(BillingRecord, { travelerParty: envelope.travelerParty, messages: detailMessages }), _jsx(TravelersRecord, { travelerParty: envelope.travelerParty, messages: detailMessages })] }), _jsxs("section", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]", children: [_jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: detailMessages.components.component }), _jsx(TableHead, { children: detailMessages.components.status }), _jsx(TableHead, { children: detailMessages.components.tax }), _jsx(TableHead, { children: detailMessages.components.total }), _jsx(TableHead, { className: "text-right", children: detailMessages.components.record })] }) }), _jsx(TableBody, { children: activeComponents.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-28 text-center text-muted-foreground text-sm", children: detailMessages.noComponentsOnTrip }) })) : (activeComponents.map((component) => (_jsx(ComponentRow, { component: component, messages: detailMessages, onOpenBooking: (bookingId) => navigateTo("booking.detail", { bookingId }) }, component.id)))) })] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: detailMessages.summary.record }) }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx(SummaryLine, { label: detailMessages.summary.status, value: messages.statuses[envelope.status] }), _jsx(SummaryLine, { label: detailMessages.summary.updated, value: formatDate(envelope.updatedAt) }), _jsx(SummaryLine, { label: detailMessages.summary.reserved, value: formatDate(envelope.reservedAt) }), _jsx(SummaryLine, { label: detailMessages.summary.checkoutStarted, value: formatDate(envelope.checkoutStartedAt) }), envelope.paymentSessionId ? (_jsxs("div", { className: "flex items-center justify-between gap-4 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: detailMessages.summary.paymentLink }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void copyPaymentLink(envelope.paymentSessionId ?? "", baseUrl).then((copied) => {
|
|
66
|
+
setCopiedPaymentLink(copied);
|
|
67
|
+
if (copied) {
|
|
68
|
+
window.setTimeout(() => setCopiedPaymentLink(false), 2000);
|
|
69
|
+
}
|
|
70
|
+
}), children: [copiedPaymentLink ? (_jsx(Check, { className: "size-4", "aria-hidden": "true" })) : (_jsx(Copy, { className: "size-4", "aria-hidden": "true" })), copiedPaymentLink
|
|
71
|
+
? detailMessages.summary.copied
|
|
72
|
+
: detailMessages.summary.copyLink] })] })) : null, _jsx(Separator, {}), _jsx(SummaryLine, { label: detailMessages.summary.subtotal, value: formatMoney(envelope.aggregateSubtotalAmountCents, envelope.aggregateCurrency) }), _jsx(SummaryLine, { label: detailMessages.summary.tax, value: formatMoney(envelope.aggregateTaxAmountCents, envelope.aggregateCurrency) }), _jsx(SummaryLine, { label: detailMessages.summary.total, value: formatMoney(envelope.aggregateTotalAmountCents, envelope.aggregateCurrency), strong: true })] })] })] })] }));
|
|
73
|
+
}
|
|
74
|
+
async function copyPaymentLink(paymentSessionId, apiBaseUrl) {
|
|
75
|
+
if (!paymentSessionId || typeof window === "undefined")
|
|
76
|
+
return false;
|
|
77
|
+
const publicCheckoutBaseUrl = await fetchPublicCheckoutBaseUrl(apiBaseUrl);
|
|
78
|
+
const url = buildPaymentLinkUrl(paymentSessionId, {
|
|
79
|
+
baseUrl: publicCheckoutBaseUrl ?? window.location.origin,
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
await navigator.clipboard.writeText(url);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function fetchPublicCheckoutBaseUrl(apiBaseUrl) {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${apiBaseUrl}/v1/public/payment-link-config`, {
|
|
92
|
+
headers: { Accept: "application/json" },
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
return null;
|
|
96
|
+
const body = (await res.json());
|
|
97
|
+
return body.data?.publicCheckoutBaseUrl ?? null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function BillingRecord({ travelerParty, messages, }) {
|
|
104
|
+
const billing = readBilling(travelerParty);
|
|
105
|
+
const personQuery = usePerson(billing?.personId, { enabled: Boolean(billing?.personId) });
|
|
106
|
+
const orgQuery = useOrganization(billing?.organizationId, {
|
|
107
|
+
enabled: Boolean(billing?.organizationId),
|
|
108
|
+
});
|
|
109
|
+
const resolvedPersonName = formatPersonName(personQuery.data);
|
|
110
|
+
const primary = orgQuery.data?.name ??
|
|
111
|
+
resolvedPersonName ??
|
|
112
|
+
formatContactName(billing?.contact) ??
|
|
113
|
+
billing?.contact?.email ??
|
|
114
|
+
null;
|
|
115
|
+
const secondary = [
|
|
116
|
+
orgQuery.data?.name ? resolvedPersonName : null,
|
|
117
|
+
billing?.contact?.email,
|
|
118
|
+
billing?.buyerType,
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: messages.billing.title }) }), _jsx(CardContent, { className: "space-y-3", children: primary ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium", children: billing?.personId && !orgQuery.data?.name ? (_jsx(PersonLink, { personId: billing.personId, children: primary })) : (primary) }), secondary.length > 0 ? (_jsxs("div", { className: "flex flex-wrap gap-x-1 text-muted-foreground text-sm", children: [orgQuery.data?.name && billing?.personId && resolvedPersonName ? (_jsxs(_Fragment, { children: [_jsx(PersonLink, { personId: billing.personId, children: resolvedPersonName }), billing.contact?.email || billing.buyerType ? _jsx("span", { children: "\u00B7" }) : null] })) : null, billing?.contact?.email ? _jsx("span", { children: billing.contact.email }) : null, billing?.contact?.email && billing?.buyerType ? _jsx("span", { children: "\u00B7" }) : null, billing?.buyerType ? _jsx("span", { children: billing.buyerType }) : null] })) : null] }), _jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-muted-foreground text-xs", children: [billing?.personId ? (_jsxs("span", { children: [messages.billing.person, ": ", billing.personId] })) : null, billing?.organizationId ? (_jsxs("span", { children: [messages.billing.organization, ": ", billing.organizationId] })) : null] })] })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.billing.noProfile })) })] }));
|
|
121
|
+
}
|
|
122
|
+
function TravelersRecord({ travelerParty, messages, }) {
|
|
123
|
+
const travelers = readTravelers(travelerParty);
|
|
124
|
+
return (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center gap-2 text-base", children: [_jsx(Users, { className: "size-4", "aria-hidden": "true" }), messages.travelers.title] }) }), _jsx(CardContent, { children: travelers.length > 0 ? (_jsx("ul", { className: "divide-y", children: travelers.map((traveler, index) => (_jsx(TravelerRecordRow, { traveler: traveler, index: index, messages: messages }, traveler.localId ?? traveler.personId ?? index))) })) : (_jsx("p", { className: "text-muted-foreground text-sm", children: messages.travelers.none })) })] }));
|
|
125
|
+
}
|
|
126
|
+
function TravelerRecordRow({ traveler, index, messages, }) {
|
|
127
|
+
const personQuery = usePerson(traveler.personId ?? undefined, {
|
|
128
|
+
enabled: Boolean(traveler.personId),
|
|
129
|
+
});
|
|
130
|
+
const inlineName = [traveler.firstName, traveler.lastName]
|
|
131
|
+
.filter((part) => (part ?? "").trim().length > 0)
|
|
132
|
+
.join(" ")
|
|
133
|
+
.trim();
|
|
134
|
+
const name = inlineName ||
|
|
135
|
+
formatPersonName(personQuery.data) ||
|
|
136
|
+
formatMessage(messages.travelers.fallbackName, { index: String(index + 1) });
|
|
137
|
+
const email = traveler.email ?? personQuery.data?.email ?? null;
|
|
138
|
+
return (_jsxs("li", { className: "flex items-center justify-between gap-4 py-3 first:pt-0 last:pb-0", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium", children: traveler.personId ? _jsx(PersonLink, { personId: traveler.personId, children: name }) : name }), email ? _jsx("p", { className: "truncate text-muted-foreground text-sm", children: email }) : null] }), _jsx(Badge, { variant: "secondary", className: "shrink-0 capitalize", children: traveler.role ?? messages.travelers.fallbackRole })] }));
|
|
139
|
+
}
|
|
140
|
+
function PersonLink({ personId, children }) {
|
|
141
|
+
const resolveHref = useAdminHref();
|
|
142
|
+
const navigateTo = useAdminNavigate();
|
|
143
|
+
return (_jsx("a", { href: resolveHref("person.detail", { personId }), onClick: (event) => {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
navigateTo("person.detail", { personId });
|
|
146
|
+
}, className: "text-primary hover:underline", children: children }));
|
|
147
|
+
}
|
|
148
|
+
function SummaryCard({ label, value, icon: Icon, }) {
|
|
149
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "flex items-center gap-3 p-4", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-muted", children: _jsx(Icon, { className: "size-4", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-muted-foreground text-xs", children: label }), _jsx("p", { className: "truncate font-semibold text-lg", children: value })] })] }) }));
|
|
150
|
+
}
|
|
151
|
+
function ComponentRow({ component, messages, onOpenBooking, }) {
|
|
152
|
+
const Icon = componentIcon(component);
|
|
153
|
+
const componentName = componentTitleFor(component);
|
|
154
|
+
const scheduleLabel = formatScheduleLabel(component);
|
|
155
|
+
const referenceLabel = componentReferenceLabelFor(component);
|
|
156
|
+
const secondary = [scheduleLabel, referenceLabel === componentName ? null : referenceLabel]
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(" · ");
|
|
159
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-md bg-muted", children: _jsx(Icon, { className: "size-4", "aria-hidden": "true" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate font-medium", children: componentName }), secondary ? (_jsx("p", { className: "truncate text-muted-foreground text-xs", children: secondary })) : null] })] }) }), _jsx(TableCell, { children: _jsx(Badge, { variant: component.status === "failed" ? "destructive" : "secondary", children: formatStatus(component.status) }) }), _jsx(TableCell, { children: formatMoney(component.componentTaxAmountCents, component.componentCurrency) }), _jsx(TableCell, { children: formatMoney(component.componentTotalAmountCents, component.componentCurrency) }), _jsx(TableCell, { className: "text-right", children: component.bookingId ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => onOpenBooking(component.bookingId ?? ""), children: [_jsx(ExternalLink, { className: "size-4", "aria-hidden": "true" }), messages.components.booking] })) : component.orderId || component.paymentSessionId ? (_jsx("span", { className: "text-muted-foreground text-sm", children: component.orderId ?? component.paymentSessionId })) : (_jsx("span", { className: "text-muted-foreground text-sm", children: messages.components.notCommitted })) })] }));
|
|
160
|
+
}
|
|
161
|
+
function tripScheduleLabel(components) {
|
|
162
|
+
const labels = components
|
|
163
|
+
.map(formatScheduleLabel)
|
|
164
|
+
.filter((label) => Boolean(label));
|
|
165
|
+
if (labels.length === 0)
|
|
166
|
+
return null;
|
|
167
|
+
if (labels.length === 1)
|
|
168
|
+
return labels[0];
|
|
169
|
+
return `${labels[0]} -> ${labels.at(-1)}`;
|
|
170
|
+
}
|
|
171
|
+
function SummaryLine({ label, value, strong = false, }) {
|
|
172
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: strong ? "font-semibold" : "font-medium", children: value })] }));
|
|
173
|
+
}
|
|
174
|
+
function componentIcon(component) {
|
|
175
|
+
if (component.kind === "flight_placeholder" || component.kind === "flight_order")
|
|
176
|
+
return Plane;
|
|
177
|
+
if (component.entityModule === "accommodations")
|
|
178
|
+
return BedDouble;
|
|
179
|
+
if (component.entityModule === "cruises")
|
|
180
|
+
return Ship;
|
|
181
|
+
return RouteIcon;
|
|
182
|
+
}
|
|
183
|
+
function formatStatus(value) {
|
|
184
|
+
return value.replaceAll("_", " ");
|
|
185
|
+
}
|
|
186
|
+
function formatDate(value) {
|
|
187
|
+
if (!value)
|
|
188
|
+
return "-";
|
|
189
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
190
|
+
if (Number.isNaN(date.getTime()))
|
|
191
|
+
return "-";
|
|
192
|
+
return date.toLocaleDateString();
|
|
193
|
+
}
|
|
194
|
+
function formatMoney(amountCents, currency) {
|
|
195
|
+
if (amountCents == null)
|
|
196
|
+
return "-";
|
|
197
|
+
return (amountCents / 100).toLocaleString(undefined, {
|
|
198
|
+
style: "currency",
|
|
199
|
+
currency: currency ?? "EUR",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function readBilling(travelerParty) {
|
|
203
|
+
const billing = travelerParty.billing;
|
|
204
|
+
if (!isRecord(billing))
|
|
205
|
+
return null;
|
|
206
|
+
const contact = isRecord(billing.contact) ? billing.contact : undefined;
|
|
207
|
+
return {
|
|
208
|
+
buyerType: stringValue(billing.buyerType),
|
|
209
|
+
personId: stringValue(billing.personId),
|
|
210
|
+
organizationId: stringValue(billing.organizationId),
|
|
211
|
+
contact: contact
|
|
212
|
+
? {
|
|
213
|
+
firstName: stringValue(contact.firstName),
|
|
214
|
+
lastName: stringValue(contact.lastName),
|
|
215
|
+
email: stringValue(contact.email),
|
|
216
|
+
phone: stringValue(contact.phone),
|
|
217
|
+
}
|
|
218
|
+
: undefined,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function readTravelers(travelerParty) {
|
|
222
|
+
const travelers = travelerParty.travelers;
|
|
223
|
+
if (!Array.isArray(travelers))
|
|
224
|
+
return [];
|
|
225
|
+
return travelers.filter(isRecord).map((traveler) => ({
|
|
226
|
+
localId: stringValue(traveler.localId),
|
|
227
|
+
personId: stringValue(traveler.personId) ?? null,
|
|
228
|
+
firstName: stringValue(traveler.firstName),
|
|
229
|
+
lastName: stringValue(traveler.lastName),
|
|
230
|
+
email: stringValue(traveler.email),
|
|
231
|
+
role: stringValue(traveler.role),
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
function formatPersonName(person) {
|
|
235
|
+
if (!person)
|
|
236
|
+
return null;
|
|
237
|
+
const name = [person.firstName, person.lastName]
|
|
238
|
+
.filter((part) => (part ?? "").trim().length > 0)
|
|
239
|
+
.join(" ")
|
|
240
|
+
.trim();
|
|
241
|
+
return name || person.email || null;
|
|
242
|
+
}
|
|
243
|
+
function formatContactName(contact) {
|
|
244
|
+
if (!contact)
|
|
245
|
+
return null;
|
|
246
|
+
const name = [contact.firstName, contact.lastName]
|
|
247
|
+
.filter((part) => (part ?? "").trim().length > 0)
|
|
248
|
+
.join(" ")
|
|
249
|
+
.trim();
|
|
250
|
+
return name || null;
|
|
251
|
+
}
|
|
252
|
+
function isRecord(value) {
|
|
253
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
254
|
+
}
|
|
255
|
+
function stringValue(value) {
|
|
256
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
257
|
+
}
|