@voyant-travel/bookings-react 0.199.0 → 0.200.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,16 @@
1
1
  // agent-quality: file-size exception -- owner: bookings-react; existing UI surface stays co-located until a dedicated split preserves behavior and tests.
2
2
  "use client";
3
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
- import { useQueryClient } from "@tanstack/react-query";
5
4
  import { useAdminNavigate, useLocale, useOperatorAdminMessages } from "@voyant-travel/admin";
6
- import { legalQueryKeys, useLegalContractAttachments, useLegalContracts, useVoyantLegalContext, } from "@voyant-travel/legal-react";
5
+ import { useLegalContractAttachments, useLegalContracts, useVoyantLegalContext, } from "@voyant-travel/legal-react";
7
6
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, } from "@voyant-travel/ui/components";
8
7
  import { DataTable } from "@voyant-travel/ui/components/data-table";
9
- import { ArrowUpRight, Download, FileText, Loader2, Plus, RotateCw, Trash2 } from "lucide-react";
8
+ import { ArrowUpRight, Download, FileText, Loader2, Plus, Trash2 } from "lucide-react";
10
9
  import { useMemo, useState } from "react";
11
10
  import { BookingDocumentDialog } from "../components/booking-document-dialog.js";
12
11
  import { IconActionButton } from "../components/icon-action-button.js";
13
12
  import { StatusBadge } from "../components/status-badge.js";
