@voyantjs/bookings-ui 0.34.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/components/booking-combobox.d.ts +13 -0
  2. package/dist/components/booking-combobox.d.ts.map +1 -0
  3. package/dist/components/booking-combobox.js +44 -0
  4. package/dist/components/booking-create-dialog.d.ts +9 -0
  5. package/dist/components/booking-create-dialog.d.ts.map +1 -1
  6. package/dist/components/booking-create-dialog.js +110 -90
  7. package/dist/components/booking-create-page.d.ts +11 -0
  8. package/dist/components/booking-create-page.d.ts.map +1 -0
  9. package/dist/components/booking-create-page.js +11 -0
  10. package/dist/components/booking-detail-page.d.ts.map +1 -1
  11. package/dist/components/booking-detail-page.js +4 -1
  12. package/dist/components/booking-item-list.d.ts.map +1 -1
  13. package/dist/components/booking-item-list.js +5 -3
  14. package/dist/components/booking-list-filters.d.ts +35 -0
  15. package/dist/components/booking-list-filters.d.ts.map +1 -0
  16. package/dist/components/booking-list-filters.js +148 -0
  17. package/dist/components/booking-list.d.ts.map +1 -1
  18. package/dist/components/booking-list.js +30 -46
  19. package/dist/components/person-picker-section.d.ts +15 -7
  20. package/dist/components/person-picker-section.d.ts.map +1 -1
  21. package/dist/components/person-picker-section.js +100 -21
  22. package/dist/components/price-breakdown-section.d.ts +16 -1
  23. package/dist/components/price-breakdown-section.d.ts.map +1 -1
  24. package/dist/components/price-breakdown-section.js +36 -5
  25. package/dist/components/product-picker-section.d.ts.map +1 -1
  26. package/dist/components/product-picker-section.js +38 -4
  27. package/dist/components/shared-room-section.d.ts +9 -8
  28. package/dist/components/shared-room-section.d.ts.map +1 -1
  29. package/dist/components/shared-room-section.js +67 -14
  30. package/dist/components/traveler-list.d.ts.map +1 -1
  31. package/dist/components/traveler-list.js +27 -16
  32. package/dist/i18n/en.d.ts +284 -1
  33. package/dist/i18n/en.d.ts.map +1 -1
  34. package/dist/i18n/en.js +300 -17
  35. package/dist/i18n/messages.d.ts +255 -1
  36. package/dist/i18n/messages.d.ts.map +1 -1
  37. package/dist/i18n/provider.d.ts +568 -2
  38. package/dist/i18n/provider.d.ts.map +1 -1
  39. package/dist/i18n/ro.d.ts +284 -1
  40. package/dist/i18n/ro.d.ts.map +1 -1
  41. package/dist/i18n/ro.js +301 -18
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -1
  45. package/dist/journey/components/booking-journey.d.ts.map +1 -1
  46. package/dist/journey/components/booking-journey.js +22 -13
  47. package/dist/journey/components/contract-preview-dialog.d.ts.map +1 -1
  48. package/dist/journey/components/contract-preview-dialog.js +9 -4
  49. package/dist/journey/components/journey-steps.d.ts.map +1 -1
  50. package/dist/journey/components/journey-steps.js +94 -72
  51. package/dist/journey/components/side-panel.d.ts.map +1 -1
  52. package/dist/journey/components/side-panel.js +58 -35
  53. package/dist/journey/components/step-header.d.ts.map +1 -1
  54. package/dist/journey/components/step-header.js +3 -19
  55. package/package.json +24 -20
