@voyantjs/bookings-ui 0.81.17 → 0.81.19
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-activity-timeline.d.ts +28 -1
- package/dist/components/booking-activity-timeline.d.ts.map +1 -1
- package/dist/components/booking-activity-timeline.js +50 -6
- package/dist/components/booking-detail-page.d.ts +22 -3
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +33 -6
- package/dist/components/booking-list.d.ts +37 -2
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +214 -53
- package/dist/components/booking-note-dialog.d.ts +16 -0
- package/dist/components/booking-note-dialog.d.ts.map +1 -0
- package/dist/components/booking-note-dialog.js +41 -0
- package/dist/components/booking-notes.d.ts.map +1 -1
- package/dist/components/booking-notes.js +36 -10
- package/dist/components/bookings-page.d.ts +5 -1
- package/dist/components/bookings-page.d.ts.map +1 -1
- package/dist/components/bookings-page.js +2 -2
- package/dist/components/status-change-dialog.d.ts.map +1 -1
- package/dist/components/status-change-dialog.js +12 -2
- package/dist/i18n/en.d.ts +38 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +42 -9
- package/dist/i18n/messages.d.ts +39 -5
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +76 -10
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +38 -5
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +41 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +32 -32
|
@@ -4,6 +4,7 @@ import { useBookings, } from "@voyantjs/bookings-react";
|
|
|
4
4
|
import { Button } from "@voyantjs/ui/components/button";
|
|
5
5
|
import { Input } from "@voyantjs/ui/components/input";
|
|
6
6
|
import { Label } from "@voyantjs/ui/components/label";
|
|
7
|
+
import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@voyantjs/ui/components/pagination";
|
|
7
8
|
import { Skeleton } from "@voyantjs/ui/components/skeleton";
|
|
8
9
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
|
|
9
10
|
import { ArrowDown, ArrowUp, ArrowUpDown, Plus, Search } from "lucide-react";
|
|
@@ -12,6 +13,32 @@ import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefau
|
|
|
12
13
|
import { BookingDialog } from "./booking-dialog.js";
|
|
13
14
|
import { BOOKING_STATUS_ALL, BookingListFiltersPopover } from "./booking-list-filters.js";
|
|
14
15
|
import { StatusBadge } from "./status-badge.js";
|
|
16
|
+
function stripUndefined(input) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const [key, value] of Object.entries(input)) {
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_FILTERS = {
|
|
25
|
+
search: "",
|
|
26
|
+
status: BOOKING_STATUS_ALL,
|
|
27
|
+
productId: null,
|
|
28
|
+
optionId: null,
|
|
29
|
+
supplierId: null,
|
|
30
|
+
productCategoryId: null,
|
|
31
|
+
personId: null,
|
|
32
|
+
organizationId: null,
|
|
33
|
+
availabilitySlotId: null,
|
|
34
|
+
dateFrom: null,
|
|
35
|
+
dateTo: null,
|
|
36
|
+
paxMin: "",
|
|
37
|
+
paxMax: "",
|
|
38
|
+
sortBy: "createdAt",
|
|
39
|
+
sortDir: "desc",
|
|
40
|
+
offset: 0,
|
|
41
|
+
};
|
|
15
42
|
const SORTABLE_COLUMNS = {
|
|
16
43
|
bookingNumber: "bookingNumber",
|
|
17
44
|
status: "status",
|
|
@@ -23,26 +50,42 @@ const SORTABLE_COLUMNS = {
|
|
|
23
50
|
};
|
|
24
51
|
const SKELETON_ROW_COUNT = 6;
|
|
25
52
|
const TABLE_COLUMN_COUNT = 8;
|
|
26
|
-
export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, headerActions, } = {}) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
53
|
+
export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, headerActions, initialFilters, onFiltersChange, } = {}) {
|
|
54
|
+
// Single bag of filter / sort / paging state so we can hand the host
|
|
55
|
+
// a snapshot whenever anything changes. We seed once from
|
|
56
|
+
// `initialFilters` and don't re-seed if the prop later mutates —
|
|
57
|
+
// hosts that want to drive controlled changes can just remount.
|
|
58
|
+
//
|
|
59
|
+
// Strip `undefined` keys before merging: a host passing
|
|
60
|
+
// `{ status: undefined }` would otherwise clobber the
|
|
61
|
+
// `BOOKING_STATUS_ALL` default and show "2" active filters on an
|
|
62
|
+
// empty URL.
|
|
63
|
+
const [filters, setFilters] = React.useState(() => ({
|
|
64
|
+
...DEFAULT_FILTERS,
|
|
65
|
+
...stripUndefined(initialFilters ?? {}),
|
|
66
|
+
}));
|
|
67
|
+
const onFiltersChangeRef = React.useRef(onFiltersChange);
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
onFiltersChangeRef.current = onFiltersChange;
|
|
70
|
+
});
|
|
71
|
+
// Notify the host on every state change. Skip the initial render
|
|
72
|
+
// because the URL already reflects whatever was passed in via
|
|
73
|
+
// `initialFilters`.
|
|
74
|
+
const isFirstRender = React.useRef(true);
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (isFirstRender.current) {
|
|
77
|
+
isFirstRender.current = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
onFiltersChangeRef.current?.(filters);
|
|
81
|
+
}, [filters]);
|
|
82
|
+
const updateFilters = React.useCallback((patch) => setFilters((prev) => ({ ...prev, ...patch })), []);
|
|
83
|
+
const { search, status, productId, optionId, supplierId, productCategoryId, personId, organizationId, availabilitySlotId, dateFrom, dateTo, paxMin, paxMax, sortBy, sortDir, offset, } = filters;
|
|
84
|
+
const dateRange = dateFrom != null || dateTo != null ? { from: dateFrom, to: dateTo } : null;
|
|
42
85
|
const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false);
|
|
43
86
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
44
87
|
const [editing, setEditing] = React.useState(undefined);
|
|
45
|
-
const { formatDateTime, formatNumber } = useBookingsUiI18nOrDefault();
|
|
88
|
+
const { formatDate, formatDateTime, formatNumber, locale } = useBookingsUiI18nOrDefault();
|
|
46
89
|
const messages = useBookingsUiMessagesOrDefault();
|
|
47
90
|
const paxMinNumber = paxMin === "" ? undefined : Number.parseInt(paxMin, 10);
|
|
48
91
|
const paxMaxNumber = paxMax === "" ? undefined : Number.parseInt(paxMax, 10);
|
|
@@ -83,20 +126,17 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
|
|
|
83
126
|
setEditing(booking);
|
|
84
127
|
setDialogOpen(true);
|
|
85
128
|
};
|
|
86
|
-
const resetOffset = () =>
|
|
129
|
+
const resetOffset = () => updateFilters({ offset: 0 });
|
|
87
130
|
const handleSort = (field) => {
|
|
88
|
-
setOffset(0);
|
|
89
131
|
if (sortBy !== field) {
|
|
90
|
-
|
|
91
|
-
setSortDir("asc");
|
|
132
|
+
updateFilters({ offset: 0, sortBy: field, sortDir: "asc" });
|
|
92
133
|
return;
|
|
93
134
|
}
|
|
94
135
|
if (sortDir === "asc") {
|
|
95
|
-
|
|
136
|
+
updateFilters({ offset: 0, sortDir: "desc" });
|
|
96
137
|
return;
|
|
97
138
|
}
|
|
98
|
-
|
|
99
|
-
setSortDir("desc");
|
|
139
|
+
updateFilters({ offset: 0, sortBy: "createdAt", sortDir: "desc" });
|
|
100
140
|
};
|
|
101
141
|
const activeFilterCount = (status !== BOOKING_STATUS_ALL ? 1 : 0) +
|
|
102
142
|
(productId !== null ? 1 : 0) +
|
|
@@ -110,51 +150,85 @@ export function BookingList({ pageSize = 25, onSelectBooking, onCreateBooking, h
|
|
|
110
150
|
(paxMin !== "" || paxMax !== "" ? 1 : 0);
|
|
111
151
|
const hasActiveFilters = activeFilterCount > 0 || search !== "";
|
|
112
152
|
const clearFilters = () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
updateFilters({
|
|
154
|
+
search: "",
|
|
155
|
+
status: BOOKING_STATUS_ALL,
|
|
156
|
+
productId: null,
|
|
157
|
+
optionId: null,
|
|
158
|
+
availabilitySlotId: null,
|
|
159
|
+
supplierId: null,
|
|
160
|
+
productCategoryId: null,
|
|
161
|
+
personId: null,
|
|
162
|
+
organizationId: null,
|
|
163
|
+
dateFrom: null,
|
|
164
|
+
dateTo: null,
|
|
165
|
+
paxMin: "",
|
|
166
|
+
paxMax: "",
|
|
167
|
+
offset: 0,
|
|
168
|
+
});
|
|
126
169
|
};
|
|
127
170
|
const columnMessages = messages.bookingList.columns;
|
|
128
171
|
const statusLabels = messages.common.bookingStatusLabels;
|
|
129
|
-
return (_jsxs("div", { "data-slot": "booking-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [_jsx(Label, { htmlFor: "bookings-search", className: "sr-only", children: messages.bookingList.searchPlaceholder }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { id: "bookings-search", placeholder: messages.bookingList.searchPlaceholder, value: search, onChange: (event) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}, className: "pl-9" })] }), _jsx(BookingListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: setStatus, productId: productId, onProductIdChange: (next) => {
|
|
133
|
-
setProductId(next);
|
|
134
|
-
// Slot picker is product-scoped; clear when the product changes.
|
|
135
|
-
setAvailabilitySlotId(null);
|
|
136
|
-
}, optionId: optionId, onOptionIdChange: setOptionId, availabilitySlotId: availabilitySlotId, onAvailabilitySlotIdChange: setAvailabilitySlotId, supplierId: supplierId, onSupplierIdChange: setSupplierId, productCategoryId: productCategoryId, onProductCategoryIdChange: setProductCategoryId, personId: personId, onPersonIdChange: setPersonId, organizationId: organizationId, onOrganizationIdChange: setOrganizationId, dateRange: dateRange, onDateRangeChange: setDateRange, paxMin: paxMin, onPaxMinChange: setPaxMin, paxMax: paxMax, onPaxMaxChange: setPaxMax, onFiltersChanged: resetOffset, hasActiveFilters: hasActiveFilters, onClearFilters: clearFilters }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [headerActions, _jsxs(Button, { onClick: () => {
|
|
172
|
+
return (_jsxs("div", { "data-slot": "booking-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [_jsx(Label, { htmlFor: "bookings-search", className: "sr-only", children: messages.bookingList.searchPlaceholder }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { id: "bookings-search", placeholder: messages.bookingList.searchPlaceholder, value: search, onChange: (event) => updateFilters({ search: event.target.value, offset: 0 }), className: "pl-9" })] }), _jsx(BookingListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: (next) => updateFilters({ status: next }), productId: productId, onProductIdChange: (next) =>
|
|
173
|
+
// Slot picker is product-scoped; clear when the product changes.
|
|
174
|
+
updateFilters({ productId: next, availabilitySlotId: null }), optionId: optionId, onOptionIdChange: (next) => updateFilters({ optionId: next }), availabilitySlotId: availabilitySlotId, onAvailabilitySlotIdChange: (next) => updateFilters({ availabilitySlotId: next }), supplierId: supplierId, onSupplierIdChange: (next) => updateFilters({ supplierId: next }), productCategoryId: productCategoryId, onProductCategoryIdChange: (next) => updateFilters({ productCategoryId: next }), personId: personId, onPersonIdChange: (next) => updateFilters({ personId: next }), organizationId: organizationId, onOrganizationIdChange: (next) => updateFilters({ organizationId: next }), dateRange: dateRange, onDateRangeChange: (next) => updateFilters({ dateFrom: next?.from ?? null, dateTo: next?.to ?? null }), paxMin: paxMin, onPaxMinChange: (next) => updateFilters({ paxMin: next }), paxMax: paxMax, onPaxMaxChange: (next) => updateFilters({ paxMax: next }), onFiltersChanged: resetOffset, hasActiveFilters: hasActiveFilters, onClearFilters: clearFilters }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [headerActions, _jsxs(Button, { onClick: () => {
|
|
137
175
|
if (onCreateBooking) {
|
|
138
176
|
onCreateBooking();
|
|
139
177
|
return;
|
|
140
178
|
}
|
|
141
179
|
setEditing(undefined);
|
|
142
180
|
setDialogOpen(true);
|
|
143
|
-
}, children: [_jsx(Plus, { className: "mr-2 size-4" }), messages.bookingList.newBooking] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.bookingNumber, field: SORTABLE_COLUMNS.bookingNumber, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children:
|
|
181
|
+
}, children: [_jsx(Plus, { className: "mr-2 size-4" }), messages.bookingList.newBooking] })] })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.bookingNumber, field: SORTABLE_COLUMNS.bookingNumber, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.createdAt, field: SORTABLE_COLUMNS.createdAt, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: columnMessages.lead }), _jsx(TableHead, { children: columnMessages.whatBooked }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.status, field: SORTABLE_COLUMNS.status, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.sellAmount, field: SORTABLE_COLUMNS.sellAmount, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.pax, field: SORTABLE_COLUMNS.pax, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.startDate, field: SORTABLE_COLUMNS.startDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(BookingTableSkeleton, { rows: SKELETON_ROW_COUNT })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: messages.bookingList.loadingError }) })) : bookings.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: messages.bookingList.empty }) })) : (bookings.map((booking) => (_jsxs(TableRow, { onClick: () => handleSelect(booking), className: "cursor-pointer", children: [_jsx(TableCell, { className: "font-medium", children: booking.bookingNumber }), _jsx(TableCell, { children: formatBookingDateTime(booking.createdAt, formatDateTime) }), _jsx(TableCell, { children: formatLead(booking) }), _jsx(TableCell, { children: formatBookingItems(booking, messages.bookingList.itemsMore, messages.bookingList.itemDays) }), _jsx(TableCell, { children: _jsx(StatusBadge, { status: booking.status, children: statusLabels[booking.status] }) }), _jsx(TableCell, { children: booking.sellAmountCents == null
|
|
144
182
|
? "—"
|
|
145
183
|
: `${formatNumber(booking.sellAmountCents / 100, {
|
|
146
184
|
minimumFractionDigits: 2,
|
|
147
185
|
maximumFractionDigits: 2,
|
|
148
|
-
})} ${booking.sellCurrency}` }), _jsx(TableCell, { children: booking.pax ?? "—" }),
|
|
186
|
+
})} ${booking.sellCurrency}` }), _jsx(TableCell, { children: booking.pax ?? "—" }), _jsx(TableCell, { className: "whitespace-nowrap", children: formatBookingDateRange(booking.startsAt ?? booking.startDate, booking.endsAt ?? booking.endDate, formatDate, locale) })] }, booking.id)))) })] }) }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(messages.bookingList.showingSummary, {
|
|
149
187
|
count: bookings.length,
|
|
150
188
|
total,
|
|
151
|
-
}) }),
|
|
152
|
-
page,
|
|
153
|
-
pageCount,
|
|
154
|
-
}) }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + pageSize >= total, onClick: () => setOffset((prev) => prev + pageSize), children: messages.bookingList.nextPage })] })] }), _jsx(BookingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, booking: editing, onSuccess: (booking) => {
|
|
189
|
+
}) }), pageCount > 1 ? (_jsx(BookingListPagination, { page: page, pageCount: pageCount, previousLabel: messages.bookingList.previousPage, nextLabel: messages.bookingList.nextPage, onPageChange: (nextPage) => updateFilters({ offset: (nextPage - 1) * pageSize }) })) : null] }), _jsx(BookingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, booking: editing, onSuccess: (booking) => {
|
|
155
190
|
onSelectBooking?.(booking);
|
|
156
191
|
} })] }));
|
|
157
192
|
}
|
|
193
|
+
function BookingListPagination({ page, pageCount, previousLabel, nextLabel, onPageChange, }) {
|
|
194
|
+
const canPrev = page > 1;
|
|
195
|
+
const canNext = page < pageCount;
|
|
196
|
+
const pages = computePageWindow(page, pageCount);
|
|
197
|
+
return (_jsx(Pagination, { className: "mx-0 w-auto justify-end", children: _jsxs(PaginationContent, { children: [_jsx(PaginationItem, { children: _jsx(PaginationPrevious, { href: "#", text: previousLabel, "aria-disabled": !canPrev, tabIndex: canPrev ? 0 : -1, className: canPrev ? undefined : "pointer-events-none opacity-50", onClick: (event) => {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
if (canPrev)
|
|
200
|
+
onPageChange(page - 1);
|
|
201
|
+
} }) }), pages.map((entry, idx) => (_jsx(PaginationItem
|
|
202
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: ellipsis sentinels collide on value alone
|
|
203
|
+
, { children: entry === "…" ? (_jsx("span", { className: "px-2 text-muted-foreground", "aria-hidden": true, children: "\u2026" })) : (_jsx(PaginationLink, { href: "#", isActive: entry === page, onClick: (event) => {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
if (entry !== page)
|
|
206
|
+
onPageChange(entry);
|
|
207
|
+
}, children: entry })) }, `${entry}-${idx}`))), _jsx(PaginationItem, { children: _jsx(PaginationNext, { href: "#", text: nextLabel, "aria-disabled": !canNext, tabIndex: canNext ? 0 : -1, className: canNext ? undefined : "pointer-events-none opacity-50", onClick: (event) => {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
if (canNext)
|
|
210
|
+
onPageChange(page + 1);
|
|
211
|
+
} }) })] }) }));
|
|
212
|
+
}
|
|
213
|
+
/** Build a 1-indexed page list with ellipses for tables with many
|
|
214
|
+
* pages. Always shows first, last, current, and one neighbour on
|
|
215
|
+
* either side. */
|
|
216
|
+
function computePageWindow(page, pageCount) {
|
|
217
|
+
if (pageCount <= 7) {
|
|
218
|
+
return Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
219
|
+
}
|
|
220
|
+
const out = [1];
|
|
221
|
+
const start = Math.max(2, page - 1);
|
|
222
|
+
const end = Math.min(pageCount - 1, page + 1);
|
|
223
|
+
if (start > 2)
|
|
224
|
+
out.push("…");
|
|
225
|
+
for (let i = start; i <= end; i += 1)
|
|
226
|
+
out.push(i);
|
|
227
|
+
if (end < pageCount - 1)
|
|
228
|
+
out.push("…");
|
|
229
|
+
out.push(pageCount);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
158
232
|
function SortHeader({ label, field, sortBy, sortDir, onSort }) {
|
|
159
233
|
const active = sortBy === field;
|
|
160
234
|
const Icon = active ? (sortDir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
|
|
@@ -163,7 +237,7 @@ function SortHeader({ label, field, sortBy, sortDir, onSort }) {
|
|
|
163
237
|
function BookingTableSkeleton({ rows }) {
|
|
164
238
|
return (_jsx(_Fragment, { children: Array.from({ length: rows }).map((_, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-24" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-48" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-20 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-8" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-32" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-32" }) })] }, `skeleton-${idx}`))) }));
|
|
165
239
|
}
|
|
166
|
-
function formatBookingItems(booking, moreTemplate) {
|
|
240
|
+
function formatBookingItems(booking, moreTemplate, daysTemplate) {
|
|
167
241
|
const items = booking.items ?? [];
|
|
168
242
|
if (items.length === 0)
|
|
169
243
|
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
@@ -171,8 +245,24 @@ function formatBookingItems(booking, moreTemplate) {
|
|
|
171
245
|
if (!first)
|
|
172
246
|
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
173
247
|
const label = first.productName ?? first.title;
|
|
248
|
+
const days = computeItemDays(first.startsAt, first.endsAt);
|
|
249
|
+
const daysSuffix = days > 0 ? ` ${formatMessage(daysTemplate, { count: days })}` : "";
|
|
174
250
|
const moreSuffix = rest.length === 0 ? "" : ` ${formatMessage(moreTemplate, { count: rest.length })}`;
|
|
175
|
-
return (_jsxs("div", { className: "max-w-[320px] truncate", title: `${label}${moreSuffix}`, children: [label, rest.length > 0 ? (_jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: formatMessage(moreTemplate, { count: rest.length }) })) : null] }));
|
|
251
|
+
return (_jsxs("div", { className: "max-w-[320px] truncate", title: `${label}${daysSuffix}${moreSuffix}`, children: [label, days > 0 ? (_jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: formatMessage(daysTemplate, { count: days }) })) : null, rest.length > 0 ? (_jsx("span", { className: "ml-1 text-xs text-muted-foreground", children: formatMessage(moreTemplate, { count: rest.length }) })) : null] }));
|
|
252
|
+
}
|
|
253
|
+
/** Inclusive day-count between two ISO timestamps, rounded up so a
|
|
254
|
+
* trip spanning two calendar days reads "2 days". Returns 0 when
|
|
255
|
+
* either bound is missing so the caller can drop the tag entirely. */
|
|
256
|
+
function computeItemDays(startValue, endValue) {
|
|
257
|
+
const start = toDate(startValue);
|
|
258
|
+
const end = toDate(endValue);
|
|
259
|
+
if (!start || !end)
|
|
260
|
+
return 0;
|
|
261
|
+
const ms = end.getTime() - start.getTime();
|
|
262
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
263
|
+
return 0;
|
|
264
|
+
const days = Math.ceil(ms / (1000 * 60 * 60 * 24));
|
|
265
|
+
return Math.max(1, days);
|
|
176
266
|
}
|
|
177
267
|
function formatBookingDateTime(value, formatDateTime) {
|
|
178
268
|
if (!value)
|
|
@@ -182,6 +272,77 @@ function formatBookingDateTime(value, formatDateTime) {
|
|
|
182
272
|
}
|
|
183
273
|
return formatDateTime(value);
|
|
184
274
|
}
|
|
275
|
+
function toDate(value) {
|
|
276
|
+
if (!value)
|
|
277
|
+
return null;
|
|
278
|
+
const iso = /^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T00:00:00` : value;
|
|
279
|
+
const date = new Date(iso);
|
|
280
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Compact date-range formatter for the bookings table — collapses
|
|
284
|
+
* shared month/year so a 3-day trip reads "Jun 15 – 20, 2026" in
|
|
285
|
+
* English and "15 – 20 iun., 2026" in Romanian. Output respects the
|
|
286
|
+
* locale's day/month order.
|
|
287
|
+
*
|
|
288
|
+
* NOTE: `Intl.DateTimeFormat` produces nonsense (e.g.
|
|
289
|
+
* `"2026 (day: 20)"`) for incomplete combinations like `{ day, year }`
|
|
290
|
+
* without a month. We build the compact range from named parts
|
|
291
|
+
* instead of asking Intl to skip the month.
|
|
292
|
+
*/
|
|
293
|
+
function formatBookingDateRange(startValue, endValue, formatDate, locale) {
|
|
294
|
+
const start = toDate(startValue);
|
|
295
|
+
const end = toDate(endValue);
|
|
296
|
+
if (!start && !end)
|
|
297
|
+
return "—";
|
|
298
|
+
if (start && !end)
|
|
299
|
+
return formatDate(start, { month: "short", day: "numeric", year: "numeric" });
|
|
300
|
+
if (!start || !end)
|
|
301
|
+
return formatDate(end, { month: "short", day: "numeric", year: "numeric" });
|
|
302
|
+
const s = start;
|
|
303
|
+
const e = end;
|
|
304
|
+
const sameDay = s.getFullYear() === e.getFullYear() &&
|
|
305
|
+
s.getMonth() === e.getMonth() &&
|
|
306
|
+
s.getDate() === e.getDate();
|
|
307
|
+
if (sameDay)
|
|
308
|
+
return formatDate(s, { month: "short", day: "numeric", year: "numeric" });
|
|
309
|
+
const sameYear = s.getFullYear() === e.getFullYear();
|
|
310
|
+
const sameMonth = sameYear && s.getMonth() === e.getMonth();
|
|
311
|
+
// For collapsed ranges we need to know whether the locale puts the
|
|
312
|
+
// month before or after the day (en-US: "Jun 15", ro-RO: "15 iun.")
|
|
313
|
+
// so the result reads naturally.
|
|
314
|
+
const dayFirst = isLocaleDayFirst(locale);
|
|
315
|
+
const monthShortStart = formatDate(s, { month: "short" });
|
|
316
|
+
const monthShortEnd = formatDate(e, { month: "short" });
|
|
317
|
+
const startDay = formatDate(s, { day: "numeric" });
|
|
318
|
+
const endDay = formatDate(e, { day: "numeric" });
|
|
319
|
+
if (sameMonth) {
|
|
320
|
+
const body = dayFirst
|
|
321
|
+
? `${startDay} – ${endDay} ${monthShortStart}`
|
|
322
|
+
: `${monthShortStart} ${startDay} – ${endDay}`;
|
|
323
|
+
return `${body}, ${e.getFullYear()}`;
|
|
324
|
+
}
|
|
325
|
+
if (sameYear) {
|
|
326
|
+
const body = dayFirst
|
|
327
|
+
? `${startDay} ${monthShortStart} – ${endDay} ${monthShortEnd}`
|
|
328
|
+
: `${monthShortStart} ${startDay} – ${monthShortEnd} ${endDay}`;
|
|
329
|
+
return `${body}, ${e.getFullYear()}`;
|
|
330
|
+
}
|
|
331
|
+
return `${formatDate(s, { month: "short", day: "numeric", year: "numeric" })} – ${formatDate(e, { month: "short", day: "numeric", year: "numeric" })}`;
|
|
332
|
+
}
|
|
333
|
+
/** Detect whether the locale renders the day before the month in a
|
|
334
|
+
* short date format (e.g. ro-RO: "15 iun." vs en-US: "Jun 15"). */
|
|
335
|
+
function isLocaleDayFirst(locale) {
|
|
336
|
+
const parts = new Intl.DateTimeFormat(locale, {
|
|
337
|
+
month: "short",
|
|
338
|
+
day: "numeric",
|
|
339
|
+
}).formatToParts(new Date(2026, 0, 15));
|
|
340
|
+
const dayIndex = parts.findIndex((p) => p.type === "day");
|
|
341
|
+
const monthIndex = parts.findIndex((p) => p.type === "month");
|
|
342
|
+
if (dayIndex === -1 || monthIndex === -1)
|
|
343
|
+
return false;
|
|
344
|
+
return dayIndex < monthIndex;
|
|
345
|
+
}
|
|
185
346
|
function formatLead(booking) {
|
|
186
347
|
const name = [booking.contactFirstName, booking.contactLastName].filter(Boolean).join(" ").trim();
|
|
187
348
|
if (name)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BookingNoteRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface BookingNoteDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
bookingId: string;
|
|
6
|
+
/** When set, the dialog opens in edit mode against this note. */
|
|
7
|
+
note?: BookingNoteRecord | null;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Add / edit a booking note. Mirrors the supplier-status / traveler
|
|
12
|
+
* dialog pattern so the activity tab is consistent with the rest of
|
|
13
|
+
* the booking-detail surface.
|
|
14
|
+
*/
|
|
15
|
+
export declare function BookingNoteDialog({ open, onOpenChange, bookingId, note, onSuccess, }: BookingNoteDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=booking-note-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-note-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-note-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAA0B,MAAM,0BAA0B,CAAA;AAiBzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,IAAI,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,sBAAsB,2CA6DxB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingNoteMutation } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Textarea, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Loader2 } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
|
+
/**
|
|
9
|
+
* Add / edit a booking note. Mirrors the supplier-status / traveler
|
|
10
|
+
* dialog pattern so the activity tab is consistent with the rest of
|
|
11
|
+
* the booking-detail surface.
|
|
12
|
+
*/
|
|
13
|
+
export function BookingNoteDialog({ open, onOpenChange, bookingId, note, onSuccess, }) {
|
|
14
|
+
const [content, setContent] = React.useState("");
|
|
15
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
16
|
+
const dialog = messages.bookingNotes.dialog;
|
|
17
|
+
const mutation = useBookingNoteMutation(bookingId);
|
|
18
|
+
const isEditing = Boolean(note);
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
if (open)
|
|
21
|
+
setContent(note?.content ?? "");
|
|
22
|
+
}, [note, open]);
|
|
23
|
+
const submit = async () => {
|
|
24
|
+
const trimmed = content.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
return;
|
|
27
|
+
if (note) {
|
|
28
|
+
await mutation.update.mutateAsync({ id: note.id, content: trimmed });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await mutation.create.mutateAsync({ content: trimmed });
|
|
32
|
+
}
|
|
33
|
+
onOpenChange(false);
|
|
34
|
+
onSuccess?.();
|
|
35
|
+
};
|
|
36
|
+
const pending = isEditing ? mutation.update.isPending : mutation.create.isPending;
|
|
37
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? dialog.editTitle : dialog.createTitle }) }), _jsxs("form", { onSubmit: (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
void submit();
|
|
40
|
+
}, className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "booking-note-content", children: dialog.contentLabel }), _jsx(Textarea, { id: "booking-note-content", placeholder: dialog.contentPlaceholder, value: content, onChange: (e) => setContent(e.target.value), className: "min-h-[120px]" })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: dialog.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: !content.trim() || pending, children: [pending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? dialog.save : dialog.create] })] })] })] }) }));
|
|
41
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-notes.d.ts","sourceRoot":"","sources":["../../src/components/booking-notes.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-notes.d.ts","sourceRoot":"","sources":["../../src/components/booking-notes.tsx"],"names":[],"mappings":"AA6BA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,iBAAiB,2CAmG5D"}
|
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useBookingNoteMutation, useBookingNotes } from "@voyantjs/bookings-react";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { useBookingNoteMutation, useBookingNotes, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, } from "@voyantjs/ui/components";
|
|
5
|
+
import { Pencil, Plus, StickyNote, Trash2 } from "lucide-react";
|
|
6
6
|
import * as React from "react";
|
|
7
|
-
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
7
|
+
import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider.js";
|
|
8
|
+
import { BookingNoteDialog } from "./booking-note-dialog.js";
|
|
9
|
+
import { IconActionButton } from "./icon-action-button.js";
|
|
8
10
|
export function BookingNotes({ bookingId }) {
|
|
9
|
-
const [
|
|
11
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
12
|
+
const [editing, setEditing] = React.useState(null);
|
|
13
|
+
const [deleteTarget, setDeleteTarget] = React.useState(null);
|
|
10
14
|
const { data } = useBookingNotes(bookingId);
|
|
11
15
|
const mutation = useBookingNoteMutation(bookingId);
|
|
12
16
|
const { formatDateTime } = useBookingsUiI18nOrDefault();
|
|
13
17
|
const messages = useBookingsUiMessagesOrDefault();
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
const card = messages.bookingNotes;
|
|
19
|
+
const notes = (data?.data ?? []);
|
|
20
|
+
const handleConfirmDelete = async () => {
|
|
21
|
+
if (!deleteTarget)
|
|
22
|
+
return;
|
|
23
|
+
await mutation.remove.mutateAsync(deleteTarget.id);
|
|
24
|
+
setDeleteTarget(null);
|
|
25
|
+
};
|
|
26
|
+
return (_jsxs("div", { "data-slot": "booking-notes", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("h2", { className: "flex items-center gap-2 text-base font-semibold", children: [_jsx(StickyNote, { className: "h-4 w-4 text-muted-foreground" }), card.title] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
27
|
+
setEditing(null);
|
|
28
|
+
setDialogOpen(true);
|
|
29
|
+
}, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), card.addAction] })] }), notes.length === 0 ? (_jsx("div", { className: "rounded-md border bg-background p-6 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: card.empty }) })) : (_jsx("div", { className: "grid gap-2 md:grid-cols-2", children: notes.map((note) => (_jsx(NoteCard, { note: note, authorTemplate: card.authorLabel, editLabel: card.actions.edit, deleteLabel: card.actions.delete, formatDateTime: formatDateTime, onEdit: () => {
|
|
30
|
+
setEditing(note);
|
|
31
|
+
setDialogOpen(true);
|
|
32
|
+
}, onDelete: () => setDeleteTarget(note) }, note.id))) })), _jsx(BookingNoteDialog, { open: dialogOpen, onOpenChange: (next) => {
|
|
33
|
+
setDialogOpen(next);
|
|
34
|
+
if (!next)
|
|
35
|
+
setEditing(null);
|
|
36
|
+
}, bookingId: bookingId, note: editing }), _jsx(AlertDialog, { open: Boolean(deleteTarget), onOpenChange: (next) => {
|
|
37
|
+
if (!next && !mutation.remove.isPending)
|
|
38
|
+
setDeleteTarget(null);
|
|
39
|
+
}, children: _jsxs(AlertDialogContent, { size: "sm", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: card.deleteConfirm.title }), _jsx(AlertDialogDescription, { children: card.deleteConfirm.description })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: mutation.remove.isPending, children: card.deleteConfirm.cancel }), _jsx(AlertDialogAction, { variant: "destructive", disabled: mutation.remove.isPending, onClick: () => void handleConfirmDelete(), children: card.deleteConfirm.confirm })] })] }) })] }));
|
|
40
|
+
}
|
|
41
|
+
function NoteCard({ note, authorTemplate, editLabel, deleteLabel, formatDateTime, onEdit, onDelete, }) {
|
|
42
|
+
return (_jsxs("div", { className: "group flex flex-col gap-2 rounded-md border bg-background p-3", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("p", { className: "whitespace-pre-wrap text-sm", children: note.content }), _jsxs("div", { className: "flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100", children: [_jsx(IconActionButton, { label: editLabel, icon: _jsx(Pencil, { className: "h-3.5 w-3.5" }), onClick: onEdit }), _jsx(IconActionButton, { label: deleteLabel, icon: _jsx(Trash2, { className: "h-3.5 w-3.5" }), onClick: onDelete })] })] }), _jsxs("p", { className: "text-muted-foreground text-xs", children: [formatMessage(authorTemplate, {
|
|
43
|
+
actor: note.authorName || note.authorEmail || note.authorId,
|
|
44
|
+
}), " ", "\u00B7 ", formatDateTime(note.createdAt)] })] }));
|
|
19
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BookingRecord } from "@voyantjs/bookings-react";
|
|
2
2
|
import type * as React from "react";
|
|
3
|
+
import { type BookingListFiltersState } from "./booking-list.js";
|
|
3
4
|
export interface BookingsPageProps {
|
|
4
5
|
pageSize?: number;
|
|
5
6
|
onBookingOpen?: (booking: BookingRecord) => void;
|
|
@@ -10,6 +11,9 @@ export interface BookingsPageProps {
|
|
|
10
11
|
*/
|
|
11
12
|
headerActions?: React.ReactNode;
|
|
12
13
|
className?: string;
|
|
14
|
+
/** Forwarded to `BookingList` — see prop docs there. */
|
|
15
|
+
initialFilters?: Partial<BookingListFiltersState>;
|
|
16
|
+
onFiltersChange?: (filters: BookingListFiltersState) => void;
|
|
13
17
|
}
|
|
14
|
-
export declare function BookingsPage({ pageSize, onBookingOpen, onCreateBooking, headerActions, className, }?: BookingsPageProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function BookingsPage({ pageSize, onBookingOpen, onCreateBooking, headerActions, className, initialFilters, onFiltersChange, }?: BookingsPageProps): import("react/jsx-runtime").JSX.Element;
|
|
15
19
|
//# sourceMappingURL=bookings-page.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bookings-page.d.ts","sourceRoot":"","sources":["../../src/components/bookings-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"bookings-page.d.ts","sourceRoot":"","sources":["../../src/components/bookings-page.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAE7D,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAe,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAE7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAChD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B;;;OAGG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,cAAc,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAA;IACjD,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;CAC7D;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,cAAc,EACd,eAAe,GAChB,GAAE,iBAAsB,2CAoBxB"}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { cn } from "@voyantjs/ui/lib/utils";
|
|
4
4
|
import { useBookingsUiMessagesOrDefault } from "../i18n/index.js";
|
|
5
5
|
import { BookingList } from "./booking-list.js";
|
|
6
|
-
export function BookingsPage({ pageSize, onBookingOpen, onCreateBooking, headerActions, className, } = {}) {
|
|
6
|
+
export function BookingsPage({ pageSize, onBookingOpen, onCreateBooking, headerActions, className, initialFilters, onFiltersChange, } = {}) {
|
|
7
7
|
const messages = useBookingsUiMessagesOrDefault().bookingsPage;
|
|
8
|
-
return (_jsxs("div", { "data-slot": "bookings-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: messages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.description })] }), _jsx(BookingList, { pageSize: pageSize, onSelectBooking: onBookingOpen, onCreateBooking: onCreateBooking, headerActions: headerActions })] }));
|
|
8
|
+
return (_jsxs("div", { "data-slot": "bookings-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: messages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.description })] }), _jsx(BookingList, { pageSize: pageSize, onSelectBooking: onBookingOpen, onCreateBooking: onCreateBooking, headerActions: headerActions, initialFilters: initialFilters, onFiltersChange: onFiltersChange })] }));
|
|
9
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-change-dialog.d.ts","sourceRoot":"","sources":["../../src/components/status-change-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"status-change-dialog.d.ts","sourceRoot":"","sources":["../../src/components/status-change-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AAkCjC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IACtC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,aAAa,EACb,SAAS,GACV,EAAE,uBAAuB,2CAkIzB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { bookingStatusOptions, bookingStatusSchema, useBookingStatusMutation, } from "@voyantjs/bookings-react";
|
|
4
|
-
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
|
|
5
5
|
import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
|
|
6
6
|
import { Loader2 } from "lucide-react";
|
|
7
7
|
import { useEffect, useMemo } from "react";
|
|
@@ -11,6 +11,7 @@ import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
|
11
11
|
const statusChangeFormSchema = z.object({
|
|
12
12
|
status: bookingStatusSchema,
|
|
13
13
|
note: z.string().optional().nullable(),
|
|
14
|
+
suppressNotifications: z.boolean(),
|
|
14
15
|
});
|
|
15
16
|
export function StatusChangeDialog({ open, onOpenChange, bookingId, currentStatus, onSuccess, }) {
|
|
16
17
|
const mutation = useBookingStatusMutation(bookingId);
|
|
@@ -24,6 +25,7 @@ export function StatusChangeDialog({ open, onOpenChange, bookingId, currentStatu
|
|
|
24
25
|
defaultValues: {
|
|
25
26
|
status: "draft",
|
|
26
27
|
note: "",
|
|
28
|
+
suppressNotifications: false,
|
|
27
29
|
},
|
|
28
30
|
});
|
|
29
31
|
useEffect(() => {
|
|
@@ -31,17 +33,25 @@ export function StatusChangeDialog({ open, onOpenChange, bookingId, currentStatu
|
|
|
31
33
|
form.reset({
|
|
32
34
|
status: currentStatus,
|
|
33
35
|
note: "",
|
|
36
|
+
suppressNotifications: false,
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
}, [currentStatus, form, open]);
|
|
40
|
+
// Suppression only takes effect on the `confirm` verb today (see
|
|
41
|
+
// status-dispatch.ts), so only show the toggle when the target is
|
|
42
|
+
// `confirmed`. Hide it otherwise to keep the dialog focused.
|
|
43
|
+
const targetStatus = form.watch("status");
|
|
44
|
+
const suppressNotifications = form.watch("suppressNotifications");
|
|
45
|
+
const showSuppressToggle = targetStatus === "confirmed";
|
|
37
46
|
const onSubmit = async (values) => {
|
|
38
47
|
await mutation.mutateAsync({
|
|
39
48
|
currentStatus,
|
|
40
49
|
status: values.status,
|
|
41
50
|
note: values.note || null,
|
|
51
|
+
suppressNotifications: values.suppressNotifications || undefined,
|
|
42
52
|
});
|
|
43
53
|
onOpenChange(false);
|
|
44
54
|
onSuccess?.();
|
|
45
55
|
};
|
|
46
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.statusChangeDialog.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: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.statusChangeDialog.fields.status }), _jsxs(Select, { items: statusItems, value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: bookingStatusOptions.map((status) => (_jsx(SelectItem, { value: status.value, children: messages.common.bookingStatusLabels[status.value] }, status.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.statusChangeDialog.fields.note }), _jsx(Textarea, { ...form.register("note"), placeholder: messages.statusChangeDialog.placeholders.note })] })] }), _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: mutation.isPending, children: [mutation.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.statusChangeDialog.actions.updateStatus] })] })] })] }) }));
|
|
56
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.statusChangeDialog.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: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.statusChangeDialog.fields.status }), _jsxs(Select, { items: statusItems, value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: bookingStatusOptions.map((status) => (_jsx(SelectItem, { value: status.value, children: messages.common.bookingStatusLabels[status.value] }, status.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.statusChangeDialog.fields.note }), _jsx(Textarea, { ...form.register("note"), placeholder: messages.statusChangeDialog.placeholders.note })] }), showSuppressToggle ? (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border bg-muted/30 p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx(Label, { htmlFor: "suppress-notifications", children: messages.statusChangeDialog.fields.suppressNotifications }), _jsx(Switch, { id: "suppress-notifications", checked: suppressNotifications, onCheckedChange: (checked) => form.setValue("suppressNotifications", checked === true) })] }), _jsx("p", { className: "text-muted-foreground text-xs", children: messages.statusChangeDialog.helpers.suppressNotifications })] })) : null] }), _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: mutation.isPending, children: [mutation.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), messages.statusChangeDialog.actions.updateStatus] })] })] })] }) }));
|
|
47
57
|
}
|