14
- import { useBooking, useBookingContractGenerationMutation, useBookingTravelerDocumentMutation, useBookingTravelerDocuments, useTravelers, } from "../index.js";
15
- import { BookingContractDialog } from "./booking-contract-dialog.js";
16
- const CONTRACT_GENERATION_FAILURE_LABELS = {
17
- render_unavailable: "contractGenerationTemplateError",
18
- generator_failed: "contractGenerationGeneratorFailed",
19
- };
20
- function resolveContractGenerationFailure(contract) {
21
- const metadata = contract.metadata;
22
- if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
23
- return null;
24
- }
25
- const status = metadata.lastGenerationStatus;
26
- if (typeof status !== "string" || status === "generated") {
27
- return null;
28
- }
29
- return {
30
- status,
31
- error: typeof metadata.lastGenerationError === "string" && metadata.lastGenerationError.trim()
32
- ? metadata.lastGenerationError
33
- : null,
34
- attemptedAt: typeof metadata.lastGenerationAttemptedAt === "string"
35
- ? metadata.lastGenerationAttemptedAt
36
- : null,
37
- };
38
- }
13
+ import { useBookingTravelerDocumentMutation, useBookingTravelerDocuments, useTravelers, } from "../index.js";
39
14
  /**
40
15
  * Unified Documents tab for a booking — flattens auto-generated legal
41
16
  * contracts and per-traveler documents (passport, visa, insurance…) into
@@ -47,11 +22,8 @@ function resolveContractGenerationFailure(contract) {
47
22
  export function BookingDocumentsTable({ bookingId, }) {
48
23
  const t = useOperatorAdminMessages().bookings.detail.documentsTable;
49
24
  const [uploadOpen, setUploadOpen] = useState(false);
50
- const [contractDialogOpen, setContractDialogOpen] = useState(false);
51
25
  const [deleteTarget, setDeleteTarget] = useState(null);
52
26
  const [deletePending, setDeletePending] = useState(false);
53
- const bookingQuery = useBooking(bookingId);
54
- const booking = bookingQuery.data?.data ?? null;
55
27
  const contractsQuery = useLegalContracts({ bookingId, limit: 25 });
56
28
  const contracts = contractsQuery.data?.data ?? [];
57
29
  const travelerDocsQuery = useBookingTravelerDocuments(bookingId);
@@ -59,7 +31,7 @@ export function BookingDocumentsTable({ bookingId, }) {
59
31
  const travelersQuery = useTravelers(bookingId);
60
32
  const travelersById = useMemo(() => new Map((travelersQuery.data?.data ?? []).map((tr) => [tr.id, tr])), [travelersQuery.data]);
61
33
  const removeTravelerDoc = useBookingTravelerDocumentMutation(bookingId).remove;
62
- const isLoading = bookingQuery.isLoading || contractsQuery.isLoading || travelerDocsQuery.isLoading;
34
+ const isLoading = contractsQuery.isLoading || travelerDocsQuery.isLoading;
63
35
  const rows = useMemo(() => [
64
36
  ...contracts.map((contract) => ({
65
37
  kind: "contract",
@@ -132,7 +104,7 @@ export function BookingDocumentsTable({ bookingId, }) {
132
104
  setDeletePending(false);
133
105
  }
134
106
  };
135
- return (_jsxs("div", { "data-slot": "booking-documents-list", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }), t.title] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setContractDialogOpen(true), disabled: !booking, children: [_jsx(FileText, { className: "mr-1.5 h-3.5 w-3.5" }), t.addContract] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setUploadOpen(true), children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), t.uploadDocument] })] })] }), isLoading ? (_jsxs("div", { className: "flex items-center justify-center gap-2 py-6 text-muted-foreground text-sm", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), t.loading] })) : (_jsx(DataTable, { columns: columns, data: rows, emptyMessage: t.empty, showPagination: false })), _jsx(BookingDocumentDialog, { open: uploadOpen, onOpenChange: setUploadOpen, bookingId: bookingId }), _jsx(BookingContractDialog, { open: contractDialogOpen, onOpenChange: setContractDialogOpen, bookingId: bookingId, bookingNumber: booking?.bookingNumber ?? null }), _jsx(AlertDialog, { open: Boolean(deleteTarget), onOpenChange: (next) => {
107
+ return (_jsxs("div", { "data-slot": "booking-documents-list", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(FileText, { className: "h-4 w-4 text-muted-foreground" }), t.title] }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setUploadOpen(true), children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), t.uploadDocument] }) })] }), isLoading ? (_jsxs("div", { className: "flex items-center justify-center gap-2 py-6 text-muted-foreground text-sm", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), t.loading] })) : (_jsx(DataTable, { columns: columns, data: rows, emptyMessage: t.empty, showPagination: false })), _jsx(BookingDocumentDialog, { open: uploadOpen, onOpenChange: setUploadOpen, bookingId: bookingId }), _jsx(AlertDialog, { open: Boolean(deleteTarget), onOpenChange: (next) => {
136
108
  if (!next && !deletePending)
137
109
  setDeleteTarget(null);
138
110
  }, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t.deleteConfirm }), deleteTarget ? (_jsx(AlertDialogDescription, { children: deleteTarget.fileName })) : null] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: deletePending, children: t.deleteCancel }), _jsx(AlertDialogAction, { variant: "destructive", disabled: deletePending, onClick: () => void handleDeleteConfirm(), children: t.deleteConfirmAction })] })] }) })] }));
@@ -159,15 +131,7 @@ function ContractDocumentCell({ contract }) {
159
131
  function TravelerDocumentCell({ doc }) {
160
132
  return (_jsxs("a", { href: doc.fileUrl, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1.5 text-primary hover:underline", children: [_jsx(FileText, { className: "h-3.5 w-3.5 shrink-0 opacity-60" }), _jsx("span", { className: "truncate", children: doc.fileName }), _jsx(ArrowUpRight, { className: "h-3 w-3" })] }));
161
133
  }
162
- function ContractStatusCell({ contract, messages, }) {
163
- const generationFailure = resolveContractGenerationFailure(contract);
164
- if (generationFailure) {
165
- const failureLabelKey = CONTRACT_GENERATION_FAILURE_LABELS[generationFailure.status];
166
- const failureLabel = failureLabelKey
167
- ? messages[failureLabelKey]
168
- : messages.contractGenerationFailed;
169
- return (_jsxs("div", { className: "max-w-80 space-y-1", children: [_jsx(StatusBadge, { status: "failed", children: failureLabel }), _jsx("p", { className: "text-muted-foreground text-xs", children: generationFailure.error ?? messages.contractGenerationErrorFallback })] }));
170
- }
134
+ function ContractStatusCell({ contract, }) {
171
135
  return _jsx(StatusBadge, { status: contract.status, children: contract.status.replace(/_/g, " ") });
172
136
  }
173
137
  function TravelerStatusCell({ doc, messages, }) {
@@ -178,16 +142,11 @@ function TravelerStatusCell({ doc, messages, }) {
178
142
  }
179
143
  function ContractDateCell({ contract, messages, }) {
180
144
  const { resolvedLocale } = useLocale();
181
- const generationFailure = resolveContractGenerationFailure(contract);
182
145
  const attachmentsQuery = useLegalContractAttachments({ contractId: contract.id });
183
146
  const attachments = (attachmentsQuery.data ?? []).filter((a) => a.kind === "document");
184
147
  const hasDocument = attachments.length > 0;
185
- const dateIso = generationFailure?.attemptedAt ?? contract.issuedAt ?? contract.createdAt ?? null;
186
- const dateLabel = generationFailure
187
- ? messages.contractGenerationAttemptedLabel
188
- : hasDocument
189
- ? messages.contractIssuedLabel
190
- : messages.contractPendingSinceLabel;
148
+ const dateIso = contract.issuedAt ?? contract.createdAt ?? null;
149
+ const dateLabel = hasDocument ? messages.contractIssuedLabel : messages.contractPendingSinceLabel;
191
150
  if (!dateIso)
192
151
  return _jsx("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
193
152
  return (_jsxs("span", { className: "text-muted-foreground text-xs", children: [_jsxs("span", { className: "opacity-60", children: [dateLabel, " "] }), formatDate(dateIso, resolvedLocale)] }));
@@ -201,30 +160,18 @@ function TravelerDateCell({ doc, messages, }) {
201
160
  return (_jsxs("span", { className: "text-muted-foreground text-xs", children: [_jsxs("span", { className: "opacity-60", children: [dateLabel, " "] }), formatDate(dateIso, resolvedLocale)] }));
202
161
  }
203
162
  function ContractActionsCell({ contract, messages, }) {
204
- const queryClient = useQueryClient();
205
163
  const navigateTo = useAdminNavigate();
206
164
  const attachmentsQuery = useLegalContractAttachments({ contractId: contract.id });
207
165
  const attachments = (attachmentsQuery.data ?? []).filter((a) => a.kind === "document");
208
166
  const latest = attachments[0] ?? null;
209
- const hasDocument = latest !== null;
210
167
  const downloadHref = useContractAttachmentDownloadHref(latest);
211
- const { generate } = useBookingContractGenerationMutation(contract.bookingId ?? "");
212
168
  return (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(IconActionButton, { label: messages.contractOpenTooltip, icon: _jsx(ArrowUpRight, { className: "h-3.5 w-3.5" }), onClick: (e) => {
213
169
  e.stopPropagation();
214
170
  navigateTo("contract.detail", { contractId: contract.id });
215
171
  } }), downloadHref ? (_jsx(IconActionButton, { label: messages.downloadDocumentAria, icon: _jsx(Download, { className: "h-3.5 w-3.5" }), onClick: (e) => {
216
172
  e.stopPropagation();
217
173
  window.open(downloadHref, "_blank", "noopener,noreferrer");
218
- } })) : null, _jsx(IconActionButton, { label: hasDocument ? messages.contractRegenerateTooltip : messages.contractGenerateTooltip, icon: generate.isPending ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(RotateCw, { className: "h-3.5 w-3.5" })), disabled: generate.isPending || !contract.bookingId, onClick: (e) => {
219
- e.stopPropagation();
220
- if (!contract.bookingId)
221
- return;
222
- generate.mutate({ force: hasDocument }, {
223
- onSuccess: () => {
224
- void queryClient.invalidateQueries({ queryKey: legalQueryKeys.contracts() });
225
- },
226
- });
227
- } })] }));
174
+ } })) : null] }));
228
175
  }
229
176
  function TravelerActionsCell({ doc, messages, onDelete, }) {
230
177
  return (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(IconActionButton, { label: messages.downloadDocumentAria, icon: _jsx(Download, { className: "h-3.5 w-3.5" }), onClick: (e) => {
@@ -73,7 +73,6 @@ declare module "@voyant-travel/admin" {
73
73
  };
74
74
  }
75
75
  }
76
- export type { BookingContractDialogProps } from "./booking-contract-dialog.js";
77
76
  export type { BookingDetailHostProps, BookingDetailHostSlot, BookingDetailHostSlotContext, BookingDetailHostSlots, BookingDetailPaymentActions, BookingDetailPaymentControllerSlotContext, } from "./booking-detail-host.js";
78
77
  export { BookingDetailSkeleton } from "./booking-detail-skeleton.js";
79
78
  export type { BookingDocumentsTableProps } from "./booking-documents-table.js";
@@ -135,9 +134,9 @@ export declare const bookingDetailTabSchema: z.ZodEnum<{
135
134
  travelers: "travelers";
136
135
  activity: "activity";
137
136
  documents: "documents";
138
- suppliers: "suppliers";
139
- invoices: "invoices";
140
137
  finance: "finance";
138
+ invoices: "invoices";
139
+ suppliers: "suppliers";
141
140
  }>;
142
141
  /**
143
142
  * Search contract for the booking detail page. `productId`/`slotId` only
@@ -153,9 +152,9 @@ export declare const bookingDetailSearchSchema: z.ZodObject<{
153
152
  travelers: "travelers";
154
153
  activity: "activity";
155
154
  documents: "documents";
156
- suppliers: "suppliers";
157
- invoices: "invoices";
158
155
  finance: "finance";
156
+ invoices: "invoices";
157
+ suppliers: "suppliers";
159
158
  }>>;
160
159
  }, z.core.$strip>;
161
160
  export type BookingDetailSearchParams = z.infer<typeof bookingDetailSearchSchema>;
@@ -2,7 +2,6 @@ export { type UseBookingOptions, useBooking } from "./use-booking.js";
2
2
  export { type UseBookingActionLedgerOptions, useBookingActionLedger, } from "./use-booking-action-ledger.js";
3
3
  export { type UseBookingActivityOptions, useBookingActivity } from "./use-booking-activity.js";
4
4
  export { type CancelBookingInput, useBookingCancelMutation, } from "./use-booking-cancel-mutation.js";
5
- export { type GenerateBookingContractInput, useBookingContractGenerationMutation, } from "./use-booking-contract-generation.js";
6
5
  export { type ConvertProductToBookingInput, useBookingConvertMutation, } from "./use-booking-convert-mutation.js";
7
6
  export { type BookingCreateExtraLineInput, type BookingCreateGroupMembershipInput, type BookingCreateInput, type BookingCreateItemLineInput, type BookingCreatePaymentScheduleInput, type BookingCreateResult, type BookingCreateTravelCreditRedemptionInput, type BookingCreateTravelerInput, useBookingCreateMutation, } from "./use-booking-create-mutation.js";
8
7
  export { type CreateBookingTravelerDocumentInput, type UseBookingTravelerDocumentsOptions, useBookingTravelerDocumentMutation, useBookingTravelerDocuments, } from "./use-booking-documents.js";
@@ -2,7 +2,6 @@ export { useBooking } from "./use-booking.js";
2
2
  export { useBookingActionLedger, } from "./use-booking-action-ledger.js";
3
3
  export { useBookingActivity } from "./use-booking-activity.js";
4
4
  export { useBookingCancelMutation, } from "./use-booking-cancel-mutation.js";
5
- export { useBookingContractGenerationMutation, } from "./use-booking-contract-generation.js";
6
5
  export { useBookingConvertMutation, } from "./use-booking-convert-mutation.js";
7
6
  export { useBookingCreateMutation, } from "./use-booking-create-mutation.js";
8
7
  export { useBookingTravelerDocumentMutation, useBookingTravelerDocuments, } from "./use-booking-documents.js";
package/dist/schemas.d.ts CHANGED
@@ -2436,27 +2436,3 @@ export declare const bookingActionLedgerListResponse: z.ZodObject<{
2436
2436
  }, z.core.$strip>;
2437
2437
  }, z.core.$strip>;
2438
2438
  export type BookingActionLedgerListResult = z.infer<typeof bookingActionLedgerListResponse>;
2439
- export declare const bookingContractPreviewSchema: z.ZodObject<{
2440
- html: z.ZodString;
2441
- templateName: z.ZodOptional<z.ZodString>;
2442
- templateLanguage: z.ZodOptional<z.ZodString>;
2443
- }, z.core.$strip>;
2444
- export type BookingContractPreview = z.infer<typeof bookingContractPreviewSchema>;
2445
- export declare const bookingContractPreviewResponse: z.ZodObject<{
2446
- data: z.ZodObject<{
2447
- html: z.ZodString;
2448
- templateName: z.ZodOptional<z.ZodString>;
2449
- templateLanguage: z.ZodOptional<z.ZodString>;
2450
- }, z.core.$strip>;
2451
- }, z.core.$strip>;
2452
- export declare const bookingGenerateContractResultSchema: z.ZodObject<{
2453
- contractId: z.ZodString;
2454
- attachmentId: z.ZodString;
2455
- }, z.core.$strip>;
2456
- export type BookingGenerateContractResult = z.infer<typeof bookingGenerateContractResultSchema>;
2457
- export declare const bookingGenerateContractResponse: z.ZodObject<{
2458
- data: z.ZodObject<{
2459
- contractId: z.ZodString;
2460
- attachmentId: z.ZodString;
2461
- }, z.core.$strip>;
2462
- }, z.core.$strip>;
package/dist/schemas.js CHANGED
@@ -513,18 +513,3 @@ export const bookingActionLedgerListResponse = z.object({
513
513
  nextCursor: bookingActionLedgerCursorSchema.nullable(),
514
514
  }),
515
515
  });
516
- // Contract generation — `POST /v1/admin/bookings/:id/generate-contract`.
517
- // The route renders the booking's contract template; `{ preview: true }`
518
- // returns the rendered HTML without persisting anything, the default
519
- // call creates the legal contract row + persists the PDF attachment.
520
- export const bookingContractPreviewSchema = z.object({
521
- html: z.string(),
522
- templateName: z.string().optional(),
523
- templateLanguage: z.string().optional(),
524
- });
525
- export const bookingContractPreviewResponse = singleEnvelope(bookingContractPreviewSchema);
526
- export const bookingGenerateContractResultSchema = z.object({
527
- contractId: z.string(),
528
- attachmentId: z.string(),
529
- });
530
- export const bookingGenerateContractResponse = singleEnvelope(bookingGenerateContractResultSchema);
@@ -27,9 +27,8 @@
27
27
  * - The booking row has not been created yet — `booking.bookingId`,
28
28
  * `booking.bookingNumber`, the persisted status, and
29
29
  * `contract.contractNumber` / `contract.signedAt` are empty. The
30
- * server-side auto-generate-contract subscriber re-renders the
31
- * same template on `booking.confirmed` with the persisted values
32
- * filled in.
30
+ * a later Legal-owned document command uses the contract's persisted,
31
+ * immutable rendered body.
33
32
  * - `acceptance.ipAddress` / `userAgent` are captured server-side
34
33
  * at `/checkout/start` from request headers. Empty during preview.
35
34
  * - `operator.*` reads from the `operatorInfo` block injected by the
@@ -107,8 +106,8 @@ export interface ResolveContractVariablesContext {
107
106
  /** Operator profile — fetched from `/v1/public/operator-profile` by
108
107
  * the storefront wrapper. Anything missing renders as empty. */