@@ -0,0 +1,148 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { bookingStatuses } from "@voyantjs/bookings-react";
4
+ import { useOrganizations, usePeople } from "@voyantjs/crm-react";
5
+ import { useProductCategories, useProductOptions, useProducts } from "@voyantjs/products-react";
6
+ import { useSuppliers } from "@voyantjs/suppliers-react";
7
+ import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
8
+ import { Badge } from "@voyantjs/ui/components/badge";
9
+ import { Button } from "@voyantjs/ui/components/button";
10
+ import { DateRangePicker } from "@voyantjs/ui/components/date-picker";
11
+ import { Input } from "@voyantjs/ui/components/input";
12
+ import { Label } from "@voyantjs/ui/components/label";
13
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
14
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
15
+ import { ListFilter } from "lucide-react";
16
+ import * as React from "react";
17
+ import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
18
+ export const BOOKING_STATUS_ALL = "__all__";
19
+ export function BookingListFiltersPopover({ open, onOpenChange, activeFilterCount, status, onStatusChange, productId, onProductIdChange, optionId, onOptionIdChange, supplierId, onSupplierIdChange, productCategoryId, onProductCategoryIdChange, personId, onPersonIdChange, organizationId, onOrganizationIdChange, dateRange, onDateRangeChange, paxMin, onPaxMinChange, paxMax, onPaxMaxChange, onFiltersChanged, }) {
20
+ const messages = useBookingsUiMessagesOrDefault();
21
+ const filterMessages = messages.bookingList.filters;
22
+ const statusLabels = messages.common.bookingStatusLabels;
23
+ const [selectedProduct, setSelectedProduct] = React.useState(null);
24
+ const [productSearch, setProductSearch] = React.useState("");
25
+ const [selectedOption, setSelectedOption] = React.useState(null);
26
+ const [selectedSupplier, setSelectedSupplier] = React.useState(null);
27
+ const [supplierSearch, setSupplierSearch] = React.useState("");
28
+ const [selectedProductCategory, setSelectedProductCategory] = React.useState(null);
29
+ const [productCategorySearch, setProductCategorySearch] = React.useState("");
30
+ const [selectedPerson, setSelectedPerson] = React.useState(null);
31
+ const [personSearch, setPersonSearch] = React.useState("");
32
+ const [selectedOrganization, setSelectedOrganization] = React.useState(null);
33
+ const [organizationSearch, setOrganizationSearch] = React.useState("");
34
+ const { data: productsData } = useProducts({
35
+ search: productSearch || undefined,
36
+ limit: 20,
37
+ });
38
+ const products = productsData?.data ?? [];
39
+ const { data: optionsData } = useProductOptions({
40
+ productId: productId ?? undefined,
41
+ status: "active",
42
+ limit: 20,
43
+ enabled: productId !== null,
44
+ });
45
+ const productOptions = optionsData?.data ?? [];
46
+ const { data: suppliersData } = useSuppliers({
47
+ search: supplierSearch || undefined,
48
+ limit: 20,
49
+ });
50
+ const suppliers = suppliersData?.data ?? [];
51
+ const { data: productCategoriesData } = useProductCategories({
52
+ search: productCategorySearch || undefined,
53
+ active: true,
54
+ limit: 20,
55
+ });
56
+ const productCategories = productCategoriesData?.data ?? [];
57
+ const { data: peopleData } = usePeople({
58
+ search: personSearch || undefined,
59
+ limit: 20,
60
+ });
61
+ const people = peopleData?.data ?? [];
62
+ const { data: organizationsData } = useOrganizations({
63
+ search: organizationSearch || undefined,
64
+ limit: 20,
65
+ });
66
+ const organizations = organizationsData?.data ?? [];
67
+ const markChanged = () => onFiltersChanged();
68
+ return (_jsxs(Popover, { open: open, onOpenChange: onOpenChange, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4" }), filterMessages.button, activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount }))] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-status", children: filterMessages.statusLabel }), _jsxs(Select, { value: status, onValueChange: (value) => {
69
+ onStatusChange(value ?? BOOKING_STATUS_ALL);
70
+ markChanged();
71
+ }, children: [_jsx(SelectTrigger, { id: "bookings-filter-status", className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: BOOKING_STATUS_ALL, children: filterMessages.statusAll }), bookingStatuses.map((value) => (_jsx(SelectItem, { value: value, children: statusLabels[value] }, value)))] })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-product", children: filterMessages.productLabel }), _jsx(AsyncCombobox, { value: productId, onChange: (value) => {
72
+ onProductIdChange(value);
73
+ onOptionIdChange(null);
74
+ setSelectedOption(null);
75
+ if (!value) {
76
+ setSelectedProduct(null);
77
+ }
78
+ else {
79
+ const match = products.find((product) => product.id === value);
80
+ if (match)
81
+ setSelectedProduct(match);
82
+ }
83
+ markChanged();
84
+ }, items: products, selectedItem: selectedProduct, getKey: (product) => product.id, getLabel: (product) => product.name, onSearchChange: setProductSearch, placeholder: filterMessages.product, emptyText: filterMessages.productEmpty })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-option", children: filterMessages.optionLabel }), _jsx(AsyncCombobox, { value: optionId, onChange: (value) => {
85
+ onOptionIdChange(value);
86
+ if (!value)
87
+ setSelectedOption(null);
88
+ else {
89
+ const match = productOptions.find((option) => option.id === value);
90
+ if (match)
91
+ setSelectedOption(match);
92
+ }
93
+ markChanged();
94
+ }, items: productOptions, selectedItem: selectedOption, getKey: (option) => option.id, getLabel: (option) => option.name, getSecondary: (option) => option.code ?? undefined, placeholder: filterMessages.option, emptyText: productId ? filterMessages.optionEmpty : filterMessages.optionNeedsProduct, disabled: productId === null })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-category", children: filterMessages.categoryLabel }), _jsx(AsyncCombobox, { value: productCategoryId, onChange: (value) => {
95
+ onProductCategoryIdChange(value);
96
+ if (!value)
97
+ setSelectedProductCategory(null);
98
+ else {
99
+ const match = productCategories.find((category) => category.id === value);
100
+ if (match)
101
+ setSelectedProductCategory(match);
102
+ }
103
+ markChanged();
104
+ }, items: productCategories, selectedItem: selectedProductCategory, getKey: (category) => category.id, getLabel: (category) => category.name, onSearchChange: setProductCategorySearch, placeholder: filterMessages.category, emptyText: filterMessages.categoryEmpty })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-supplier", children: filterMessages.supplierLabel }), _jsx(AsyncCombobox, { value: supplierId, onChange: (value) => {
105
+ onSupplierIdChange(value);
106
+ if (!value)
107
+ setSelectedSupplier(null);
108
+ else {
109
+ const match = suppliers.find((supplier) => supplier.id === value);
110
+ if (match)
111
+ setSelectedSupplier(match);
112
+ }
113
+ markChanged();
114
+ }, items: suppliers, selectedItem: selectedSupplier, getKey: (supplier) => supplier.id, getLabel: (supplier) => supplier.name, getSecondary: (supplier) => supplier.type, onSearchChange: setSupplierSearch, placeholder: filterMessages.supplier, emptyText: filterMessages.supplierEmpty })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-person", children: filterMessages.personLabel }), _jsx(AsyncCombobox, { value: personId, onChange: (value) => {
115
+ onPersonIdChange(value);
116
+ if (!value)
117
+ setSelectedPerson(null);
118
+ else {
119
+ const match = people.find((person) => person.id === value);
120
+ if (match)
121
+ setSelectedPerson(match);
122
+ }
123
+ markChanged();
124
+ }, items: people, selectedItem: selectedPerson, getKey: (person) => person.id, getLabel: formatPersonName, getSecondary: (person) => person.email ?? undefined, onSearchChange: setPersonSearch, placeholder: filterMessages.person, emptyText: filterMessages.personEmpty })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-organization", children: filterMessages.organizationLabel }), _jsx(AsyncCombobox, { value: organizationId, onChange: (value) => {
125
+ onOrganizationIdChange(value);
126
+ if (!value)
127
+ setSelectedOrganization(null);
128
+ else {
129
+ const match = organizations.find((organization) => organization.id === value);
130
+ if (match)
131
+ setSelectedOrganization(match);
132
+ }
133
+ markChanged();
134
+ }, items: organizations, selectedItem: selectedOrganization, getKey: (organization) => organization.id, getLabel: (organization) => organization.name, getSecondary: (organization) => organization.vatNumber ?? undefined, onSearchChange: setOrganizationSearch, placeholder: filterMessages.organization, emptyText: filterMessages.organizationEmpty })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-date", children: filterMessages.dateRangeLabel }), _jsx(DateRangePicker, { value: dateRange, onChange: (value) => {
135
+ onDateRangeChange(value);
136
+ markChanged();
137
+ }, placeholder: filterMessages.dateRange, clearable: true, className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: filterMessages.paxLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, placeholder: filterMessages.paxMin, value: paxMin, onChange: (event) => {
138
+ onPaxMinChange(event.target.value);
139
+ markChanged();
140
+ }, className: "w-full", "aria-label": filterMessages.paxMin }), _jsx("span", { className: "text-muted-foreground", children: "-" }), _jsx(Input, { type: "number", min: 0, placeholder: filterMessages.paxMax, value: paxMax, onChange: (event) => {
141
+ onPaxMaxChange(event.target.value);
142
+ markChanged();
143
+ }, className: "w-full", "aria-label": filterMessages.paxMax })] })] })] }) })] }));
144
+ }
145
+ function formatPersonName(person) {
146
+ const name = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
147
+ return name || person.email || person.id;
148
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"booking-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-list.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,0BAA0B,CAAA;AAoCjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACnD;AAkBD,wBAAgB,WAAW,CAAC,EAAE,QAAa,EAAE,eAAe,EAAE,GAAE,gBAAqB,2CA4ZpF"}
1
+ {"version":3,"file":"booking-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-list.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,0BAA0B,CAAA;AAyBjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACnD;AAgBD,wBAAgB,WAAW,CAAC,EAAE,QAAa,EAAE,eAAe,EAAE,GAAE,gBAAqB,2CAmVpF"}
@@ -1,22 +1,17 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { bookingStatusBadgeVariant, bookingStatuses, useBookings, } from "@voyantjs/bookings-react";
4
- import { useProducts } from "@voyantjs/products-react";
5
- import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
3
+ import { bookingStatusBadgeVariant, useBookings, } from "@voyantjs/bookings-react";
6
4
  import { Badge } from "@voyantjs/ui/components/badge";
