@voyantjs/bookings-ui 0.108.0 → 0.109.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.
Files changed (31) hide show
  1. package/dist/admin/booking-contract-dialog.d.ts +22 -0
  2. package/dist/admin/booking-contract-dialog.d.ts.map +1 -0
  3. package/dist/admin/booking-contract-dialog.js +161 -0
  4. package/dist/admin/booking-detail-host.d.ts +123 -0
  5. package/dist/admin/booking-detail-host.d.ts.map +1 -0
  6. package/dist/admin/booking-detail-host.js +143 -0
  7. package/dist/admin/booking-detail-skeleton.d.ts +7 -0
  8. package/dist/admin/booking-detail-skeleton.d.ts.map +1 -0
  9. package/dist/admin/booking-detail-skeleton.js +24 -0
  10. package/dist/admin/booking-documents-table.d.ts +13 -0
  11. package/dist/admin/booking-documents-table.d.ts.map +1 -0
  12. package/dist/admin/booking-documents-table.js +258 -0
  13. package/dist/admin/booking-invoice-sheet.d.ts +18 -0
  14. package/dist/admin/booking-invoice-sheet.d.ts.map +1 -0
  15. package/dist/admin/booking-invoice-sheet.js +101 -0
  16. package/dist/admin/bookings-host.d.ts +26 -0
  17. package/dist/admin/bookings-host.d.ts.map +1 -0
  18. package/dist/admin/bookings-host.js +18 -0
  19. package/dist/admin/bookings-list-skeleton.d.ts +10 -0
  20. package/dist/admin/bookings-list-skeleton.d.ts.map +1 -0
  21. package/dist/admin/bookings-list-skeleton.js +25 -0
  22. package/dist/admin/index.d.ts +197 -0
  23. package/dist/admin/index.d.ts.map +1 -0
  24. package/dist/admin/index.js +187 -0
  25. package/dist/admin/person-bookings-widget.d.ts +13 -0
  26. package/dist/admin/person-bookings-widget.d.ts.map +1 -0
  27. package/dist/admin/person-bookings-widget.js +48 -0
  28. package/dist/admin/use-booking-action-ledger-events.d.ts +15 -0
  29. package/dist/admin/use-booking-action-ledger-events.d.ts.map +1 -0
  30. package/dist/admin/use-booking-action-ledger-events.js +66 -0
  31. package/package.json +38 -31
