@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,22 @@
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-runtime").JSX.Element;
22
+ //# sourceMappingURL=booking-contract-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-contract-dialog.d.ts","sourceRoot":"","sources":["../../src/admin/booking-contract-dialog.tsx"],"names":[],"mappings":"AA0BA,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,aAAa,EACb,SAAS,GACV,EAAE,0BAA0B,2CA8N5B"}
@@ -0,0 +1,161 @@
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 "@voyantjs/admin";
5
+ import { useBookingContractGenerationMutation } from "@voyantjs/bookings-react";
6
+ import { legalQueryKeys, useLegalContractAttachmentMutation, useLegalContractMutation, } from "@voyantjs/legal-react";
7
+ import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Input, Label, Skeleton, } from "@voyantjs/ui/components";
8
+ import { FileText, Loader2, Paperclip, X } from "lucide-react";
9
+ import { useEffect, useState } from "react";
10
+ /**
11
+ * "Add contract" dialog for a booking. Two modes:
12
+ *
13
+ * - **Generate** (default): hits the server-side preview branch of
14
+ * `/v1/admin/bookings/:id/generate-contract` (which runs the same
15
+ * template + variable build the customer would see at checkout)
16
+ * and renders the HTML in a sandboxed iframe. Confirm fires the
17
+ * full generate, which creates the contract row + persists the PDF.
18
+ *
19
+ * - **Upload**: operator picks a pre-signed PDF (e.g. countersigned
20
+ * copy). The dialog creates a `signed`-status contract row and
21
+ * attaches the uploaded file via the legal attachment upload route.
22
+ */
23
+ export function BookingContractDialog({ open, onOpenChange, bookingId, bookingNumber, onSuccess, }) {
24
+ const t = useOperatorAdminMessages().bookings.detail.contractDialog;
25
+ const queryClient = useQueryClient();
26
+ const { create: createContract } = useLegalContractMutation();
27
+ const { upload: uploadAttachment } = useLegalContractAttachmentMutation();
28
+ const { preview, generate } = useBookingContractGenerationMutation(bookingId);
29
+ const [mode, setMode] = useState("generate");
30
+ const [uploading, setUploading] = useState(false);
31
+ const [error, setError] = useState(null);
32
+ // Upload form state
33
+ const [title, setTitle] = useState("");
34
+ const [file, setFile] = useState(null);
35
+ // Reset on open. Generate is the leading mode so the preview fetch
36
+ // kicks off immediately.
37
+ const resetPreview = preview.reset;
38
+ useEffect(() => {
39
+ if (!open)
40
+ return;
41
+ setMode("generate");
42
+ setTitle("");
43
+ setFile(null);
44
+ setError(null);
45
+ setUploading(false);
46
+ resetPreview();
47
+ }, [open, resetPreview]);
48
+ // Fetch preview HTML every time the dialog opens (or the mode flips
49
+ // back to Generate). The preview reflects current booking + template
50
+ // state so re-fetching is intentional.
51
+ const fetchPreview = preview.mutate;
52
+ useEffect(() => {
53
+ if (!open || mode !== "generate")
54
+ return;
55
+ fetchPreview();
56
+ }, [open, mode, fetchPreview]);
57
+ const handleGenerate = async () => {
58
+ setError(null);
59
+ try {
60
+ await generate.mutateAsync({});
61
+ await queryClient.invalidateQueries({ queryKey: legalQueryKeys.contracts() });
62
+ onSuccess?.();
63
+ onOpenChange(false);
64
+ }
65
+ catch (err) {
66
+ setError(err instanceof Error ? err.message : String(err));
67
+ }
68
+ };
69
+ const handleUpload = async () => {
70
+ if (!file) {
71
+ setError(t.uploadFileRequired);
72
+ return;
73
+ }
74
+ setUploading(true);
75
+ setError(null);
76
+ try {
77
+ const fallbackTitle = title.trim() ||
78
+ (bookingNumber
79
+ ? `Contract ${bookingNumber}`
80
+ : `Contract for booking ${bookingId.slice(-8)}`);
81
+ const created = await createContract.mutateAsync({
82
+ scope: "customer",
83
+ status: "signed",
84
+ title: fallbackTitle,
85
+ bookingId,
86
+ metadata: { uploadedByOperator: true },
87
+ });
88
+ await uploadAttachment.mutateAsync({
89
+ contractId: created.id,
90
+ input: { file, kind: "document", name: file.name },
91
+ });
92
+ onSuccess?.();
93
+ onOpenChange(false);
94
+ }
95
+ catch (err) {
96
+ setError(err instanceof Error ? err.message : String(err));
97
+ }
98
+ finally {
99
+ setUploading(false);
100
+ }
101
+ };
102
+ const submitting = generate.isPending || uploading;
103
+ const previewReady = preview.data != null;
104
+ const canSubmit = mode === "generate" ? previewReady && !submitting : file != null && !submitting;
105
+ 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: [
106
+ { value: "generate", label: t.modeGenerate },
107
+ { value: "upload", label: t.modeUpload },
108
+ ] })] }), 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 ? (_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" })) : null }), preview.data?.templateName ? (_jsxs("p", { className: "text-muted-foreground text-xs", children: [t.previewTemplateLabel, " ", preview.data.templateName] })) : 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) => {
109
+ const next = e.target.files?.[0] ?? null;
110
+ setFile(next);
111
+ }, 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: () => {
112
+ if (mode === "generate") {
113
+ void handleGenerate();
114
+ }
115
+ else {
116
+ void handleUpload();
117
+ }
118
+ }, 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)] })] })] }) }));
119
+ }
120
+ function SegmentedChoice({ value, onChange, options, }) {
121
+ return (_jsx("div", { className: "flex w-full rounded-md border bg-background p-0.5", children: options.map((opt) => {
122
+ const active = opt.value === value;
123
+ 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 " +
124
+ (active ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"), children: opt.label }, opt.value));
125
+ }) }));
126
+ }
127
+ /**
128
+ * Wrap the rendered template body in a light-theme HTML document so
129
+ * the iframe doesn't inherit the dark dashboard background. Mirrors
130
+ * the storefront's contract-preview wrapper to keep the WYSIWYG
131
+ * promise honest.
132
+ */
133
+ function wrapPreviewHtml(body) {
134
+ return `<!DOCTYPE html>
135
+ <html lang="en">
136
+ <head>
137
+ <meta charset="utf-8" />
138
+ <style>
139
+ :root { color-scheme: light; }
140
+ html, body { margin: 0; background: #ffffff; color: #111827; }
141
+ body {
142
+ padding: 1.5rem 2rem;
143
+ font-family: ui-serif, Georgia, "Times New Roman", serif;
144
+ font-size: 15px;
145
+ line-height: 1.6;
146
+ }
147
+ h1, h2, h3 { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; color: #0f172a; }
148
+ h1 { font-size: 1.5rem; margin: 0 0 1rem; }
149
+ h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; }
150
+ p { margin: 0.5rem 0; }
151
+ ul, ol { padding-left: 1.5rem; }
152
+ strong { color: #0f172a; }
153
+ a { color: #2563eb; }
154
+ table { border-collapse: collapse; width: 100%; margin: 0.75rem 0; }
155
+ th, td { border: 1px solid #e5e7eb; padding: 0.5rem 0.75rem; text-align: left; }
156
+ th { background: #f9fafb; }
157
+ </style>
158
+ </head>
159
+ <body>${body}</body>
160
+ </html>`;
161
+ }
@@ -0,0 +1,123 @@
1
+ import { type BookingRecord } from "@voyantjs/bookings-react";
2
+ import { type ReactNode } from "react";
3
+ import { type BookingDetailPageSlots, type BookingDetailTabValue } from "../components/booking-detail-page.js";
4
+ import type { BookingPaymentsSummaryRow } from "../components/booking-payments-summary.js";
5
+ /**
6
+ * Widget slot rendered as the booking detail page's Invoices tab
7
+ * (packaged-admin RFC §4.7 cycle resolution): `@voyantjs/finance-ui` depends
8
+ * on this package, so the host cannot import the finance-owned invoices card
9
+ * directly — instead finance's admin extension contributes a widget targeting
10
+ * this slot and the host mounts the tab whenever a contribution exists.
11
+ * Widgets receive {@link BookingDetailHostSlotContext} as props.
12
+ */
13
+ export declare const bookingDetailInvoicesTabSlot = "booking.details.invoices-tab";
14
+ /**
15
+ * Widget slot rendered at the top of the booking detail page's Finance tab
16
+ * (same §4.7 cycle resolution as {@link bookingDetailInvoicesTabSlot}).
17
+ * `@voyantjs/finance-ui` contributes its pending payment-sessions card here.
18
+ * Widgets receive {@link BookingDetailHostSlotContext} as props.
19
+ */
20
+ export declare const bookingDetailFinanceStartSlot = "booking.details.finance-start";
21
+ /**
22
+ * Widget slot rendered at the bottom of the booking detail page's Finance
23
+ * tab. `@voyantjs/finance-ui` contributes its payment-policy override card
24
+ * here. Widgets receive {@link BookingDetailHostSlotContext} as props.
25
+ */
26
+ export declare const bookingDetailFinanceEndSlot = "booking.details.finance-end";
27
+ /**
28
+ * Render context handed to the host's app-supplied slots AND to widget
29
+ * contributions targeting {@link bookingDetailInvoicesTabSlot}. Carries the
30
+ * booking plus the host-computed payment aggregates and the invoice-sheet
31
+ * opener so contributed cards can participate without re-deriving them.
32
+ */
33
+ export interface BookingDetailHostSlotContext {
34
+ booking: BookingRecord;
35
+ /** Customer payments summed across non-credit-note, non-draft invoices. */
36
+ paidAmountCents: number | null;
37
+ /**
38
+ * Localized "Booking is fully paid" reason when nothing is left to pay,
39
+ * else `null`. The host already uses it for the Record payment / Add
40
+ * schedule buttons; payment-link slots reuse it for their own gating.
41
+ */
42
+ fullyPaidReason: string | null;
43
+ /** Open an invoice in the host's side sheet (stays on the booking page). */
44
+ openInvoiceSheet: (invoiceId: string) => void;
45
+ /**
46
+ * Opens the host app's "Generate payment link" flow (a dialog the app
47
+ * owns — `@voyantjs/checkout-ui` depends on this package, so the host
48
+ * cannot import it without a cycle). Forwarded from
49
+ * {@link BookingDetailHostProps.onGenerateLink}; `undefined` when the app
50
+ * didn't wire one, in which case payment-link widgets hide the button.
51
+ */
52
+ onGenerateLink: (() => void) | undefined;
53
+ }
54
+ export type BookingDetailHostSlot = (context: BookingDetailHostSlotContext) => ReactNode;
55
+ /**
56
+ * App-supplied extension points. The host owns everything package-clean
57
+ * (the Documents tab, the action-ledger timeline merge, the finance-tab
58
+ * widget slots); these slots remain for app-local additions on top.
59
+ */
60
+ export interface BookingDetailHostSlots {
61
+ /** Top of the Finance tab, rendered before any widget contributions. */
62
+ financeStart?: BookingDetailHostSlot;
63
+ /** Bottom of the Finance tab, rendered before any widget contributions. */
64
+ financeEnd?: BookingDetailHostSlot;
65
+ /** Mounts a dedicated Invoices tab between Payments and Suppliers. */
66
+ invoicesTab?: {
67
+ label?: string;
68
+ content: BookingDetailHostSlot;
69
+ };
70
+ /** Replaces the Documents tab content (default: {@link BookingDocumentsTable}). */
71
+ documents?: BookingDetailHostSlot;
72
+ /** Extra events merged into the Activity-tab timeline (the host already
73
+ * merges the booking's central action-ledger entries). */
74
+ activityExtraEvents?: BookingDetailPageSlots["activityExtraEvents"];
75
+ /** Rendered below the activity timeline events, after the host's
76
+ * action-ledger pager. */
77
+ activityTimelineFooter?: ReactNode;
78
+ }
79
+ export interface BookingDetailHostProps {
80
+ id: string;
81
+ /** Controlled tab value; the route file mirrors it into the URL. */
82
+ activeTab?: BookingDetailTabValue;
83
+ onTabChange?: (tab: BookingDetailTabValue) => void;
84
+ /**
85
+ * Opens the app's record-payment flow (a dialog owned by the host app —
86
+ * the payment dialogs live app-side because `@voyantjs/finance-ui`
87
+ * depends on this package, so importing it here would be a cycle).
88
+ */
89
+ onRecordPayment?: () => void;
90
+ /** Opens the app's record-payment flow pre-filled with the row. */
91
+ onEditPayment?: (row: BookingPaymentsSummaryRow) => void;
92
+ /**
93
+ * Opens the app's "Generate payment link" flow (a dialog owned by the
94
+ * host app — `@voyantjs/checkout-ui` depends on this package, so
95
+ * importing it here would be a cycle). Forwarded to slot/widget
96
+ * contributions via {@link BookingDetailHostSlotContext.onGenerateLink}.
97
+ */
98
+ onGenerateLink?: () => void;
99
+ slots?: BookingDetailHostSlots;
100
+ }
101
+ /**
102
+ * Packaged admin host for the canonical `BookingDetailPage` (packaged-admin
103
+ * RFC Phase 3). Owns everything package-clean:
104
+ *
105
+ * - Cross-route links resolve through semantic destinations (RFC §4.7):
106
+ * `booking.list`, `person.detail`, `organization.detail`,
107
+ * `product.detail`, `availabilitySlot.detail`, `payment.detail`,
108
+ * `invoice.detail` — no host route tree import.
109
+ * - Admin chrome breadcrumbs (`useAdminBreadcrumbs`).
110
+ * - Admin widget extension points: the `booking.details.header` and
111
+ * `booking.details.after-summary` slots render through the shared
112
+ * `AdminWidgetSlotRenderer`, which reads the workspace shell's
113
+ * `AdminExtensionsProvider` context.
114
+ * - Paid-amount aggregation across the booking's invoices and the
115
+ * derived fully-paid disabled reasons.
116
+ * - Payment row delete (finance-react mutation) and the in-place
117
+ * invoice sheet ({@link BookingInvoiceSheet}).
118
+ *
119
+ * App-local cards without package-level data access stay injectable via
120
+ * {@link BookingDetailHostSlots}.
121
+ */
122
+ export declare function BookingDetailHost({ id, activeTab, onTabChange, onRecordPayment, onEditPayment, onGenerateLink, slots, }: BookingDetailHostProps): import("react/jsx-runtime").JSX.Element;
123
+ //# sourceMappingURL=booking-detail-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-detail-host.d.ts","sourceRoot":"","sources":["../../src/admin/booking-detail-host.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,aAAa,EAAc,MAAM,0BAA0B,CAAA;AAGzE,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,sCAAsC,CAAA;AAC7C,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,2CAA2C,CAAA;AAK1F;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAA;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,kCAAkC,CAAA;AAE5E;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,gCAAgC,CAAA;AAExE;;;;;GAKG;AACH,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,aAAa,CAAA;IACtB,2EAA2E;IAC3E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4EAA4E;IAC5E,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7C;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;CACzC;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,4BAA4B,KAAK,SAAS,CAAA;AAExF;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,2EAA2E;IAC3E,UAAU,CAAC,EAAE,qBAAqB,CAAA;IAClC,sEAAsE;IACtE,WAAW,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,qBAAqB,CAAA;KAAE,CAAA;IAChE,mFAAmF;IACnF,SAAS,CAAC,EAAE,qBAAqB,CAAA;IACjC;+DAC2D;IAC3D,mBAAmB,CAAC,EAAE,sBAAsB,CAAC,qBAAqB,CAAC,CAAA;IACnE;+BAC2B;IAC3B,sBAAsB,CAAC,EAAE,SAAS,CAAA;CACnC;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,oEAAoE;IACpE,SAAS,CAAC,EAAE,qBAAqB,CAAA;IACjC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,qBAAqB,KAAK,IAAI,CAAA;IAClD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,mEAAmE;IACnE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,IAAI,CAAA;IACxD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,sBAAsB,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,EAAE,EACF,SAAS,EACT,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,EACd,KAAK,GACN,EAAE,sBAAsB,2CA0KxB"}
@@ -0,0 +1,143 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AdminWidgetSlotRenderer, resolveAdminWidgets, useAdminBreadcrumbs, useAdminExtensions, useAdminHref, useAdminNavigate, useLocale, useOperatorAdminMessages, } from "@voyantjs/admin";
4
+ import { useBooking } from "@voyantjs/bookings-react";
5
+ import { useInvoices, usePaymentMutation } from "@voyantjs/finance-react";
6
+ import { Sheet, SheetContent } from "@voyantjs/ui/components/sheet";
7
+ import { useState } from "react";
8
+ import { BookingDetailPage, } from "../components/booking-detail-page.js";
9
+ import { BookingDocumentsTable } from "./booking-documents-table.js";
10
+ import { BookingInvoiceSheet } from "./booking-invoice-sheet.js";
11
+ import { useBookingActionLedgerEvents } from "./use-booking-action-ledger-events.js";
12
+ /**
13
+ * Widget slot rendered as the booking detail page's Invoices tab
14
+ * (packaged-admin RFC §4.7 cycle resolution): `@voyantjs/finance-ui` depends
15
+ * on this package, so the host cannot import the finance-owned invoices card
16
+ * directly — instead finance's admin extension contributes a widget targeting
17
+ * this slot and the host mounts the tab whenever a contribution exists.
18
+ * Widgets receive {@link BookingDetailHostSlotContext} as props.
19
+ */
20
+ export const bookingDetailInvoicesTabSlot = "booking.details.invoices-tab";
21
+ /**
22
+ * Widget slot rendered at the top of the booking detail page's Finance tab
23
+ * (same §4.7 cycle resolution as {@link bookingDetailInvoicesTabSlot}).
24
+ * `@voyantjs/finance-ui` contributes its pending payment-sessions card here.
25
+ * Widgets receive {@link BookingDetailHostSlotContext} as props.
26
+ */
27
+ export const bookingDetailFinanceStartSlot = "booking.details.finance-start";
28
+ /**
29
+ * Widget slot rendered at the bottom of the booking detail page's Finance
30
+ * tab. `@voyantjs/finance-ui` contributes its payment-policy override card
31
+ * here. Widgets receive {@link BookingDetailHostSlotContext} as props.
32
+ */
33
+ export const bookingDetailFinanceEndSlot = "booking.details.finance-end";
34
+ /**
35
+ * Packaged admin host for the canonical `BookingDetailPage` (packaged-admin
36
+ * RFC Phase 3). Owns everything package-clean:
37
+ *
38
+ * - Cross-route links resolve through semantic destinations (RFC §4.7):
39
+ * `booking.list`, `person.detail`, `organization.detail`,
40
+ * `product.detail`, `availabilitySlot.detail`, `payment.detail`,
41
+ * `invoice.detail` — no host route tree import.
42
+ * - Admin chrome breadcrumbs (`useAdminBreadcrumbs`).
43
+ * - Admin widget extension points: the `booking.details.header` and
44
+ * `booking.details.after-summary` slots render through the shared
45
+ * `AdminWidgetSlotRenderer`, which reads the workspace shell's
46
+ * `AdminExtensionsProvider` context.
47
+ * - Paid-amount aggregation across the booking's invoices and the
48
+ * derived fully-paid disabled reasons.
49
+ * - Payment row delete (finance-react mutation) and the in-place
50
+ * invoice sheet ({@link BookingInvoiceSheet}).
51
+ *
52
+ * App-local cards without package-level data access stay injectable via
53
+ * {@link BookingDetailHostSlots}.
54
+ */
55
+ export function BookingDetailHost({ id, activeTab, onTabChange, onRecordPayment, onEditPayment, onGenerateLink, slots, }) {
56
+ const detailMessages = useOperatorAdminMessages().bookings.detail;
57
+ const { resolvedLocale } = useLocale();
58
+ const resolveHref = useAdminHref();
59
+ const navigateTo = useAdminNavigate();
60
+ // Finance (or any extension that may not import this package) contributes
61
+ // the Invoices-tab content as widget contributions; the tab mounts whenever
62
+ // an app slot or at least one widget targets it.
63
+ const adminExtensions = useAdminExtensions();
64
+ const hasInvoicesTabWidgets = resolveAdminWidgets({ slot: bookingDetailInvoicesTabSlot, extensions: adminExtensions })
65
+ .length > 0;
66
+ const [viewingInvoiceId, setViewingInvoiceId] = useState(null);
67
+ const { remove: removePayment } = usePaymentMutation();
68
+ // Mirror the booking fetch so the admin chrome can render breadcrumbs
69
+ // without prop-drilling through the canonical page. TanStack Query
70
+ // dedupes by key, so this doesn't issue a second network request.
71
+ const { data: bookingData } = useBooking(id);
72
+ const booking = bookingData?.data;
73
+ // Sum customer payments across this booking's non-credit-note,
74
+ // non-draft invoices.
75
+ const { data: invoicesData } = useInvoices({ bookingId: id, limit: 20 });
76
+ const paidAmountCents = invoicesData?.data
77
+ ? invoicesData.data
78
+ .filter((inv) => {
79
+ const type = inv.invoiceType ?? "invoice";
80
+ return type !== "credit_note" && inv.status !== "draft";
81
+ })
82
+ .reduce((sum, inv) => sum + (inv.paidCents ?? 0), 0)
83
+ : null;
84
+ const fullyPaidReason = booking &&
85
+ booking.sellAmountCents != null &&
86
+ paidAmountCents != null &&
87
+ paidAmountCents >= booking.sellAmountCents
88
+ ? detailMessages.generateLinkFullyPaid
89
+ : null;
90
+ const bookingsHref = resolveHref("booking.list", {});
91
+ useAdminBreadcrumbs(booking
92
+ ? [
93
+ { label: detailMessages.breadcrumbBookings, href: bookingsHref },
94
+ { label: booking.bookingNumber },
95
+ ]
96
+ : [{ label: detailMessages.breadcrumbBookings, href: bookingsHref }]);
97
+ const slotContext = (b) => ({
98
+ booking: b,
99
+ paidAmountCents,
100
+ fullyPaidReason,
101
+ openInvoiceSheet: setViewingInvoiceId,
102
+ onGenerateLink,
103
+ });
104
+ // Central action-ledger entries merged into the Activity timeline —
105
+ // package-owned since the feed ships from `@voyantjs/bookings-react`.
106
+ const { events: actionLedgerEvents, footer: actionLedgerFooter } = useBookingActionLedgerEvents(id);
107
+ const activityExtraEvents = slots?.activityExtraEvents
108
+ ? [...actionLedgerEvents, ...slots.activityExtraEvents]
109
+ : actionLedgerEvents;
110
+ return (_jsxs(_Fragment, { children: [_jsx(BookingDetailPage, { id: id, locale: resolvedLocale, hideBreadcrumb: true, activeTab: activeTab, onTabChange: onTabChange, onBack: () => navigateTo("booking.list", {}), onPersonOpen: (personId) => navigateTo("person.detail", { personId }), onOrganizationOpen: (organizationId) => navigateTo("organization.detail", { organizationId }), onRecordPayment: onRecordPayment ? () => onRecordPayment() : undefined, recordPaymentDisabledReason: fullyPaidReason, addScheduleDisabledReason: fullyPaidReason, paidAmountCents: paidAmountCents, onItemResourceOpen: (kind, resourceId) => {
111
+ if (kind === "product") {
112
+ navigateTo("product.detail", { productId: resourceId });
113
+ return;
114
+ }
115
+ if (kind === "availabilitySlot") {
116
+ navigateTo("availabilitySlot.detail", { slotId: resourceId });
117
+ }
118
+ }, onInvoiceOpen: (invoiceId) => setViewingInvoiceId(invoiceId), onViewPayment: (row) => navigateTo("payment.detail", { paymentId: row.id }), onEditPayment: onEditPayment, onDeletePayment: async (row) => {
119
+ await removePayment.mutateAsync(row.id);
120
+ }, slots: {
121
+ header: (b) => (_jsx(AdminWidgetSlotRenderer, { slot: "booking.details.header", props: { booking: b } })),
122
+ afterSummary: (b) => (_jsx(AdminWidgetSlotRenderer, { slot: "booking.details.after-summary", props: { booking: b } })),
123
+ financeStart: (b) => (_jsxs(_Fragment, { children: [slots?.financeStart?.(slotContext(b)), _jsx(AdminWidgetSlotRenderer, { slot: bookingDetailFinanceStartSlot, props: { ...slotContext(b) } })] })),
124
+ financeEnd: (b) => (_jsxs(_Fragment, { children: [slots?.financeEnd?.(slotContext(b)), _jsx(AdminWidgetSlotRenderer, { slot: bookingDetailFinanceEndSlot, props: { ...slotContext(b) } })] })),
125
+ invoicesTab: slots?.invoicesTab || hasInvoicesTabWidgets
126
+ ? {
127
+ label: slots?.invoicesTab?.label,
128
+ content: (b) => (_jsxs(_Fragment, { children: [slots?.invoicesTab?.content(slotContext(b)), _jsx(AdminWidgetSlotRenderer, { slot: bookingDetailInvoicesTabSlot, props: { ...slotContext(b) } })] })),
129
+ }
130
+ : undefined,
131
+ documents: slots?.documents
132
+ ? (b) => slots.documents?.(slotContext(b))
133
+ : () => _jsx(BookingDocumentsTable, { bookingId: id }),
134
+ activityExtraEvents,
135
+ activityTimelineFooter: actionLedgerFooter || slots?.activityTimelineFooter ? (_jsxs(_Fragment, { children: [actionLedgerFooter, slots?.activityTimelineFooter] })) : undefined,
136
+ } }), _jsx(Sheet, { open: Boolean(viewingInvoiceId), onOpenChange: (open) => {
137
+ if (!open)
138
+ setViewingInvoiceId(null);
139
+ }, children: _jsx(SheetContent, { side: "right", className: "w-full! max-w-5xl!", children: viewingInvoiceId ? (_jsx(BookingInvoiceSheet, { invoiceId: viewingInvoiceId, onOpenInvoice: (invoiceId) => {
140
+ setViewingInvoiceId(null);
141
+ navigateTo("invoice.detail", { invoiceId });
142
+ } })) : null }) })] }));
143
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Route-level placeholder for the booking detail page. Mirrors the
3
+ * canonical `BookingDetailPage` layout (header row, summary card, tab
4
+ * bar, two list cards) so the pending state doesn't shift the page.
5
+ */
6
+ export declare function BookingDetailSkeleton(): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=booking-detail-skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-detail-skeleton.d.ts","sourceRoot":"","sources":["../../src/admin/booking-detail-skeleton.tsx"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,qBAAqB,4CAUpC"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Card, CardContent, CardHeader } from "@voyantjs/ui/components/card";
3
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
4
+ /**
5
+ * Route-level placeholder for the booking detail page. Mirrors the
6
+ * canonical `BookingDetailPage` layout (header row, summary card, tab
7
+ * bar, two list cards) so the pending state doesn't shift the page.
8
+ */
9
+ export function BookingDetailSkeleton() {
10
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsx(Header, {}), _jsx(SummaryCard, {}), _jsx(TabsBar, {}), _jsx(ListCard, { titleWidth: "w-32", rows: 3 }), _jsx(ListCard, { titleWidth: "w-28", rows: 2 })] }));
11
+ }
12
+ function Header() {
13
+ return (_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { className: "h-7 w-44" }), _jsx(Skeleton, { className: "h-5 w-20 rounded-full" })] }), _jsx(Skeleton, { className: "h-8 w-8 rounded-md" })] }));
14
+ }
15
+ function SummaryCard() {
16
+ return (_jsx(Card, { children: _jsx(CardContent, { className: "grid grid-cols-2 gap-6 py-6 sm:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3 w-20" }), _jsx(Skeleton, { className: "h-5 w-28" }), _jsx(Skeleton, { className: "h-3 w-12" })] }, i))) }) }));
17
+ }
18
+ function TabsBar() {
19
+ const widths = ["w-20", "w-20", "w-16", "w-20", "w-24", "w-16"];
20
+ return (_jsx("div", { className: "flex h-9 w-fit items-center gap-1 rounded-lg bg-muted p-[3px]", children: widths.map((w, i) => (_jsx(Skeleton, { className: `h-7 rounded-md ${w}` }, i))) }));
21
+ }
22
+ function ListCard({ titleWidth, rows }) {
23
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsx(Skeleton, { className: `h-5 ${titleWidth}` }), _jsx(Skeleton, { className: "h-8 w-24" })] }), _jsx(CardContent, { className: "space-y-2", children: Array.from({ length: rows }).map((_, i) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-32" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Skeleton, { className: "h-4 w-16" }), _jsx(Skeleton, { className: "h-5 w-16 rounded-full" })] })] }, i))) })] }));
24
+ }
@@ -0,0 +1,13 @@
1
+ export interface BookingDocumentsTableProps {
2
+ bookingId: string;
3
+ }
4
+ /**
5
+ * Unified Documents tab for a booking — flattens auto-generated legal
6
+ * contracts and per-traveler documents (passport, visa, insurance…) into
7
+ * a single DataTable that matches the rest of the booking detail tabs.
8
+ * Contracts render first (canonical booking docs), traveler-uploaded
9
+ * documents below; each contract row owns its own attachments fetch so
10
+ * we don't need a join endpoint server-side.
11
+ */
12
+ export declare function BookingDocumentsTable({ bookingId, }: BookingDocumentsTableProps): React.ReactElement;
13
+ //# sourceMappingURL=booking-documents-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-documents-table.d.ts","sourceRoot":"","sources":["../../src/admin/booking-documents-table.tsx"],"names":[],"mappings":"AA8CA,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;CAClB;AAmDD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,GACV,EAAE,0BAA0B,GAAG,KAAK,CAAC,YAAY,CA6MjD"}