@voyantjs/bookings-ui 0.81.18 → 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.
@@ -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
- const [search, setSearch] = React.useState("");
28
- const [status, setStatus] = React.useState(BOOKING_STATUS_ALL);
29
- const [productId, setProductId] = React.useState(null);
30
- const [optionId, setOptionId] = React.useState(null);
31
- const [supplierId, setSupplierId] = React.useState(null);
32
- const [productCategoryId, setProductCategoryId] = React.useState(null);
33
- const [personId, setPersonId] = React.useState(null);
34
- const [organizationId, setOrganizationId] = React.useState(null);
35
- const [availabilitySlotId, setAvailabilitySlotId] = React.useState(null);
36
- const [dateRange, setDateRange] = React.useState(null);
37
- const [paxMin, setPaxMin] = React.useState("");
38
- const [paxMax, setPaxMax] = React.useState("");
39
- const [sortBy, setSortBy] = React.useState("createdAt");
40
- const [sortDir, setSortDir] = React.useState("desc");
41
- const [offset, setOffset] = React.useState(0);
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 = () => setOffset(0);
129
+ const resetOffset = () => updateFilters({ offset: 0 });
87
130
  const handleSort = (field) => {
88
- setOffset(0);
89
131
  if (sortBy !== field) {
90
- setSortBy(field);
91
- setSortDir("asc");
132
+ updateFilters({ offset: 0, sortBy: field, sortDir: "asc" });
92
133
  return;
93
134
  }
94
135
  if (sortDir === "asc") {
95
- setSortDir("desc");
136
+ updateFilters({ offset: 0, sortDir: "desc" });
96
137
  return;
97
138
  }
98
- setSortBy("createdAt");
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
- setSearch("");
114
- setStatus(BOOKING_STATUS_ALL);
115
- setProductId(null);
116
- setOptionId(null);
117
- setAvailabilitySlotId(null);
118
- setSupplierId(null);
119
- setProductCategoryId(null);
120
- setPersonId(null);
121
- setOrganizationId(null);
122
- setDateRange(null);
123
- setPaxMin("");
124
- setPaxMax("");
125
- resetOffset();
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
- setSearch(event.target.value);
131
- resetOffset();
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: columnMessages.lead }), _jsx(TableHead, { children: _jsx(SortHeader, { label: columnMessages.createdAt, field: SORTABLE_COLUMNS.createdAt, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _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: formatLead(booking) }), _jsx(TableCell, { children: formatBookingDateTime(booking.createdAt, formatDateTime) }), _jsx(TableCell, { children: formatBookingItems(booking, messages.bookingList.itemsMore) }), _jsx(TableCell, { children: _jsx(StatusBadge, { status: booking.status, children: statusLabels[booking.status] }) }), _jsx(TableCell, { children: booking.sellAmountCents == null
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 ?? "—" }), _jsxs(TableCell, { children: [formatBookingDateTime(booking.startsAt ?? booking.startDate, formatDateTime), " – ", formatBookingDateTime(booking.endsAt ?? booking.endDate, formatDateTime)] })] }, booking.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(messages.bookingList.showingSummary, {
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
- }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((prev) => Math.max(0, prev - pageSize)), children: messages.bookingList.previousPage }), _jsx("span", { children: formatMessage(messages.bookingList.pageSummary, {
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":"AASA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,iBAAiB,2CAqD5D"}
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 { Button, Card, CardContent, CardHeader, CardTitle, Textarea } from "@voyantjs/ui/components";
5
- import { Loader2 } from "lucide-react";
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 [content, setContent] = React.useState("");
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 notes = data?.data ?? [];
15
- return (_jsxs(Card, { "data-slot": "booking-notes", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: messages.bookingNotes.title }) }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx(Textarea, { placeholder: messages.bookingNotes.placeholder, value: content, onChange: (event) => setContent(event.target.value), className: "min-h-[80px]" }), _jsx(Button, { className: "self-end", disabled: !content.trim() || mutation.isPending, onClick: async () => {
16
- await mutation.mutateAsync({ content: content.trim() });
17
- setContent("");
18
- }, children: mutation.isPending ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin" })) : (messages.bookingNotes.add) })] }), notes.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.bookingNotes.empty })) : (notes.map((note) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("p", { className: "whitespace-pre-wrap text-sm", children: note.content }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: formatDateTime(note.createdAt) })] }, note.id))))] })] }));
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;AAInC,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;CACnB;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,GACV,GAAE,iBAAsB,2CAkBxB"}
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
  }
package/dist/i18n/en.d.ts CHANGED
@@ -110,14 +110,22 @@ export declare const bookingsUiEn: {
110
110
  summaryCreated: string;
111
111
  summaryUpdated: string;
112
112
  tabOverview: string;
113
- tabMeta: string;
113
+ tabMetadata: string;
114
+ metadataSection: {
115
+ title: string;
116
+ bookingId: string;
117
+ bookingNumber: string;
118
+ status: string;
119
+ communicationLanguage: string;
120
+ created: string;
121
+ updated: string;
122
+ };
114
123
  tabTravelers: string;
115
124
  tabFinance: string;
116
125
  tabInvoices: string;
117
126
  tabSuppliers: string;
118
127
  tabDocuments: string;
119
128
  tabActivity: string;
120
- tabLedger: string;
121
129
  internalNotesLabel: string;
122
130
  billingPayer: string;
123
131
  billingEmail: string;
@@ -1384,10 +1392,10 @@ export declare const bookingsUiEn: {
1384
1392
  clear: string;
1385
1393
  };
1386
1394
  itemsMore: string;
1395
+ itemDays: string;
1387
1396
  loadingError: string;
1388
1397
  empty: string;
1389
1398
  showingSummary: string;
1390
- pageSummary: string;
1391
1399
  previousPage: string;
1392
1400
  nextPage: string;
1393
1401
  };
@@ -1434,9 +1442,28 @@ export declare const bookingsUiEn: {
1434
1442
  };
1435
1443
  bookingNotes: {
1436
1444
  title: string;
1437
- placeholder: string;
1438
- add: string;
1445
+ addAction: string;
1439
1446
  empty: string;
1447
+ authorLabel: string;
1448
+ actions: {
1449
+ edit: string;
1450
+ delete: string;
1451
+ };
1452
+ dialog: {
1453
+ createTitle: string;
1454
+ editTitle: string;
1455
+ contentLabel: string;
1456
+ contentPlaceholder: string;
1457
+ cancel: string;
1458
+ create: string;
1459
+ save: string;
1460
+ };
1461
+ deleteConfirm: {
1462
+ title: string;
1463
+ description: string;
1464
+ cancel: string;
1465
+ confirm: string;
1466
+ };
1440
1467
  };
1441
1468
  bookingActivityTimeline: {
1442
1469
  title: string;
@@ -1445,11 +1472,13 @@ export declare const bookingsUiEn: {
1445
1472
  activity: string;
1446
1473
  document: string;
1447
1474
  payment: string;
1475
+ action: string;
1448
1476
  };
1449
1477
  sourceLabels: {
1450
1478
  activity: string;
1451
1479
  document: string;
1452
1480
  payment: string;
1481
+ action: string;
1453
1482
  };
1454
1483
  empty: string;
1455
1484
  activityTitles: {
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm+CK,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAggDK,CAAA"}