@voyantjs/hospitality-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/cancellation-policy-combobox.d.ts +9 -0
- package/dist/components/cancellation-policy-combobox.d.ts.map +1 -0
- package/dist/components/cancellation-policy-combobox.js +49 -0
- package/dist/components/maintenance-block-dialog.d.ts +11 -0
- package/dist/components/maintenance-block-dialog.d.ts.map +1 -0
- package/dist/components/maintenance-block-dialog.js +86 -0
- package/dist/components/maintenance-blocks-tab.d.ts +5 -0
- package/dist/components/maintenance-blocks-tab.d.ts.map +1 -0
- package/dist/components/maintenance-blocks-tab.js +51 -0
- package/dist/components/meal-plan-combobox.d.ts +10 -0
- package/dist/components/meal-plan-combobox.d.ts.map +1 -0
- package/dist/components/meal-plan-combobox.js +50 -0
- package/dist/components/meal-plan-dialog.d.ts +10 -0
- package/dist/components/meal-plan-dialog.d.ts.map +1 -0
- package/dist/components/meal-plan-dialog.js +86 -0
- package/dist/components/meal-plans-tab.d.ts +5 -0
- package/dist/components/meal-plans-tab.d.ts.map +1 -0
- package/dist/components/meal-plans-tab.js +44 -0
- package/dist/components/pagination-footer.d.ts +9 -0
- package/dist/components/pagination-footer.d.ts.map +1 -0
- package/dist/components/pagination-footer.js +11 -0
- package/dist/components/price-catalog-combobox.d.ts +9 -0
- package/dist/components/price-catalog-combobox.d.ts.map +1 -0
- package/dist/components/price-catalog-combobox.js +45 -0
- package/dist/components/rate-plan-combobox.d.ts +10 -0
- package/dist/components/rate-plan-combobox.d.ts.map +1 -0
- package/dist/components/rate-plan-combobox.js +50 -0
- package/dist/components/rate-plan-dialog.d.ts +11 -0
- package/dist/components/rate-plan-dialog.d.ts.map +1 -0
- package/dist/components/rate-plan-dialog.js +120 -0
- package/dist/components/rate-plans-tab.d.ts +5 -0
- package/dist/components/rate-plans-tab.d.ts.map +1 -0
- package/dist/components/rate-plans-tab.js +54 -0
- package/dist/components/room-block-dialog.d.ts +11 -0
- package/dist/components/room-block-dialog.d.ts.map +1 -0
- package/dist/components/room-block-dialog.js +91 -0
- package/dist/components/room-blocks-tab.d.ts +5 -0
- package/dist/components/room-blocks-tab.d.ts.map +1 -0
- package/dist/components/room-blocks-tab.js +51 -0
- package/dist/components/room-inventory-dialog.d.ts +11 -0
- package/dist/components/room-inventory-dialog.d.ts.map +1 -0
- package/dist/components/room-inventory-dialog.js +98 -0
- package/dist/components/room-inventory-tab.d.ts +5 -0
- package/dist/components/room-inventory-tab.d.ts.map +1 -0
- package/dist/components/room-inventory-tab.js +61 -0
- package/dist/components/room-type-combobox.d.ts +10 -0
- package/dist/components/room-type-combobox.d.ts.map +1 -0
- package/dist/components/room-type-combobox.js +46 -0
- package/dist/components/room-type-dialog.d.ts +10 -0
- package/dist/components/room-type-dialog.d.ts.map +1 -0
- package/dist/components/room-type-dialog.js +119 -0
- package/dist/components/room-types-tab.d.ts +5 -0
- package/dist/components/room-types-tab.d.ts.map +1 -0
- package/dist/components/room-types-tab.js +33 -0
- package/dist/components/room-unit-combobox.d.ts +10 -0
- package/dist/components/room-unit-combobox.d.ts.map +1 -0
- package/dist/components/room-unit-combobox.js +50 -0
- package/dist/components/room-unit-dialog.d.ts +10 -0
- package/dist/components/room-unit-dialog.d.ts.map +1 -0
- package/dist/components/room-unit-dialog.js +93 -0
- package/dist/components/room-units-tab.d.ts +5 -0
- package/dist/components/room-units-tab.d.ts.map +1 -0
- package/dist/components/room-units-tab.js +40 -0
- package/dist/components/stay-rule-dialog.d.ts +11 -0
- package/dist/components/stay-rule-dialog.d.ts.map +1 -0
- package/dist/components/stay-rule-dialog.js +140 -0
- package/dist/components/stay-rules-tab.d.ts +5 -0
- package/dist/components/stay-rules-tab.d.ts.map +1 -0
- package/dist/components/stay-rules-tab.js +49 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/package.json +68 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRatePlan, useRatePlans } from "@voyantjs/hospitality-react";
|
|
3
|
+
import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/voyant-ui/components/combobox";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
const PAGE_SIZE = 25;
|
|
6
|
+
export function RatePlanCombobox({ propertyId, value, onChange, placeholder = "Search rate plans…", disabled, }) {
|
|
7
|
+
const [search, setSearch] = React.useState("");
|
|
8
|
+
const listQuery = useRatePlans({
|
|
9
|
+
propertyId,
|
|
10
|
+
search: search || undefined,
|
|
11
|
+
limit: PAGE_SIZE,
|
|
12
|
+
enabled: !!propertyId,
|
|
13
|
+
});
|
|
14
|
+
const selectedQuery = useRatePlan(value, { enabled: !!value });
|
|
15
|
+
const items = React.useMemo(() => {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
for (const item of listQuery.data?.data ?? [])
|
|
18
|
+
map.set(item.id, item);
|
|
19
|
+
if (selectedQuery.data)
|
|
20
|
+
map.set(selectedQuery.data.id, selectedQuery.data);
|
|
21
|
+
return Array.from(map.values());
|
|
22
|
+
}, [listQuery.data?.data, selectedQuery.data]);
|
|
23
|
+
const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
|
|
24
|
+
const selected = value ? itemMap.get(value) : undefined;
|
|
25
|
+
const selectedLabel = selected ? `${selected.name} · ${selected.code}` : "";
|
|
26
|
+
const [inputValue, setInputValue] = React.useState(selectedLabel);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (selectedLabel)
|
|
29
|
+
setInputValue(selectedLabel);
|
|
30
|
+
}, [selectedLabel]);
|
|
31
|
+
return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => {
|
|
32
|
+
const item = itemMap.get(id);
|
|
33
|
+
return item ? `${item.name} · ${item.code}` : "";
|
|
34
|
+
}, onInputValueChange: (next) => {
|
|
35
|
+
setInputValue(next);
|
|
36
|
+
setSearch(next);
|
|
37
|
+
if (!next)
|
|
38
|
+
onChange(null);
|
|
39
|
+
}, onValueChange: (next) => {
|
|
40
|
+
const id = next ?? null;
|
|
41
|
+
onChange(id);
|
|
42
|
+
const item = id ? itemMap.get(id) : null;
|
|
43
|
+
setInputValue(item ? `${item.name} · ${item.code}` : "");
|
|
44
|
+
}, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No rate plans found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
|
|
45
|
+
const item = itemMap.get(id);
|
|
46
|
+
if (!item)
|
|
47
|
+
return null;
|
|
48
|
+
return (_jsxs(ComboboxItem, { value: item.id, children: [item.name, " \u00B7 ", item.code] }, item.id));
|
|
49
|
+
} }) })] })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RatePlanRecord } from "@voyantjs/hospitality-react";
|
|
2
|
+
export type RatePlanData = RatePlanRecord;
|
|
3
|
+
export interface RatePlanDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
propertyId: string;
|
|
7
|
+
ratePlan?: RatePlanRecord;
|
|
8
|
+
onSuccess?: (ratePlan: RatePlanRecord) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function RatePlanDialog({ open, onOpenChange, propertyId, ratePlan, onSuccess, }: RatePlanDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=rate-plan-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-plan-dialog.d.ts","sourceRoot":"","sources":["../../src/components/rate-plan-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAA;AA6BtF,MAAM,MAAM,YAAY,GAAG,cAAc,CAAA;AAgCzC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAA;CAC/C;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,SAAS,GACV,EAAE,mBAAmB,2CA+OrB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRatePlanMutation } from "@voyantjs/hospitality-react";
|
|
3
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
4
|
+
import { CurrencyCombobox } from "@voyantjs/voyant-ui/components/currency-combobox";
|
|
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
|
+
import { CancellationPolicyCombobox } from "./cancellation-policy-combobox";
|
|
11
|
+
import { MealPlanCombobox } from "./meal-plan-combobox";
|
|
12
|
+
import { PriceCatalogCombobox } from "./price-catalog-combobox";
|
|
13
|
+
const CHARGE_FREQUENCIES = [
|
|
14
|
+
"per_night",
|
|
15
|
+
"per_stay",
|
|
16
|
+
"per_person_per_night",
|
|
17
|
+
"per_person_per_stay",
|
|
18
|
+
];
|
|
19
|
+
const GUARANTEE_MODES = ["none", "deposit", "on_request", "card_hold", "full_prepay"];
|
|
20
|
+
const formSchema = z.object({
|
|
21
|
+
code: z.string().min(1, "Code is required").max(50),
|
|
22
|
+
name: z.string().min(1, "Name is required").max(255),
|
|
23
|
+
description: z.string().optional().nullable(),
|
|
24
|
+
mealPlanId: z.string().optional().nullable(),
|
|
25
|
+
priceCatalogId: z.string().optional().nullable(),
|
|
26
|
+
cancellationPolicyId: z.string().optional().nullable(),
|
|
27
|
+
currencyCode: z.string().length(3, "Currency must be 3 chars"),
|
|
28
|
+
chargeFrequency: z.enum(CHARGE_FREQUENCIES),
|
|
29
|
+
guaranteeMode: z.enum(GUARANTEE_MODES),
|
|
30
|
+
commissionable: z.boolean(),
|
|
31
|
+
refundable: z.boolean(),
|
|
32
|
+
active: z.boolean(),
|
|
33
|
+
sortOrder: z.coerce.number().int(),
|
|
34
|
+
});
|
|
35
|
+
export function RatePlanDialog({ open, onOpenChange, propertyId, ratePlan, onSuccess, }) {
|
|
36
|
+
const isEditing = Boolean(ratePlan);
|
|
37
|
+
const { create, update } = useRatePlanMutation();
|
|
38
|
+
const form = useForm({
|
|
39
|
+
resolver: zodResolver(formSchema),
|
|
40
|
+
defaultValues: {
|
|
41
|
+
code: "",
|
|
42
|
+
name: "",
|
|
43
|
+
description: "",
|
|
44
|
+
mealPlanId: "",
|
|
45
|
+
priceCatalogId: "",
|
|
46
|
+
cancellationPolicyId: "",
|
|
47
|
+
currencyCode: "EUR",
|
|
48
|
+
chargeFrequency: "per_night",
|
|
49
|
+
guaranteeMode: "none",
|
|
50
|
+
commissionable: true,
|
|
51
|
+
refundable: true,
|
|
52
|
+
active: true,
|
|
53
|
+
sortOrder: 0,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (open && ratePlan) {
|
|
58
|
+
form.reset({
|
|
59
|
+
code: ratePlan.code,
|
|
60
|
+
name: ratePlan.name,
|
|
61
|
+
description: ratePlan.description ?? "",
|
|
62
|
+
mealPlanId: ratePlan.mealPlanId ?? "",
|
|
63
|
+
priceCatalogId: ratePlan.priceCatalogId ?? "",
|
|
64
|
+
cancellationPolicyId: ratePlan.cancellationPolicyId ?? "",
|
|
65
|
+
currencyCode: ratePlan.currencyCode,
|
|
66
|
+
chargeFrequency: ratePlan.chargeFrequency,
|
|
67
|
+
guaranteeMode: ratePlan.guaranteeMode,
|
|
68
|
+
commissionable: ratePlan.commissionable,
|
|
69
|
+
refundable: ratePlan.refundable,
|
|
70
|
+
active: ratePlan.active,
|
|
71
|
+
sortOrder: ratePlan.sortOrder,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (open) {
|
|
75
|
+
form.reset({
|
|
76
|
+
code: "",
|
|
77
|
+
name: "",
|
|
78
|
+
description: "",
|
|
79
|
+
mealPlanId: "",
|
|
80
|
+
priceCatalogId: "",
|
|
81
|
+
cancellationPolicyId: "",
|
|
82
|
+
currencyCode: "EUR",
|
|
83
|
+
chargeFrequency: "per_night",
|
|
84
|
+
guaranteeMode: "none",
|
|
85
|
+
commissionable: true,
|
|
86
|
+
refundable: true,
|
|
87
|
+
active: true,
|
|
88
|
+
sortOrder: 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}, [form, open, ratePlan]);
|
|
92
|
+
const onSubmit = async (values) => {
|
|
93
|
+
const payload = {
|
|
94
|
+
propertyId,
|
|
95
|
+
code: values.code,
|
|
96
|
+
name: values.name,
|
|
97
|
+
description: values.description || null,
|
|
98
|
+
mealPlanId: values.mealPlanId || null,
|
|
99
|
+
priceCatalogId: values.priceCatalogId || null,
|
|
100
|
+
cancellationPolicyId: values.cancellationPolicyId || null,
|
|
101
|
+
currencyCode: values.currencyCode.toUpperCase(),
|
|
102
|
+
chargeFrequency: values.chargeFrequency,
|
|
103
|
+
guaranteeMode: values.guaranteeMode,
|
|
104
|
+
commissionable: values.commissionable,
|
|
105
|
+
refundable: values.refundable,
|
|
106
|
+
active: values.active,
|
|
107
|
+
sortOrder: values.sortOrder,
|
|
108
|
+
};
|
|
109
|
+
const saved = isEditing
|
|
110
|
+
? await update.mutateAsync({ id: ratePlan.id, input: payload })
|
|
111
|
+
: await create.mutateAsync(payload);
|
|
112
|
+
onOpenChange(false);
|
|
113
|
+
onSuccess?.(saved);
|
|
114
|
+
};
|
|
115
|
+
const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
|
|
116
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Rate Plan" : "Add Rate Plan" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), 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: "Code" }), _jsx(Input, { ...form.register("code"), placeholder: "FLEX" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Flexible Rate" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Description" }), _jsx(Textarea, { ...form.register("description") })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Currency" }), _jsx(CurrencyCombobox, { value: form.watch("currencyCode") || null, onChange: (next) => form.setValue("currencyCode", next ?? "EUR", {
|
|
117
|
+
shouldValidate: true,
|
|
118
|
+
shouldDirty: true,
|
|
119
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Charge frequency" }), _jsxs(Select, { items: CHARGE_FREQUENCIES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("chargeFrequency"), onValueChange: (value) => form.setValue("chargeFrequency", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CHARGE_FREQUENCIES.map((frequency) => (_jsx(SelectItem, { value: frequency, className: "capitalize", children: frequency.replace(/_/g, " ") }, frequency))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Guarantee" }), _jsxs(Select, { items: GUARANTEE_MODES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("guaranteeMode"), onValueChange: (value) => form.setValue("guaranteeMode", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: GUARANTEE_MODES.map((mode) => (_jsx(SelectItem, { value: mode, className: "capitalize", children: mode.replace(/_/g, " ") }, mode))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Meal plan" }), _jsx(MealPlanCombobox, { propertyId: propertyId, value: form.watch("mealPlanId"), onChange: (value) => form.setValue("mealPlanId", value ?? ""), placeholder: "None", disabled: !open })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Price catalog" }), _jsx(PriceCatalogCombobox, { value: form.watch("priceCatalogId"), onChange: (value) => form.setValue("priceCatalogId", value ?? ""), placeholder: "None", disabled: !open })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Cancellation policy" }), _jsx(CancellationPolicyCombobox, { value: form.watch("cancellationPolicyId"), onChange: (value) => form.setValue("cancellationPolicyId", value ?? ""), placeholder: "None", disabled: !open })] })] }), _jsxs("div", { className: "flex gap-6", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("commissionable"), onCheckedChange: (checked) => form.setValue("commissionable", checked) }), _jsx(Label, { children: "Commissionable" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("refundable"), onCheckedChange: (checked) => form.setValue("refundable", checked) }), _jsx(Label, { children: "Refundable" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { children: "Active" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sort order" }), _jsx(Input, { ...form.register("sortOrder"), type: "number", className: "w-32" })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Add Rate Plan"] })] })] })] }) }));
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-plans-tab.d.ts","sourceRoot":"","sources":["../../src/components/rate-plans-tab.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,iBAAiB,2CA8J7D"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQueries } from "@tanstack/react-query";
|
|
4
|
+
import { getMealPlanQueryOptions, useRatePlanMutation, useRatePlans, useVoyantHospitalityContext, } from "@voyantjs/hospitality-react";
|
|
5
|
+
import { getCancellationPolicyQueryOptions, getPriceCatalogQueryOptions, useVoyantPricingContext, } from "@voyantjs/pricing-react";
|
|
6
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
7
|
+
import { Button } from "@voyantjs/voyant-ui/components/button";
|
|
8
|
+
import { Loader2, Pencil, Plus, Trash2 } from "lucide-react";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { PaginationFooter } from "./pagination-footer";
|
|
11
|
+
import { RatePlanDialog } from "./rate-plan-dialog";
|
|
12
|
+
const PAGE_SIZE = 25;
|
|
13
|
+
export function RatePlansTab({ propertyId }) {
|
|
14
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
15
|
+
const [editing, setEditing] = React.useState(undefined);
|
|
16
|
+
const [pageIndex, setPageIndex] = React.useState(0);
|
|
17
|
+
const { data, isPending } = useRatePlans({
|
|
18
|
+
propertyId,
|
|
19
|
+
limit: PAGE_SIZE,
|
|
20
|
+
offset: pageIndex * PAGE_SIZE,
|
|
21
|
+
});
|
|
22
|
+
const { remove } = useRatePlanMutation();
|
|
23
|
+
const { baseUrl: hospitalityBaseUrl, fetcher: hospitalityFetcher } = useVoyantHospitalityContext();
|
|
24
|
+
const { baseUrl: pricingBaseUrl, fetcher: pricingFetcher } = useVoyantPricingContext();
|
|
25
|
+
const rows = (data?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder);
|
|
26
|
+
const catalogIds = Array.from(new Set(rows.map((row) => row.priceCatalogId).filter(Boolean)));
|
|
27
|
+
const cancelIds = Array.from(new Set(rows.map((row) => row.cancellationPolicyId).filter(Boolean)));
|
|
28
|
+
const mealIds = Array.from(new Set(rows.map((row) => row.mealPlanId).filter(Boolean)));
|
|
29
|
+
const catalogQueries = useQueries({
|
|
30
|
+
queries: catalogIds.map((id) => getPriceCatalogQueryOptions({ baseUrl: pricingBaseUrl, fetcher: pricingFetcher }, id)),
|
|
31
|
+
});
|
|
32
|
+
const cancelQueries = useQueries({
|
|
33
|
+
queries: cancelIds.map((id) => getCancellationPolicyQueryOptions({ baseUrl: pricingBaseUrl, fetcher: pricingFetcher }, id)),
|
|
34
|
+
});
|
|
35
|
+
const mealQueries = useQueries({
|
|
36
|
+
queries: mealIds.map((id) => getMealPlanQueryOptions({ baseUrl: hospitalityBaseUrl, fetcher: hospitalityFetcher }, id)),
|
|
37
|
+
});
|
|
38
|
+
const catalogMap = new Map(catalogQueries.flatMap((query) => (query.data ? [[query.data.id, query.data.name]] : [])));
|
|
39
|
+
const cancelMap = new Map(cancelQueries.flatMap((query) => (query.data ? [[query.data.id, query.data.name]] : [])));
|
|
40
|
+
const mealMap = new Map(mealQueries.flatMap((query) => (query.data ? [[query.data.id, query.data.name]] : [])));
|
|
41
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Commercial rate plans with pricing, guarantee, and cancellation defaults." }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
42
|
+
setEditing(undefined);
|
|
43
|
+
setDialogOpen(true);
|
|
44
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Rate Plan"] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No rate plans yet." }) })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-3 text-left font-medium", children: "Code" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Name" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Catalog" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Cancellation" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Meal Plan" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Currency" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Charge" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Status" }), _jsx("th", { className: "w-20 p-3" })] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-3 font-mono text-xs", children: row.code }), _jsx("td", { className: "p-3 font-medium", children: row.name }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.priceCatalogId ? (catalogMap.get(row.priceCatalogId) ?? "—") : "—" }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.cancellationPolicyId
|
|
45
|
+
? (cancelMap.get(row.cancellationPolicyId) ?? "—")
|
|
46
|
+
: "—" }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.mealPlanId ? (mealMap.get(row.mealPlanId) ?? "—") : "—" }), _jsx("td", { className: "p-3 font-mono text-xs", children: row.currencyCode }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: "outline", className: "capitalize", children: row.chargeFrequency.replace(/_/g, " ") }) }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: row.active ? "default" : "outline", children: row.active ? "Active" : "Inactive" }) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
47
|
+
setEditing(row);
|
|
48
|
+
setDialogOpen(true);
|
|
49
|
+
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
50
|
+
if (confirm(`Delete rate plan "${row.name}"?`)) {
|
|
51
|
+
remove.mutate(row.id);
|
|
52
|
+
}
|
|
53
|
+
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, row.id))) })] }) })), _jsx(PaginationFooter, { pageIndex: pageIndex, pageSize: PAGE_SIZE, total: data?.total ?? 0, onPageIndexChange: setPageIndex }), _jsx(RatePlanDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, ratePlan: editing })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RoomBlockRecord } from "@voyantjs/hospitality-react";
|
|
2
|
+
export type RoomBlockData = RoomBlockRecord;
|
|
3
|
+
export interface RoomBlockDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
propertyId: string;
|
|
7
|
+
block?: RoomBlockRecord;
|
|
8
|
+
onSuccess?: (block: RoomBlockRecord) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function RoomBlockDialog({ open, onOpenChange, propertyId, block, onSuccess, }: RoomBlockDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=room-block-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-block-dialog.d.ts","sourceRoot":"","sources":["../../src/components/room-block-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,6BAA6B,CAAA;AA2BxF,MAAM,MAAM,aAAa,GAAG,eAAe,CAAA;AAmB3C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,eAAe,CAAA;IACvB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CAC7C;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,KAAK,EACL,SAAS,GACV,EAAE,oBAAoB,2CAiLtB"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRoomBlockMutation } from "@voyantjs/hospitality-react";
|
|
3
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
4
|
+
import { DatePicker } from "@voyantjs/voyant-ui/components/date-picker";
|
|
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
|
+
import { RoomTypeCombobox } from "./room-type-combobox";
|
|
11
|
+
import { RoomUnitCombobox } from "./room-unit-combobox";
|
|
12
|
+
const STATUSES = ["draft", "held", "confirmed", "released", "cancelled"];
|
|
13
|
+
const formSchema = z.object({
|
|
14
|
+
roomTypeId: z.string().optional().nullable(),
|
|
15
|
+
roomUnitId: z.string().optional().nullable(),
|
|
16
|
+
startsOn: z.string().min(1, "Start date is required"),
|
|
17
|
+
endsOn: z.string().min(1, "End date is required"),
|
|
18
|
+
status: z.enum(STATUSES),
|
|
19
|
+
blockReason: z.string().optional().nullable(),
|
|
20
|
+
quantity: z.coerce.number().int().min(1),
|
|
21
|
+
notes: z.string().optional().nullable(),
|
|
22
|
+
});
|
|
23
|
+
export function RoomBlockDialog({ open, onOpenChange, propertyId, block, onSuccess, }) {
|
|
24
|
+
const isEditing = Boolean(block);
|
|
25
|
+
const { create, update } = useRoomBlockMutation();
|
|
26
|
+
const form = useForm({
|
|
27
|
+
resolver: zodResolver(formSchema),
|
|
28
|
+
defaultValues: {
|
|
29
|
+
roomTypeId: "",
|
|
30
|
+
roomUnitId: "",
|
|
31
|
+
startsOn: "",
|
|
32
|
+
endsOn: "",
|
|
33
|
+
status: "draft",
|
|
34
|
+
blockReason: "",
|
|
35
|
+
quantity: 1,
|
|
36
|
+
notes: "",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (open && block) {
|
|
41
|
+
form.reset({
|
|
42
|
+
roomTypeId: block.roomTypeId ?? "",
|
|
43
|
+
roomUnitId: block.roomUnitId ?? "",
|
|
44
|
+
startsOn: block.startsOn,
|
|
45
|
+
endsOn: block.endsOn,
|
|
46
|
+
status: block.status,
|
|
47
|
+
blockReason: block.blockReason ?? "",
|
|
48
|
+
quantity: block.quantity,
|
|
49
|
+
notes: block.notes ?? "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (open) {
|
|
53
|
+
form.reset({
|
|
54
|
+
roomTypeId: "",
|
|
55
|
+
roomUnitId: "",
|
|
56
|
+
startsOn: "",
|
|
57
|
+
endsOn: "",
|
|
58
|
+
status: "draft",
|
|
59
|
+
blockReason: "",
|
|
60
|
+
quantity: 1,
|
|
61
|
+
notes: "",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, [open, block, form]);
|
|
65
|
+
const onSubmit = async (values) => {
|
|
66
|
+
const payload = {
|
|
67
|
+
propertyId,
|
|
68
|
+
roomTypeId: values.roomTypeId || null,
|
|
69
|
+
roomUnitId: values.roomUnitId || null,
|
|
70
|
+
startsOn: values.startsOn,
|
|
71
|
+
endsOn: values.endsOn,
|
|
72
|
+
status: values.status,
|
|
73
|
+
blockReason: values.blockReason || null,
|
|
74
|
+
quantity: values.quantity,
|
|
75
|
+
notes: values.notes || null,
|
|
76
|
+
};
|
|
77
|
+
const saved = isEditing
|
|
78
|
+
? await update.mutateAsync({ id: block.id, input: payload })
|
|
79
|
+
: await create.mutateAsync(payload);
|
|
80
|
+
onOpenChange(false);
|
|
81
|
+
onSuccess?.(saved);
|
|
82
|
+
};
|
|
83
|
+
const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
|
|
84
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Room Block" : "Add Room Block" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), 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: "Room type (optional)" }), _jsx(RoomTypeCombobox, { propertyId: propertyId, value: form.watch("roomTypeId"), onChange: (value) => form.setValue("roomTypeId", value ?? ""), placeholder: "None", disabled: !open })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Room unit (optional)" }), _jsx(RoomUnitCombobox, { propertyId: propertyId, value: form.watch("roomUnitId"), onChange: (value) => form.setValue("roomUnitId", value ?? ""), placeholder: "None", disabled: !open })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Starts on" }), _jsx(DatePicker, { value: form.watch("startsOn") || null, onChange: (next) => form.setValue("startsOn", next ?? "", {
|
|
85
|
+
shouldValidate: true,
|
|
86
|
+
shouldDirty: true,
|
|
87
|
+
}), placeholder: "Select start date", className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Ends on" }), _jsx(DatePicker, { value: form.watch("endsOn") || null, onChange: (next) => form.setValue("endsOn", next ?? "", {
|
|
88
|
+
shouldValidate: true,
|
|
89
|
+
shouldDirty: true,
|
|
90
|
+
}), placeholder: "Select end date", className: "w-full" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: STATUSES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: STATUSES.map((status) => (_jsx(SelectItem, { value: status, className: "capitalize", children: status }, status))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Quantity" }), _jsx(Input, { ...form.register("quantity"), type: "number", min: "1" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Reason" }), _jsx(Input, { ...form.register("blockReason"), placeholder: "Group block" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes") })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Add Block"] })] })] })] }) }));
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-blocks-tab.d.ts","sourceRoot":"","sources":["../../src/components/room-blocks-tab.tsx"],"names":[],"mappings":"AAmBA,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,aAAa,CAAC,EAAE,UAAU,EAAE,EAAE,kBAAkB,2CAyI/D"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQueries } from "@tanstack/react-query";
|
|
4
|
+
import { getRoomTypeQueryOptions, getRoomUnitQueryOptions, useRoomBlockMutation, useRoomBlocks, useVoyantHospitalityContext, } from "@voyantjs/hospitality-react";
|
|
5
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
6
|
+
import { Button } from "@voyantjs/voyant-ui/components/button";
|
|
7
|
+
import { Loader2, Pencil, Plus, Trash2 } from "lucide-react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { PaginationFooter } from "./pagination-footer";
|
|
10
|
+
import { RoomBlockDialog } from "./room-block-dialog";
|
|
11
|
+
const PAGE_SIZE = 25;
|
|
12
|
+
export function RoomBlocksTab({ propertyId }) {
|
|
13
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
14
|
+
const [editing, setEditing] = React.useState(undefined);
|
|
15
|
+
const [pageIndex, setPageIndex] = React.useState(0);
|
|
16
|
+
const { data, isPending } = useRoomBlocks({
|
|
17
|
+
propertyId,
|
|
18
|
+
limit: PAGE_SIZE,
|
|
19
|
+
offset: pageIndex * PAGE_SIZE,
|
|
20
|
+
});
|
|
21
|
+
const { remove } = useRoomBlockMutation();
|
|
22
|
+
const rows = data?.data ?? [];
|
|
23
|
+
const { baseUrl, fetcher } = useVoyantHospitalityContext();
|
|
24
|
+
const roomTypeIds = Array.from(new Set(rows.map((row) => row.roomTypeId).filter(Boolean)));
|
|
25
|
+
const roomUnitIds = Array.from(new Set(rows.map((row) => row.roomUnitId).filter(Boolean)));
|
|
26
|
+
const roomTypeQueries = useQueries({
|
|
27
|
+
queries: roomTypeIds.map((id) => getRoomTypeQueryOptions({ baseUrl, fetcher }, id)),
|
|
28
|
+
});
|
|
29
|
+
const roomUnitQueries = useQueries({
|
|
30
|
+
queries: roomUnitIds.map((id) => getRoomUnitQueryOptions({ baseUrl, fetcher }, id)),
|
|
31
|
+
});
|
|
32
|
+
const roomTypeById = new Map(roomTypeQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
|
|
33
|
+
const roomUnitById = new Map(roomUnitQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
|
|
34
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Hold rooms for group bookings, allotments, or other commitments." }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
35
|
+
setEditing(undefined);
|
|
36
|
+
setDialogOpen(true);
|
|
37
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Block"] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No room blocks yet." }) })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-3 text-left font-medium", children: "Dates" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Room type / unit" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Qty" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Reason" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Status" }), _jsx("th", { className: "w-20 p-3" })] }) }), _jsx("tbody", { children: rows.map((row) => {
|
|
38
|
+
const roomType = row.roomTypeId ? roomTypeById.get(row.roomTypeId)?.name : null;
|
|
39
|
+
const roomUnit = row.roomUnitId
|
|
40
|
+
? roomUnitById.get(row.roomUnitId)?.roomNumber
|
|
41
|
+
: null;
|
|
42
|
+
return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsxs("td", { className: "p-3 font-mono text-xs", children: [row.startsOn, " \u2192 ", row.endsOn] }), _jsx("td", { className: "p-3 text-muted-foreground", children: roomType ?? roomUnit ?? row.roomTypeId ?? row.roomUnitId ?? "—" }), _jsx("td", { className: "p-3 font-mono", children: row.quantity }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.blockReason ?? "—" }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: "outline", className: "capitalize", children: row.status }) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
43
|
+
setEditing(row);
|
|
44
|
+
setDialogOpen(true);
|
|
45
|
+
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
46
|
+
if (confirm("Delete block?")) {
|
|
47
|
+
remove.mutate(row.id);
|
|
48
|
+
}
|
|
49
|
+
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, row.id));
|
|
50
|
+
}) })] }) })), _jsx(PaginationFooter, { pageIndex: pageIndex, pageSize: PAGE_SIZE, total: data?.total ?? 0, onPageIndexChange: setPageIndex }), _jsx(RoomBlockDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, block: editing })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RoomInventoryRecord } from "@voyantjs/hospitality-react";
|
|
2
|
+
export type RoomInventoryData = RoomInventoryRecord;
|
|
3
|
+
export interface RoomInventoryDialogProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onOpenChange: (open: boolean) => void;
|
|
6
|
+
propertyId: string;
|
|
7
|
+
inventory?: RoomInventoryRecord;
|
|
8
|
+
onSuccess?: (inventory: RoomInventoryRecord) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function RoomInventoryDialog({ open, onOpenChange, propertyId, inventory, onSuccess, }: RoomInventoryDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=room-inventory-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-inventory-dialog.d.ts","sourceRoot":"","sources":["../../src/components/room-inventory-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAA4B,MAAM,6BAA6B,CAAA;AAsBhG,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,CAAA;AAoBnD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,mBAAmB,CAAA;IAC/B,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,mBAAmB,KAAK,IAAI,CAAA;CACrD;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,SAAS,EACT,SAAS,GACV,EAAE,wBAAwB,2CAuK1B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRoomInventoryMutation } from "@voyantjs/hospitality-react";
|
|
3
|
+
import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Switch, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
4
|
+
import { DatePicker } from "@voyantjs/voyant-ui/components/date-picker";
|
|
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
|
+
import { RoomTypeCombobox } from "./room-type-combobox";
|
|
11
|
+
const intOrEmpty = z.coerce.number().int().optional().or(z.literal("")).nullable();
|
|
12
|
+
const formSchema = z.object({
|
|
13
|
+
roomTypeId: z.string().min(1, "Room type is required"),
|
|
14
|
+
date: z.string().min(1, "Date is required"),
|
|
15
|
+
totalUnits: z.coerce.number().int().min(0),
|
|
16
|
+
availableUnits: z.coerce.number().int().min(0),
|
|
17
|
+
heldUnits: z.coerce.number().int().min(0),
|
|
18
|
+
soldUnits: z.coerce.number().int().min(0),
|
|
19
|
+
outOfOrderUnits: z.coerce.number().int().min(0),
|
|
20
|
+
overbookLimit: intOrEmpty,
|
|
21
|
+
stopSell: z.boolean(),
|
|
22
|
+
notes: z.string().optional().nullable(),
|
|
23
|
+
});
|
|
24
|
+
export function RoomInventoryDialog({ open, onOpenChange, propertyId, inventory, onSuccess, }) {
|
|
25
|
+
const isEditing = Boolean(inventory);
|
|
26
|
+
const { create, update } = useRoomInventoryMutation();
|
|
27
|
+
const form = useForm({
|
|
28
|
+
resolver: zodResolver(formSchema),
|
|
29
|
+
defaultValues: {
|
|
30
|
+
roomTypeId: "",
|
|
31
|
+
date: "",
|
|
32
|
+
totalUnits: 0,
|
|
33
|
+
availableUnits: 0,
|
|
34
|
+
heldUnits: 0,
|
|
35
|
+
soldUnits: 0,
|
|
36
|
+
outOfOrderUnits: 0,
|
|
37
|
+
overbookLimit: "",
|
|
38
|
+
stopSell: false,
|
|
39
|
+
notes: "",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (open && inventory) {
|
|
44
|
+
form.reset({
|
|
45
|
+
roomTypeId: inventory.roomTypeId,
|
|
46
|
+
date: inventory.date,
|
|
47
|
+
totalUnits: inventory.totalUnits,
|
|
48
|
+
availableUnits: inventory.availableUnits,
|
|
49
|
+
heldUnits: inventory.heldUnits,
|
|
50
|
+
soldUnits: inventory.soldUnits,
|
|
51
|
+
outOfOrderUnits: inventory.outOfOrderUnits,
|
|
52
|
+
overbookLimit: inventory.overbookLimit ?? "",
|
|
53
|
+
stopSell: inventory.stopSell,
|
|
54
|
+
notes: inventory.notes ?? "",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else if (open) {
|
|
58
|
+
form.reset({
|
|
59
|
+
roomTypeId: "",
|
|
60
|
+
date: "",
|
|
61
|
+
totalUnits: 0,
|
|
62
|
+
availableUnits: 0,
|
|
63
|
+
heldUnits: 0,
|
|
64
|
+
soldUnits: 0,
|
|
65
|
+
outOfOrderUnits: 0,
|
|
66
|
+
overbookLimit: "",
|
|
67
|
+
stopSell: false,
|
|
68
|
+
notes: "",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}, [open, inventory, form]);
|
|
72
|
+
const onSubmit = async (values) => {
|
|
73
|
+
const toInt = (value) => typeof value === "number" ? value : null;
|
|
74
|
+
const payload = {
|
|
75
|
+
propertyId,
|
|
76
|
+
roomTypeId: values.roomTypeId,
|
|
77
|
+
date: values.date,
|
|
78
|
+
totalUnits: values.totalUnits,
|
|
79
|
+
availableUnits: values.availableUnits,
|
|
80
|
+
heldUnits: values.heldUnits,
|
|
81
|
+
soldUnits: values.soldUnits,
|
|
82
|
+
outOfOrderUnits: values.outOfOrderUnits,
|
|
83
|
+
overbookLimit: toInt(values.overbookLimit),
|
|
84
|
+
stopSell: values.stopSell,
|
|
85
|
+
notes: values.notes || null,
|
|
86
|
+
};
|
|
87
|
+
const saved = isEditing
|
|
88
|
+
? await update.mutateAsync({ id: inventory.id, input: payload })
|
|
89
|
+
: await create.mutateAsync(payload);
|
|
90
|
+
onOpenChange(false);
|
|
91
|
+
onSuccess?.(saved);
|
|
92
|
+
};
|
|
93
|
+
const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
|
|
94
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Room Inventory" : "Add Room Inventory" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), 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: "Room type" }), _jsx(RoomTypeCombobox, { propertyId: propertyId, value: form.watch("roomTypeId"), onChange: (value) => form.setValue("roomTypeId", value ?? ""), placeholder: "Select a room type\u2026", disabled: isEditing })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Date" }), _jsx(DatePicker, { value: form.watch("date") || null, onChange: (next) => form.setValue("date", next ?? "", {
|
|
95
|
+
shouldValidate: true,
|
|
96
|
+
shouldDirty: true,
|
|
97
|
+
}), placeholder: "Select date", className: "w-full", disabled: isEditing })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Total" }), _jsx(Input, { ...form.register("totalUnits"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Available" }), _jsx(Input, { ...form.register("availableUnits"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Held" }), _jsx(Input, { ...form.register("heldUnits"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sold" }), _jsx(Input, { ...form.register("soldUnits"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Out of order" }), _jsx(Input, { ...form.register("outOfOrderUnits"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Overbook limit" }), _jsx(Input, { ...form.register("overbookLimit"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("stopSell"), onCheckedChange: (checked) => form.setValue("stopSell", checked) }), _jsx(Label, { children: "Stop sell" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes") })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Add Inventory"] })] })] })] }) }));
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"room-inventory-tab.d.ts","sourceRoot":"","sources":["../../src/components/room-inventory-tab.tsx"],"names":[],"mappings":"AAqBA,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,gBAAgB,CAAC,EAAE,UAAU,EAAE,EAAE,qBAAqB,2CAyKrE"}
|