7
5
  import { Button } from "@voyantjs/ui/components/button";
8
- import { DateRangePicker } from "@voyantjs/ui/components/date-picker";
9
6
  import { Input } from "@voyantjs/ui/components/input";
10
7
  import { Label } from "@voyantjs/ui/components/label";
11
- import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
12
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
13
8
  import { Skeleton } from "@voyantjs/ui/components/skeleton";
14
9
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
15
- import { ArrowDown, ArrowUp, ArrowUpDown, ListFilter, Plus, Search, X } from "lucide-react";
10
+ import { ArrowDown, ArrowUp, ArrowUpDown, Plus, Search, X } from "lucide-react";
16
11
  import * as React from "react";
17
12
  import { formatMessage, useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault, } from "../i18n/provider.js";
18
13
  import { BookingDialog } from "./booking-dialog.js";
19
- const STATUS_ALL = "__all__";
14
+ import { BOOKING_STATUS_ALL, BookingListFiltersPopover } from "./booking-list-filters.js";
20
15
  const SORTABLE_COLUMNS = {
21
16
  bookingNumber: "bookingNumber",
22
17
  status: "status",
@@ -29,10 +24,13 @@ const SKELETON_ROW_COUNT = 6;
29
24
  const TABLE_COLUMN_COUNT = 7;
30
25
  export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
31
26
  const [search, setSearch] = React.useState("");
32
- const [status, setStatus] = React.useState(STATUS_ALL);
27
+ const [status, setStatus] = React.useState(BOOKING_STATUS_ALL);
33
28
  const [productId, setProductId] = React.useState(null);
34
- const [selectedProduct, setSelectedProduct] = React.useState(null);
35
- const [productSearch, setProductSearch] = React.useState("");
29
+ const [optionId, setOptionId] = React.useState(null);
30
+ const [supplierId, setSupplierId] = React.useState(null);
31
+ const [productCategoryId, setProductCategoryId] = React.useState(null);
32
+ const [personId, setPersonId] = React.useState(null);
33
+ const [organizationId, setOrganizationId] = React.useState(null);
36
34
  const [dateRange, setDateRange] = React.useState(null);
37
35
  const [paxMin, setPaxMin] = React.useState("");
38
36
  const [paxMax, setPaxMax] = React.useState("");
@@ -48,8 +46,13 @@ export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
48
46
  const paxMaxNumber = paxMax === "" ? undefined : Number.parseInt(paxMax, 10);
49
47
  const { data, isPending, isFetching, isError } = useBookings({
50
48
  search: search || undefined,
51
- status: status === STATUS_ALL ? undefined : status,
49
+ status: status === BOOKING_STATUS_ALL ? undefined : status,
52
50
  productId: productId ?? undefined,
51
+ optionId: optionId ?? undefined,
52
+ supplierId: supplierId ?? undefined,
53
+ productCategoryId: productCategoryId ?? undefined,
54
+ personId: personId ?? undefined,
55
+ organizationId: organizationId ?? undefined,
53
56
  dateFrom: dateRange?.from ?? undefined,
54
57
  dateTo: dateRange?.to ?? undefined,
55
58
  paxMin: Number.isFinite(paxMinNumber) ? paxMinNumber : undefined,
@@ -59,11 +62,6 @@ export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
59
62
  limit: pageSize,
60
63
  offset,
61
64
  });
62
- const { data: productsData } = useProducts({
63
- search: productSearch || undefined,
64
- limit: 20,
65
- });
66
- const products = productsData?.data ?? [];
67
65
  const bookings = data?.data ?? [];
68
66
  const total = data?.total ?? 0;
69
67
  const page = Math.floor(offset / pageSize) + 1;
@@ -92,17 +90,25 @@ export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
92
90
  setSortBy("createdAt");
93
91
  setSortDir("desc");
94
92
  };