109
108
  operatorInfo?: OperatorInfoVariables;
110
- /** Acceptance fingerprint — populated only on server-side renders
111
- * (post-confirm contract auto-generation). At preview time the
109
+ /** Acceptance fingerprint — populated only on server-side persistence.
110
+ * At preview time the
112
111
  * storefront leaves this undefined and the variables render
113
112
  * empty. */
114
113
  acceptance?: AcceptanceContextVariables;
@@ -28,9 +28,8 @@
28
28
  * - The booking row has not been created yet — `booking.bookingId`,
29
29
  * `booking.bookingNumber`, the persisted status, and
30
30
  * `contract.contractNumber` / `contract.signedAt` are empty. The
31
- * server-side auto-generate-contract subscriber re-renders the
32
- * same template on `booking.confirmed` with the persisted values
33
- * filled in.
31
+ * a later Legal-owned document command uses the contract's persisted,
32
+ * immutable rendered body.
34
33
  * - `acceptance.ipAddress` / `userAgent` are captured server-side
35
34
  * at `/checkout/start` from request headers. Empty during preview.
36
35
  * - `operator.*` reads from the `operatorInfo` block injected by the
@@ -31,6 +31,6 @@ export declare function buildStorefrontCheckoutStartBody(input: {
31
31
  renderedHtml: string;
32
32
  } | undefined;
33
33
  bookingId: string;
34
- paymentIntent: "hold" | "card" | "bank_transfer" | "inquiry";
34
+ paymentIntent: "hold" | "bank_transfer" | "card" | "inquiry";
35
35
  };
