@voyantjs/bookings-ui 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/components/booking-activity-timeline.d.ts +5 -0
- package/dist/components/booking-activity-timeline.d.ts.map +1 -0
- package/dist/components/booking-activity-timeline.js +83 -0
- package/dist/components/booking-cancellation-dialog.d.ts +18 -0
- package/dist/components/booking-cancellation-dialog.d.ts.map +1 -0
- package/dist/components/booking-cancellation-dialog.js +80 -0
- package/dist/components/booking-create-dialog.d.ts +21 -0
- package/dist/components/booking-create-dialog.d.ts.map +1 -0
- package/dist/components/booking-create-dialog.js +313 -0
- package/dist/components/booking-dialog.d.ts +23 -0
- package/dist/components/booking-dialog.d.ts.map +1 -0
- package/dist/components/booking-dialog.js +108 -0
- package/dist/components/booking-document-dialog.d.ts +8 -0
- package/dist/components/booking-document-dialog.d.ts.map +1 -0
- package/dist/components/booking-document-dialog.js +67 -0
- package/dist/components/booking-document-list.d.ts +5 -0
- package/dist/components/booking-document-list.d.ts.map +1 -0
- package/dist/components/booking-document-list.js +38 -0
- package/dist/components/booking-group-link-dialog.d.ts +10 -0
- package/dist/components/booking-group-link-dialog.d.ts.map +1 -0
- package/dist/components/booking-group-link-dialog.js +68 -0
- package/dist/components/booking-group-section.d.ts +17 -0
- package/dist/components/booking-group-section.d.ts.map +1 -0
- package/dist/components/booking-group-section.js +31 -0
- package/dist/components/booking-guarantee-dialog.d.ts +10 -0
- package/dist/components/booking-guarantee-dialog.d.ts.map +1 -0
- package/dist/components/booking-guarantee-dialog.js +101 -0
- package/dist/components/booking-guarantee-list.d.ts +5 -0
- package/dist/components/booking-guarantee-list.d.ts.map +1 -0
- package/dist/components/booking-guarantee-list.js +45 -0
- package/dist/components/booking-item-dialog.d.ts +10 -0
- package/dist/components/booking-item-dialog.d.ts.map +1 -0
- package/dist/components/booking-item-dialog.js +119 -0
- package/dist/components/booking-item-list.d.ts +5 -0
- package/dist/components/booking-item-list.d.ts.map +1 -0
- package/dist/components/booking-item-list.js +50 -0
- package/dist/components/booking-item-travelers.d.ts +6 -0
- package/dist/components/booking-item-travelers.d.ts.map +1 -0
- package/dist/components/booking-item-travelers.js +50 -0
- package/dist/components/booking-list.d.ts +7 -0
- package/dist/components/booking-list.d.ts.map +1 -0
- package/dist/components/booking-list.js +47 -0
- package/dist/components/booking-notes.d.ts +5 -0
- package/dist/components/booking-notes.d.ts.map +1 -0
- package/dist/components/booking-notes.js +16 -0
- package/dist/components/booking-payment-schedule-dialog.d.ts +10 -0
- package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -0
- package/dist/components/booking-payment-schedule-dialog.js +77 -0
- package/dist/components/booking-payment-schedule-list.d.ts +5 -0
- package/dist/components/booking-payment-schedule-list.d.ts.map +1 -0
- package/dist/components/booking-payment-schedule-list.js +43 -0
- package/dist/components/booking-payments-summary.d.ts +5 -0
- package/dist/components/booking-payments-summary.d.ts.map +1 -0
- package/dist/components/booking-payments-summary.js +19 -0
- package/dist/components/file-dropzone.d.ts +25 -0
- package/dist/components/file-dropzone.d.ts.map +1 -0
- package/dist/components/file-dropzone.js +92 -0
- package/dist/components/passengers-section.d.ts +72 -0
- package/dist/components/passengers-section.d.ts.map +1 -0
- package/dist/components/passengers-section.js +74 -0
- package/dist/components/payment-schedule-section.d.ts +62 -0
- package/dist/components/payment-schedule-section.d.ts.map +1 -0
- package/dist/components/payment-schedule-section.js +88 -0
- package/dist/components/person-picker-section.d.ts +53 -0
- package/dist/components/person-picker-section.d.ts.map +1 -0
- package/dist/components/person-picker-section.js +71 -0
- package/dist/components/price-breakdown-section.d.ts +48 -0
- package/dist/components/price-breakdown-section.d.ts.map +1 -0
- package/dist/components/price-breakdown-section.js +165 -0
- package/dist/components/product-picker-section.d.ts +27 -0
- package/dist/components/product-picker-section.d.ts.map +1 -0
- package/dist/components/product-picker-section.js +41 -0
- package/dist/components/rooms-stepper-section.d.ts +45 -0
- package/dist/components/rooms-stepper-section.d.ts.map +1 -0
- package/dist/components/rooms-stepper-section.js +60 -0
- package/dist/components/shared-room-section.d.ts +37 -0
- package/dist/components/shared-room-section.d.ts.map +1 -0
- package/dist/components/shared-room-section.js +40 -0
- package/dist/components/status-change-dialog.d.ts +10 -0
- package/dist/components/status-change-dialog.d.ts.map +1 -0
- package/dist/components/status-change-dialog.js +41 -0
- package/dist/components/supplier-status-dialog.d.ts +10 -0
- package/dist/components/supplier-status-dialog.d.ts.map +1 -0
- package/dist/components/supplier-status-dialog.js +77 -0
- package/dist/components/supplier-status-list.d.ts +5 -0
- package/dist/components/supplier-status-list.d.ts.map +1 -0
- package/dist/components/supplier-status-list.js +33 -0
- package/dist/components/traveler-dialog.d.ts +10 -0
- package/dist/components/traveler-dialog.d.ts.map +1 -0
- package/dist/components/traveler-dialog.js +64 -0
- package/dist/components/traveler-list.d.ts +5 -0
- package/dist/components/traveler-list.d.ts.map +1 -0
- package/dist/components/traveler-list.js +32 -0
- package/dist/components/voucher-picker-section.d.ts +50 -0
- package/dist/components/voucher-picker-section.d.ts.map +1 -0
- package/dist/components/voucher-picker-section.js +94 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/package.json +76 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { usePricingPreview } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Label } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
const DEFAULT_LABELS = {
|
|
7
|
+
heading: "Price breakdown",
|
|
8
|
+
total: "Total",
|
|
9
|
+
onRequest: "On request",
|
|
10
|
+
groupRate: "group rate",
|
|
11
|
+
empty: "Pick units above to see the breakdown.",
|
|
12
|
+
noPricing: "No pricing catalog available for this product.",
|
|
13
|
+
};
|
|
14
|
+
function formatCents(cents, currency) {
|
|
15
|
+
if (cents === null)
|
|
16
|
+
return "";
|
|
17
|
+
const major = (cents / 100).toFixed(2);
|
|
18
|
+
return currency ? `${major} ${currency}` : major;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Picks the tier whose quantity range contains `qty`. Tiers are expected
|
|
22
|
+
* oldest-to-newest, `minQuantity`-ascending. Ties are broken by first-match —
|
|
23
|
+
* the server sorts by sort_order and then min_quantity, so the selection here
|
|
24
|
+
* mirrors the storefront engine.
|
|
25
|
+
*/
|
|
26
|
+
function matchTier(tiers, qty) {
|
|
27
|
+
for (const tier of tiers) {
|
|
28
|
+
if (qty >= tier.minQuantity && (tier.maxQuantity === null || qty <= tier.maxQuantity)) {
|
|
29
|
+
return tier;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Live price-breakdown preview for booking-create flows. Read-only — uses
|
|
36
|
+
* `usePricingPreview` (#237) to fetch the catalog-resolved snapshot the
|
|
37
|
+
* storefront also uses, then computes lines against the operator's current
|
|
38
|
+
* unit quantities so the operator sees the same numbers the customer would.
|
|
39
|
+
*
|
|
40
|
+
* ### Pricing mode handling
|
|
41
|
+
*
|
|
42
|
+
* - `per_unit` — multiply the matched tier's `sellAmountCents` by quantity.
|
|
43
|
+
* - `free` / `included` — render 0.00 without an on-request badge.
|
|
44
|
+
* - `on_request` / anything else — render "On request"; total excludes it.
|
|
45
|
+
*/
|
|
46
|
+
export function PriceBreakdownSection({ productId, optionId, unitQuantities, catalogId, labels, }) {
|
|
47
|
+
const merged = { ...DEFAULT_LABELS, ...labels };
|
|
48
|
+
const preview = usePricingPreview({
|
|
49
|
+
productId: productId ?? "",
|
|
50
|
+
optionId: optionId ?? null,
|
|
51
|
+
catalogId: catalogId ?? null,
|
|
52
|
+
enabled: Boolean(productId),
|
|
53
|
+
});
|
|
54
|
+
const snapshot = preview.data?.data;
|
|
55
|
+
const currency = snapshot?.catalog.currencyCode ?? null;
|
|
56
|
+
const { lines, total } = React.useMemo(() => {
|
|
57
|
+
const out = [];
|
|
58
|
+
let runningTotal = 0;
|
|
59
|
+
let anyOnRequest = false;
|
|
60
|
+
if (!snapshot)
|
|
61
|
+
return { lines: out, total: null };
|
|
62
|
+
// Pick the default price rule for the resolved option (snapshot already
|
|
63
|
+
// filters options by the caller's optionId; rules keep isDefault-first
|
|
64
|
+
// ordering from the server).
|
|
65
|
+
const rulesByOption = new Map();
|
|
66
|
+
for (const rule of snapshot.rules) {
|
|
67
|
+
const existing = rulesByOption.get(rule.optionId) ?? [];
|
|
68
|
+
existing.push(rule);
|
|
69
|
+
rulesByOption.set(rule.optionId, existing);
|
|
70
|
+
}
|
|
71
|
+
const unitPricesByUnit = new Map();
|
|
72
|
+
for (const up of snapshot.unitPrices) {
|
|
73
|
+
if (!unitPricesByUnit.has(up.unitId)) {
|
|
74
|
+
unitPricesByUnit.set(up.unitId, up);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [unitId, quantity] of Object.entries(unitQuantities)) {
|
|
78
|
+
if (quantity <= 0)
|
|
79
|
+
continue;
|
|
80
|
+
const up = unitPricesByUnit.get(unitId);
|
|
81
|
+
if (!up) {
|
|
82
|
+
// The unit isn't priced in this catalog — show it on-request so the
|
|
83
|
+
// operator knows they need to quote manually.
|
|
84
|
+
out.push({
|
|
85
|
+
unitId,
|
|
86
|
+
label: unitId,
|
|
87
|
+
quantity,
|
|
88
|
+
unitAmountCents: null,
|
|
89
|
+
totalAmountCents: null,
|
|
90
|
+
tierLabel: null,
|
|
91
|
+
isGroupRate: false,
|
|
92
|
+
});
|
|
93
|
+
anyOnRequest = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const label = up.unitName || unitId;
|
|
97
|
+
if (up.pricingMode === "on_request") {
|
|
98
|
+
out.push({
|
|
99
|
+
unitId,
|
|
100
|
+
label,
|
|
101
|
+
quantity,
|
|
102
|
+
unitAmountCents: null,
|
|
103
|
+
totalAmountCents: null,
|
|
104
|
+
tierLabel: merged.onRequest,
|
|
105
|
+
isGroupRate: false,
|
|
106
|
+
});
|
|
107
|
+
anyOnRequest = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (up.pricingMode === "free" || up.pricingMode === "included") {
|
|
111
|
+
out.push({
|
|
112
|
+
unitId,
|
|
113
|
+
label,
|
|
114
|
+
quantity,
|
|
115
|
+
unitAmountCents: 0,
|
|
116
|
+
totalAmountCents: 0,
|
|
117
|
+
tierLabel: null,
|
|
118
|
+
isGroupRate: false,
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// per_unit (and anything else that falls through to explicit amounts).
|
|
123
|
+
const matchedTier = matchTier(up.tiers, quantity);
|
|
124
|
+
const unitAmount = matchedTier?.sellAmountCents ?? up.sellAmountCents;
|
|
125
|
+
if (unitAmount === null) {
|
|
126
|
+
out.push({
|
|
127
|
+
unitId,
|
|
128
|
+
label,
|
|
129
|
+
quantity,
|
|
130
|
+
unitAmountCents: null,
|
|
131
|
+
totalAmountCents: null,
|
|
132
|
+
tierLabel: merged.onRequest,
|
|
133
|
+
isGroupRate: false,
|
|
134
|
+
});
|
|
135
|
+
anyOnRequest = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const lineTotal = unitAmount * quantity;
|
|
139
|
+
const isGroupRate = matchedTier !== null && matchedTier.minQuantity > 1;
|
|
140
|
+
out.push({
|
|
141
|
+
unitId,
|
|
142
|
+
label,
|
|
143
|
+
quantity,
|
|
144
|
+
unitAmountCents: unitAmount,
|
|
145
|
+
totalAmountCents: lineTotal,
|
|
146
|
+
tierLabel: isGroupRate ? merged.groupRate : null,
|
|
147
|
+
isGroupRate,
|
|
148
|
+
});
|
|
149
|
+
runningTotal += lineTotal;
|
|
150
|
+
}
|
|
151
|
+
return { lines: out, total: anyOnRequest ? null : runningTotal };
|
|
152
|
+
}, [snapshot, unitQuantities, merged.onRequest, merged.groupRate]);
|
|
153
|
+
// Empty states
|
|
154
|
+
if (!productId)
|
|
155
|
+
return null;
|
|
156
|
+
if (preview.isError || (preview.isSuccess && !snapshot)) {
|
|
157
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noPricing })] }));
|
|
158
|
+
}
|
|
159
|
+
if (lines.length === 0) {
|
|
160
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.empty })] }));
|
|
161
|
+
}
|
|
162
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-col gap-1.5", children: lines.map((line) => (_jsxs("div", { className: "flex items-baseline justify-between text-sm", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsxs("span", { className: "tabular-nums", children: [line.quantity, "\u00D7"] }), _jsx("span", { children: line.label }), line.tierLabel ? (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u00B7 ", line.tierLabel] })) : null] }), _jsx("div", { className: "tabular-nums", children: line.totalAmountCents === null
|
|
163
|
+
? merged.onRequest
|
|
164
|
+
: formatCents(line.totalAmountCents, currency) })] }, line.unitId))) }), _jsxs("div", { className: "mt-1 flex items-baseline justify-between border-t pt-2 text-sm font-medium", children: [_jsx("span", { children: merged.total }), _jsx("span", { className: "tabular-nums", children: total === null ? merged.onRequest : formatCents(total, currency) })] })] }));
|
|
165
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ProductPickerValue {
|
|
2
|
+
productId: string;
|
|
3
|
+
/** `null` means "no specific option" — the product has options but none was picked. */
|
|
4
|
+
optionId: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface ProductPickerSectionProps {
|
|
7
|
+
value: ProductPickerValue;
|
|
8
|
+
onChange: (value: ProductPickerValue) => void;
|
|
9
|
+
/** When true, skip data fetches (dialog closed / parent gated). */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** When true, hide the product picker and fix the productId (e.g., launched from a product page). */
|
|
12
|
+
lockProduct?: boolean;
|
|
13
|
+
labels?: {
|
|
14
|
+
product?: string;
|
|
15
|
+
productSearchPlaceholder?: string;
|
|
16
|
+
productSelectPlaceholder?: string;
|
|
17
|
+
option?: string;
|
|
18
|
+
optionNone?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Controlled product + option picker. Splits `value` + `onChange` so apps can
|
|
23
|
+
* replace the whole section (e.g., with a typeahead against a custom catalog)
|
|
24
|
+
* without reimplementing the cascade logic, or keep this one and swap labels.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ProductPickerSection({ value, onChange, enabled, lockProduct, labels, }: ProductPickerSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=product-picker-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAUD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAiF3B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useProductOptions, useProducts } from "@voyantjs/products-react";
|
|
4
|
+
import { Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
const OPTION_NONE = "__none__";
|
|
7
|
+
const DEFAULT_LABELS = {
|
|
8
|
+
product: "Product",
|
|
9
|
+
productSearchPlaceholder: "Search products...",
|
|
10
|
+
productSelectPlaceholder: "Select a product...",
|
|
11
|
+
option: "Option",
|
|
12
|
+
optionNone: "No specific option",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Controlled product + option picker. Splits `value` + `onChange` so apps can
|
|
16
|
+
* replace the whole section (e.g., with a typeahead against a custom catalog)
|
|
17
|
+
* without reimplementing the cascade logic, or keep this one and swap labels.
|
|
18
|
+
*/
|
|
19
|
+
export function ProductPickerSection({ value, onChange, enabled = true, lockProduct = false, labels, }) {
|
|
20
|
+
const [productSearch, setProductSearch] = React.useState("");
|
|
21
|
+
const merged = { ...DEFAULT_LABELS, ...labels };
|
|
22
|
+
const { data: productsData } = useProducts({
|
|
23
|
+
search: productSearch || undefined,
|
|
24
|
+
limit: 20,
|
|
25
|
+
enabled: enabled && !lockProduct,
|
|
26
|
+
});
|
|
27
|
+
const products = productsData?.data ?? [];
|
|
28
|
+
const { data: optionsData } = useProductOptions({
|
|
29
|
+
productId: value.productId || undefined,
|
|
30
|
+
limit: 50,
|
|
31
|
+
enabled: enabled && Boolean(value.productId),
|
|
32
|
+
});
|
|
33
|
+
const options = optionsData?.data ?? [];
|
|
34
|
+
return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Input, { placeholder: merged.productSearchPlaceholder, value: productSearch, onChange: (e) => setProductSearch(e.target.value) }), _jsxs(Select, { items: products.map((p) => ({ label: p.name, value: p.id })), value: value.productId, onValueChange: (v) => onChange({ productId: v ?? "", optionId: null }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: merged.productSelectPlaceholder }) }), _jsx(SelectContent, { children: products.map((p) => (_jsx(SelectItem, { value: p.id, children: p.name }, p.id))) })] })] })), value.productId && options.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: merged.option }), _jsxs(Select, { items: [
|
|
35
|
+
{ label: merged.optionNone, value: OPTION_NONE },
|
|
36
|
+
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
37
|
+
], value: value.optionId ?? OPTION_NONE, onValueChange: (v) => onChange({
|
|
38
|
+
productId: value.productId,
|
|
39
|
+
optionId: v === OPTION_NONE ? null : (v ?? null),
|
|
40
|
+
}), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: OPTION_NONE, children: merged.optionNone }), options.map((o) => (_jsx(SelectItem, { value: o.id, children: o.name }, o.id)))] })] })] }))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Quantity per option_unit id; omitted ids are treated as 0. */
|
|
2
|
+
export interface RoomsStepperValue {
|
|
3
|
+
quantities: Record<string, number>;
|
|
4
|
+
}
|
|
5
|
+
export declare const emptyRoomsStepperValue: RoomsStepperValue;
|
|
6
|
+
export interface RoomsStepperSectionProps {
|
|
7
|
+
value: RoomsStepperValue;
|
|
8
|
+
onChange: (value: RoomsStepperValue) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Departure the operator picked. Section renders nothing until a slot is
|
|
11
|
+
* chosen — per-unit availability is a property of the slot, not the
|
|
12
|
+
* product, so there's nothing to show before then.
|
|
13
|
+
*/
|
|
14
|
+
slotId?: string;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
labels?: {
|
|
17
|
+
heading?: string;
|
|
18
|
+
noSlot?: string;
|
|
19
|
+
noUnits?: string;
|
|
20
|
+
remaining?: string;
|
|
21
|
+
unlimited?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
26
|
+
* `GET /v1/availability/slots/:id/unit-availability` from #235 so the
|
|
27
|
+
* operator sees authoritative "3 doubles available" numbers instead of
|
|
28
|
+
* client-side math against a sampled booking list.
|
|
29
|
+
*
|
|
30
|
+
* The section only tracks **intent** (how many of each unit the operator
|
|
31
|
+
* wants to book). Actual hold/reservation happens when the parent submits
|
|
32
|
+
* the booking — capacity drops the moment the reservation transaction
|
|
33
|
+
* commits; the next refetch of `useSlotUnitAvailability` reflects it.
|
|
34
|
+
*
|
|
35
|
+
* ### Stepper bounds
|
|
36
|
+
*
|
|
37
|
+
* - Minimum is 0 (operator can deselect).
|
|
38
|
+
* - Maximum is the unit's `remaining` count from the server. Unlimited
|
|
39
|
+
* pools (`remaining === null`) have no upper bound.
|
|
40
|
+
* - The server is the truth: entering `3 doubles` when only 2 remain just
|
|
41
|
+
* disables the "+" button — we don't let the UI submit a request that
|
|
42
|
+
* would 409 at insert time.
|
|
43
|
+
*/
|
|
44
|
+
export declare function RoomsStepperSection({ value, onChange, slotId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
//# sourceMappingURL=rooms-stepper-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAMA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,OAAc,EACd,MAAM,GACP,EAAE,wBAAwB,2CAmF1B"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSlotUnitAvailability } from "@voyantjs/availability-react";
|
|
4
|
+
import { Button, Label } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { Minus, Plus } from "lucide-react";
|
|
6
|
+
export const emptyRoomsStepperValue = { quantities: {} };
|
|
7
|
+
const DEFAULT_LABELS = {
|
|
8
|
+
heading: "Rooms",
|
|
9
|
+
noSlot: "Pick a departure first to see available rooms.",
|
|
10
|
+
noUnits: "This departure has no per-unit availability configured.",
|
|
11
|
+
remaining: "left",
|
|
12
|
+
unlimited: "unlimited",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
16
|
+
* `GET /v1/availability/slots/:id/unit-availability` from #235 so the
|
|
17
|
+
* operator sees authoritative "3 doubles available" numbers instead of
|
|
18
|
+
* client-side math against a sampled booking list.
|
|
19
|
+
*
|
|
20
|
+
* The section only tracks **intent** (how many of each unit the operator
|
|
21
|
+
* wants to book). Actual hold/reservation happens when the parent submits
|
|
22
|
+
* the booking — capacity drops the moment the reservation transaction
|
|
23
|
+
* commits; the next refetch of `useSlotUnitAvailability` reflects it.
|
|
24
|
+
*
|
|
25
|
+
* ### Stepper bounds
|
|
26
|
+
*
|
|
27
|
+
* - Minimum is 0 (operator can deselect).
|
|
28
|
+
* - Maximum is the unit's `remaining` count from the server. Unlimited
|
|
29
|
+
* pools (`remaining === null`) have no upper bound.
|
|
30
|
+
* - The server is the truth: entering `3 doubles` when only 2 remain just
|
|
31
|
+
* disables the "+" button — we don't let the UI submit a request that
|
|
32
|
+
* would 409 at insert time.
|
|
33
|
+
*/
|
|
34
|
+
export function RoomsStepperSection({ value, onChange, slotId, enabled = true, labels, }) {
|
|
35
|
+
const merged = { ...DEFAULT_LABELS, ...labels };
|
|
36
|
+
const availability = useSlotUnitAvailability({ slotId, enabled: enabled && Boolean(slotId) });
|
|
37
|
+
const units = availability.data?.data ?? [];
|
|
38
|
+
if (!slotId) {
|
|
39
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noSlot })] }));
|
|
40
|
+
}
|
|
41
|
+
if (availability.isSuccess && units.length === 0) {
|
|
42
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noUnits })] }));
|
|
43
|
+
}
|
|
44
|
+
const setQuantity = (unitId, qty) => {
|
|
45
|
+
const next = { ...value.quantities };
|
|
46
|
+
if (qty <= 0) {
|
|
47
|
+
delete next[unitId];
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
next[unitId] = qty;
|
|
51
|
+
}
|
|
52
|
+
onChange({ quantities: next });
|
|
53
|
+
};
|
|
54
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-col gap-2", children: units.map((unit) => {
|
|
55
|
+
const qty = value.quantities[unit.optionUnitId] ?? 0;
|
|
56
|
+
const remainingLabel = unit.remaining === null ? merged.unlimited : `${unit.remaining} ${merged.remaining}`;
|
|
57
|
+
const atMax = unit.remaining !== null && qty >= unit.remaining;
|
|
58
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-md border px-3 py-2", children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: unit.unitName }), _jsx("div", { className: "text-xs text-muted-foreground", children: remainingLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(unit.optionUnitId, Math.max(0, qty - 1)), disabled: qty <= 0, "aria-label": `Decrease ${unit.unitName}`, children: _jsx(Minus, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "min-w-[1.5rem] text-center text-sm tabular-nums", children: qty }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(unit.optionUnitId, qty + 1), disabled: atMax, "aria-label": `Increase ${unit.unitName}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] })] }, unit.optionUnitId));
|
|
59
|
+
}) })] }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type SharedRoomMode = "create" | "join";
|
|
2
|
+
export interface SharedRoomValue {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
mode: SharedRoomMode;
|
|
5
|
+
/** Only meaningful in "join" mode. */
|
|
6
|
+
groupId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const emptySharedRoomValue: SharedRoomValue;
|
|
9
|
+
export interface SharedRoomSectionProps {
|
|
10
|
+
value: SharedRoomValue;
|
|
11
|
+
onChange: (value: SharedRoomValue) => void;
|
|
12
|
+
/**
|
|
13
|
+
* The product context for fetching joinable groups. When unset, the join
|
|
14
|
+
* dropdown is disabled even if the user toggles into join mode.
|
|
15
|
+
*/
|
|
16
|
+
productId?: string;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
labels?: {
|
|
19
|
+
toggle?: string;
|
|
20
|
+
createMode?: string;
|
|
21
|
+
joinMode?: string;
|
|
22
|
+
selectPlaceholder?: string;
|
|
23
|
+
noGroups?: string;
|
|
24
|
+
createHint?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shared-room (partaj) attachment section. Operators use it to either create a
|
|
29
|
+
* new `booking_groups` row at booking-create time or join an existing group
|
|
30
|
+
* that already has a primary booking.
|
|
31
|
+
*
|
|
32
|
+
* The section only handles the *selection* — the parent dialog owns the
|
|
33
|
+
* create-group + add-member mutations because they fire *after* the booking
|
|
34
|
+
* insert (we need the new booking id to attach).
|
|
35
|
+
*/
|
|
36
|
+
export declare function SharedRoomSection({ value, onChange, productId, enabled, labels, }: SharedRoomSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
//# sourceMappingURL=shared-room-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"AAgBA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAIlC,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAWD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAc,EACd,MAAM,GACP,EAAE,sBAAsB,2CAmFxB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingGroups } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Button, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
const GROUP_NONE = "__none__";
|
|
6
|
+
export const emptySharedRoomValue = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
mode: "create",
|
|
9
|
+
groupId: "",
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_LABELS = {
|
|
12
|
+
toggle: "Link to a shared-room group",
|
|
13
|
+
createMode: "Create new group",
|
|
14
|
+
joinMode: "Join existing",
|
|
15
|
+
selectPlaceholder: "Select a group...",
|
|
16
|
+
noGroups: "No existing groups for this product",
|
|
17
|
+
createHint: "A new group will be created with this booking as the primary member.",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Shared-room (partaj) attachment section. Operators use it to either create a
|
|
21
|
+
* new `booking_groups` row at booking-create time or join an existing group
|
|
22
|
+
* that already has a primary booking.
|
|
23
|
+
*
|
|
24
|
+
* The section only handles the *selection* — the parent dialog owns the
|
|
25
|
+
* create-group + add-member mutations because they fire *after* the booking
|
|
26
|
+
* insert (we need the new booking id to attach).
|
|
27
|
+
*/
|
|
28
|
+
export function SharedRoomSection({ value, onChange, productId, enabled = true, labels, }) {
|
|
29
|
+
const merged = { ...DEFAULT_LABELS, ...labels };
|
|
30
|
+
const { data: groupsData } = useBookingGroups({
|
|
31
|
+
productId: productId || undefined,
|
|
32
|
+
limit: 50,
|
|
33
|
+
enabled: enabled && value.enabled && value.mode === "join" && Boolean(productId),
|
|
34
|
+
});
|
|
35
|
+
const existingGroups = groupsData?.data ?? [];
|
|
36
|
+
const set = (patch) => onChange({ ...value, ...patch });
|
|
37
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: "shared-room-toggle", type: "checkbox", checked: value.enabled, onChange: (e) => set({ enabled: e.target.checked }) }), _jsx(Label, { htmlFor: "shared-room-toggle", className: "text-sm", children: merged.toggle })] }), value.enabled && (_jsxs("div", { className: "flex flex-col gap-2 pl-6", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", size: "sm", variant: value.mode === "create" ? "default" : "ghost", onClick: () => set({ mode: "create" }), children: merged.createMode }), _jsx(Button, { type: "button", size: "sm", variant: value.mode === "join" ? "default" : "ghost", onClick: () => set({ mode: "join" }), children: merged.joinMode })] }), value.mode === "join" && (_jsxs(Select, { items: existingGroups.length === 0
|
|
38
|
+
? [{ label: merged.noGroups, value: GROUP_NONE }]
|
|
39
|
+
: existingGroups.map((g) => ({ label: g.label, value: g.id })), value: value.groupId || GROUP_NONE, onValueChange: (v) => set({ groupId: v === GROUP_NONE ? "" : (v ?? "") }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: merged.selectPlaceholder }) }), _jsx(SelectContent, { children: existingGroups.length === 0 ? (_jsx(SelectItem, { value: GROUP_NONE, disabled: true, children: merged.noGroups })) : (existingGroups.map((g) => (_jsx(SelectItem, { value: g.id, children: g.label }, g.id)))) })] })), value.mode === "create" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint }))] }))] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface StatusChangeDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
bookingId: string;
|
|
6
|
+
currentStatus: BookingRecord["status"];
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function StatusChangeDialog({ open, onOpenChange, bookingId, currentStatus, onSuccess, }: StatusChangeDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=status-change-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-change-dialog.d.ts","sourceRoot":"","sources":["../../src/components/status-change-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AA+BjC,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IACtC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,aAAa,EACb,SAAS,GACV,EAAE,uBAAuB,2CAiFzB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { bookingStatusOptions, bookingStatusSchema, useBookingStatusMutation, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
|
|
6
|
+
import { Loader2 } from "lucide-react";
|
|
7
|
+
import { useEffect } from "react";
|
|
8
|
+
import { useForm } from "react-hook-form";
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
|
+
const statusChangeFormSchema = z.object({
|
|
11
|
+
status: bookingStatusSchema,
|
|
12
|
+
note: z.string().optional().nullable(),
|
|
13
|
+
});
|
|
14
|
+
export function StatusChangeDialog({ open, onOpenChange, bookingId, currentStatus, onSuccess, }) {
|
|
15
|
+
const mutation = useBookingStatusMutation(bookingId);
|
|
16
|
+
const form = useForm({
|
|
17
|
+
resolver: zodResolver(statusChangeFormSchema),
|
|
18
|
+
defaultValues: {
|
|
19
|
+
status: "draft",
|
|
20
|
+
note: "",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (open) {
|
|
25
|
+
form.reset({
|
|
26
|
+
status: currentStatus,
|
|
27
|
+
note: "",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}, [currentStatus, form, open]);
|
|
31
|
+
const onSubmit = async (values) => {
|
|
32
|
+
await mutation.mutateAsync({
|
|
33
|
+
currentStatus,
|
|
34
|
+
status: values.status,
|
|
35
|
+
note: values.note || null,
|
|
36
|
+
});
|
|
37
|
+
onOpenChange(false);
|
|
38
|
+
onSuccess?.();
|
|
39
|
+
};
|
|
40
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Change Booking Status" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "New Status" }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), items: bookingStatusOptions, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: bookingStatusOptions.map((status) => (_jsx(SelectItem, { value: status.value, children: status.label }, status.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Note (optional)" }), _jsx(Textarea, { ...form.register("note"), placeholder: "Reason for status change..." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", size: "sm", disabled: mutation.isPending, children: [mutation.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Update Status"] })] })] })] }) }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BookingSupplierStatusRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface SupplierStatusDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
bookingId: string;
|
|
6
|
+
supplierStatus?: BookingSupplierStatusRecord;
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function SupplierStatusDialog({ open, onOpenChange, bookingId, supplierStatus, onSuccess, }: SupplierStatusDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=supplier-status-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supplier-status-dialog.d.ts","sourceRoot":"","sources":["../../src/components/supplier-status-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,2BAA2B,EAEjC,MAAM,0BAA0B,CAAA;AAqCjC,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,2BAA2B,CAAA;IAC5C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AASD,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,cAAc,EACd,SAAS,GACV,EAAE,yBAAyB,2CAmJ3B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSupplierStatusMutation, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { CurrencyCombobox } from "@voyantjs/voyant-ui/components/currency-combobox";
|
|
6
|
+
import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
import { useForm } from "react-hook-form";
|
|
10
|
+
import { z } from "zod/v4";
|
|
11
|
+
const supplierStatusFormSchema = z.object({
|
|
12
|
+
serviceName: z.string().min(1, "Service name is required"),
|
|
13
|
+
status: z.enum(["pending", "confirmed", "rejected", "cancelled"]),
|
|
14
|
+
supplierReference: z.string().optional().nullable(),
|
|
15
|
+
costCurrency: z.string().min(3).max(3, "Use 3-letter ISO code"),
|
|
16
|
+
costAmountCents: z.coerce.number().int().min(0),
|
|
17
|
+
notes: z.string().optional().nullable(),
|
|
18
|
+
});
|
|
19
|
+
const CONFIRMATION_STATUSES = [
|
|
20
|
+
{ value: "pending", label: "Pending" },
|
|
21
|
+
{ value: "confirmed", label: "Confirmed" },
|
|
22
|
+
{ value: "rejected", label: "Rejected" },
|
|
23
|
+
{ value: "cancelled", label: "Cancelled" },
|
|
24
|
+
];
|
|
25
|
+
export function SupplierStatusDialog({ open, onOpenChange, bookingId, supplierStatus, onSuccess, }) {
|
|
26
|
+
const isEditing = Boolean(supplierStatus);
|
|
27
|
+
const { create, update } = useSupplierStatusMutation(bookingId);
|
|
28
|
+
const form = useForm({
|
|
29
|
+
resolver: zodResolver(supplierStatusFormSchema),
|
|
30
|
+
defaultValues: {
|
|
31
|
+
serviceName: "",
|
|
32
|
+
status: "pending",
|
|
33
|
+
supplierReference: "",
|
|
34
|
+
costCurrency: "EUR",
|
|
35
|
+
costAmountCents: 0,
|
|
36
|
+
notes: "",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (open && supplierStatus) {
|
|
41
|
+
form.reset({
|
|
42
|
+
serviceName: supplierStatus.serviceName,
|
|
43
|
+
status: supplierStatus.status,
|
|
44
|
+
supplierReference: supplierStatus.supplierReference ?? "",
|
|
45
|
+
costCurrency: supplierStatus.costCurrency,
|
|
46
|
+
costAmountCents: supplierStatus.costAmountCents,
|
|
47
|
+
notes: supplierStatus.notes ?? "",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else if (open) {
|
|
51
|
+
form.reset();
|
|
52
|
+
}
|
|
53
|
+
}, [form, open, supplierStatus]);
|
|
54
|
+
const onSubmit = async (values) => {
|
|
55
|
+
const payload = {
|
|
56
|
+
serviceName: values.serviceName,
|
|
57
|
+
status: values.status,
|
|
58
|
+
supplierReference: values.supplierReference || null,
|
|
59
|
+
costCurrency: values.costCurrency,
|
|
60
|
+
costAmountCents: values.costAmountCents,
|
|
61
|
+
notes: values.notes || null,
|
|
62
|
+
};
|
|
63
|
+
if (isEditing) {
|
|
64
|
+
await update.mutateAsync({ id: supplierStatus.id, input: payload });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await create.mutateAsync(payload);
|
|
68
|
+
}
|
|
69
|
+
onOpenChange(false);
|
|
70
|
+
onSuccess?.();
|
|
71
|
+
};
|
|
72
|
+
const isSubmitting = create.isPending || update.isPending;
|
|
73
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Update Supplier Status" : "Add Supplier Status" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Service Name" }), _jsx(Input, { ...form.register("serviceName"), placeholder: "Hotel Dubrovnik Palace", disabled: isEditing }), form.formState.errors.serviceName && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.serviceName.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), items: CONFIRMATION_STATUSES, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CONFIRMATION_STATUSES.map((status) => (_jsx(SelectItem, { value: status.value, children: status.label }, status.value))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Cost Currency" }), _jsx(CurrencyCombobox, { value: form.watch("costCurrency") || null, onChange: (next) => form.setValue("costCurrency", next ?? "EUR", {
|
|
74
|
+
shouldValidate: true,
|
|
75
|
+
shouldDirty: true,
|
|
76
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Cost Amount (cents)" }), _jsx(Input, { ...form.register("costAmountCents", { valueAsNumber: true }), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Supplier Reference" }), _jsx(Input, { ...form.register("supplierReference"), placeholder: "CONF-12345" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes"), placeholder: "Additional notes..." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? "Save Changes" : "Add"] })] })] })] }) }));
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supplier-status-list.d.ts","sourceRoot":"","sources":["../../src/components/supplier-status-list.tsx"],"names":[],"mappings":"AAgBA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;CAClB;AASD,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE,uBAAuB,2CA+FxE"}
|