@@ -0,0 +1,258 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { useAdminNavigate, useOperatorAdminMessages } from "@voyantjs/admin";
5
+ import { useBooking, useBookingContractGenerationMutation, useBookingTravelerDocumentMutation, useBookingTravelerDocuments, useTravelers, } from "@voyantjs/bookings-react";
6
+ import { legalQueryKeys, useLegalContractAttachments, useLegalContracts, useVoyantLegalContext, } from "@voyantjs/legal-react";
7
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, } from "@voyantjs/ui/components";
8
+ import { DataTable } from "@voyantjs/ui/components/data-table";
9
+ import { ArrowUpRight, Download, FileText, Loader2, Plus, RotateCw, Trash2 } from "lucide-react";
10
+ import { useMemo, useState } from "react";
11
+ import { BookingDocumentDialog } from "../components/booking-document-dialog.js";
12
+ import { IconActionButton } from "../components/icon-action-button.js";
13
+ import { StatusBadge } from "../components/status-badge.js";
14
+ import { BookingContractDialog } from "./booking-contract-dialog.js";
15
+ const CONTRACT_GENERATION_FAILURE_LABELS = {
16
+ render_unavailable: "contractGenerationTemplateError",
17
+ generator_failed: "contractGenerationGeneratorFailed",
18
+ };
19
+ function resolveContractGenerationFailure(contract) {
20
+ const metadata = contract.metadata;
21
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
22
+ return null;
23
+ }
24
+ const status = metadata.lastGenerationStatus;
25
+ if (typeof status !== "string" || status === "generated") {
26
+ return null;
27
+ }
28
+ return {
29
+ status,
30
+ error: typeof metadata.lastGenerationError === "string" && metadata.lastGenerationError.trim()
31
+ ? metadata.lastGenerationError
32
+ : null,
33
+ attemptedAt: typeof metadata.lastGenerationAttemptedAt === "string"
34
+ ? metadata.lastGenerationAttemptedAt
35
+ : null,
36
+ };
37
+ }
38
+ /**
39
+ * Unified Documents tab for a booking — flattens auto-generated legal
40
+ * contracts and per-traveler documents (passport, visa, insurance…) into
41
+ * a single DataTable that matches the rest of the booking detail tabs.
42
+ * Contracts render first (canonical booking docs), traveler-uploaded
43
+ * documents below; each contract row owns its own attachments fetch so
44
+ * we don't need a join endpoint server-side.
45
+ */
46
+ export function BookingDocumentsTable({ bookingId, }) {
47
+ const t = useOperatorAdminMessages().bookings.detail.documentsTable;
48
+ const [uploadOpen, setUploadOpen] = useState(false);
49
+ const [contractDialogOpen, setContractDialogOpen] = useState(false);
50
+ const [deleteTarget, setDeleteTarget] = useState(null);
51
+ const [deletePending, setDeletePending] = useState(false);
52
+ const bookingQuery = useBooking(bookingId);
53
+ const booking = bookingQuery.data?.data ?? null;
54
+ const contractsQuery = useLegalContracts({ bookingId, limit: 25 });
55
+ const contracts = contractsQuery.data?.data ?? [];
56
+ const travelerDocsQuery = useBookingTravelerDocuments(bookingId);
57
+ const travelerDocs = travelerDocsQuery.data?.data ?? [];
58
+ const travelersQuery = useTravelers(bookingId);
59
+ const travelersById = useMemo(() => new Map((travelersQuery.data?.data ?? []).map((tr) => [tr.id, tr])), [travelersQuery.data]);
60
+ const removeTravelerDoc = useBookingTravelerDocumentMutation(bookingId).remove;
61
+ const isLoading = bookingQuery.isLoading || contractsQuery.isLoading || travelerDocsQuery.isLoading;
62
+ const rows = useMemo(() => [
63
+ ...contracts.map((contract) => ({
64
+ kind: "contract",
65
+ id: contract.id,
66
+ contract,
67
+ })),
68
+ ...travelerDocs.map((doc) => ({
69
+ kind: "traveler",
70
+ id: doc.id,
71
+ doc,
72
+ traveler: doc.travelerId ? (travelersById.get(doc.travelerId) ?? null) : null,
73
+ })),
74
+ ], [contracts, travelerDocs, travelersById]);
75
+ const columns = useMemo(() => [
76
+ {
77
+ id: "category",
78
+ header: t.headerCategory,
79
+ cell: ({ row }) => row.original.kind === "contract" ? (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: t.contractBadge })) : (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: humanizeDocType(row.original.doc.type) })),
80
+ },
81
+ {
82
+ id: "document",
83
+ header: t.headerDocument,
84
+ cell: ({ row }) => row.original.kind === "contract" ? (_jsx(ContractDocumentCell, { contract: row.original.contract })) : (_jsx(TravelerDocumentCell, { doc: row.original.doc })),
85
+ },
86
+ {
87
+ id: "for",
88
+ header: t.headerFor,
89
+ cell: ({ row }) => {
90
+ if (row.original.kind === "contract") {
91
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: t.forBooking });
92
+ }
93
+ const traveler = row.original.traveler;
94
+ const name = traveler
95
+ ? `${traveler.firstName ?? ""} ${traveler.lastName ?? ""}`.trim() || t.travelerFallback
96
+ : t.forBooking;
97
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: name });
98
+ },
99
+ },
100
+ {
101
+ id: "status",
102
+ header: t.headerStatus,
103
+ cell: ({ row }) => row.original.kind === "contract" ? (_jsx(ContractStatusCell, { contract: row.original.contract, messages: t })) : (_jsx(TravelerStatusCell, { doc: row.original.doc, messages: t })),
104
+ },
105
+ {
106
+ id: "date",
107
+ header: t.headerDate,
108
+ cell: ({ row }) => row.original.kind === "contract" ? (_jsx(ContractDateCell, { contract: row.original.contract, messages: t })) : (_jsx(TravelerDateCell, { doc: row.original.doc, messages: t })),
109
+ },
110
+ {
111
+ id: "actions",
112
+ header: () => _jsx("span", { className: "sr-only", children: t.headerCategory }),
113
+ cell: ({ row }) => {
114
+ if (row.original.kind === "contract") {
115
+ return _jsx(ContractActionsCell, { contract: row.original.contract, messages: t });
116
+ }
117
+ const docPayload = row.original.doc;
118
+ return (_jsx(TravelerActionsCell, { doc: docPayload, messages: t, onDelete: () => setDeleteTarget(docPayload) }));
119
+ },
120
+ },
121
+ ], [t]);
122
+ const handleDeleteConfirm = async () => {
123
+ if (!deleteTarget)
124
+ return;
125
+ setDeletePending(true);
126
+ try {
127
+ await removeTravelerDoc.mutateAsync(deleteTarget.id);
128
+ setDeleteTarget(null);
129
+ }
130
+ finally {
131
+ setDeletePending(false);
132
+ }
133
+ };
134
+ 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) => {
135
+ if (!next && !deletePending)
136
+ setDeleteTarget(null);
137
+ }, 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 })] })] }) })] }));
138
+ }
139
+ /**
140
+ * Admin download URL of a contract attachment, resolved against the
141
+ * legal provider context so the table works in any host app without an
142
+ * `apiBaseUrl` prop.
143
+ */
144
+ function useContractAttachmentDownloadHref(attachment) {
145
+ const { baseUrl } = useVoyantLegalContext();
146
+ if (!attachment)
147
+ return null;
148
+ return `${baseUrl}/v1/admin/legal/contracts/attachments/${attachment.id}/download`;
149
+ }
150
+ function ContractDocumentCell({ contract }) {
151
+ const attachmentsQuery = useLegalContractAttachments({ contractId: contract.id });
152
+ const attachments = (attachmentsQuery.data ?? []).filter((a) => a.kind === "document");
153
+ const latest = attachments[0] ?? null;
154
+ const downloadHref = useContractAttachmentDownloadHref(latest);
155
+ const titleText = latest?.name ?? contract.contractNumber ?? `Contract ${contract.id.slice(-8)}`;
156
+ return downloadHref ? (_jsxs("a", { href: downloadHref, 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: titleText }), _jsx(ArrowUpRight, { className: "h-3 w-3" }), latest?.fileSize != null ? (_jsx("span", { className: "ml-1 text-muted-foreground text-xs", children: formatBytes(latest.fileSize) })) : null] })) : (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-muted-foreground", children: [_jsx(FileText, { className: "h-3.5 w-3.5 shrink-0 opacity-60" }), titleText] }));
157
+ }
158
+ function TravelerDocumentCell({ doc }) {
159
+ 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" })] }));
160
+ }
161
+ function ContractStatusCell({ contract, messages, }) {
162
+ const generationFailure = resolveContractGenerationFailure(contract);
163
+ if (generationFailure) {
164
+ const failureLabelKey = CONTRACT_GENERATION_FAILURE_LABELS[generationFailure.status];
165
+ const failureLabel = failureLabelKey
166
+ ? messages[failureLabelKey]
167
+ : messages.contractGenerationFailed;
168
+ 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 })] }));
169
+ }
170
+ return _jsx(StatusBadge, { status: contract.status, children: contract.status.replace(/_/g, " ") });
171
+ }
172
+ function TravelerStatusCell({ doc, messages, }) {
173
+ const isExpired = doc.expiresAt && Number.isFinite(new Date(doc.expiresAt).getTime())
174
+ ? new Date(doc.expiresAt).getTime() < Date.now()
175
+ : false;
176
+ return (_jsx(StatusBadge, { status: isExpired ? "expired" : "active", children: isExpired ? messages.travelerStatusExpired : messages.travelerStatusOnFile }));
177
+ }
178
+ function ContractDateCell({ contract, messages, }) {
179
+ const generationFailure = resolveContractGenerationFailure(contract);
180
+ const attachmentsQuery = useLegalContractAttachments({ contractId: contract.id });
181
+ const attachments = (attachmentsQuery.data ?? []).filter((a) => a.kind === "document");
182
+ const hasDocument = attachments.length > 0;
183
+ const dateIso = generationFailure?.attemptedAt ?? contract.issuedAt ?? contract.createdAt ?? null;
184
+ const dateLabel = generationFailure
185
+ ? messages.contractGenerationAttemptedLabel
186
+ : hasDocument
187
+ ? messages.contractIssuedLabel
188
+ : messages.contractPendingSinceLabel;
189
+ if (!dateIso)
190
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
191
+ return (_jsxs("span", { className: "text-muted-foreground text-xs", children: [_jsxs("span", { className: "opacity-60", children: [dateLabel, " "] }), formatDate(dateIso)] }));
192
+ }
193
+ function TravelerDateCell({ doc, messages, }) {
194
+ const dateIso = doc.expiresAt ?? doc.createdAt ?? null;
195
+ const dateLabel = doc.expiresAt ? messages.travelerExpiresLabel : messages.travelerUploadedLabel;
196
+ if (!dateIso)
197
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: "\u2014" });
198
+ return (_jsxs("span", { className: "text-muted-foreground text-xs", children: [_jsxs("span", { className: "opacity-60", children: [dateLabel, " "] }), formatDate(dateIso)] }));
199
+ }
200
+ function ContractActionsCell({ contract, messages, }) {
201
+ const queryClient = useQueryClient();
202
+ const navigateTo = useAdminNavigate();
203
+ const attachmentsQuery = useLegalContractAttachments({ contractId: contract.id });
204
+ const attachments = (attachmentsQuery.data ?? []).filter((a) => a.kind === "document");
205
+ const latest = attachments[0] ?? null;
206
+ const hasDocument = latest !== null;
207
+ const downloadHref = useContractAttachmentDownloadHref(latest);
208
+ const { generate } = useBookingContractGenerationMutation(contract.bookingId ?? "");
209
+ 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) => {
210
+ e.stopPropagation();
211
+ navigateTo("contract.detail", { contractId: contract.id });
212
+ } }), downloadHref ? (_jsx(IconActionButton, { label: messages.downloadDocumentAria, icon: _jsx(Download, { className: "h-3.5 w-3.5" }), onClick: (e) => {
213
+ e.stopPropagation();
214
+ window.open(downloadHref, "_blank", "noopener,noreferrer");
215
+ } })) : 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) => {
216
+ e.stopPropagation();
217
+ if (!contract.bookingId)
218
+ return;
219
+ generate.mutate({ force: hasDocument }, {
220
+ onSuccess: () => {
221
+ void queryClient.invalidateQueries({ queryKey: legalQueryKeys.contracts() });
222
+ },
223
+ });
224
+ } })] }));
225
+ }
226
+ function TravelerActionsCell({ doc, messages, onDelete, }) {
227
+ 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) => {
228
+ e.stopPropagation();
229
+ window.open(doc.fileUrl, "_blank", "noopener,noreferrer");
230
+ } }), _jsx(IconActionButton, { label: messages.deleteDocumentAria, icon: _jsx(Trash2, { className: "h-3.5 w-3.5" }), className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive", onClick: (e) => {
231
+ e.stopPropagation();
232
+ onDelete();
233
+ } })] }));
234
+ }
235
+ function humanizeDocType(type) {
236
+ return type
237
+ .split("_")
238
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
239
+ .join(" ");
240
+ }
241
+ function formatBytes(bytes) {
242
+ if (bytes < 1024)
243
+ return `${bytes} B`;
244
+ if (bytes < 1024 * 1024)
245
+ return `${Math.round(bytes / 1024)} KB`;
246
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
247
+ }
248
+ function formatDate(iso) {
249
+ try {
250
+ const d = new Date(iso);
251
+ if (!Number.isFinite(d.getTime()))
252
+ return iso;
253
+ return d.toLocaleDateString(undefined, { day: "numeric", month: "short", year: "numeric" });
254
+ }
255
+ catch {
256
+ return iso;
257
+ }
258
+ }
@@ -0,0 +1,18 @@
1
+ export interface BookingInvoiceSheetProps {
2
+ invoiceId: string;
3
+ /** Navigate to the full invoice detail page. */
4
+ onOpenInvoice?: (invoiceId: string) => void;
5
+ }
6
+ /**
7
+ * Compact invoice view designed to live inside a `Sheet` next to the
8
+ * booking detail page. Unlike `InvoiceDetailPage` (a standalone page
9
+ * with breadcrumb + action bar + collapsible cards), this component
10
+ * trims the chrome and focuses on the operator's reconciliation needs:
11
+ * summary numbers, line items, payments. Big actions (edit, void)
12
+ * stay on the dedicated invoice page.
13
+ *
14
+ * Packaged host piece (packaged-admin RFC Phase 3): API base URL comes
15
+ * from the shell's finance provider context — no app env import.
16
+ */
17
+ export declare function BookingInvoiceSheet({ invoiceId, onOpenInvoice }: BookingInvoiceSheetProps): import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=booking-invoice-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-invoice-sheet.d.ts","sourceRoot":"","sources":["../../src/admin/booking-invoice-sheet.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,EAAE,wBAAwB,2CAwTzF"}
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLocale, useOperatorAdminMessages } from "@voyantjs/admin";
4
+ import { useInvoice, useInvoiceAttachments, useInvoiceLineItems, useInvoiceMutation, useInvoicePayments, useVoyantFinanceContext, } from "@voyantjs/finance-react";
5
+ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
6
+ import { ArrowRightLeft, ArrowUpRight, Download, FileText, Loader2 } from "lucide-react";
7
+ import { useState } from "react";
8
+ import { StatusBadge } from "../components/status-badge.js";
9
+ /**
10
+ * Compact invoice view designed to live inside a `Sheet` next to the
11
+ * booking detail page. Unlike `InvoiceDetailPage` (a standalone page
12
+ * with breadcrumb + action bar + collapsible cards), this component
13
+ * trims the chrome and focuses on the operator's reconciliation needs:
14
+ * summary numbers, line items, payments. Big actions (edit, void)
15
+ * stay on the dedicated invoice page.
16
+ *
17
+ * Packaged host piece (packaged-admin RFC Phase 3): API base URL comes
18
+ * from the shell's finance provider context — no app env import.
19
+ */
20
+ export function BookingInvoiceSheet({ invoiceId, onOpenInvoice }) {
21
+ const { resolvedLocale } = useLocale();
22
+ const adminMessages = useOperatorAdminMessages();
23
+ const messages = adminMessages.bookings.detail.invoiceSheet;
24
+ const financeMessages = adminMessages.finance;
25
+ const { data: invoiceData, isPending: invoicePending } = useInvoice(invoiceId);
26
+ const { data: lineItemsData, isPending: lineItemsPending } = useInvoiceLineItems(invoiceId);
27
+ const { data: paymentsData, isPending: paymentsPending } = useInvoicePayments(invoiceId);
28
+ const { data: attachmentsData, isPending: attachmentsPending } = useInvoiceAttachments(invoiceId);
29
+ const { convertToInvoice } = useInvoiceMutation();
30
+ const [confirmConvert, setConfirmConvert] = useState(false);
31
+ if (invoicePending) {
32
+ return (_jsxs(_Fragment, { children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: messages.invoiceTypes.invoice }) }), _jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) })] }));
33
+ }
34
+ const invoice = invoiceData?.data;
35
+ if (!invoice) {
36
+ return (_jsxs(_Fragment, { children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: messages.invoiceTypes.invoice }) }), _jsx("div", { className: "p-6 text-sm text-muted-foreground", children: messages.notFound })] }));
37
+ }
38
+ const invoiceType = invoice.invoiceType ?? "invoice";
39
+ const sheetTitle = messages.invoiceTypes[invoiceType] ?? messages.invoiceTypes.invoice;
40
+ const canConvert = invoiceType === "proforma" && invoice.status !== "void";
41
+ // A void proforma with a `convertedToInvoiceId` is "Invoiced" in the
42
+ // operator's mental model — the void is purely how our DB models the
43
+ // hand-off. Display the friendlier label and link to the final
44
+ // invoice on the same sheet (no extra navigation).
45
+ const convertedToInvoiceId = invoice.convertedToInvoiceId ?? null;
46
+ const convertedToInvoiceNumber = invoice.convertedToInvoiceNumber ?? null;
47
+ const showAsInvoiced = invoiceType === "proforma" && invoice.status === "void" && Boolean(convertedToInvoiceId);
48
+ const displayStatusKey = showAsInvoiced ? "invoiced" : invoice.status;
49
+ const displayStatusLabel = showAsInvoiced
50
+ ? (messages.invoiceStatusLabels.invoiced ??
51
+ financeMessages.invoiceStatusInvoiced ??
52
+ "Invoiced")
53
+ : (messages.invoiceStatusLabels[invoice.status] ??
54
+ invoice.status);
55
+ const lineItems = lineItemsData?.data ?? [];
56
+ const payments = paymentsData?.data ?? [];
57
+ const formatMoney = makeFormatMoney(resolvedLocale, invoice.currency);
58
+ const formatDate = (iso) => iso ? new Date(iso).toLocaleDateString(resolvedLocale, { dateStyle: "medium" }) : "—";
59
+ const formatDateTime = (iso) => iso
60
+ ? new Date(iso).toLocaleString(resolvedLocale, {
61
+ dateStyle: "medium",
62
+ timeStyle: "short",
63
+ })
64
+ : "—";
65
+ return (_jsxs(_Fragment, { children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: sheetTitle }) }), _jsxs("div", { className: "flex-1 overflow-y-auto px-6 pb-6", children: [_jsxs("header", { className: "mb-6 flex flex-col gap-3 border-b pb-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-base font-semibold", children: invoice.invoiceNumber }), _jsx(StatusBadge, { status: showAsInvoiced ? "paid" : invoice.status, children: displayStatusLabel }), showAsInvoiced && convertedToInvoiceId ? (_jsxs("button", { type: "button", onClick: () => onOpenInvoice?.(convertedToInvoiceId), className: "inline-flex items-center gap-1 font-mono text-xs text-primary hover:underline", title: messages.convertedToInvoiceTitle, children: ["\u2192 ", convertedToInvoiceNumber ?? displayStatusKey, _jsx(ArrowUpRight, { className: "h-3 w-3" })] })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [canConvert ? (_jsxs(Button, { variant: "outline", size: "sm", disabled: convertToInvoice.isPending, onClick: () => setConfirmConvert(true), children: [_jsx(ArrowRightLeft, { className: "mr-1 h-3.5 w-3.5" }), financeMessages.convertToInvoice] })) : null, onOpenInvoice ? (_jsxs(Button, { size: "sm", onClick: () => onOpenInvoice(invoiceId), children: [messages.openInvoice, _jsx(ArrowUpRight, { className: "ml-1 h-3.5 w-3.5" })] })) : null] })] }), invoice.notes ? (_jsx("p", { className: "whitespace-pre-wrap text-sm text-muted-foreground", children: invoice.notes })) : null] }), _jsxs("section", { className: "mb-6", children: [_jsx("h3", { className: "mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: messages.summarySection }), _jsxs("dl", { className: "rounded-md border divide-y", children: [_jsx(SummaryRow, { label: messages.subtotal, value: formatMoney(invoice.subtotalCents) }), _jsx(SummaryRow, { label: messages.tax, value: formatMoney(invoice.taxCents) }), _jsx(SummaryRow, { label: messages.total, value: formatMoney(invoice.totalCents), emphasis: true }), _jsx(SummaryRow, { label: messages.paid, value: formatMoney(invoice.paidCents) }), _jsx(SummaryRow, { label: messages.balanceDue, value: formatMoney(invoice.balanceDueCents), emphasis: invoice.balanceDueCents > 0 })] })] }), _jsxs("section", { className: "mb-6 grid grid-cols-1 gap-3 sm:grid-cols-2", children: [_jsx(DatesCard, { label: messages.issueDate, value: formatDate(invoice.issueDate) }), _jsx(DatesCard, { label: messages.dueDate, value: formatDate(invoice.dueDate) }), _jsx(DatesCard, { label: messages.createdAt, value: formatDateTime(invoice.createdAt) }), _jsx(DatesCard, { label: messages.updatedAt, value: formatDateTime(invoice.updatedAt) })] }), _jsxs("section", { className: "mb-6", children: [_jsx("h3", { className: "mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: messages.lineItemsSection }), lineItemsPending ? (_jsxs("div", { className: "flex items-center gap-2 rounded-md border p-3 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), messages.loading] })) : lineItems.length === 0 ? (_jsx("p", { className: "rounded-md border py-4 text-center text-sm text-muted-foreground", children: messages.lineItemsEmpty })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-3 py-2 text-left font-medium", children: messages.colDescription }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: messages.colQty }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: messages.colUnitPrice }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: messages.colTax }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: messages.colLineTotal })] }) }), _jsx("tbody", { children: lineItems.map((line) => {
66
+ const taxAmountCents = line.taxRate != null
67
+ ? Math.round((line.totalCents * line.taxRate) / 100)
68
+ : null;
69
+ return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "px-3 py-2", children: line.description }), _jsx("td", { className: "px-3 py-2 text-right font-mono", children: line.quantity }), _jsx("td", { className: "px-3 py-2 text-right font-mono", children: formatMoney(line.unitPriceCents) }), _jsx("td", { className: "px-3 py-2 text-right font-mono", children: taxAmountCents != null && line.taxRate != null ? (_jsxs(_Fragment, { children: [formatMoney(taxAmountCents), " ", _jsxs("span", { className: "text-muted-foreground", children: ["(", line.taxRate, "%)"] })] })) : ("—") }), _jsx("td", { className: "px-3 py-2 text-right font-mono", children: formatMoney(line.totalCents) })] }, line.id));
70
+ }) })] }) }))] }), _jsxs("section", { children: [_jsx("h3", { className: "mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: messages.paymentsSection }), paymentsPending ? (_jsxs("div", { className: "flex items-center gap-2 rounded-md border p-3 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), messages.loading] })) : payments.length === 0 ? (_jsx("p", { className: "rounded-md border py-4 text-center text-sm text-muted-foreground", children: messages.paymentsEmpty })) : (_jsx("div", { className: "overflow-hidden rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "px-3 py-2 text-left font-medium", children: messages.colDate }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: messages.colMethod }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: messages.colStatus }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: messages.colReference }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: messages.colAmount })] }) }), _jsx("tbody", { children: payments.map((payment) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "px-3 py-2", children: formatDateTime(payment.paymentDate) }), _jsx("td", { className: "px-3 py-2 capitalize", children: payment.paymentMethod.replaceAll("_", " ") }), _jsx("td", { className: "px-3 py-2", children: _jsx(StatusBadge, { status: payment.status, children: payment.status }) }), _jsx("td", { className: "px-3 py-2 font-mono text-xs text-muted-foreground", children: payment.referenceNumber ?? "—" }), _jsx("td", { className: "px-3 py-2 text-right font-mono font-medium", children: formatMoney(payment.amountCents) })] }, payment.id))) })] }) }))] }), _jsxs("section", { className: "mt-6", children: [_jsx("h3", { className: "mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: messages.attachmentsSection }), attachmentsPending ? (_jsxs("div", { className: "flex items-center gap-2 rounded-md border p-3 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), messages.loading] })) : (attachmentsData?.data ?? []).length === 0 ? (_jsx("p", { className: "rounded-md border py-4 text-center text-sm text-muted-foreground", children: messages.attachmentsEmpty })) : (_jsx("ul", { className: "flex flex-col divide-y rounded-md border", children: (attachmentsData?.data ?? []).map((attachment) => (_jsx(AttachmentRow, { attachment: attachment, downloadLabel: messages.attachmentDownload }, attachment.id))) }))] })] }), _jsx(AlertDialog, { open: confirmConvert, onOpenChange: (next) => {
71
+ if (!next && !convertToInvoice.isPending)
72
+ setConfirmConvert(false);
73
+ }, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: financeMessages.convertToInvoice }), _jsx(AlertDialogDescription, { children: financeMessages.convertConfirm })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: convertToInvoice.isPending, children: financeMessages.detailPage.cancel }), _jsx(AlertDialogAction, { disabled: convertToInvoice.isPending, onClick: () => {
74
+ convertToInvoice.mutate({ id: invoiceId }, { onSuccess: () => setConfirmConvert(false) });
75
+ }, children: financeMessages.convertToInvoice })] })] }) })] }));
76
+ }
77
+ function AttachmentRow({ attachment, downloadLabel, }) {
78
+ // Same admin endpoint the finance invoice page links to; the base URL
79
+ // comes from the finance provider context instead of an app env helper.
80
+ const { baseUrl } = useVoyantFinanceContext();
81
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
82
+ const href = `${trimmedBase}/v1/admin/finance/invoice-attachments/${attachment.id}/download`;
83
+ const sizeKb = typeof attachment.fileSize === "number" ? `${Math.round(attachment.fileSize / 1024)} KB` : null;
84
+ return (_jsxs("li", { className: "flex items-center justify-between gap-3 px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(FileText, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: attachment.name }), _jsxs("span", { className: "text-muted-foreground text-xs uppercase", children: [attachment.kind, sizeKb ? ` · ${sizeKb}` : null] })] })] }), _jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground", children: [_jsx(Download, { className: "h-3.5 w-3.5" }), downloadLabel] })] }));
85
+ }
86
+ function SummaryRow({ label, value, emphasis, }) {
87
+ return (_jsxs("div", { className: "flex items-baseline justify-between gap-4 px-4 py-2.5 text-sm", children: [_jsx("dt", { className: emphasis ? "font-semibold" : "text-muted-foreground", children: label }), _jsx("dd", { className: emphasis ? "font-mono text-base font-semibold" : "font-mono", children: value })] }));
88
+ }
89
+ function DatesCard({ label, value }) {
90
+ return (_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("div", { className: "mb-0.5 text-xs uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "text-sm", children: value })] }));
91
+ }
92
+ function makeFormatMoney(locale, currency) {
93
+ return (cents) => {
94
+ try {
95
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(cents / 100);
96
+ }
97
+ catch {
98
+ return `${(cents / 100).toFixed(2)} ${currency}`;
99
+ }
100
+ };
101
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ import type { BookingListFiltersState } from "../components/booking-list.js";
3
+ export interface BookingsHostProps {
4
+ /** Filter / sort / paging state parsed from the URL by the route file. */
5
+ initialFilters?: Partial<BookingListFiltersState>;
6
+ /** Fires on any filter change; the route file projects it back into the URL. */
7
+ onFiltersChange?: (filters: BookingListFiltersState) => void;
8
+ /**
9
+ * Extra action(s) rendered alongside the primary "New booking" button.
10
+ * App-owned adjacent flows (e.g. the operator's "Compose trip" link)
11
+ * land here so the packaged page doesn't hardcode other domains.
12
+ */
13
+ headerActions?: ReactNode;
14
+ }
15
+ /**
16
+ * Packaged admin host for `BookingsPage` (packaged-admin RFC Phase 3).
17
+ *
18
+ * Proof-of-contract for semantic destinations (RFC §4.7): no host route
19
+ * tree is imported — opening a booking resolves `"booking.detail"` and the
20
+ * "New booking" button resolves `"booking.create"` through the resolvers
21
+ * the workspace shell registered. The route file stays the thin binding
22
+ * layer for search-state (via {@link bookingsIndexSearchSchema} and the
23
+ * `bookingsSearchToFilters`/`bookingsFiltersToSearch` helpers).
24
+ */
25
+ export declare function BookingsHost({ initialFilters, onFiltersChange, headerActions, }: BookingsHostProps): import("react/jsx-runtime").JSX.Element;
26
+ //# sourceMappingURL=bookings-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bookings-host.d.ts","sourceRoot":"","sources":["../../src/admin/bookings-host.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA;AAG5E,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,cAAc,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAA;IACjD,gFAAgF;IAChF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAC5D;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,CAAA;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,iBAAiB,2CAYnB"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useAdminNavigate } from "@voyantjs/admin";
4
+ import { BookingsPage } from "../components/bookings-page.js";
5
+ /**
6
+ * Packaged admin host for `BookingsPage` (packaged-admin RFC Phase 3).
7
+ *
8
+ * Proof-of-contract for semantic destinations (RFC §4.7): no host route
9
+ * tree is imported — opening a booking resolves `"booking.detail"` and the
10
+ * "New booking" button resolves `"booking.create"` through the resolvers
11
+ * the workspace shell registered. The route file stays the thin binding
12
+ * layer for search-state (via {@link bookingsIndexSearchSchema} and the
13
+ * `bookingsSearchToFilters`/`bookingsFiltersToSearch` helpers).
14
+ */
15
+ export function BookingsHost({ initialFilters, onFiltersChange, headerActions, }) {
16
+ const navigateTo = useAdminNavigate();
17
+ return (_jsx(BookingsPage, { onCreateBooking: () => navigateTo("booking.create", {}), onBookingOpen: (booking) => navigateTo("booking.detail", { bookingId: booking.id }), headerActions: headerActions, initialFilters: initialFilters, onFiltersChange: onFiltersChange }));
18
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Route-level placeholder for the bookings list. Mirrors `BookingsPage` +
3
+ * `BookingList`:
4
+ * - Page title + description
5
+ * - Search input (left) + "New booking" button (right)
6
+ * - 5-column table: Booking # / Status / Sell Amount / Pax / Start Date
7
+ * - Pagination bar
8
+ */
9
+ export declare function BookingsListSkeleton(): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=bookings-list-skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bookings-list-skeleton.d.ts","sourceRoot":"","sources":["../../src/admin/bookings-list-skeleton.tsx"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,4CA2CnC"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useOperatorAdminMessages } from "@voyantjs/admin";
4
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
6
+ /**
7
+ * Route-level placeholder for the bookings list. Mirrors `BookingsPage` +
8
+ * `BookingList`:
9
+ * - Page title + description
10
+ * - Search input (left) + "New booking" button (right)
11
+ * - 5-column table: Booking # / Status / Sell Amount / Pax / Start Date
12
+ * - Pagination bar
13
+ */
14
+ export function BookingsListSkeleton() {
15
+ const bookingMessages = useOperatorAdminMessages().bookings.list;
16
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Skeleton, { className: "h-7 w-28" }), _jsx(Skeleton, { className: "h-4 w-80" })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsx(Skeleton, { className: "h-9 w-full max-w-sm" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-9 w-32" }), _jsx(Skeleton, { className: "h-9 w-36" })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: bookingMessages.tableBookingNumber }), _jsx(TableHead, { children: bookingMessages.tableStatus }), _jsx(TableHead, { children: bookingMessages.tableSellAmount }), _jsx(TableHead, { children: bookingMessages.tablePax }), _jsx(TableHead, { children: bookingMessages.tableStartDate })] }) }), _jsx(SkeletonRows, { rows: 8, widths: ["w-28", "w-20", "w-24", "w-6", "w-24"] })] }) }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-8 w-20" }), _jsx(Skeleton, { className: "h-4 w-20" }), _jsx(Skeleton, { className: "h-8 w-16" })] })] })] }));
17
+ }
18
+ /** Placeholder `<TableBody>` with one skeleton line per cell. */
19
+ function SkeletonRows({ rows, widths }) {
20
+ return (_jsx(TableBody, { children: Array.from({ length: rows }).map((_, r) => (_jsx(TableRow
21
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable placeholders
22
+ , { children: widths.map((width, c) => (_jsx(TableCell
23
+ // biome-ignore lint/suspicious/noArrayIndexKey: stable placeholders
24
+ , { children: _jsx(Skeleton, { className: `h-4 ${width}` }) }, c))) }, r))) }));
25
+ }