36
36
  export declare function buildStorefrontCommitParty(draft: Draft): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyant-travel/bookings-react",
3
- "version": "0.199.0",
3
+ "version": "0.200.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -151,23 +151,23 @@
151
151
  "react-hook-form": "^7.80.0",
152
152
  "zod": "^4.0.0",
153
153
  "@voyant-travel/admin": "^0.129.1",
154
- "@voyant-travel/bookings": "^0.199.0",
155
- "@voyant-travel/accommodations": "^0.159.0",
156
- "@voyant-travel/catalog-react": "^0.197.0",
157
- "@voyant-travel/distribution-react": "^0.189.0",
158
- "@voyant-travel/catalog": "^0.197.0",
159
- "@voyant-travel/finance": "^0.199.0",
160
- "@voyant-travel/relationships-react": "^0.199.0",
161
- "@voyant-travel/finance-react": "^0.199.0",
162
- "@voyant-travel/identity-react": "^0.199.0",
163
- "@voyant-travel/commerce-react": "^0.81.0",
164
- "@voyant-travel/cruises": "^0.198.0",
165
- "@voyant-travel/inventory-react": "^0.81.0",
166
- "@voyant-travel/legal-react": "^0.199.0",
167
- "@voyant-travel/inventory": "^0.19.2",
168
- "@voyant-travel/storefront-react": "^0.201.0",
169
- "@voyant-travel/ui": "^0.109.5",
170
- "@voyant-travel/operations-react": "^0.80.0"
154
+ "@voyant-travel/accommodations": "^0.160.0",
155
+ "@voyant-travel/bookings": "^0.200.0",
156
+ "@voyant-travel/catalog": "^0.198.0",
157
+ "@voyant-travel/catalog-react": "^0.198.0",
158
+ "@voyant-travel/relationships-react": "^0.200.0",
159
+ "@voyant-travel/distribution-react": "^0.190.0",
160
+ "@voyant-travel/finance": "^0.200.0",
161
+ "@voyant-travel/finance-react": "^0.200.0",
162
+ "@voyant-travel/identity-react": "^0.200.0",
163
+ "@voyant-travel/legal-react": "^0.200.0",
164
+ "@voyant-travel/commerce-react": "^0.82.0",
165
+ "@voyant-travel/cruises": "^0.199.0",
166
+ "@voyant-travel/inventory-react": "^0.82.0",
167
+ "@voyant-travel/inventory": "^0.19.3",
168
+ "@voyant-travel/storefront-react": "^0.202.0",
169
+ "@voyant-travel/ui": "^0.109.6",
170
+ "@voyant-travel/operations-react": "^0.81.0"
171
171
  },
