@voyantjs/bookings-ui 0.81.14 → 0.81.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/booking-billing-dialog.d.ts.map +1 -1
- package/dist/components/booking-billing-dialog.js +2 -2
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +53 -67
- package/dist/components/booking-detail-page.d.ts +44 -6
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +117 -42
- package/dist/components/booking-guarantee-list.d.ts.map +1 -1
- package/dist/components/booking-guarantee-list.js +66 -28
- package/dist/components/booking-item-list.d.ts +8 -1
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +157 -96
- package/dist/components/booking-list-filters.d.ts +3 -1
- package/dist/components/booking-list-filters.d.ts.map +1 -1
- package/dist/components/booking-list-filters.js +5 -3
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +12 -8
- package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-dialog.js +23 -8
- package/dist/components/booking-payment-schedule-list.d.ts +6 -1
- package/dist/components/booking-payment-schedule-list.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-list.js +131 -27
- package/dist/components/booking-payments-summary.d.ts +15 -7
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +99 -74
- package/dist/components/icon-action-button.d.ts +18 -0
- package/dist/components/icon-action-button.d.ts.map +1 -0
- package/dist/components/icon-action-button.js +13 -0
- package/dist/components/payment-schedule-section.d.ts +41 -46
- package/dist/components/payment-schedule-section.d.ts.map +1 -1
- package/dist/components/payment-schedule-section.js +150 -58
- package/dist/components/status-badge.d.ts +24 -0
- package/dist/components/status-badge.d.ts.map +1 -0
- package/dist/components/status-badge.js +65 -0
- package/dist/components/supplier-status-list.d.ts.map +1 -1
- package/dist/components/supplier-status-list.js +64 -27
- package/dist/components/traveler-dialog.d.ts.map +1 -1
- package/dist/components/traveler-dialog.js +10 -5
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +194 -80
- package/dist/i18n/en.d.ts +98 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +101 -8
- package/dist/i18n/messages.d.ts +108 -5
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +196 -10
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +98 -5
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +101 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +34 -32
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
3
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBooking, useBookingItems, useBookingMutation, } from "@voyantjs/bookings-react";
|
|
4
4
|
import { useOrganization, usePerson } from "@voyantjs/crm-react";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Badge, Button, Card, CardContent, cn, } from "@voyantjs/ui/components";
|
|
6
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@voyantjs/ui/components/collapsible";
|
|
7
7
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "@voyantjs/ui/components/tooltip";
|
|
9
|
+
import { Ban, ChevronDown, ChevronRight, CreditCard, Mail, MapPin, Pencil, Phone, Plus, RefreshCw, Trash2, } from "lucide-react";
|
|
10
|
+
import { Fragment, useState } from "react";
|
|
11
|
+
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/index.js";
|
|
11
12
|
import { BookingActivityTimeline } from "./booking-activity-timeline.js";
|
|
12
13
|
import { BookingBillingDialog } from "./booking-billing-dialog.js";
|
|
13
14
|
import { BookingCancellationDialog } from "./booking-cancellation-dialog.js";
|
|
@@ -16,13 +17,13 @@ import { BookingGroupSection } from "./booking-group-section.js";
|
|
|
16
17
|
import { BookingGuaranteeList } from "./booking-guarantee-list.js";
|
|
17
18
|
import { BookingItemList } from "./booking-item-list.js";
|
|
18
19
|
import { BookingNotes } from "./booking-notes.js";
|
|
19
|
-
import { BookingPaymentReconciliationBanner } from "./booking-payment-reconciliation-banner.js";
|
|
20
20
|
import { BookingPaymentScheduleList } from "./booking-payment-schedule-list.js";
|
|
21
21
|
import { BookingPaymentsSummary, } from "./booking-payments-summary.js";
|
|
22
|
+
import { StatusBadge } from "./status-badge.js";
|
|
22
23
|
import { StatusChangeDialog } from "./status-change-dialog.js";
|
|
23
24
|
import { SupplierStatusList } from "./supplier-status-list.js";
|
|
24
25
|
import { TravelerList } from "./traveler-list.js";
|
|
25
|
-
export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBack, onPersonOpen, onOrganizationOpen,
|
|
26
|
+
export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBack, onRecordPayment, recordPaymentDisabledReason, addScheduleDisabledReason, paidAmountCents, onItemResourceOpen, onPersonOpen, onOrganizationOpen, onInvoiceOpen, onViewPayment, onEditPayment, onDeletePayment, slots, }) {
|
|
26
27
|
const i18n = useBookingsUiI18nOrDefault();
|
|
27
28
|
const messages = useBookingsUiMessagesOrDefault();
|
|
28
29
|
const detailMessages = messages.bookingDetailPage;
|
|
@@ -30,9 +31,24 @@ export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBac
|
|
|
30
31
|
const [editOpen, setEditOpen] = useState(false);
|
|
31
32
|
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
|
|
32
33
|
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
|
34
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
33
35
|
const { data: bookingData, isPending } = useBooking(id);
|
|
34
36
|
const { remove } = useBookingMutation();
|
|
35
|
-
const
|
|
37
|
+
const headerPersonId = bookingData?.data?.personId ?? null;
|
|
38
|
+
const headerOrganizationId = bookingData?.data?.organizationId ?? null;
|
|
39
|
+
const headerPerson = usePerson(headerPersonId ?? undefined, {
|
|
40
|
+
enabled: Boolean(headerPersonId),
|
|
41
|
+
}).data;
|
|
42
|
+
const headerOrganization = useOrganization(headerOrganizationId ?? undefined, {
|
|
43
|
+
enabled: Boolean(headerOrganizationId) && !headerPersonId,
|
|
44
|
+
}).data;
|
|
45
|
+
// Pull booking items so the subtitle can link to the primary product
|
|
46
|
+
// + availability slot. We pick the first item (or the one flagged
|
|
47
|
+
// `isPrimary` when present) — most bookings only have one product.
|
|
48
|
+
const headerItems = useBookingItems(id).data?.data ?? [];
|
|
49
|
+
const primaryItem = headerItems.find((item) => item.isPrimary) ??
|
|
50
|
+
headerItems[0] ??
|
|
51
|
+
null;
|
|
36
52
|
if (isPending) {
|
|
37
53
|
return (_jsx("div", { className: cn("flex items-center justify-center py-12", className), children: _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }) }));
|
|
38
54
|
}
|
|
@@ -43,15 +59,56 @@ export function BookingDetailPage({ id, className, locale, hideBreadcrumb, onBac
|
|
|
43
59
|
const canCancel = ["draft", "on_hold", "confirmed", "in_progress"].includes(booking.status);
|
|
44
60
|
const sellHint = booking.priceOverride?.isManual
|
|
45
61
|
? `${detailMessages.summaryPriceOverride}: ${booking.priceOverride.reason}`
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
: undefined;
|
|
63
|
+
const billingPersonName = [booking.contactFirstName, booking.contactLastName].filter(Boolean).join(" ") ||
|
|
64
|
+
(headerPerson
|
|
65
|
+
? [headerPerson.firstName, headerPerson.lastName].filter(Boolean).join(" ")
|
|
66
|
+
: "") ||
|
|
67
|
+
headerOrganization?.name ||
|
|
68
|
+
"";
|
|
69
|
+
const billingHref = headerPersonId
|
|
70
|
+
? () => onPersonOpen?.(headerPersonId)
|
|
71
|
+
: headerOrganizationId
|
|
72
|
+
? () => onOrganizationOpen?.(headerOrganizationId)
|
|
73
|
+
: null;
|
|
74
|
+
const billingClickable = billingHref && (headerPersonId ? Boolean(onPersonOpen) : Boolean(onOrganizationOpen));
|
|
75
|
+
const headerDateRange = booking.startDate
|
|
76
|
+
? `${formatDate(booking.startDate, resolvedLocale, detailMessages.noValue)} - ${formatDate(booking.endDate, resolvedLocale, detailMessages.noValue)}`
|
|
77
|
+
: null;
|
|
78
|
+
const headerPax = booking.pax != null ? `${booking.pax} PAX` : null;
|
|
79
|
+
const headerProductTitle = primaryItem?.productNameSnapshot ?? primaryItem?.title ?? null;
|
|
80
|
+
const headerProductId = primaryItem?.productId ?? null;
|
|
81
|
+
const headerSlotId = primaryItem?.availabilitySlotId ?? null;
|
|
82
|
+
const headerSubtitleParts = [
|
|
83
|
+
billingPersonName ? (billingClickable ? (_jsx("button", { type: "button", onClick: billingHref, className: "hover:text-foreground hover:underline", children: billingPersonName }, "billing")) : (_jsx("span", { children: billingPersonName }, "billing"))) : null,
|
|
84
|
+
headerProductTitle ? (headerProductId && onItemResourceOpen ? (_jsx("button", { type: "button", onClick: () => onItemResourceOpen("product", headerProductId), className: "inline-block max-w-[18rem] truncate align-bottom hover:text-foreground hover:underline", title: headerProductTitle, children: headerProductTitle }, "product")) : (_jsx("span", { className: "inline-block max-w-[18rem] truncate align-bottom", title: headerProductTitle, children: headerProductTitle }, "product"))) : null,
|
|
85
|
+
headerDateRange ? (headerSlotId && onItemResourceOpen ? (_jsx("button", { type: "button", onClick: () => onItemResourceOpen("availabilitySlot", headerSlotId), className: "hover:text-foreground hover:underline", children: headerDateRange }, "dates")) : (_jsx("span", { children: headerDateRange }, "dates"))) : null,
|
|
86
|
+
headerPax ? _jsx("span", { children: headerPax }, "pax") : null,
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
return (_jsxs("div", { "data-slot": "booking-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [hideBreadcrumb ? null : (_jsxs("div", { className: "flex items-center gap-1.5 text-sm text-muted-foreground", children: [onBack ? (_jsx("button", { type: "button", onClick: onBack, className: "transition-colors hover:text-foreground", children: detailMessages.breadcrumbBookings })) : (_jsx("span", { children: detailMessages.breadcrumbBookings })), _jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "font-normal text-foreground", children: booking.bookingNumber })] })), _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: booking.bookingNumber }), _jsx(StatusBadge, { status: booking.status, children: getBookingStatusLabel(booking.status, messages.common.bookingStatusLabels) })] }), headerSubtitleParts.length > 0 ? (_jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground", children: headerSubtitleParts.map((part, idx) => (_jsxs(Fragment, { children: [idx > 0 ? _jsx("span", { className: "text-muted-foreground/60", children: "/" }) : null, part] }, idx))) })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": "true" }), detailMessages.editAction] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setStatusDialogOpen(true), children: [_jsx(RefreshCw, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": "true" }), detailMessages.changeStatusAction] }), canCancel ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setCancelDialogOpen(true), children: [_jsx(Ban, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": "true" }), detailMessages.cancelBookingAction] })) : null, _jsxs(Button, { variant: "outline", size: "sm", className: "text-destructive hover:bg-destructive/10 hover:text-destructive", disabled: remove.isPending, onClick: () => setDeleteDialogOpen(true), children: [_jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5", "aria-hidden": "true" }), detailMessages.deleteAction] })] })] }), slots?.header?.(booking), _jsxs("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-4", children: [_jsx(StatCard, { label: detailMessages.summaryTotal, hint: sellHint, children: formatAmount(booking.sellAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue) }), _jsx(StatCard, { label: detailMessages.summaryPaid, badge: paidAmountCents != null && booking.sellAmountCents
|
|
89
|
+
? renderPercentBadge(Math.round((paidAmountCents / booking.sellAmountCents) * 100), paidBadgeClass)
|
|
90
|
+
: null, children: paidAmountCents != null
|
|
91
|
+
? formatAmount(paidAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue)
|
|
92
|
+
: detailMessages.noValue }), _jsx(StatCard, { label: detailMessages.summaryCostMargin, badge: booking.marginPercent != null
|
|
93
|
+
? renderPercentBadge(booking.marginPercent, marginBadgeClass)
|
|
94
|
+
: null, children: formatAmount(booking.costAmountCents, booking.sellCurrency, resolvedLocale, detailMessages.noValue) }), _jsx(StatCard, { label: detailMessages.summaryCreated, children: formatDate(booking.createdAt, resolvedLocale, detailMessages.noValue) })] }), slots?.afterSummary?.(booking), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full justify-start", children: [_jsx(TabsTrigger, { value: "overview", children: detailMessages.tabOverview }), _jsx(TabsTrigger, { value: "travelers", children: detailMessages.tabTravelers }), _jsx(TabsTrigger, { value: "finance", children: detailMessages.tabFinance }), slots?.invoicesTab ? (_jsx(TabsTrigger, { value: "invoices", children: slots.invoicesTab.label ?? detailMessages.tabInvoices })) : null, _jsx(TabsTrigger, { value: "documents", children: detailMessages.tabDocuments }), _jsx(TabsTrigger, { value: "suppliers", children: detailMessages.tabSuppliers }), _jsx(TabsTrigger, { value: "activity", children: detailMessages.tabActivity }), slots?.ledgerTab ? (_jsx(TabsTrigger, { value: "ledger", children: slots.ledgerTab.label ?? detailMessages.tabLedger })) : null, _jsx(TabsTrigger, { value: "meta", children: detailMessages.tabMeta })] }), _jsxs(TabsContent, { value: "overview", className: "mt-4 flex flex-col gap-6", children: [slots?.overviewStart?.(booking), _jsx(BookingItemList, { bookingId: id, onResourceOpen: onItemResourceOpen }), _jsx(BookingGroupSection, { bookingId: id }), visibleInternalNotes(booking.internalNotes) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-5", children: [_jsx("p", { className: "mb-1 text-xs font-medium text-muted-foreground", children: detailMessages.internalNotesLabel }), _jsx("p", { className: "whitespace-pre-wrap text-sm", children: visibleInternalNotes(booking.internalNotes) })] }) })) : null, slots?.overviewEnd?.(booking)] }), _jsxs(TabsContent, { value: "travelers", className: "mt-4 flex flex-col gap-6", children: [slots?.travelersStart?.(booking), _jsx(BookingBillingContextCard, { booking: booking, onPersonOpen: onPersonOpen, onOrganizationOpen: onOrganizationOpen }), _jsx(TravelerList, { bookingId: id, autoReveal: true })] }), _jsxs(TabsContent, { value: "finance", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingPaymentScheduleList, { bookingId: id, addScheduleDisabledReason: addScheduleDisabledReason ?? null }), slots?.paymentsContent ? (renderDetailSlot(slots.paymentsContent, booking)) : (_jsx(BookingPaymentsSummary, { bookingId: id, variant: "admin", onViewPayment: onViewPayment, onInvoiceOpen: onInvoiceOpen, onEditPayment: onEditPayment, onDeletePayment: onDeletePayment, headerAction: onRecordPayment ? (_jsx(RecordPaymentHeaderButton, { label: detailMessages.recordPaymentAction, disabledReason: recordPaymentDisabledReason ?? null, onClick: () => onRecordPayment(booking) })) : null })), slots?.financeStart?.(booking), _jsxs(Collapsible, { children: [_jsxs(CollapsibleTrigger, { className: "group flex w-full items-center justify-between rounded-md border bg-background px-4 py-3 text-sm font-semibold hover:bg-muted/30", children: [messages.bookingGuaranteeList.title, _jsx(ChevronDown, { className: "h-4 w-4 transition-transform group-data-panel-open:rotate-180" })] }), _jsx(CollapsibleContent, { className: "mt-3", children: _jsx(BookingGuaranteeList, { bookingId: id }) })] }), slots?.financeEnd?.(booking)] }), slots?.invoicesTab ? (_jsx(TabsContent, { value: "invoices", className: "mt-4 flex flex-col gap-6", children: renderDetailSlot(slots.invoicesTab.content, booking) })) : null, _jsx(TabsContent, { value: "documents", className: "mt-4 flex flex-col gap-4", children: slots?.documents ? (slots.documents(booking)) : (_jsx("p", { className: "rounded-md border border-dashed p-4 text-sm text-muted-foreground", children: detailMessages.documentsSlotEmpty })) }), _jsx(TabsContent, { value: "suppliers", className: "mt-4", children: _jsx(SupplierStatusList, { bookingId: id }) }), _jsxs(TabsContent, { value: "activity", className: "mt-4 flex flex-col gap-6", children: [_jsx(BookingActivityTimeline, { bookingId: id }), _jsx(BookingNotes, { bookingId: id }), slots?.activityEnd?.(booking)] }), slots?.ledgerTab ? (_jsx(TabsContent, { value: "ledger", className: "mt-4 flex flex-col gap-6", children: renderDetailSlot(slots.ledgerTab.content, booking) })) : null, _jsx(TabsContent, { value: "meta", className: "mt-4", children: _jsx(Card, { children: _jsx(CardContent, { className: "grid grid-cols-2 gap-6 py-6 sm:grid-cols-4", children: _jsx(SummaryStat, { label: detailMessages.summaryUpdated, value: formatDate(booking.updatedAt, resolvedLocale, detailMessages.noValue) }) }) }) })] }), _jsx(BookingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking }), _jsx(StatusChangeDialog, { open: statusDialogOpen, onOpenChange: setStatusDialogOpen, bookingId: id, currentStatus: booking.status }), _jsx(BookingCancellationDialog, { open: cancelDialogOpen, onOpenChange: setCancelDialogOpen, booking: booking }), _jsx(AlertDialog, { open: deleteDialogOpen, onOpenChange: (next) => {
|
|
95
|
+
if (!next && !remove.isPending)
|
|
96
|
+
setDeleteDialogOpen(false);
|
|
97
|
+
}, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: detailMessages.deleteConfirm }), _jsx(AlertDialogDescription, { children: booking.bookingNumber
|
|
98
|
+
? formatMessage(detailMessages.deleteConfirmDescription, {
|
|
99
|
+
number: booking.bookingNumber,
|
|
100
|
+
})
|
|
101
|
+
: detailMessages.deleteConfirmDescriptionFallback })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: remove.isPending, children: detailMessages.deleteCancel }), _jsx(AlertDialogAction, { variant: "destructive", disabled: remove.isPending, onClick: async () => {
|
|
49
102
|
await remove.mutateAsync(id);
|
|
103
|
+
setDeleteDialogOpen(false);
|
|
50
104
|
onBack?.();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
105
|
+
}, children: detailMessages.deleteConfirmAction })] })] }) })] }));
|
|
106
|
+
}
|
|
107
|
+
function RecordPaymentHeaderButton({ label, disabledReason, onClick, }) {
|
|
108
|
+
if (disabledReason) {
|
|
109
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { tabIndex: 0, className: "inline-block" }), children: _jsxs(Button, { variant: "outline", size: "sm", disabled: true, className: "pointer-events-none", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), label] }) }), _jsx(TooltipContent, { children: disabledReason })] }));
|
|
110
|
+
}
|
|
111
|
+
return (_jsxs(Button, { variant: "outline", size: "sm", onClick: onClick, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), label] }));
|
|
55
112
|
}
|
|
56
113
|
function renderDetailSlot(content, booking) {
|
|
57
114
|
return typeof content === "function" ? content(booking) : content;
|
|
@@ -64,7 +121,7 @@ function renderDetailSlot(content, booking) {
|
|
|
64
121
|
* person / organization so the operator still sees who they're
|
|
65
122
|
* billing.
|
|
66
123
|
*/
|
|
67
|
-
export function BookingBillingContextCard({ booking }) {
|
|
124
|
+
export function BookingBillingContextCard({ booking, onPersonOpen, onOrganizationOpen, }) {
|
|
68
125
|
const messages = useBookingsUiMessagesOrDefault().bookingDetailPage;
|
|
69
126
|
const [editOpen, setEditOpen] = useState(false);
|
|
70
127
|
const person = usePerson(booking.personId ?? undefined, {
|
|
@@ -89,30 +146,38 @@ export function BookingBillingContextCard({ booking }) {
|
|
|
89
146
|
]
|
|
90
147
|
.filter(Boolean)
|
|
91
148
|
.join(", ");
|
|
92
|
-
return (_jsxs(
|
|
149
|
+
return (_jsxs("div", { "data-slot": "booking-billing-context", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(CreditCard, { className: "h-4 w-4", "aria-hidden": "true" }), messages.billingPayer] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "mr-1 h-3.5 w-3.5", "aria-hidden": "true" }), messages.editAction] })] }), _jsxs("div", { className: "flex flex-col gap-4 rounded-md border p-4", children: [_jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [_jsx(BillingField, { label: messages.billingPayer, value: payerName ? (booking.personId && onPersonOpen ? (_jsx("button", { type: "button", onClick: () => onPersonOpen(booking.personId), className: "text-left text-primary hover:underline", children: payerName })) : booking.organizationId && !booking.personId && onOrganizationOpen ? (_jsx("button", { type: "button", onClick: () => onOrganizationOpen(booking.organizationId), className: "text-left text-primary hover:underline", children: payerName })) : (payerName)) : (messages.noValue) }), _jsx(BillingField, { label: messages.billingEmail, value: email ?? messages.noValue, icon: _jsx(Mail, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) }), _jsx(BillingField, { label: messages.billingPhone, value: phone ?? messages.noValue, icon: _jsx(Phone, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] }), _jsx(BillingField, { label: messages.billingAddress, value: address || messages.noValue, icon: _jsx(MapPin, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) })] }), _jsx(BookingBillingDialog, { open: editOpen, onOpenChange: setEditOpen, booking: booking })] }));
|
|
93
150
|
}
|
|
94
151
|
function SummaryStat({ label, value, hint, icon, }) {
|
|
95
152
|
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [icon, label] }), _jsx("div", { className: "text-base font-semibold tabular-nums", children: value }), hint ? _jsx("div", { className: "text-xs text-muted-foreground", children: hint }) : null] }));
|
|
96
153
|
}
|
|
97
|
-
function
|
|
98
|
-
return (_jsxs("div", { className: "
|
|
154
|
+
function StatCard({ label, children, hint, badge, }) {
|
|
155
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("div", { className: "text-xl font-semibold tabular-nums leading-none", children: children }), badge] }), hint ? _jsx("div", { className: "text-xs text-muted-foreground", children: hint }) : null] }) }));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Traffic-light badge for a percentage value. Color thresholds are
|
|
159
|
+
* passed in by the caller (Paid uses 0 → red, 0..100 → yellow, 100 →
|
|
160
|
+
* green; Margin uses <0 → red, 0..10 → yellow, >10 → green).
|
|
161
|
+
*/
|
|
162
|
+
function renderPercentBadge(percent, classFor) {
|
|
163
|
+
return (_jsxs(Badge, { variant: "outline", className: cn("border-transparent", classFor(percent)), children: [percent, "%"] }));
|
|
99
164
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
165
|
+
function paidBadgeClass(percent) {
|
|
166
|
+
if (percent <= 0)
|
|
167
|
+
return "bg-red-500/10 text-red-600 dark:text-red-400";
|
|
168
|
+
if (percent >= 100)
|
|
169
|
+
return "bg-green-500/10 text-green-600 dark:text-green-400";
|
|
170
|
+
return "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400";
|
|
102
171
|
}
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const display = name || personId;
|
|
110
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(personId), className: "truncate text-left text-sm font-medium text-primary hover:underline", children: display })) : (_jsx("span", { className: "truncate text-sm font-medium", children: display }))] }));
|
|
172
|
+
function marginBadgeClass(percent) {
|
|
173
|
+
if (percent < 0)
|
|
174
|
+
return "bg-red-500/10 text-red-600 dark:text-red-400";
|
|
175
|
+
if (percent > 10)
|
|
176
|
+
return "bg-green-500/10 text-green-600 dark:text-green-400";
|
|
177
|
+
return "bg-yellow-500/10 text-yellow-700 dark:text-yellow-400";
|
|
111
178
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
const display = organization?.name || organizationId;
|
|
115
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: label }), onOpen ? (_jsx("button", { type: "button", onClick: () => onOpen(organizationId), className: "truncate text-left text-sm font-medium text-primary hover:underline", children: display })) : (_jsx("span", { className: "truncate text-sm font-medium", children: display }))] }));
|
|
179
|
+
function BillingField({ label, value, icon, }) {
|
|
180
|
+
return (_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [icon, label] }), _jsx("div", { className: "truncate text-sm font-medium", children: value })] }));
|
|
116
181
|
}
|
|
117
182
|
function getBookingStatusLabel(status, labels) {
|
|
118
183
|
return labels[status] ?? status;
|
|
@@ -120,16 +185,26 @@ function getBookingStatusLabel(status, labels) {
|
|
|
120
185
|
function formatAmount(cents, currency, locale, empty) {
|
|
121
186
|
if (cents == null)
|
|
122
187
|
return empty;
|
|
123
|
-
|
|
188
|
+
const amount = cents / 100;
|
|
189
|
+
const amountText = new Intl.NumberFormat(locale, {
|
|
190
|
+
maximumFractionDigits: 0,
|
|
191
|
+
}).format(amount);
|
|
192
|
+
// RON's "symbol" is just the ISO code itself, so a `<symbol> <amount> <code>`
|
|
193
|
+
// layout would print "RON 1.185 RON" — collapse it back to "1.185 RON".
|
|
194
|
+
if (currency.toUpperCase() === "RON") {
|
|
195
|
+
return `${amountText} ${currency}`;
|
|
196
|
+
}
|
|
197
|
+
// Extract the locale's native symbol so we can always render
|
|
198
|
+
// `<symbol> <amount> <code>` regardless of where Intl would otherwise
|
|
199
|
+
// place the symbol for this locale (e.g. Romanian puts it after).
|
|
200
|
+
const parts = new Intl.NumberFormat(locale, {
|
|
124
201
|
style: "currency",
|
|
125
202
|
currency,
|
|
203
|
+
currencyDisplay: "narrowSymbol",
|
|
126
204
|
maximumFractionDigits: 0,
|
|
127
|
-
}).
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (percent == null)
|
|
131
|
-
return empty;
|
|
132
|
-
return `${percent.toFixed(0)}%`;
|
|
205
|
+
}).formatToParts(amount);
|
|
206
|
+
const symbol = parts.find((p) => p.type === "currency")?.value ?? currency;
|
|
207
|
+
return `${symbol} ${amountText} ${currency}`;
|
|
133
208
|
}
|
|
134
209
|
function formatDate(iso, locale, empty) {
|
|
135
210
|
if (!iso)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-guarantee-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-guarantee-list.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-guarantee-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-guarantee-list.tsx"],"names":[],"mappings":"AA4BA,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE,yBAAyB,2CAkK5E"}
|
|
@@ -1,48 +1,86 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useBookingGuaranteeMutation, useBookingGuarantees, } from "@voyantjs/finance-react";
|
|
4
|
-
import {
|
|
4
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, } from "@voyantjs/ui/components";
|
|
5
|
+
import { DataTable } from "@voyantjs/ui/components/data-table";
|
|
5
6
|
import { Pencil, Plus, ShieldCheck, Trash2 } from "lucide-react";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
9
|
import { BookingGuaranteeDialog } from "./booking-guarantee-dialog.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
active: "default",
|
|
12
|
-
released: "secondary",
|
|
13
|
-
failed: "destructive",
|
|
14
|
-
cancelled: "destructive",
|
|
15
|
-
expired: "secondary",
|
|
16
|
-
};
|
|
10
|
+
import { IconActionButton } from "./icon-action-button.js";
|
|
11
|
+
import { StatusBadge } from "./status-badge.js";
|
|
17
12
|
export function BookingGuaranteeList({ bookingId }) {
|
|
18
13
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
19
14
|
const [editing, setEditing] = React.useState(undefined);
|
|
15
|
+
const [deleteTarget, setDeleteTarget] = React.useState(null);
|
|
20
16
|
const { data } = useBookingGuarantees(bookingId);
|
|
21
17
|
const { remove } = useBookingGuaranteeMutation(bookingId);
|
|
22
18
|
const { formatCurrency, formatDate } = useBookingsUiI18nOrDefault();
|
|
23
19
|
const messages = useBookingsUiMessagesOrDefault();
|
|
20
|
+
const t = messages.bookingGuaranteeList;
|
|
21
|
+
const deleteMessages = t.actions.deleteConfirm;
|
|
24
22
|
const guarantees = data?.data ?? [];
|
|
25
|
-
|
|
23
|
+
const handleConfirmDelete = async () => {
|
|
24
|
+
if (!deleteTarget)
|
|
25
|
+
return;
|
|
26
|
+
await remove.mutateAsync(deleteTarget.id);
|
|
27
|
+
setDeleteTarget(null);
|
|
28
|
+
};
|
|
29
|
+
const columns = React.useMemo(() => [
|
|
30
|
+
{
|
|
31
|
+
accessorKey: "guaranteeType",
|
|
32
|
+
header: t.columns.type,
|
|
33
|
+
cell: ({ row }) => messages.bookingGuaranteeDialog.guaranteeTypeLabels[row.original.guaranteeType],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
accessorKey: "amountCents",
|
|
37
|
+
header: t.columns.amount,
|
|
38
|
+
cell: ({ row }) => (_jsx("span", { className: "font-mono", children: row.original.amountCents == null || !row.original.currency
|
|
39
|
+
? t.values.amountUnavailable
|
|
40
|
+
: formatCurrency(row.original.amountCents / 100, row.original.currency) })),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
accessorKey: "status",
|
|
44
|
+
header: t.columns.status,
|
|
45
|
+
cell: ({ row }) => (_jsx(StatusBadge, { status: row.original.status, children: messages.bookingGuaranteeDialog.guaranteeStatusLabels[row.original.status] })),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
accessorKey: "provider",
|
|
49
|
+
header: t.columns.provider,
|
|
50
|
+
cell: ({ row }) => row.original.provider ?? t.values.providerUnavailable,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
accessorKey: "referenceNumber",
|
|
54
|
+
header: t.columns.reference,
|
|
55
|
+
cell: ({ row }) => (_jsx("span", { className: "block max-w-[180px] truncate font-mono text-xs", children: row.original.referenceNumber ?? t.values.referenceUnavailable })),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
accessorKey: "expiresAt",
|
|
59
|
+
header: t.columns.expires,
|
|
60
|
+
cell: ({ row }) => row.original.expiresAt ? formatDate(row.original.expiresAt) : t.values.expiresUnavailable,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "actions",
|
|
64
|
+
header: "",
|
|
65
|
+
cell: ({ row }) => (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(IconActionButton, { label: t.actions.editGuarantee, icon: _jsx(Pencil, { className: "h-3.5 w-3.5" }), onClick: (e) => {
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
setEditing(row.original);
|
|
68
|
+
setDialogOpen(true);
|
|
69
|
+
} }), _jsx(IconActionButton, { label: t.actions.deleteGuarantee, icon: _jsx(Trash2, { className: "h-3.5 w-3.5" }), className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive", onClick: (e) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
setDeleteTarget(row.original);
|
|
72
|
+
} })] })),
|
|
73
|
+
},
|
|
74
|
+
], [formatCurrency, formatDate, messages, t]);
|
|
75
|
+
return (_jsxs("div", { "data-slot": "booking-guarantee-list", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(ShieldCheck, { className: "h-4 w-4" }), t.title] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
26
76
|
setEditing(undefined);
|
|
27
77
|
setDialogOpen(true);
|
|
28
|
-
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }),
|
|
29
|
-
? messages.bookingGuaranteeList.values.amountUnavailable
|
|
30
|
-
: formatCurrency(g.amountCents / 100, g.currency) }), _jsx("td", { className: "p-2", children: g.provider ?? messages.bookingGuaranteeList.values.providerUnavailable }), _jsx("td", { className: "max-w-[150px] truncate p-2 font-mono text-xs", children: g.referenceNumber ??
|
|
31
|
-
messages.bookingGuaranteeList.values.referenceUnavailable }), _jsx("td", { className: "p-2", children: g.expiresAt
|
|
32
|
-
? formatDate(g.expiresAt)
|
|
33
|
-
: messages.bookingGuaranteeList.values.expiresUnavailable }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
34
|
-
setEditing(g);
|
|
35
|
-
setDialogOpen(true);
|
|
36
|
-
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
37
|
-
if (confirm(messages.bookingGuaranteeList.actions.deleteConfirm)) {
|
|
38
|
-
remove.mutate(g.id);
|
|
39
|
-
}
|
|
40
|
-
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, g.id))) })] }) })) }), _jsx(BookingGuaranteeDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
|
|
78
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t.addGuarantee] })] }), _jsx(DataTable, { columns: columns, data: guarantees, emptyMessage: t.empty, showPagination: false }), _jsx(BookingGuaranteeDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
|
|
41
79
|
setDialogOpen(nextOpen);
|
|
42
|
-
if (!nextOpen)
|
|
80
|
+
if (!nextOpen)
|
|
43
81
|
setEditing(undefined);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} })] }));
|
|
82
|
+
}, bookingId: bookingId, guarantee: editing, onSuccess: () => setEditing(undefined) }), _jsx(AlertDialog, { open: Boolean(deleteTarget), onOpenChange: (next) => {
|
|
83
|
+
if (!next && !remove.isPending)
|
|
84
|
+
setDeleteTarget(null);
|
|
85
|
+
}, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: deleteMessages.title }), _jsx(AlertDialogDescription, { children: deleteMessages.description })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: remove.isPending, children: deleteMessages.cancel }), _jsx(AlertDialogAction, { variant: "destructive", disabled: remove.isPending, onClick: () => void handleConfirmDelete(), children: deleteMessages.confirm })] })] }) })] }));
|
|
48
86
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
export type BookingItemResourceKind = "product" | "availabilitySlot";
|
|
1
2
|
export interface BookingItemListProps {
|
|
2
3
|
bookingId: string;
|
|
4
|
+
/**
|
|
5
|
+
* Open a linked resource (product / availability slot) in the host app.
|
|
6
|
+
* When omitted, the snapshot sheet renders the names as plain text
|
|
7
|
+
* instead of clickable links.
|
|
8
|
+
*/
|
|
9
|
+
onResourceOpen?: (kind: BookingItemResourceKind, id: string) => void;
|
|
3
10
|
}
|
|
4
|
-
export declare function BookingItemList({ bookingId }: BookingItemListProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function BookingItemList({ bookingId, onResourceOpen }: BookingItemListProps): import("react/jsx-runtime").JSX.Element;
|
|
5
12
|
//# sourceMappingURL=booking-item-list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-item-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-list.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-item-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-list.tsx"],"names":[],"mappings":"AAmCA,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,kBAAkB,CAAA;AAEpE,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACrE;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE,oBAAoB,2CA+NlF"}
|