@voyantjs/bookings-ui 0.35.0 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/booking-combobox.d.ts +13 -0
- package/dist/components/booking-combobox.d.ts.map +1 -0
- package/dist/components/booking-combobox.js +44 -0
- package/dist/components/booking-create-dialog.d.ts +9 -0
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +110 -90
- package/dist/components/booking-create-page.d.ts +11 -0
- package/dist/components/booking-create-page.d.ts.map +1 -0
- package/dist/components/booking-create-page.js +11 -0
- package/dist/components/booking-detail-page.d.ts.map +1 -1
- package/dist/components/booking-detail-page.js +4 -1
- package/dist/components/booking-item-list.d.ts.map +1 -1
- package/dist/components/booking-item-list.js +5 -3
- package/dist/components/booking-list-filters.d.ts +35 -0
- package/dist/components/booking-list-filters.d.ts.map +1 -0
- package/dist/components/booking-list-filters.js +148 -0
- package/dist/components/booking-list.d.ts.map +1 -1
- package/dist/components/booking-list.js +30 -46
- package/dist/components/person-picker-section.d.ts +15 -7
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +100 -21
- package/dist/components/price-breakdown-section.d.ts +16 -1
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +36 -5
- package/dist/components/product-picker-section.d.ts.map +1 -1
- package/dist/components/product-picker-section.js +38 -4
- package/dist/components/shared-room-section.d.ts +9 -8
- package/dist/components/shared-room-section.d.ts.map +1 -1
- package/dist/components/shared-room-section.js +67 -14
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +27 -16
- package/dist/i18n/en.d.ts +284 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +300 -17
- package/dist/i18n/messages.d.ts +255 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +568 -2
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +284 -1
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +301 -18
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/journey/components/booking-journey.d.ts.map +1 -1
- package/dist/journey/components/booking-journey.js +22 -13
- package/dist/journey/components/contract-preview-dialog.d.ts.map +1 -1
- package/dist/journey/components/contract-preview-dialog.js +9 -4
- package/dist/journey/components/journey-steps.d.ts.map +1 -1
- package/dist/journey/components/journey-steps.js +94 -72
- package/dist/journey/components/side-panel.d.ts.map +1 -1
- package/dist/journey/components/side-panel.js +58 -35
- package/dist/journey/components/step-header.d.ts.map +1 -1
- package/dist/journey/components/step-header.js +3 -19
- 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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
27
|
+
const [status, setStatus] = React.useState(BOOKING_STATUS_ALL);
|
|
33
28
|
const [productId, setProductId] = React.useState(null);
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
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 ===
|
|
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 !==
|
|
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(
|
|
105
|
+
setStatus(BOOKING_STATUS_ALL);
|
|
103
106
|
setProductId(null);
|
|
104
|
-
|
|
105
|
-
|
|
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" })] }),
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
55
|
+
* Billing target picker for booking create.
|
|
45
56
|
*
|
|
46
|
-
* State is fully controlled
|
|
47
|
-
*
|
|
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":"
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
*
|
|
24
|
+
* Billing target picker for booking create.
|
|
23
25
|
*
|
|
24
|
-
* State is fully controlled
|
|
25
|
-
*
|
|
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 &&
|
|
42
|
+
enabled: enabled && billingTarget === "person",
|
|
39
43
|
});
|
|
40
|
-
const
|
|
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 =
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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":"
|
|
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"}
|