@voyantjs/bookings-ui 0.16.0 → 0.17.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.
- package/README.md +11 -0
- package/dist/components/booking-activity-timeline.d.ts.map +1 -1
- package/dist/components/booking-activity-timeline.js +34 -14
- package/dist/components/booking-cancellation-dialog.d.ts.map +1 -1
- package/dist/components/booking-cancellation-dialog.js +15 -16
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +77 -13
- package/dist/components/booking-dialog.d.ts.map +1 -1
- package/dist/components/booking-dialog.js +27 -21
- package/dist/components/booking-document-dialog.d.ts.map +1 -1
- package/dist/components/booking-document-dialog.js +27 -13
- package/dist/components/booking-document-list.d.ts.map +1 -1
- package/dist/components/booking-document-list.js +9 -4
- package/dist/components/booking-group-link-dialog.d.ts.map +1 -1
- package/dist/components/booking-group-link-dialog.js +17 -6
- package/dist/components/booking-group-section.d.ts.map +1 -1
- package/dist/components/booking-group-section.js +8 -2
- package/dist/components/booking-guarantee-dialog.d.ts.map +1 -1
- package/dist/components/booking-guarantee-dialog.js +30 -14
- package/dist/components/booking-guarantee-list.d.ts.map +1 -1
- package/dist/components/booking-guarantee-list.js +11 -8
- package/dist/components/booking-item-dialog.d.ts.map +1 -1
- package/dist/components/booking-item-dialog.js +34 -20
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +10 -9
- package/dist/components/booking-item-travelers.d.ts.map +1 -1
- package/dist/components/booking-item-travelers.js +9 -4
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +17 -8
- package/dist/components/booking-notes.d.ts.map +1 -1
- package/dist/components/booking-notes.js +5 -2
- package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-dialog.js +31 -12
- package/dist/components/booking-payment-schedule-list.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-list.js +7 -6
- package/dist/components/booking-payments-summary.d.ts.map +1 -1
- package/dist/components/booking-payments-summary.js +7 -4
- package/dist/components/file-dropzone.d.ts.map +1 -1
- package/dist/components/file-dropzone.js +25 -15
- package/dist/components/passengers-section.d.ts.map +1 -1
- package/dist/components/passengers-section.js +3 -17
- package/dist/components/payment-schedule-section.d.ts.map +1 -1
- package/dist/components/payment-schedule-section.js +3 -14
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +3 -19
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +15 -18
- package/dist/components/product-picker-section.d.ts.map +1 -1
- package/dist/components/product-picker-section.js +3 -8
- package/dist/components/rooms-stepper-section.d.ts.map +1 -1
- package/dist/components/rooms-stepper-section.js +4 -9
- package/dist/components/shared-room-section.d.ts.map +1 -1
- package/dist/components/shared-room-section.js +3 -9
- package/dist/components/status-change-dialog.d.ts.map +1 -1
- package/dist/components/status-change-dialog.js +6 -1
- package/dist/components/supplier-status-dialog.d.ts.map +1 -1
- package/dist/components/supplier-status-dialog.js +31 -15
- package/dist/components/supplier-status-list.d.ts.map +1 -1
- package/dist/components/supplier-status-list.js +10 -2
- package/dist/components/traveler-dialog.d.ts.map +1 -1
- package/dist/components/traveler-dialog.js +17 -8
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +5 -3
- package/dist/components/voucher-picker-section.d.ts.map +1 -1
- package/dist/components/voucher-picker-section.js +11 -26
- package/dist/i18n/en.d.ts +797 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +796 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/messages.d.ts +684 -0
- package/dist/i18n/messages.d.ts.map +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/i18n/provider.d.ts +1617 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +45 -0
- package/dist/i18n/ro.d.ts +797 -0
- package/dist/i18n/ro.d.ts.map +1 -0
- package/dist/i18n/ro.js +796 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +32 -17
package/README.md
CHANGED
|
@@ -11,3 +11,14 @@ pnpm add @voyantjs/bookings-ui @voyantjs/bookings-react @voyantjs/ui @tanstack/r
|
|
|
11
11
|
`@voyantjs/ui` provides the design-system primitives. `@voyantjs/bookings-react` provides the data-layer hooks. Both are required peers.
|
|
12
12
|
|
|
13
13
|
All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
|
|
14
|
+
|
|
15
|
+
## I18n
|
|
16
|
+
|
|
17
|
+
Components render English by default. To localize them, wrap your UI in
|
|
18
|
+
`BookingsUiMessagesProvider` and import only the locales your app supports.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { BookingsUiMessagesProvider } from "@voyantjs/bookings-ui"
|
|
22
|
+
import { bookingsUiEn } from "@voyantjs/bookings-ui/i18n/en"
|
|
23
|
+
import { bookingsUiRo } from "@voyantjs/bookings-ui/i18n/ro"
|
|
24
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-activity-timeline.d.ts","sourceRoot":"","sources":["../../src/components/booking-activity-timeline.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-activity-timeline.d.ts","sourceRoot":"","sources":["../../src/components/booking-activity-timeline.tsx"],"names":[],"mappings":"AAyBA,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,MAAM,CAAA;CAClB;AAyCD,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE,4BAA4B,2CAgHlF"}
|
|
@@ -5,6 +5,7 @@ import { usePublicBookingPayments } from "@voyantjs/finance-react";
|
|
|
5
5
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
|
|
6
6
|
import { Activity, Clock, CreditCard, ExternalLink, FileText, Pencil, Plus, RefreshCw, UserPlus, } from "lucide-react";
|
|
7
7
|
import * as React from "react";
|
|
8
|
+
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider";
|
|
8
9
|
const activityIcons = {
|
|
9
10
|
booking_created: Plus,
|
|
10
11
|
booking_reserved: Plus,
|
|
@@ -22,11 +23,6 @@ const activityIcons = {
|
|
|
22
23
|
passenger_update: UserPlus,
|
|
23
24
|
note_added: Pencil,
|
|
24
25
|
};
|
|
25
|
-
const sourceLabel = {
|
|
26
|
-
activity: "Activity",
|
|
27
|
-
document: "Document",
|
|
28
|
-
payment: "Payment",
|
|
29
|
-
};
|
|
30
26
|
const sourceVariant = {
|
|
31
27
|
activity: "outline",
|
|
32
28
|
document: "secondary",
|
|
@@ -37,13 +33,21 @@ export function BookingActivityTimeline({ bookingId }) {
|
|
|
37
33
|
const { data: activityData } = useBookingActivity(bookingId);
|
|
38
34
|
const { data: documentsData } = useBookingTravelerDocuments(bookingId);
|
|
39
35
|
const { data: paymentsData } = usePublicBookingPayments(bookingId);
|
|
36
|
+
const { formatNumber } = useBookingsUiI18nOrDefault();
|
|
37
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
38
|
+
const sourceLabel = {
|
|
39
|
+
activity: messages.bookingActivityTimeline.sourceLabels.activity,
|
|
40
|
+
document: messages.bookingActivityTimeline.sourceLabels.document,
|
|
41
|
+
payment: messages.bookingActivityTimeline.sourceLabels.payment,
|
|
42
|
+
};
|
|
40
43
|
const events = React.useMemo(() => {
|
|
41
44
|
const merged = [];
|
|
42
45
|
for (const entry of activityData?.data ?? []) {
|
|
43
46
|
merged.push({
|
|
44
47
|
id: `activity:${entry.id}`,
|
|
45
48
|
source: "activity",
|
|
46
|
-
title: entry.description,
|
|
49
|
+
title: messages.bookingActivityTimeline.activityTitles[entry.activityType] ?? entry.description,
|
|
50
|
+
description: entry.description,
|
|
47
51
|
actorId: entry.actorId,
|
|
48
52
|
timestamp: entry.createdAt,
|
|
49
53
|
icon: activityIcons[entry.activityType] ?? Activity,
|
|
@@ -53,31 +57,47 @@ export function BookingActivityTimeline({ bookingId }) {
|
|
|
53
57
|
merged.push({
|
|
54
58
|
id: `document:${doc.id}`,
|
|
55
59
|
source: "document",
|
|
56
|
-
title: `${doc.type.replace(/_/g, " ")}
|
|
60
|
+
title: `${doc.type.replace(/_/g, " ")} ${messages.bookingActivityTimeline.documentUploadedSuffix}`,
|
|
57
61
|
description: doc.fileName,
|
|
58
62
|
timestamp: doc.createdAt,
|
|
59
63
|
icon: FileText,
|
|
60
|
-
link: { href: doc.fileUrl, label:
|
|
64
|
+
link: { href: doc.fileUrl, label: messages.bookingActivityTimeline.viewFile },
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
for (const payment of paymentsData?.data?.payments ?? []) {
|
|
68
|
+
const status = messages.bookingPaymentsSummary.paymentStatusLabels[payment.status] ?? payment.status;
|
|
69
|
+
const amount = `${formatNumber(payment.amountCents / 100, {
|
|
70
|
+
minimumFractionDigits: 2,
|
|
71
|
+
maximumFractionDigits: 2,
|
|
72
|
+
})} ${payment.currency}`;
|
|
73
|
+
const method = messages.bookingPaymentsSummary.paymentMethodLabels[payment.paymentMethod] ?? payment.paymentMethod;
|
|
64
74
|
merged.push({
|
|
65
75
|
id: `payment:${payment.id}`,
|
|
66
76
|
source: "payment",
|
|
67
|
-
title:
|
|
68
|
-
description:
|
|
77
|
+
title: formatMessage(messages.bookingActivityTimeline.paymentTitle, { status, amount }),
|
|
78
|
+
description: formatMessage(messages.bookingActivityTimeline.paymentDescription, {
|
|
79
|
+
invoice: payment.invoiceNumber,
|
|
80
|
+
method,
|
|
81
|
+
}),
|
|
69
82
|
timestamp: payment.paymentDate,
|
|
70
83
|
icon: CreditCard,
|
|
71
84
|
});
|
|
72
85
|
}
|
|
73
86
|
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
74
87
|
return merged;
|
|
75
|
-
}, [activityData, documentsData, paymentsData]);
|
|
88
|
+
}, [activityData, documentsData, formatNumber, messages, paymentsData]);
|
|
76
89
|
const visible = filter === "all" ? events : events.filter((e) => e.source === filter);
|
|
77
90
|
const filterChips = ["all", "activity", "document", "payment"];
|
|
78
|
-
return (_jsxs(Card, { "data-slot": "booking-activity-timeline", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Activity, { className: "h-4 w-4" }),
|
|
91
|
+
return (_jsxs(Card, { "data-slot": "booking-activity-timeline", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Activity, { className: "h-4 w-4" }), messages.bookingActivityTimeline.title] }), _jsx("div", { className: "flex items-center gap-1", children: filterChips.map((chip) => (_jsx(Button, { variant: filter === chip ? "default" : "ghost", size: "sm", className: "h-7 capitalize", onClick: () => setFilter(chip), children: chip === "all" ? messages.bookingActivityTimeline.filters.all : sourceLabel[chip] }, chip))) })] }), _jsx(CardContent, { children: visible.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.bookingActivityTimeline.empty })) : (_jsx("div", { className: "flex flex-col gap-3", children: visible.map((event) => (_jsx(TimelineEventItem, { event: event, sourceLabel: sourceLabel }, event.id))) })) })] }));
|
|
79
92
|
}
|
|
80
|
-
function TimelineEventItem({ event }) {
|
|
93
|
+
function TimelineEventItem({ event, sourceLabel, }) {
|
|
81
94
|
const Icon = event.icon;
|
|
82
|
-
|
|
95
|
+
const { formatDateTime } = useBookingsUiI18nOrDefault();
|
|
96
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
97
|
+
return (_jsxs("div", { className: "flex items-start gap-3 rounded-md border p-3", children: [_jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("p", { className: "text-sm font-medium capitalize", children: event.title }), _jsx(Badge, { variant: sourceVariant[event.source], className: "text-xs", children: sourceLabel[event.source] })] }), event.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: event.description })), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: event.actorId && event.actorId !== "system"
|
|
98
|
+
? formatMessage(messages.bookingActivityTimeline.byActor, {
|
|
99
|
+
actor: event.actorId,
|
|
100
|
+
timestamp: formatDateTime(event.timestamp),
|
|
101
|
+
})
|
|
102
|
+
: formatDateTime(event.timestamp) }), event.link && (_jsxs("a", { href: event.link.href, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground hover:underline", children: [event.link.label, _jsx(ExternalLink, { className: "h-3 w-3" })] }))] })] }));
|
|
83
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-cancellation-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-cancellation-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-cancellation-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-cancellation-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AAkCjC,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,EAAE,aAAa,CAAA;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,yBAAyB,CAAC,EACxC,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,SAAS,EACT,SAAS,GACV,EAAE,8BAA8B,2CAiPhC"}
|
|
@@ -5,22 +5,11 @@ import { useEvaluateCancellation, useResolvePolicy } from "@voyantjs/legal-react
|
|
|
5
5
|
import { Badge, Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Textarea, } from "@voyantjs/ui/components";
|
|
6
6
|
import { AlertTriangle, Loader2 } from "lucide-react";
|
|
7
7
|
import * as React from "react";
|
|
8
|
-
|
|
9
|
-
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
10
|
-
}
|
|
11
|
-
function formatPercent(basisPoints) {
|
|
12
|
-
return `${(basisPoints / 100).toFixed(0)}%`;
|
|
13
|
-
}
|
|
8
|
+
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider";
|
|
14
9
|
function daysBetween(from, to) {
|
|
15
10
|
const diffMs = to.getTime() - from.getTime();
|
|
16
11
|
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
|
|
17
12
|
}
|
|
18
|
-
const refundTypeLabel = {
|
|
19
|
-
cash: "Cash refund",
|
|
20
|
-
credit: "Credit",
|
|
21
|
-
cash_or_credit: "Cash or credit",
|
|
22
|
-
none: "No refund",
|
|
23
|
-
};
|
|
24
13
|
const refundTypeVariant = {
|
|
25
14
|
cash: "default",
|
|
26
15
|
credit: "secondary",
|
|
@@ -29,6 +18,8 @@ const refundTypeVariant = {
|
|
|
29
18
|
};
|
|
30
19
|
export function BookingCancellationDialog({ open, onOpenChange, booking, productId, onSuccess, }) {
|
|
31
20
|
const [reason, setReason] = React.useState("");
|
|
21
|
+
const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
|
|
22
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
32
23
|
const daysBeforeDeparture = React.useMemo(() => {
|
|
33
24
|
if (!booking.startDate)
|
|
34
25
|
return 0;
|
|
@@ -71,10 +62,18 @@ export function BookingCancellationDialog({ open, onOpenChange, booking, product
|
|
|
71
62
|
const total = booking.sellAmountCents;
|
|
72
63
|
const refund = evaluation?.refundCents ?? 0;
|
|
73
64
|
const penalty = total != null ? Math.max(0, total - refund) : 0;
|
|
74
|
-
|
|
65
|
+
const formatAmount = React.useCallback((cents, currency) => formatCurrency(cents / 100, currency), [formatCurrency]);
|
|
66
|
+
const formatPercent = React.useCallback((basisPoints) => `${formatNumber(basisPoints / 100, {
|
|
67
|
+
maximumFractionDigits: 0,
|
|
68
|
+
})}%`, [formatNumber]);
|
|
69
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-destructive" }), messages.bookingCancellationDialog.title] }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-md border bg-muted/30 p-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.summary.booking }), _jsx("div", { className: "font-mono text-xs", children: booking.bookingNumber })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.summary.startDate }), _jsx("div", { children: booking.startDate ?? messages.bookingCancellationDialog.values.startDateTbd })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.summary.total }), _jsx("div", { className: "font-mono", children: total != null
|
|
70
|
+
? formatAmount(total, booking.sellCurrency)
|
|
71
|
+
: messages.bookingCancellationDialog.values.amountUnavailable })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.summary.daysBeforeDeparture }), _jsx("div", { children: daysBeforeDeparture })] })] }), resolveLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), messages.bookingCancellationDialog.policy.resolving] })) : !policy ? (_jsxs("div", { className: "rounded-md border border-dashed p-3 text-sm text-muted-foreground", children: [messages.bookingCancellationDialog.policy.missing, " ", messages.bookingCancellationDialog.policy.missingHint] })) : (_jsxs("div", { className: "space-y-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.policy.applicablePolicy }), _jsx("div", { className: "text-sm font-medium", children: policy.policy.name })] }), evaluation && (_jsx(Badge, { variant: refundTypeVariant[evaluation.refundType] ?? "secondary", children: messages.bookingCancellationDialog.refundTypeLabels[evaluation.refundType] ?? evaluation.refundType }))] }), evaluationLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), messages.bookingCancellationDialog.policy.calculating] })) : evaluation && total != null ? (_jsxs("div", { className: "grid grid-cols-3 gap-3 border-t pt-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.policy.refund }), _jsx("div", { className: "font-mono font-medium", children: formatAmount(evaluation.refundCents, booking.sellCurrency) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["(", formatPercent(evaluation.refundPercent), ")"] })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.policy.penalty }), _jsx("div", { className: "font-mono font-medium text-destructive", children: formatAmount(penalty, booking.sellCurrency) })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: messages.bookingCancellationDialog.policy.rule }), _jsx("div", { className: "text-xs", children: evaluation.appliedRule?.label ??
|
|
75
72
|
(evaluation.appliedRule?.daysBeforeDeparture != null
|
|
76
|
-
?
|
|
77
|
-
|
|
73
|
+
? formatMessage(messages.bookingCancellationDialog.values.ruleDaysBeforeDeparture, {
|
|
74
|
+
days: evaluation.appliedRule.daysBeforeDeparture,
|
|
75
|
+
})
|
|
76
|
+
: messages.bookingCancellationDialog.values.ruleFallback) })] })] })) : total == null ? (_jsx("p", { className: "border-t pt-3 text-sm text-muted-foreground", children: messages.bookingCancellationDialog.policy.noTotalAmount })) : null] })), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [messages.bookingCancellationDialog.fields.reason, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: messages.bookingCancellationDialog.placeholders.reason, required: true })] }), cancelMutation.error && (_jsx("p", { className: "text-xs text-destructive", children: cancelMutation.error instanceof Error
|
|
78
77
|
? cancelMutation.error.message
|
|
79
|
-
:
|
|
78
|
+
: messages.bookingCancellationDialog.validation.cancellationFailed }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), disabled: cancelMutation.isPending, children: messages.bookingCancellationDialog.actions.close }), _jsxs(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleConfirm, disabled: !reason.trim() || cancelMutation.isPending, children: [cancelMutation.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingCancellationDialog.actions.confirm] })] })] }) }));
|
|
80
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,0BAA0B,CAAA;AA2JjC,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,GACjB,EAAE,wBAAwB,2CA6b1B"}
|
|
@@ -6,6 +6,7 @@ import { usePersonMutation } from "@voyantjs/crm-react";
|
|
|
6
6
|
import { Button, Checkbox, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
|
|
7
7
|
import { Loader2 } from "lucide-react";
|
|
8
8
|
import * as React from "react";
|
|
9
|
+
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider";
|
|
9
10
|
import { emptyPassengerListValue, PassengersSection, } from "./passengers-section";
|
|
10
11
|
import { emptyPaymentScheduleValue, PaymentScheduleSection, } from "./payment-schedule-section";
|
|
11
12
|
import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-section";
|
|
@@ -132,6 +133,8 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
132
133
|
*/
|
|
133
134
|
const [confirmAfterCreate, setConfirmAfterCreate] = React.useState(false);
|
|
134
135
|
const [error, setError] = React.useState(null);
|
|
136
|
+
const { formatDate } = useBookingsUiI18nOrDefault();
|
|
137
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
135
138
|
React.useEffect(() => {
|
|
136
139
|
if (!open) {
|
|
137
140
|
setProduct({ productId: defaultProductId ?? "", optionId: null });
|
|
@@ -175,14 +178,16 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
175
178
|
.sort((a, b) => a.startsAt.localeCompare(b.startsAt));
|
|
176
179
|
}, [slotsData, product.optionId]);
|
|
177
180
|
const formatSlotLabel = React.useCallback((slot) => {
|
|
178
|
-
const date =
|
|
181
|
+
const date = formatDate(slot.startsAt, {
|
|
179
182
|
year: "numeric",
|
|
180
183
|
month: "short",
|
|
181
184
|
day: "numeric",
|
|
182
185
|
});
|
|
183
|
-
const remaining = !slot.unlimited && typeof slot.remainingPax === "number"
|
|
186
|
+
const remaining = !slot.unlimited && typeof slot.remainingPax === "number"
|
|
187
|
+
? ` · ${slot.remainingPax} ${messages.bookingCreateDialog.labels.remainingCapacity}`
|
|
188
|
+
: "";
|
|
184
189
|
return `${date}${remaining}`;
|
|
185
|
-
}, []);
|
|
190
|
+
}, [formatDate, messages]);
|
|
186
191
|
const slotUnitAvailability = useSlotUnitAvailability({
|
|
187
192
|
slotId: slotId ?? undefined,
|
|
188
193
|
enabled: open && Boolean(slotId),
|
|
@@ -208,28 +213,28 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
208
213
|
// Currency placeholder — used for voucher + payment schedule display.
|
|
209
214
|
// Consumers hooking in real product data should override this by wrapping
|
|
210
215
|
// the component or swapping in their own currency-aware hook.
|
|
211
|
-
const currency =
|
|
216
|
+
const currency = messages.bookingCreateDialog.labels.currency;
|
|
212
217
|
const { create: createPerson } = usePersonMutation();
|
|
213
218
|
const quickCreateMutation = useBookingQuickCreateMutation();
|
|
214
219
|
const statusMutation = useBookingStatusByIdMutation();
|
|
215
220
|
const handleSubmit = async () => {
|
|
216
221
|
setError(null);
|
|
217
222
|
if (!product.productId) {
|
|
218
|
-
setError(
|
|
223
|
+
setError(messages.bookingCreateDialog.validation.selectProduct);
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
221
226
|
let resolvedPersonId = null;
|
|
222
227
|
try {
|
|
223
228
|
if (person.mode === "existing") {
|
|
224
229
|
if (!person.personId) {
|
|
225
|
-
setError(
|
|
230
|
+
setError(messages.bookingCreateDialog.validation.selectPerson);
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
228
233
|
resolvedPersonId = person.personId;
|
|
229
234
|
}
|
|
230
235
|
else {
|
|
231
236
|
if (!person.newPerson.firstName.trim() || !person.newPerson.lastName.trim()) {
|
|
232
|
-
setError(
|
|
237
|
+
setError(messages.bookingCreateDialog.validation.firstAndLastNameRequired);
|
|
233
238
|
return;
|
|
234
239
|
}
|
|
235
240
|
const created = await createPerson.mutateAsync({
|
|
@@ -241,7 +246,7 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
241
246
|
resolvedPersonId = created.id;
|
|
242
247
|
}
|
|
243
248
|
if (sharedRoom.enabled && sharedRoom.mode === "join" && !sharedRoom.groupId) {
|
|
244
|
-
setError(
|
|
249
|
+
setError(messages.bookingCreateDialog.validation.selectSharedRoomGroup);
|
|
245
250
|
return;
|
|
246
251
|
}
|
|
247
252
|
const bookingNumber = generateBookingNumber();
|
|
@@ -258,7 +263,7 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
258
263
|
? {
|
|
259
264
|
action: "create",
|
|
260
265
|
kind: "shared_room",
|
|
261
|
-
label:
|
|
266
|
+
label: `${messages.bookingCreateDialog.labels.sharedRoomGeneratedLabelPrefix} - ${bookingNumber}`,
|
|
262
267
|
optionUnitId: product.optionId,
|
|
263
268
|
makeBookingPrimary: true,
|
|
264
269
|
}
|
|
@@ -295,8 +300,10 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
295
300
|
}
|
|
296
301
|
catch (statusErr) {
|
|
297
302
|
setError(statusErr instanceof Error
|
|
298
|
-
?
|
|
299
|
-
|
|
303
|
+
? formatMessage(messages.bookingCreateDialog.validation.confirmFailedPrefix, {
|
|
304
|
+
message: statusErr.message,
|
|
305
|
+
})
|
|
306
|
+
: messages.bookingCreateDialog.validation.confirmFailed);
|
|
300
307
|
onCreated?.(booking);
|
|
301
308
|
return;
|
|
302
309
|
}
|
|
@@ -305,9 +312,66 @@ export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProd
|
|
|
305
312
|
onCreated?.(finalBooking);
|
|
306
313
|
}
|
|
307
314
|
catch (err) {
|
|
308
|
-
setError(err instanceof Error ? err.message :
|
|
315
|
+
setError(err instanceof Error ? err.message : messages.bookingCreateDialog.validation.createFailed);
|
|
309
316
|
}
|
|
310
317
|
};
|
|
311
318
|
const isSubmitting = quickCreateMutation.isPending || createPerson.isPending || statusMutation.isPending;
|
|
312
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children:
|
|
319
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.bookingCreateDialog.title }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductPickerSection, { value: product, onChange: setProduct, enabled: open, lockProduct: Boolean(defaultProductId), labels: {
|
|
320
|
+
optionNone: messages.bookingCreateDialog.labels.noSpecificOption,
|
|
321
|
+
} }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.departure }), _jsxs(Select, { value: slotId ?? "__none__", onValueChange: (v) => setSlotId(v === "__none__" ? null : (v ?? null)), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: messages.bookingCreateDialog.placeholders.departure }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none__", children: messages.bookingCreateDialog.placeholders.departureNone }), slots.length === 0 ? (_jsx(SelectItem, { value: "__empty__", disabled: true, children: messages.bookingCreateDialog.placeholders.departureEmpty })) : (slots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLabel(slot) }, slot.id))))] })] })] })) : null, slotId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, slotId: slotId, enabled: open, labels: {
|
|
322
|
+
heading: messages.bookingCreateDialog.labels.roomsHeading,
|
|
323
|
+
noSlot: messages.bookingCreateDialog.labels.roomsNoSlot,
|
|
324
|
+
noUnits: messages.bookingCreateDialog.labels.roomsNoUnits,
|
|
325
|
+
remaining: messages.bookingCreateDialog.labels.roomsRemaining,
|
|
326
|
+
unlimited: messages.bookingCreateDialog.labels.roomsUnlimited,
|
|
327
|
+
} })) : null, _jsx(PersonPickerSection, { value: person, onChange: setPerson, enabled: open, labels: {
|
|
328
|
+
createNewPerson: messages.bookingCreateDialog.labels.createNewPerson,
|
|
329
|
+
selectExistingPerson: messages.bookingCreateDialog.labels.selectExistingPerson,
|
|
330
|
+
organizationNone: messages.bookingCreateDialog.labels.organizationNone,
|
|
331
|
+
} }), _jsx(SharedRoomSection, { value: sharedRoom, onChange: setSharedRoom, productId: product.productId || undefined, enabled: open, labels: {
|
|
332
|
+
toggle: messages.bookingCreateDialog.labels.sharedRoomToggle,
|
|
333
|
+
createMode: messages.bookingCreateDialog.labels.sharedRoomCreateMode,
|
|
334
|
+
joinMode: messages.bookingCreateDialog.labels.sharedRoomJoinMode,
|
|
335
|
+
selectPlaceholder: messages.bookingCreateDialog.labels.sharedRoomSelectPlaceholder,
|
|
336
|
+
noGroups: messages.bookingCreateDialog.labels.sharedRoomNoGroups,
|
|
337
|
+
createHint: messages.bookingCreateDialog.labels.sharedRoomCreateHint,
|
|
338
|
+
} }), product.productId ? (_jsx(PassengersSection, { value: passengers, onChange: setPassengers, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined, labels: {
|
|
339
|
+
heading: messages.bookingCreateDialog.labels.passengerHeading,
|
|
340
|
+
addPassenger: messages.bookingCreateDialog.labels.addPassenger,
|
|
341
|
+
role: messages.bookingCreateDialog.labels.passengerRole,
|
|
342
|
+
roleLead: messages.bookingCreateDialog.labels.passengerLead,
|
|
343
|
+
roleAdult: messages.bookingCreateDialog.labels.passengerAdult,
|
|
344
|
+
roleChild: messages.bookingCreateDialog.labels.passengerChild,
|
|
345
|
+
roleInfant: messages.bookingCreateDialog.labels.passengerInfant,
|
|
346
|
+
room: messages.bookingCreateDialog.labels.passengerRoom,
|
|
347
|
+
noRoom: messages.bookingCreateDialog.labels.passengerNoRoom,
|
|
348
|
+
remove: messages.bookingCreateDialog.labels.passengerRemove,
|
|
349
|
+
empty: messages.bookingCreateDialog.labels.passengerEmpty,
|
|
350
|
+
} })) : null, product.productId ? (_jsx(PriceBreakdownSection, { productId: product.productId, optionId: product.optionId, unitQuantities: rooms.quantities, labels: {
|
|
351
|
+
heading: messages.bookingCreateDialog.labels.breakdownHeading,
|
|
352
|
+
total: messages.bookingCreateDialog.labels.breakdownTotal,
|
|
353
|
+
onRequest: messages.bookingCreateDialog.labels.breakdownOnRequest,
|
|
354
|
+
groupRate: messages.bookingCreateDialog.labels.breakdownGroupRate,
|
|
355
|
+
empty: messages.bookingCreateDialog.labels.breakdownEmpty,
|
|
356
|
+
noPricing: messages.bookingCreateDialog.labels.breakdownNoPricing,
|
|
357
|
+
} })) : null, _jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency, labels: {
|
|
358
|
+
heading: messages.bookingCreateDialog.labels.voucherHeading,
|
|
359
|
+
codePlaceholder: messages.bookingCreateDialog.labels.voucherCodePlaceholder,
|
|
360
|
+
apply: messages.bookingCreateDialog.labels.voucherApply,
|
|
361
|
+
clear: messages.bookingCreateDialog.labels.voucherClear,
|
|
362
|
+
remainingLabel: messages.bookingCreateDialog.labels.voucherRemainingLabel,
|
|
363
|
+
invalidLabel: messages.bookingCreateDialog.labels.voucherInvalidLabel,
|
|
364
|
+
} }), _jsx(PaymentScheduleSection, { value: paymentSchedule, onChange: setPaymentSchedule, currency: currency, labels: {
|
|
365
|
+
heading: messages.bookingCreateDialog.labels.paymentHeading,
|
|
366
|
+
modeUnpaid: messages.bookingCreateDialog.labels.paymentModeUnpaid,
|
|
367
|
+
modeFull: messages.bookingCreateDialog.labels.paymentModeFull,
|
|
368
|
+
modeAdvance: messages.bookingCreateDialog.labels.paymentModeAdvance,
|
|
369
|
+
modeSplit: messages.bookingCreateDialog.labels.paymentModeSplit,
|
|
370
|
+
dueDate: messages.bookingCreateDialog.labels.paymentDueDate,
|
|
371
|
+
amount: messages.bookingCreateDialog.labels.paymentAmount,
|
|
372
|
+
firstInstallment: messages.bookingCreateDialog.labels.paymentFirstInstallment,
|
|
373
|
+
secondInstallment: messages.bookingCreateDialog.labels.paymentSecondInstallment,
|
|
374
|
+
preset5050: messages.bookingCreateDialog.labels.paymentPreset5050,
|
|
375
|
+
unpaidHint: messages.bookingCreateDialog.labels.paymentUnpaidHint,
|
|
376
|
+
} }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingCreateDialog.fields.internalNotes }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: messages.bookingCreateDialog.placeholders.internalNotes })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-md border p-3", children: [_jsx(Checkbox, { id: "quickbook-confirm-after-create", checked: confirmAfterCreate, onCheckedChange: (v) => setConfirmAfterCreate(v === true), className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { htmlFor: "quickbook-confirm-after-create", className: "cursor-pointer text-sm", children: messages.bookingCreateDialog.fields.confirmAfterCreate }), _jsx("p", { className: "text-xs text-muted-foreground", children: messages.bookingCreateDialog.fields.confirmAfterCreateHint })] })] }), error && _jsx("p", { className: "text-xs text-destructive", children: error })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), disabled: isSubmitting, children: messages.common.cancel }), _jsxs(Button, { type: "button", size: "sm", onClick: handleSubmit, disabled: isSubmitting || !product.productId, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingCreateDialog.actions.createDraftBooking] })] })] }) }));
|
|
313
377
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"booking-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,0BAA0B,CAAA;AA8CjF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAWD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,SAAS,EACT,gBAAgB,GACjB,EAAE,kBAAkB,2CAoBpB"}
|
|
@@ -9,25 +9,29 @@ import { Loader2 } from "lucide-react";
|
|
|
9
9
|
import { useEffect } from "react";
|
|
10
10
|
import { useForm } from "react-hook-form";
|
|
11
11
|
import { z } from "zod/v4";
|
|
12
|
+
import { useBookingsUiMessagesOrDefault } from "../i18n/provider";
|
|
12
13
|
import { BookingCreateDialog } from "./booking-create-dialog";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
function createBookingFormSchema(messages) {
|
|
15
|
+
return z.object({
|
|
16
|
+
bookingNumber: z.string().min(1, messages.bookingDialog.validation.bookingNumberRequired),
|
|
17
|
+
status: z.enum(["draft", "confirmed", "in_progress", "completed", "cancelled"]),
|
|
18
|
+
sellCurrency: z.string().min(3).max(3, messages.bookingDialog.validation.sellCurrencyInvalid),
|
|
19
|
+
sellAmountCents: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
|
|
20
|
+
costAmountCents: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
|
|
21
|
+
startDate: z.string().optional().nullable(),
|
|
22
|
+
endDate: z.string().optional().nullable(),
|
|
23
|
+
pax: z.coerce.number().int().positive().optional().or(z.literal("")).nullable(),
|
|
24
|
+
internalNotes: z.string().optional().nullable(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const BOOKING_STATUS_VALUES = [
|
|
28
|
+
"draft",
|
|
29
|
+
"confirmed",
|
|
30
|
+
"in_progress",
|
|
31
|
+
"completed",
|
|
32
|
+
"cancelled",
|
|
30
33
|
];
|
|
34
|
+
const DEFAULT_CURRENCY = "EUR"; // i18n-literal-ok ISO default currency
|
|
31
35
|
/**
|
|
32
36
|
* Single booking dialog that handles both create and edit:
|
|
33
37
|
* - Create (no `booking` prop): renders the rich product → option → person
|
|
@@ -45,12 +49,14 @@ export function BookingDialog({ open, onOpenChange, booking, onSuccess, defaultP
|
|
|
45
49
|
}
|
|
46
50
|
function BookingEditDialog({ open, onOpenChange, booking, onSuccess }) {
|
|
47
51
|
const { update } = useBookingMutation();
|
|
52
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
53
|
+
const bookingFormSchema = createBookingFormSchema(messages);
|
|
48
54
|
const form = useForm({
|
|
49
55
|
resolver: zodResolver(bookingFormSchema),
|
|
50
56
|
defaultValues: {
|
|
51
57
|
bookingNumber: "",
|
|
52
58
|
status: "draft",
|
|
53
|
-
sellCurrency:
|
|
59
|
+
sellCurrency: DEFAULT_CURRENCY,
|
|
54
60
|
sellAmountCents: "",
|
|
55
61
|
costAmountCents: "",
|
|
56
62
|
startDate: "",
|
|
@@ -95,14 +101,14 @@ function BookingEditDialog({ open, onOpenChange, booking, onSuccess }) {
|
|
|
95
101
|
onSuccess?.(saved);
|
|
96
102
|
};
|
|
97
103
|
const isSubmitting = update.isPending;
|
|
98
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children:
|
|
104
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.bookingDialog.editTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.bookingNumber }), _jsx(Input, { ...form.register("bookingNumber"), placeholder: messages.bookingDialog.placeholders.bookingNumber }), form.formState.errors.bookingNumber && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.bookingNumber.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.status }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: BOOKING_STATUS_VALUES.map((status) => (_jsx(SelectItem, { value: status, children: messages.common.bookingStatusLabels[status] }, status))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.sellCurrency }), _jsx(CurrencyCombobox, { value: form.watch("sellCurrency") || null, onChange: (next) => form.setValue("sellCurrency", next ?? DEFAULT_CURRENCY, {
|
|
99
105
|
shouldValidate: true,
|
|
100
106
|
shouldDirty: true,
|
|
101
|
-
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children:
|
|
107
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.travelDates }), _jsx(DateRangePicker, { value: {
|
|
102
108
|
from: form.watch("startDate") || null,
|
|
103
109
|
to: form.watch("endDate") || null,
|
|
104
110
|
}, onChange: (nextValue) => {
|
|
105
111
|
form.setValue("startDate", nextValue?.from ?? "", { shouldDirty: true });
|
|
106
112
|
form.setValue("endDate", nextValue?.to ?? "", { shouldDirty: true });
|
|
107
|
-
}, placeholder:
|
|
113
|
+
}, placeholder: messages.bookingDialog.placeholders.travelDates, className: "w-full" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.sellAmountCents }), _jsx(Input, { ...form.register("sellAmountCents"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.costAmountCents }), _jsx(Input, { ...form.register("costAmountCents"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.pax }), _jsx(Input, { ...form.register("pax"), type: "number", min: "1", placeholder: messages.bookingDialog.placeholders.pax })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDialog.fields.internalNotes }), _jsx(Textarea, { ...form.register("internalNotes"), placeholder: messages.bookingDialog.placeholders.internalNotes })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.common.saveChanges] })] })] })] }) }));
|
|
108
114
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-document-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-dialog.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-document-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-dialog.tsx"],"names":[],"mappings":"AAkDA,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,GACV,EAAE,0BAA0B,2CAgK5B"}
|
|
@@ -8,21 +8,29 @@ import { Loader2 } from "lucide-react";
|
|
|
8
8
|
import { useEffect } from "react";
|
|
9
9
|
import { useForm } from "react-hook-form";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
|
+
import { useBookingsUiMessagesOrDefault } from "../i18n/provider";
|
|
11
12
|
import { FileDropzone } from "./file-dropzone";
|
|
12
13
|
const documentTypes = ["visa", "insurance", "health", "passport_copy", "other"];
|
|
13
14
|
const UNASSIGNED = "__unassigned__";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
function createDocumentFormSchema(messages) {
|
|
16
|
+
return z.object({
|
|
17
|
+
type: z.enum(documentTypes).default("other"),
|
|
18
|
+
fileName: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1, messages.bookingDocumentDialog.validation.fileNameRequired)
|
|
21
|
+
.max(500),
|
|
22
|
+
fileUrl: z.string().url(messages.bookingDocumentDialog.validation.fileUrlInvalid),
|
|
23
|
+
travelerId: z.string().optional().nullable(),
|
|
24
|
+
expiresAt: z.string().optional().nullable(),
|
|
25
|
+
notes: z.string().optional().nullable(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
22
28
|
export function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess, }) {
|
|
23
29
|
const { create } = useBookingTravelerDocumentMutation(bookingId);
|
|
24
30
|
const { data: travelersData } = useTravelers(bookingId);
|
|
25
31
|
const travelers = travelersData?.data ?? [];
|
|
32
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
33
|
+
const documentFormSchema = createDocumentFormSchema(messages);
|
|
26
34
|
const form = useForm({
|
|
27
35
|
resolver: zodResolver(documentFormSchema),
|
|
28
36
|
defaultValues: {
|
|
@@ -51,17 +59,23 @@ export function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess
|
|
|
51
59
|
onOpenChange(false);
|
|
52
60
|
onSuccess?.();
|
|
53
61
|
};
|
|
54
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children:
|
|
55
|
-
|
|
62
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.bookingDocumentDialog.title }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.type }), _jsxs(Select, { items: documentTypes.map((t) => ({
|
|
63
|
+
label: messages.bookingDocumentDialog.documentTypeLabels[t],
|
|
64
|
+
value: t,
|
|
65
|
+
})), value: form.watch("type"), onValueChange: (v) => form.setValue("type", (v ?? "other")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: documentTypes.map((t) => (_jsx(SelectItem, { value: t, children: messages.bookingDocumentDialog.documentTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.traveler }), _jsxs(Select, { items: [
|
|
66
|
+
{
|
|
67
|
+
label: messages.bookingDocumentDialog.placeholders.travelerUnassigned,
|
|
68
|
+
value: UNASSIGNED,
|
|
69
|
+
},
|
|
56
70
|
...travelers.map((traveler) => ({
|
|
57
71
|
label: `${traveler.firstName} ${traveler.lastName}`,
|
|
58
72
|
value: traveler.id,
|
|
59
73
|
})),
|
|
60
|
-
], value: form.watch("travelerId") ?? UNASSIGNED, onValueChange: (v) => form.setValue("travelerId", v ?? UNASSIGNED), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: UNASSIGNED, children:
|
|
74
|
+
], value: form.watch("travelerId") ?? UNASSIGNED, onValueChange: (v) => form.setValue("travelerId", v ?? UNASSIGNED), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: UNASSIGNED, children: messages.bookingDocumentDialog.placeholders.travelerUnassigned }), travelers.map((traveler) => (_jsxs(SelectItem, { value: traveler.id, children: [traveler.firstName, " ", traveler.lastName] }, traveler.id)))] })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.file }), _jsx(FileDropzone, { accept: "application/pdf,image/*", maxSize: 10 * 1024 * 1024, onUploaded: (upload) => {
|
|
61
75
|
form.setValue("fileUrl", upload.url, { shouldValidate: true });
|
|
62
76
|
form.setValue("fileName", upload.name, { shouldValidate: true });
|
|
63
|
-
}, helperText:
|
|
77
|
+
}, helperText: messages.bookingDocumentDialog.placeholders.helperText }), form.formState.errors.fileUrl && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.fileUrl.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.expiresAt }), _jsx(DatePicker, { value: form.watch("expiresAt") || null, onChange: (next) => form.setValue("expiresAt", next ?? "", {
|
|
64
78
|
shouldValidate: true,
|
|
65
79
|
shouldDirty: true,
|
|
66
|
-
}), placeholder:
|
|
80
|
+
}), placeholder: messages.bookingDocumentDialog.placeholders.expiresAt, className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.notes }), _jsx(Textarea, { ...form.register("notes"), placeholder: messages.bookingDocumentDialog.placeholders.notes })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: create.isPending, children: [create.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.bookingDocumentDialog.actions.addDocument] })] })] })] }) }));
|
|
67
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-document-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-list.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-document-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-list.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,EAAE,wBAAwB,2CAqH1E"}
|