95
- const activeFilterCount = (status !== STATUS_ALL ? 1 : 0) +
93
+ const activeFilterCount = (status !== BOOKING_STATUS_ALL ? 1 : 0) +
96
94
  (productId !== null ? 1 : 0) +
95
+ (optionId !== null ? 1 : 0) +
96
+ (supplierId !== null ? 1 : 0) +
97
+ (productCategoryId !== null ? 1 : 0) +
98
+ (personId !== null ? 1 : 0) +
99
+ (organizationId !== null ? 1 : 0) +
97
100
  (dateRange?.from || dateRange?.to ? 1 : 0) +
98
101
  (paxMin !== "" || paxMax !== "" ? 1 : 0);
99
102
  const hasActiveFilters = activeFilterCount > 0 || search !== "";
100
103
  const clearFilters = () => {
101
104
  setSearch("");
102
- setStatus(STATUS_ALL);
105
+ setStatus(BOOKING_STATUS_ALL);
103
106
  setProductId(null);
104
- setSelectedProduct(null);
105
- setProductSearch("");
107
+ setOptionId(null);
108
+ setSupplierId(null);
109
+ setProductCategoryId(null);
110
+ setPersonId(null);
111
+ setOrganizationId(null);
106
112
  setDateRange(null);
107
113
  setPaxMin("");
108
114
  setPaxMax("");
@@ -114,29 +120,7 @@ export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
114
120
  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) => {
115
121
  setSearch(event.target.value);
116
122
  resetOffset();
117
- }, className: "pl-9" })] }), _jsxs(Popover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4" }), filterMessages.button, activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount }))] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-status", children: filterMessages.statusLabel }), _jsxs(Select, { value: status, onValueChange: (value) => {
118
- setStatus(value ?? STATUS_ALL);
119
- resetOffset();
120
- }, children: [_jsx(SelectTrigger, { id: "bookings-filter-status", className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: STATUS_ALL, children: filterMessages.statusAll }), bookingStatuses.map((value) => (_jsx(SelectItem, { value: value, children: statusLabels[value] }, value)))] })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-product", children: filterMessages.productLabel }), _jsx(AsyncCombobox, { value: productId, onChange: (value) => {
121
- setProductId(value);
122
- if (!value)
123
- setSelectedProduct(null);
124
- else {
125
- const match = products.find((p) => p.id === value);
126
- if (match)
127
- setSelectedProduct(match);
128
- }
129
- resetOffset();
130
- }, items: products, selectedItem: selectedProduct, getKey: (p) => p.id, getLabel: (p) => p.name, onSearchChange: setProductSearch, placeholder: filterMessages.product, emptyText: filterMessages.productEmpty })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "bookings-filter-date", children: filterMessages.dateRangeLabel }), _jsx(DateRangePicker, { value: dateRange, onChange: (value) => {
131
- setDateRange(value);
132
- resetOffset();
133
- }, placeholder: filterMessages.dateRange, clearable: true, className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: filterMessages.paxLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, placeholder: filterMessages.paxMin, value: paxMin, onChange: (event) => {
134
- setPaxMin(event.target.value);
135
- resetOffset();
136
- }, className: "w-full", "aria-label": filterMessages.paxMin }), _jsx("span", { className: "text-muted-foreground", children: "\u2013" }), _jsx(Input, { type: "number", min: 0, placeholder: filterMessages.paxMax, value: paxMax, onChange: (event) => {
137
- setPaxMax(event.target.value);
138
- resetOffset();
139
- }, className: "w-full", "aria-label": filterMessages.paxMax })] })] })] }) })] }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), filterMessages.clear] })), _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => {
123
+ }, className: "pl-9" })] }), _jsx(BookingListFiltersPopover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, activeFilterCount: activeFilterCount, status: status, onStatusChange: setStatus, productId: productId, onProductIdChange: setProductId, optionId: optionId, onOptionIdChange: setOptionId, 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 && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4" }), filterMessages.clear] })), _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: () => {
140
124
  setEditing(undefined);
141
125
  setDialogOpen(true);
142
126
  }, 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.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(TableHead, { children: _jsx(SortHeader, { label: columnMessages.endDate, field: SORTABLE_COLUMNS.endDate, 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: formatBookingItems(booking, messages.bookingList.itemsMore) }), _jsx(TableCell, { children: _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: statusLabels[booking.status] }) }), _jsx(TableCell, { children: booking.sellAmountCents == null
@@ -147,10 +131,10 @@ export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
147
131
  })} ${booking.sellCurrency}` }), _jsx(TableCell, { children: booking.pax ?? "—" }), _jsx(TableCell, { children: formatBookingDateTime(booking.startsAt ?? booking.startDate, formatDateTime) }), _jsx(TableCell, { children: 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, {
148
132
  count: bookings.length,
149
133
  total,
150
- }) }), _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: "Previous" }), _jsx("span", { children: formatMessage(messages.bookingList.pageSummary, {
134
+ }) }), _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, {
151
135
  page,
152
136
  pageCount,
153
- }) }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + pageSize >= total, onClick: () => setOffset((prev) => prev + pageSize), children: "Next" })] })] }), _jsx(BookingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, booking: editing, onSuccess: (booking) => {
137
+ }) }), _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) => {
154
138
  onSelectBooking?.(booking);
155
139
  } })] }));
156
140
  }
@@ -1,4 +1,5 @@
1
1
  export type PersonPickerMode = "existing" | "new";
2
+ export type BillingTargetMode = "person" | "organization";
2
3
  export interface NewPersonValue {
3
4
  firstName: string;
4
5
  lastName: string;
@@ -6,6 +7,7 @@ export interface NewPersonValue {
6
7
  phone: string;
7
8
  }
8
9
  export interface PersonPickerValue {
10
+ billTo?: BillingTargetMode;
9
11
  mode: PersonPickerMode;
10
12
  /** Set when mode === "existing". */
11
13
  personId: string;
@@ -23,10 +25,18 @@ export interface PersonPickerSectionProps {
23
25
  showOrganization?: boolean;
24
26
  labels?: {
25
27
  person?: string;
28
+ organization?: string;
29
+ billTo?: string;
30
+ billToPerson?: string;
31
+ billToOrganization?: string;
26
32
  createNewPerson?: string;
33
+ createNewOrganization?: string;
34
+ createPersonSheetTitle?: string;
35
+ createOrganizationSheetTitle?: string;
27
36
  selectExistingPerson?: string;
28
37
  personSearchPlaceholder?: string;
29
38
  personSelectPlaceholder?: string;
39
+ personEmpty?: string;
30
40
  firstName?: string;
31
41
  firstNamePlaceholder?: string;
32
42
  lastName?: string;
@@ -35,19 +45,17 @@ export interface PersonPickerSectionProps {
35
45
  emailPlaceholder?: string;
36
46
  phone?: string;
37
47
  phonePlaceholder?: string;
38
- organization?: string;
39
48
  organizationSearchPlaceholder?: string;
49
+ organizationSelectPlaceholder?: string;
50
+ organizationEmpty?: string;
40
51
  organizationNone?: string;
41
52
  };
42
53
  }
43
54
  /**
44
- * Person picker with inline-create + optional organization attachment.
55
+ * Billing target picker for booking create.
45
56
  *
46
- * State is fully controlled the caller owns both existing-person selection
47
- * and the inline-create form. The section does *not* call any mutation itself;
48
- * the parent decides when to commit a newly-created person (typically at
49
- * submit time, so we don't leak orphan CRM records when the dialog is
50
- * cancelled).
57
+ * State is fully controlled. The embedded create sheets use the CRM forms and
58
+ * select the newly-created person or organization after save.
51
59
  */
52
60
  export declare function PersonPickerSection({ value, onChange, enabled, showOrganization, labels, }: PersonPickerSectionProps): import("react/jsx-runtime").JSX.Element;
53
61
  //# sourceMappingURL=person-picker-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"person-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/person-picker-section.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,CAAA;AAEjD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,gBAAgB,CAAA;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,SAAS,EAAE,cAAc,CAAA;IACzB,yCAAyC;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAK5B,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAKpC,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,gBAAuB,EACvB,MAAM,GACP,EAAE,wBAAwB,2CA4J1B"}
1
+ {"version":3,"file":"person-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/person-picker-section.tsx"],"names":[],"mappings":"AAiCA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,KAAK,CAAA;AACjD,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,cAAc,CAAA;AAEzD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,IAAI,EAAE,gBAAgB,CAAA;IACtB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,SAAS,EAAE,cAAc,CAAA;IACzB,yCAAyC;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,eAAO,MAAM,cAAc,EAAE,cAK5B,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAMpC,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,oBAAoB,CAAC,EAAE,MAAM,CAAA;QAC7B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;QAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,6BAA6B,CAAC,EAAE,MAAM,CAAA;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,gBAAuB,EACvB,MAAM,GACP,EAAE,wBAAwB,2CAiQ1B"}
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useOrganizations, usePeople } from "@voyantjs/crm-react";
4
- import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
5
- import { UserPlus } from "lucide-react";
3
+ import { useOrganization, useOrganizations, usePeople, usePerson, } from "@voyantjs/crm-react";
4
+ import { OrganizationForm, PersonForm } from "@voyantjs/crm-ui";
5
+ import { Button, Label, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
6
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
7
+ import { Building2, User, UserPlus } from "lucide-react";
6
8
  import * as React from "react";
7
9
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
8
- const ORG_NONE = "__none__";
9
10
  export const emptyNewPerson = {
10
11
  firstName: "",
11
12
  lastName: "",
@@ -13,43 +14,121 @@ export const emptyNewPerson = {
13
14
  phone: "",
14
15
  };
15
16
  export const emptyPersonPickerValue = {
17
+ billTo: "person",
16
18
  mode: "existing",
17
19
  personId: "",
18
20
  newPerson: emptyNewPerson,
19
21
  organizationId: null,
20
22
  };
21
23
  /**
22
- * Person picker with inline-create + optional organization attachment.
24
+ * Billing target picker for booking create.
23
25
  *
24
- * State is fully controlled the caller owns both existing-person selection
25
- * and the inline-create form. The section does *not* call any mutation itself;
26
- * the parent decides when to commit a newly-created person (typically at
27
- * submit time, so we don't leak orphan CRM records when the dialog is
28
- * cancelled).
26
+ * State is fully controlled. The embedded create sheets use the CRM forms and
27
+ * select the newly-created person or organization after save.
29
28
  */
30
29
  export function PersonPickerSection({ value, onChange, enabled = true, showOrganization = true, labels, }) {
31
30
  const [personSearch, setPersonSearch] = React.useState("");
32
31
  const [orgSearch, setOrgSearch] = React.useState("");
32
+ const [personInputValue, setPersonInputValue] = React.useState("");
33
+ const [orgInputValue, setOrgInputValue] = React.useState("");
34
+ const [personSheetOpen, setPersonSheetOpen] = React.useState(false);
35
+ const [orgSheetOpen, setOrgSheetOpen] = React.useState(false);
33
36
  const messages = useBookingsUiMessagesOrDefault();
34
37
  const merged = { ...messages.personPickerSection.labels, ...labels };
38
+ const billingTarget = value.billTo ?? "person";
35
39
  const { data: peopleData } = usePeople({
36
40
  search: personSearch || undefined,
37
41
  limit: 20,
38
- enabled: enabled && value.mode === "existing",
42
+ enabled: enabled && billingTarget === "person",
39
43
  });
40
- const people = peopleData?.data ?? [];
44
+ const selectedPersonQuery = usePerson(value.personId || undefined, {
45
+ enabled: enabled && billingTarget === "person" && Boolean(value.personId),
46
+ });
47
+ const people = React.useMemo(() => {
48
+ const map = new Map();
49
+ for (const person of peopleData?.data ?? [])
50
+ map.set(person.id, person);
51
+ if (selectedPersonQuery.data)
52
+ map.set(selectedPersonQuery.data.id, selectedPersonQuery.data);
53
+ return Array.from(map.values());
54
+ }, [peopleData?.data, selectedPersonQuery.data]);
55
+ const peopleMap = React.useMemo(() => new Map(people.map((person) => [person.id, person])), [people]);
41
56
  const { data: orgsData } = useOrganizations({
42
57
  search: orgSearch || undefined,
43
58
  limit: 20,
44
- enabled: enabled && showOrganization,
59
+ enabled: enabled && showOrganization && billingTarget === "organization",
60
+ });
61
+ const selectedOrgQuery = useOrganization(value.organizationId || undefined, {
62
+ enabled: enabled && billingTarget === "organization" && Boolean(value.organizationId),
45
63
  });
46
- const orgs = orgsData?.data ?? [];
64
+ const orgs = React.useMemo(() => {
65
+ const map = new Map();
66
+ for (const org of orgsData?.data ?? [])
67
+ map.set(org.id, org);
68
+ if (selectedOrgQuery.data)
69
+ map.set(selectedOrgQuery.data.id, selectedOrgQuery.data);
70
+ return Array.from(map.values());
71
+ }, [orgsData?.data, selectedOrgQuery.data]);
72
+ const orgsMap = React.useMemo(() => new Map(orgs.map((org) => [org.id, org])), [orgs]);
47
73
  const setPerson = (patch) => onChange({ ...value, ...patch });
48
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPerson({ mode: value.mode === "existing" ? "new" : "existing" }), children: value.mode === "existing" ? (_jsxs(_Fragment, { children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })) : (merged.selectExistingPerson) })] }), value.mode === "existing" ? (_jsxs(_Fragment, { children: [_jsx(Input, { placeholder: merged.personSearchPlaceholder, value: personSearch, onChange: (e) => setPersonSearch(e.target.value) }), _jsxs(Select, { items: people.map((p) => ({
49
- label: `${p.firstName} ${p.lastName}${p.email ? ` · ${p.email}` : ""}`,
50
- value: p.id,
51
- })), value: value.personId, onValueChange: (v) => setPerson({ personId: v ?? "" }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: merged.personSelectPlaceholder }) }), _jsx(SelectContent, { children: people.map((p) => (_jsxs(SelectItem, { value: p.id, children: [p.firstName, " ", p.lastName, p.email ? ` · ${p.email}` : ""] }, p.id))) })] })] })) : (_jsxs("div", { className: "grid grid-cols-2 gap-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.firstName }), _jsx(Input, { value: value.newPerson.firstName, onChange: (e) => setPerson({ newPerson: { ...value.newPerson, firstName: e.target.value } }), placeholder: merged.firstNamePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.lastName }), _jsx(Input, { value: value.newPerson.lastName, onChange: (e) => setPerson({ newPerson: { ...value.newPerson, lastName: e.target.value } }), placeholder: merged.lastNamePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.email }), _jsx(Input, { type: "email", value: value.newPerson.email, onChange: (e) => setPerson({ newPerson: { ...value.newPerson, email: e.target.value } }), placeholder: merged.emailPlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.phone }), _jsx(Input, { value: value.newPerson.phone, onChange: (e) => setPerson({ newPerson: { ...value.newPerson, phone: e.target.value } }), placeholder: merged.phonePlaceholder })] })] }))] }), showOrganization && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.organization }), _jsx(Input, { placeholder: merged.organizationSearchPlaceholder, value: orgSearch, onChange: (e) => setOrgSearch(e.target.value) }), _jsxs(Select, { items: [
52
- { label: merged.organizationNone, value: ORG_NONE },
53
- ...orgs.map((o) => ({ label: o.name, value: o.id })),
54
- ], value: value.organizationId ?? ORG_NONE, onValueChange: (v) => setPerson({ organizationId: v === ORG_NONE ? null : (v ?? null) }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: merged.organizationNone }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: ORG_NONE, children: merged.organizationNone }), orgs.map((o) => (_jsx(SelectItem, { value: o.id, children: o.name }, o.id)))] })] })] }))] }));
74
+ const selectedPersonLabel = value.personId ? formatPerson(peopleMap.get(value.personId)) : "";
75
+ const selectedOrgLabel = value.organizationId
76
+ ? (orgsMap.get(value.organizationId)?.name ?? "")
77
+ : "";
78
+ React.useEffect(() => {
79
+ if (selectedPersonLabel)
80
+ setPersonInputValue(selectedPersonLabel);
81
+ }, [selectedPersonLabel]);
82
+ React.useEffect(() => {
83
+ if (selectedOrgLabel)
84
+ setOrgInputValue(selectedOrgLabel);
85
+ }, [selectedOrgLabel]);
86
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.billTo }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs(Button, { type: "button", variant: billingTarget === "person" ? "default" : "outline", onClick: () => setPerson({ billTo: "person", organizationId: null }), disabled: !enabled, children: [_jsx(User, { className: "mr-2 h-4 w-4" }), merged.billToPerson] }), _jsxs(Button, { type: "button", variant: billingTarget === "organization" ? "default" : "outline", onClick: () => setPerson({ billTo: "organization", personId: "" }), disabled: !enabled || !showOrganization, children: [_jsx(Building2, { className: "mr-2 h-4 w-4" }), merged.billToOrganization] })] })] }), billingTarget === "person" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.person, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setPersonSheetOpen(true), disabled: !enabled, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: value.personId || null, inputValue: personInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => formatPerson(peopleMap.get(id)), onInputValueChange: (next) => {
87
+ setPersonInputValue(next);
88
+ setPersonSearch(next);
89
+ if (!next)
90
+ setPerson({ personId: "" });
91
+ }, onValueChange: (next) => {
92
+ const personId = next ?? "";
93
+ setPerson({ personId });
94
+ setPersonInputValue(personId ? formatPerson(peopleMap.get(personId)) : "");
95
+ }, children: [_jsx(ComboboxInput, { placeholder: merged.personSearchPlaceholder, showClear: !!value.personId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.personEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
96
+ const person = peopleMap.get(id);
97
+ if (!person)
98
+ return null;
99
+ return (_jsx(ComboboxItem, { value: person.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: formatPersonName(person) }), person.email ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: person.email })) : null] }) }, person.id));
100
+ } }) })] })] })] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs(Label, { children: [merged.organization, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setOrgSheetOpen(true), disabled: !enabled, children: [_jsx(Building2, { className: "mr-1 h-3.5 w-3.5" }), merged.createNewOrganization] })] }), _jsxs(Combobox, { items: orgs.map((org) => org.id), value: value.organizationId ?? null, inputValue: orgInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => orgsMap.get(id)?.name ?? "", onInputValueChange: (next) => {
101
+ setOrgInputValue(next);
102
+ setOrgSearch(next);
103
+ if (!next)
104
+ setPerson({ organizationId: null });
105
+ }, onValueChange: (next) => {
106
+ const organizationId = next ?? null;
107
+ setPerson({ organizationId });
108
+ setOrgInputValue(organizationId ? (orgsMap.get(organizationId)?.name ?? "") : "");
109
+ }, children: [_jsx(ComboboxInput, { placeholder: merged.organizationSearchPlaceholder, showClear: !!value.organizationId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.organizationEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
110
+ const org = orgsMap.get(id);
111
+ if (!org)
112
+ return null;
113
+ return (_jsx(ComboboxItem, { value: org.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: org.name }), org.legalName ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: org.legalName })) : null] }) }, org.id));
114
+ } }) })] })] })] })), _jsx(Sheet, { open: personSheetOpen, onOpenChange: setPersonSheetOpen, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: merged.createPersonSheetTitle }) }), _jsx(SheetBody, { children: _jsx(PersonForm, { mode: { kind: "create" }, onCancel: () => setPersonSheetOpen(false), onSuccess: (saved) => {
115
+ setPerson({ billTo: "person", personId: saved.id, organizationId: null });
116
+ setPersonInputValue(formatPerson(saved));
117
+ setPersonSheetOpen(false);
118
+ } }) })] }) }), _jsx(Sheet, { open: orgSheetOpen, onOpenChange: setOrgSheetOpen, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: merged.createOrganizationSheetTitle }) }), _jsx(SheetBody, { children: _jsx(OrganizationForm, { mode: { kind: "create" }, onCancel: () => setOrgSheetOpen(false), onSuccess: (saved) => {
119
+ setPerson({ billTo: "organization", personId: "", organizationId: saved.id });
120
+ setOrgInputValue(saved.name);
121
+ setOrgSheetOpen(false);
122
+ } }) })] }) })] }));
123
+ }
124
+ function formatPersonName(person) {
125
+ if (!person)
126
+ return "";
127
+ return [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
128
+ }
129
+ function formatPerson(person) {
130
+ if (!person)
131
+ return "";
132
+ const name = formatPersonName(person);
133
+ return person.email ? `${name} · ${person.email}` : name;
55
134
  }
@@ -13,6 +13,14 @@ export interface PriceBreakdownLine {
13
13
  tierLabel: string | null;
14
14
  isGroupRate: boolean;
15
15
  }
16
+ export interface PriceBreakdownValue {
17
+ catalogAmountCents: number | null;
18
+ confirmedAmountCents: number | null;
19
+ currency: string | null;
20
+ priceOverrideReason: string;
21
+ isManualOverride: boolean;
22
+ requiresReason: boolean;
23
+ }
16
24
  export interface PriceBreakdownSectionProps {
17
25
  productId?: string;
18
26
  optionId?: string | null;
@@ -30,7 +38,14 @@ export interface PriceBreakdownSectionProps {
30
38
  groupRate?: string;
31
39
  empty?: string;
32
40
  noPricing?: string;
41
+ confirmedTotal?: string;
42
+ manualTotal?: string;
43
+ useCatalogTotal?: string;
44
+ overrideReason?: string;
45
+ overrideReasonPlaceholder?: string;
46
+ overrideReasonRequired?: string;
33
47
  };
48
+ onChange?: (value: PriceBreakdownValue) => void;
34
49
  }
35
50
  /**
36
51
  * Live price-breakdown preview for booking-create flows. Read-only — uses
@@ -44,5 +59,5 @@ export interface PriceBreakdownSectionProps {
44
59
  * - `free` / `included` — render 0.00 without an on-request badge.
45
60
  * - `on_request` / anything else — render "On request"; total excludes it.
46
61
  */
47
- export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
62
+ export declare function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, onChange, }: PriceBreakdownSectionProps): import("react/jsx-runtime").JSX.Element | null;
48
63
  //# sourceMappingURL=price-breakdown-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,GACP,EAAE,0BAA0B,kDAkL5B"}
1
+ {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAoQ5B"}