@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.
Files changed (74) hide show
  1. package/README.md +13 -0
  2. package/dist/components/cancellation-policy-combobox.d.ts +9 -0
  3. package/dist/components/cancellation-policy-combobox.d.ts.map +1 -0
  4. package/dist/components/cancellation-policy-combobox.js +49 -0
  5. package/dist/components/maintenance-block-dialog.d.ts +11 -0
  6. package/dist/components/maintenance-block-dialog.d.ts.map +1 -0
  7. package/dist/components/maintenance-block-dialog.js +86 -0
  8. package/dist/components/maintenance-blocks-tab.d.ts +5 -0
  9. package/dist/components/maintenance-blocks-tab.d.ts.map +1 -0
  10. package/dist/components/maintenance-blocks-tab.js +51 -0
  11. package/dist/components/meal-plan-combobox.d.ts +10 -0
  12. package/dist/components/meal-plan-combobox.d.ts.map +1 -0
  13. package/dist/components/meal-plan-combobox.js +50 -0
  14. package/dist/components/meal-plan-dialog.d.ts +10 -0
  15. package/dist/components/meal-plan-dialog.d.ts.map +1 -0
  16. package/dist/components/meal-plan-dialog.js +86 -0
  17. package/dist/components/meal-plans-tab.d.ts +5 -0
  18. package/dist/components/meal-plans-tab.d.ts.map +1 -0
  19. package/dist/components/meal-plans-tab.js +44 -0
  20. package/dist/components/pagination-footer.d.ts +9 -0
  21. package/dist/components/pagination-footer.d.ts.map +1 -0
  22. package/dist/components/pagination-footer.js +11 -0
  23. package/dist/components/price-catalog-combobox.d.ts +9 -0
  24. package/dist/components/price-catalog-combobox.d.ts.map +1 -0
  25. package/dist/components/price-catalog-combobox.js +45 -0
  26. package/dist/components/rate-plan-combobox.d.ts +10 -0
  27. package/dist/components/rate-plan-combobox.d.ts.map +1 -0
  28. package/dist/components/rate-plan-combobox.js +50 -0
  29. package/dist/components/rate-plan-dialog.d.ts +11 -0
  30. package/dist/components/rate-plan-dialog.d.ts.map +1 -0
  31. package/dist/components/rate-plan-dialog.js +120 -0
  32. package/dist/components/rate-plans-tab.d.ts +5 -0
  33. package/dist/components/rate-plans-tab.d.ts.map +1 -0
  34. package/dist/components/rate-plans-tab.js +54 -0
  35. package/dist/components/room-block-dialog.d.ts +11 -0
  36. package/dist/components/room-block-dialog.d.ts.map +1 -0
  37. package/dist/components/room-block-dialog.js +91 -0
  38. package/dist/components/room-blocks-tab.d.ts +5 -0
  39. package/dist/components/room-blocks-tab.d.ts.map +1 -0
  40. package/dist/components/room-blocks-tab.js +51 -0
  41. package/dist/components/room-inventory-dialog.d.ts +11 -0
  42. package/dist/components/room-inventory-dialog.d.ts.map +1 -0
  43. package/dist/components/room-inventory-dialog.js +98 -0
  44. package/dist/components/room-inventory-tab.d.ts +5 -0
  45. package/dist/components/room-inventory-tab.d.ts.map +1 -0
  46. package/dist/components/room-inventory-tab.js +61 -0
  47. package/dist/components/room-type-combobox.d.ts +10 -0
  48. package/dist/components/room-type-combobox.d.ts.map +1 -0
  49. package/dist/components/room-type-combobox.js +46 -0
  50. package/dist/components/room-type-dialog.d.ts +10 -0
  51. package/dist/components/room-type-dialog.d.ts.map +1 -0
  52. package/dist/components/room-type-dialog.js +119 -0
  53. package/dist/components/room-types-tab.d.ts +5 -0
  54. package/dist/components/room-types-tab.d.ts.map +1 -0
  55. package/dist/components/room-types-tab.js +33 -0
  56. package/dist/components/room-unit-combobox.d.ts +10 -0
  57. package/dist/components/room-unit-combobox.d.ts.map +1 -0
  58. package/dist/components/room-unit-combobox.js +50 -0
  59. package/dist/components/room-unit-dialog.d.ts +10 -0
  60. package/dist/components/room-unit-dialog.d.ts.map +1 -0
  61. package/dist/components/room-unit-dialog.js +93 -0
  62. package/dist/components/room-units-tab.d.ts +5 -0
  63. package/dist/components/room-units-tab.d.ts.map +1 -0
  64. package/dist/components/room-units-tab.js +40 -0
  65. package/dist/components/stay-rule-dialog.d.ts +11 -0
  66. package/dist/components/stay-rule-dialog.d.ts.map +1 -0
  67. package/dist/components/stay-rule-dialog.js +140 -0
  68. package/dist/components/stay-rules-tab.d.ts +5 -0
  69. package/dist/components/stay-rules-tab.d.ts.map +1 -0
  70. package/dist/components/stay-rules-tab.js +49 -0
  71. package/dist/index.d.ts +24 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +23 -0
  74. 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,5 @@
1
+ export interface RatePlansTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function RatePlansTab({ propertyId }: RatePlansTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=rate-plans-tab.d.ts.map
@@ -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,5 @@
1
+ export interface RoomBlocksTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function RoomBlocksTab({ propertyId }: RoomBlocksTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=room-blocks-tab.d.ts.map
@@ -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,5 @@
1
+ export interface RoomInventoryTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function RoomInventoryTab({ propertyId }: RoomInventoryTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=room-inventory-tab.d.ts.map
@@ -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"}