172
172
  "peerDependenciesMeta": {
173
173
  "@tanstack/react-table": {
@@ -233,8 +233,8 @@
233
233
  },
234
234
  "dependencies": {
235
235
  "sonner": "^2.0.7",
236
- "@voyant-travel/catalog-contracts": "^0.112.1",
237
236
  "@voyant-travel/i18n": "^0.117.2",
237
+ "@voyant-travel/catalog-contracts": "^0.112.1",
238
238
  "@voyant-travel/react": "^0.104.2",
239
239
  "@voyant-travel/types": "^0.109.9"
240
240
  },
@@ -251,26 +251,26 @@
251
251
  "typescript": "^6.0.3",
252
252
  "vitest": "^4.1.9",
253
253
  "zod": "^4.4.3",
254
- "@voyant-travel/accommodations": "^0.159.0",
255
- "@voyant-travel/catalog": "^0.197.0",
256
- "@voyant-travel/bookings": "^0.199.0",
257
- "@voyant-travel/catalog-react": "^0.197.0",
258
- "@voyant-travel/distribution-react": "^0.189.0",
259
- "@voyant-travel/relationships-react": "^0.199.0",
260
- "@voyant-travel/finance": "^0.199.0",
261
- "@voyant-travel/finance-react": "^0.199.0",
262
- "@voyant-travel/identity-react": "^0.199.0",
263
- "@voyant-travel/legal-react": "^0.199.0",
264
- "@voyant-travel/commerce-react": "^0.81.0",
265
- "@voyant-travel/inventory-react": "^0.81.0",
266
- "@voyant-travel/inventory": "^0.19.2",
267
- "@voyant-travel/react": "^0.104.2",
268
- "@voyant-travel/storefront-react": "^0.201.0",
269
- "@voyant-travel/ui": "^0.109.5",
270
- "@voyant-travel/voyant-typescript-config": "^0.1.0",
271
- "@voyant-travel/operations-react": "^0.80.0",
254
+ "@voyant-travel/accommodations": "^0.160.0",
272
255
  "@voyant-travel/admin": "^0.129.1",
273
- "@voyant-travel/cruises": "^0.198.0"
256
+ "@voyant-travel/catalog-react": "^0.198.0",
257
+ "@voyant-travel/distribution-react": "^0.190.0",
258
+ "@voyant-travel/bookings": "^0.200.0",
259
+ "@voyant-travel/relationships-react": "^0.200.0",
260
+ "@voyant-travel/finance": "^0.200.0",
261
+ "@voyant-travel/finance-react": "^0.200.0",
262
+ "@voyant-travel/identity-react": "^0.200.0",
263
+ "@voyant-travel/legal-react": "^0.200.0",
264
+ "@voyant-travel/commerce-react": "^0.82.0",
265
+ "@voyant-travel/cruises": "^0.199.0",
266
+ "@voyant-travel/inventory-react": "^0.82.0",
267
+ "@voyant-travel/inventory": "^0.19.3",
268
+ "@voyant-travel/react": "^0.104.2",
269
+ "@voyant-travel/catalog": "^0.198.0",
270
+ "@voyant-travel/storefront-react": "^0.202.0",
271
+ "@voyant-travel/ui": "^0.109.6",
272
+ "@voyant-travel/operations-react": "^0.81.0",
273
+ "@voyant-travel/voyant-typescript-config": "^0.1.0"
274
274
  },
275
275
  "files": [
276
276
  "dist",
@@ -1,21 +0,0 @@
1
- export interface BookingContractDialogProps {
2
- open: boolean;
3
- onOpenChange: (open: boolean) => void;
4
- bookingId: string;
5
- bookingNumber?: string | null;
6
- onSuccess?: () => void;
7
- }
8
- /**
9
- * "Add contract" dialog for a booking. Two modes:
10
- *
11
- * - **Generate** (default): hits the server-side preview branch of
12
- * `/v1/admin/bookings/:id/generate-contract` (which runs the same
13
- * template + variable build the customer would see at checkout)
14
- * and renders the HTML in a sandboxed iframe. Confirm fires the
15
- * full generate, which creates the contract row + persists the PDF.
16
- *
17
- * - **Upload**: operator picks a pre-signed PDF (e.g. countersigned
18
- * copy). The dialog creates a `signed`-status contract row and
19
- * attaches the uploaded file via the legal attachment upload route.
20
- */
21
- export declare function BookingContractDialog({ open, onOpenChange, bookingId, bookingNumber, onSuccess, }: BookingContractDialogProps): import("react").JSX.Element;
@@ -1,171 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useQueryClient } from "@tanstack/react-query";
4
- import { useOperatorAdminMessages } from "@voyant-travel/admin";
5
- import { legalQueryKeys, useLegalContractAttachmentMutation, useLegalContractMutation, } from "@voyant-travel/legal-react";
6
- import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Label, Skeleton, } from "@voyant-travel/ui/components";
7
- import { FileText, Loader2, Paperclip, X } from "lucide-react";
8
- import { useEffect, useState } from "react";
9
- import { VoyantApiError } from "../client.js";
10
- import { useBookingContractGenerationMutation } from "../index.js";
11
- /**
12
- * "Add contract" dialog for a booking. Two modes:
13
- *
14
- * - **Generate** (default): hits the server-side preview branch of
15
- * `/v1/admin/bookings/:id/generate-contract` (which runs the same
16
- * template + variable build the customer would see at checkout)
17
- * and renders the HTML in a sandboxed iframe. Confirm fires the
18
- * full generate, which creates the contract row + persists the PDF.
19
- *
20
- * - **Upload**: operator picks a pre-signed PDF (e.g. countersigned
21
- * copy). The dialog creates a `signed`-status contract row and
22
- * attaches the uploaded file via the legal attachment upload route.
23
- */
24
- export function BookingContractDialog({ open, onOpenChange, bookingId, bookingNumber, onSuccess, }) {
25
- const t = useOperatorAdminMessages().bookings.detail.contractDialog;
26
- const queryClient = useQueryClient();
27
- const { create: createContract } = useLegalContractMutation();
28
- const { upload: uploadAttachment } = useLegalContractAttachmentMutation();
29
- const { preview, generate } = useBookingContractGenerationMutation(bookingId);
30
- const [mode, setMode] = useState("generate");
31
- const [uploading, setUploading] = useState(false);
32
- const [error, setError] = useState(null);
33
- const [previewRequested, setPreviewRequested] = useState(false);
34
- // Upload form state
35
- const [title, setTitle] = useState("");
36
- const [file, setFile] = useState(null);
37
- // Reset on open. Generate is the leading mode so the preview fetch
38
- // kicks off immediately.
39
- const resetPreview = preview.reset;
40
- useEffect(() => {
41
- if (!open)
42
- return;
43
- setMode("generate");
44
- setTitle("");
45
- setFile(null);
46
- setError(null);
47
- setUploading(false);
48
- setPreviewRequested(false);
49
- resetPreview();
50
- }, [open, resetPreview]);
51
- // Fetch preview HTML every time the dialog opens (or the mode flips
52
- // back to Generate). The preview reflects current booking + template
53
- // state so re-fetching is intentional.
54
- const fetchPreview = preview.mutate;
55
- useEffect(() => {
56
- if (!open || mode !== "generate")
57
- return;
58
- setPreviewRequested(true);
59
- fetchPreview();
60
- }, [open, mode, fetchPreview]);
61
- const handleGenerate = async () => {
62
- setError(null);
63
- try {
64
- await generate.mutateAsync({});
65
- await queryClient.invalidateQueries({ queryKey: legalQueryKeys.contracts() });
66
- onSuccess?.();
67
- onOpenChange(false);
68
- }
69
- catch (err) {
70
- setError(err instanceof Error ? err.message : String(err));
71
- }
72
- };
73
- const handleUpload = async () => {
74
- if (!file) {
75
- setError(t.uploadFileRequired);
76
- return;
77
- }
78
- setUploading(true);
79
- setError(null);
80
- try {
81
- const fallbackTitle = title.trim() ||
82
- (bookingNumber
83
- ? `Contract ${bookingNumber}`
84
- : `Contract for booking ${bookingId.slice(-8)}`);
85
- const created = await createContract.mutateAsync({
86
- scope: "customer",
87
- status: "signed",
88
- title: fallbackTitle,
89
- bookingId,
90
- metadata: { uploadedByOperator: true },
91
- });
92
- await uploadAttachment.mutateAsync({
93
- contractId: created.id,
94
- input: { file, kind: "document", name: file.name },
95
- });
96
- onSuccess?.();
97
- onOpenChange(false);
98
- }
99
- catch (err) {
100
- setError(err instanceof Error ? err.message : String(err));
101
- }
102
- finally {
103
- setUploading(false);
104
- }
105
- };
106
- const submitting = generate.isPending || uploading;
107
- const previewReady = preview.data != null;
108
- const canSubmit = mode === "generate" ? previewReady && !submitting : file != null && !submitting;
109
- const previewSetupMissing = preview.error instanceof VoyantApiError && preview.error.status === 404;
110
- const showPreviewSetupHint = mode === "generate" &&
111
- previewRequested &&
112
- !preview.isPending &&
113
- !preview.data &&
114
- (!preview.isError || previewSetupMissing);
115
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "w-full! max-w-4xl! gap-0 p-0", children: [_jsxs(DialogHeader, { className: "shrink-0 border-b px-6 py-4", children: [_jsx(DialogTitle, { children: t.title }), _jsx(DialogDescription, { children: t.description })] }), _jsx("div", { className: "max-h-[70vh] overflow-y-auto", children: _jsxs("div", { className: "flex flex-col gap-4 px-6 py-5", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.modeLabel }), _jsx(SegmentedChoice, { value: mode, onChange: setMode, options: [
116
- { value: "generate", label: t.modeGenerate },
117
- { value: "upload", label: t.modeUpload },
118
- ] })] }), mode === "generate" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.previewLabel }), _jsx("div", { className: "overflow-hidden rounded-md border bg-muted/30", children: preview.isPending ? (_jsxs("div", { className: "flex flex-col gap-3 p-6", children: [_jsx(Skeleton, { className: "h-6 w-1/2" }), _jsx(Skeleton, { className: "h-4 w-full" }), _jsx(Skeleton, { className: "h-4 w-5/6" }), _jsx(Skeleton, { className: "h-4 w-4/5" }), _jsx(Skeleton, { className: "h-4 w-full" })] })) : preview.isError && !previewSetupMissing ? (_jsxs("p", { className: "p-6 text-destructive text-sm", children: [t.previewErrorPrefix, " ", preview.error instanceof Error ? preview.error.message : t.previewFailed] })) : preview.data ? (_jsx("iframe", { title: preview.data.templateName || t.previewIframeFallback, srcDoc: wrapPreviewHtml(preview.data.html), sandbox: "", className: "h-[60vh] w-full border-0 bg-white" })) : (_jsx("div", { className: "p-6 text-muted-foreground text-sm", children: t.previewUnavailable })) }), preview.data?.templateName ? (_jsxs("p", { className: "text-muted-foreground text-xs", children: [t.previewTemplateLabel, " ", preview.data.templateName] })) : null, showPreviewSetupHint ? (_jsx("p", { className: "text-muted-foreground text-xs", children: t.previewSetupHint })) : null] })) : (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.uploadTitleLabel }), _jsx(Input, { value: title, onChange: (e) => setTitle(e.target.value), placeholder: bookingNumber ? `Contract ${bookingNumber}` : t.uploadTitlePlaceholder }), _jsx("p", { className: "text-muted-foreground text-xs", children: t.uploadTitleHint })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.uploadFileLabel }), _jsx("input", { type: "file", accept: "application/pdf", onChange: (e) => {
119
- const next = e.target.files?.[0] ?? null;
120
- setFile(next);
121
- }, className: "block w-full text-sm file:mr-3 file:rounded-md file:border file:bg-muted file:px-3 file:py-1.5 file:text-sm file:font-medium hover:file:bg-muted/70" }), file ? (_jsxs("div", { className: "flex items-center justify-between gap-2 rounded-md border bg-background px-3 py-1.5 text-sm", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Paperclip, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: file.name })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: () => setFile(null), children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })) : null] })] })), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }) }), _jsxs("div", { className: "flex shrink-0 items-center justify-end gap-2 border-t px-6 py-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), disabled: submitting, children: t.cancel }), _jsxs(Button, { type: "button", disabled: !canSubmit, onClick: () => {
122
- if (mode === "generate") {
123
- void handleGenerate();
124
- }
125
- else {
126
- void handleUpload();
127
- }
128
- }, children: [submitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, mode === "generate" ? (_jsxs(_Fragment, { children: [_jsx(FileText, { className: "mr-1.5 h-3.5 w-3.5" }), t.generateAction] })) : (t.uploadAction)] })] })] }) }));
129
- }
130
- function SegmentedChoice({ value, onChange, options, }) {
131
- return (_jsx("div", { className: "flex w-full rounded-md border bg-background p-0.5", children: options.map((opt) => {
132
- const active = opt.value === value;
133
- return (_jsx("button", { type: "button", onClick: () => onChange(opt.value), className: "flex-1 rounded-sm px-3 py-1.5 text-sm font-medium transition-colors " +
134
- (active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"), children: opt.label }, opt.value));
135
- }) }));
136
- }
137
- /**
138
- * Wrap the rendered template body in a light-theme HTML document so
139
- * the iframe doesn't inherit the dark dashboard background. Mirrors
140
- * the storefront's contract-preview wrapper to keep the WYSIWYG
141
- * promise honest.
142
- */
143
- function wrapPreviewHtml(body) {
144
- return `<!DOCTYPE html>
145
- <html lang="en">
146
- <head>
147
- <meta charset="utf-8" />
148
- <style>
149
- :root { color-scheme: light; }
150
- html, body { margin: 0; background: #ffffff; color: #111827; }
151
- body {
152
- padding: 1.5rem 2rem;
153
- font-family: ui-serif, Georgia, "Times New Roman", serif;
154
- font-size: 15px;
155
- line-height: 1.6;
156
- }
157
- h1, h2, h3 { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #0f172a; }
158
- h1 { font-size: 1.5rem; margin: 0 0 1rem; }
159
- h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
160
- p { margin: 0.5rem 0; }
161
- ul, ol { padding-left: 1.5rem; }
162
- strong { color: #0f172a; }
163
- a { color: #2563eb; }
164
- table { border-collapse: collapse; width: 100%; margin: 0.75rem 0; }
165
- th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; text-align: left; }
166
- th { background: #f9fafb; }
167
- </style>
168
- </head>
169
- <body>${body}</body>
170
- </html>`;
171
- }
@@ -1,30 +0,0 @@
1
- export interface GenerateBookingContractInput {
2
- /** Regenerate even when the contract already has a persisted PDF. */
3
- force?: boolean;
4
- }
5
- /**
6
- * Contract generation for a booking
7
- * (`POST /v1/admin/bookings/:id/generate-contract`):
8
- *
9
- * - `preview` runs the server-side preview branch (`{ preview: true }`),
10
- * which renders the same template + variable build the customer would
11
- * see at checkout and returns the HTML without persisting anything.
12
- * - `generate` creates the legal contract row + persists the PDF
13
- * attachment; pass `{ force: true }` to regenerate an existing PDF.
14
- *
15
- * The created contract lives in the legal module — callers that display
16
- * contract lists should invalidate `legalQueryKeys.contracts()` (from
17
- * `@voyant-travel/legal-react`) on `generate` success; this package cannot do
18
- * that without taking a dependency on the legal client.
19
- */
20
- export declare function useBookingContractGenerationMutation(bookingId: string): {
21
- preview: import("@tanstack/react-query").UseMutationResult<{
22
- html: string;
23
- templateName?: string | undefined;
24
- templateLanguage?: string | undefined;
25
- }, Error, void, unknown>;
26
- generate: import("@tanstack/react-query").UseMutationResult<{
27
- contractId: string;
28
- attachmentId: string;
29
- }, Error, GenerateBookingContractInput, unknown>;
30
- };
@@ -1,36 +0,0 @@
1
- "use client";
2
- import { useMutation } from "@tanstack/react-query";
3
- import { fetchWithValidation } from "../client.js";
4
- import { useVoyantBookingsContext } from "../provider.js";
5
- import { bookingContractPreviewResponse, bookingGenerateContractResponse } from "../schemas.js";
6
- /**
7
- * Contract generation for a booking
8
- * (`POST /v1/admin/bookings/:id/generate-contract`):
9
- *
10
- * - `preview` runs the server-side preview branch (`{ preview: true }`),
11
- * which renders the same template + variable build the customer would
12
- * see at checkout and returns the HTML without persisting anything.
13
- * - `generate` creates the legal contract row + persists the PDF
14
- * attachment; pass `{ force: true }` to regenerate an existing PDF.
15
- *
16
- * The created contract lives in the legal module — callers that display
17
- * contract lists should invalidate `legalQueryKeys.contracts()` (from
18
- * `@voyant-travel/legal-react`) on `generate` success; this package cannot do
19
- * that without taking a dependency on the legal client.
20
- */
21
- export function useBookingContractGenerationMutation(bookingId) {
22
- const { baseUrl, fetcher } = useVoyantBookingsContext();
23
- const preview = useMutation({
24
- mutationFn: async () => {
25
- const { data } = await fetchWithValidation(`/v1/admin/bookings/${bookingId}/generate-contract`, bookingContractPreviewResponse, { baseUrl, fetcher }, { method: "POST", body: JSON.stringify({ preview: true }) });
26
- return data;
27
- },
28
- });
29
- const generate = useMutation({
30
- mutationFn: async (input = {}) => {
31
- const { data } = await fetchWithValidation(`/v1/admin/bookings/${bookingId}/generate-contract`, bookingGenerateContractResponse, { baseUrl, fetcher }, { method: "POST", body: JSON.stringify(input.force ? { force: true } : {}) });
32
- return data;
33
- },
34
- });
35
- return { preview, generate };
36
- }