@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,61 @@
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, useRoomInventory, useRoomInventoryMutation, 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 { Input } from "@voyantjs/voyant-ui/components/input";
8
+ import { Label } from "@voyantjs/voyant-ui/components/label";
9
+ import { Loader2, Pencil, Plus, Trash2 } from "lucide-react";
10
+ import * as React from "react";
11
+ import { PaginationFooter } from "./pagination-footer";
12
+ import { RoomInventoryDialog } from "./room-inventory-dialog";
13
+ import { RoomTypeCombobox } from "./room-type-combobox";
14
+ const PAGE_SIZE = 25;
15
+ export function RoomInventoryTab({ propertyId }) {
16
+ const [dialogOpen, setDialogOpen] = React.useState(false);
17
+ const [editing, setEditing] = React.useState(undefined);
18
+ const [dateFrom, setDateFrom] = React.useState("");
19
+ const [dateTo, setDateTo] = React.useState("");
20
+ const [roomTypeId, setRoomTypeId] = React.useState("");
21
+ const [pageIndex, setPageIndex] = React.useState(0);
22
+ const { data, isPending } = useRoomInventory({
23
+ propertyId,
24
+ roomTypeId: roomTypeId || undefined,
25
+ dateFrom: dateFrom || undefined,
26
+ dateTo: dateTo || undefined,
27
+ limit: PAGE_SIZE,
28
+ offset: pageIndex * PAGE_SIZE,
29
+ });
30
+ const { remove } = useRoomInventoryMutation();
31
+ const rows = data?.data ?? [];
32
+ const { baseUrl, fetcher } = useVoyantHospitalityContext();
33
+ const roomTypeIds = Array.from(new Set(rows.map((row) => row.roomTypeId)));
34
+ if (roomTypeId)
35
+ roomTypeIds.push(roomTypeId);
36
+ const uniqueRoomTypeIds = Array.from(new Set(roomTypeIds));
37
+ const roomTypeQueries = useQueries({
38
+ queries: uniqueRoomTypeIds.map((id) => getRoomTypeQueryOptions({ baseUrl, fetcher }, id)),
39
+ });
40
+ const roomTypeById = new Map(roomTypeQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
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: "Daily unit availability per room type." }), _jsxs(Button, { size: "sm", onClick: () => {
42
+ setEditing(undefined);
43
+ setDialogOpen(true);
44
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Inventory"] })] }), _jsxs("div", { className: "grid max-w-3xl grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Room type" }), _jsx(RoomTypeCombobox, { propertyId: propertyId, value: roomTypeId, onChange: (value) => {
45
+ setRoomTypeId(value ?? "");
46
+ setPageIndex(0);
47
+ }, placeholder: "All" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "From" }), _jsx(Input, { value: dateFrom, onChange: (event) => {
48
+ setDateFrom(event.target.value);
49
+ setPageIndex(0);
50
+ }, type: "date" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "To" }), _jsx(Input, { value: dateTo, onChange: (event) => {
51
+ setDateTo(event.target.value);
52
+ setPageIndex(0);
53
+ }, type: "date" })] })] }), 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 inventory rows 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: "Date" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Room type" }), _jsx("th", { className: "p-3 text-right font-medium", children: "Total" }), _jsx("th", { className: "p-3 text-right font-medium", children: "Avail" }), _jsx("th", { className: "p-3 text-right font-medium", children: "Held" }), _jsx("th", { className: "p-3 text-right font-medium", children: "Sold" }), _jsx("th", { className: "p-3 text-right font-medium", children: "OOO" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Stop" }), _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.date }), _jsx("td", { className: "p-3 text-muted-foreground", children: roomTypeById.get(row.roomTypeId)?.name ?? row.roomTypeId }), _jsx("td", { className: "p-3 text-right font-mono", children: row.totalUnits }), _jsx("td", { className: "p-3 text-right font-mono", children: row.availableUnits }), _jsx("td", { className: "p-3 text-right font-mono", children: row.heldUnits }), _jsx("td", { className: "p-3 text-right font-mono", children: row.soldUnits }), _jsx("td", { className: "p-3 text-right font-mono", children: row.outOfOrderUnits }), _jsx("td", { className: "p-3", children: row.stopSell ? _jsx(Badge, { variant: "destructive", children: "Stop" }) : null }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
54
+ setEditing(row);
55
+ setDialogOpen(true);
56
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
57
+ if (confirm("Delete inventory row?")) {
58
+ remove.mutate(row.id);
59
+ }
60
+ }, 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(RoomInventoryDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, inventory: editing })] }));
61
+ }
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ propertyId: string;
3
+ value: string | null | undefined;
4
+ onChange: (value: string | null) => void;
5
+ placeholder?: string;
6
+ disabled?: boolean;
7
+ };
8
+ export declare function RoomTypeCombobox({ propertyId, value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=room-type-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-type-combobox.d.ts","sourceRoot":"","sources":["../../src/components/room-type-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,WAAkC,EAClC,QAAQ,GACT,EAAE,KAAK,2CAgEP"}
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRoomType, useRoomTypes } 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 RoomTypeCombobox({ propertyId, value, onChange, placeholder = "Search room types…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useRoomTypes({
9
+ propertyId,
10
+ search: search || undefined,
11
+ limit: PAGE_SIZE,
12
+ enabled: !!propertyId,
13
+ });
14
+ const selectedQuery = useRoomType(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 : "";
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) => itemMap.get(id)?.name ?? "", onInputValueChange: (next) => {
32
+ setInputValue(next);
33
+ setSearch(next);
34
+ if (!next)
35
+ onChange(null);
36
+ }, onValueChange: (next) => {
37
+ const id = next ?? null;
38
+ onChange(id);
39
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
40
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No room types found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
41
+ const item = itemMap.get(id);
42
+ if (!item)
43
+ return null;
44
+ return (_jsx(ComboboxItem, { value: item.id, children: item.name }, item.id));
45
+ } }) })] })] }));
46
+ }
@@ -0,0 +1,10 @@
1
+ import { type RoomTypeRecord } from "@voyantjs/hospitality-react";
2
+ export interface RoomTypeDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ propertyId: string;
6
+ roomType?: RoomTypeRecord;
7
+ onSuccess?: (roomType: RoomTypeRecord) => void;
8
+ }
9
+ export declare function RoomTypeDialog({ open, onOpenChange, propertyId, roomType, onSuccess, }: RoomTypeDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=room-type-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-type-dialog.d.ts","sourceRoot":"","sources":["../../src/components/room-type-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAA;AAoDtF,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,2CA2NrB"}
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRoomTypeMutation } 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 { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect } from "react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod/v4";
9
+ const INVENTORY_MODES = ["virtual", "pooled", "serialized"];
10
+ const intOrEmpty = z.coerce.number().int().optional().or(z.literal("")).nullable();
11
+ const formSchema = z.object({
12
+ name: z.string().min(1, "Name is required").max(255),
13
+ code: z.string().optional().nullable(),
14
+ description: z.string().optional().nullable(),
15
+ inventoryMode: z.enum(INVENTORY_MODES),
16
+ maxAdults: intOrEmpty,
17
+ maxChildren: intOrEmpty,
18
+ maxInfants: intOrEmpty,
19
+ standardOccupancy: intOrEmpty,
20
+ maxOccupancy: intOrEmpty,
21
+ minOccupancy: intOrEmpty,
22
+ bedroomCount: intOrEmpty,
23
+ bathroomCount: intOrEmpty,
24
+ smokingAllowed: z.boolean(),
25
+ active: z.boolean(),
26
+ sortOrder: z.coerce.number().int(),
27
+ });
28
+ export function RoomTypeDialog({ open, onOpenChange, propertyId, roomType, onSuccess, }) {
29
+ const isEditing = Boolean(roomType);
30
+ const { create, update } = useRoomTypeMutation();
31
+ const form = useForm({
32
+ resolver: zodResolver(formSchema),
33
+ defaultValues: {
34
+ name: "",
35
+ code: "",
36
+ description: "",
37
+ inventoryMode: "pooled",
38
+ maxAdults: "",
39
+ maxChildren: "",
40
+ maxInfants: "",
41
+ standardOccupancy: "",
42
+ maxOccupancy: "",
43
+ minOccupancy: "",
44
+ bedroomCount: "",
45
+ bathroomCount: "",
46
+ smokingAllowed: false,
47
+ active: true,
48
+ sortOrder: 0,
49
+ },
50
+ });
51
+ useEffect(() => {
52
+ if (open && roomType) {
53
+ form.reset({
54
+ name: roomType.name,
55
+ code: roomType.code ?? "",
56
+ description: roomType.description ?? "",
57
+ inventoryMode: roomType.inventoryMode,
58
+ maxAdults: roomType.maxAdults ?? "",
59
+ maxChildren: roomType.maxChildren ?? "",
60
+ maxInfants: roomType.maxInfants ?? "",
61
+ standardOccupancy: roomType.standardOccupancy ?? "",
62
+ maxOccupancy: roomType.maxOccupancy ?? "",
63
+ minOccupancy: roomType.minOccupancy ?? "",
64
+ bedroomCount: roomType.bedroomCount ?? "",
65
+ bathroomCount: roomType.bathroomCount ?? "",
66
+ smokingAllowed: roomType.smokingAllowed,
67
+ active: roomType.active,
68
+ sortOrder: roomType.sortOrder,
69
+ });
70
+ }
71
+ else if (open) {
72
+ form.reset({
73
+ name: "",
74
+ code: "",
75
+ description: "",
76
+ inventoryMode: "pooled",
77
+ maxAdults: "",
78
+ maxChildren: "",
79
+ maxInfants: "",
80
+ standardOccupancy: "",
81
+ maxOccupancy: "",
82
+ minOccupancy: "",
83
+ bedroomCount: "",
84
+ bathroomCount: "",
85
+ smokingAllowed: false,
86
+ active: true,
87
+ sortOrder: 0,
88
+ });
89
+ }
90
+ }, [form, open, roomType]);
91
+ const onSubmit = async (values) => {
92
+ const toInt = (value) => typeof value === "number" ? value : null;
93
+ const payload = {
94
+ propertyId,
95
+ name: values.name,
96
+ code: values.code || null,
97
+ description: values.description || null,
98
+ inventoryMode: values.inventoryMode,
99
+ maxAdults: toInt(values.maxAdults),
100
+ maxChildren: toInt(values.maxChildren),
101
+ maxInfants: toInt(values.maxInfants),
102
+ standardOccupancy: toInt(values.standardOccupancy),
103
+ maxOccupancy: toInt(values.maxOccupancy),
104
+ minOccupancy: toInt(values.minOccupancy),
105
+ bedroomCount: toInt(values.bedroomCount),
106
+ bathroomCount: toInt(values.bathroomCount),
107
+ smokingAllowed: values.smokingAllowed,
108
+ active: values.active,
109
+ sortOrder: values.sortOrder,
110
+ };
111
+ const saved = isEditing
112
+ ? await update.mutateAsync({ id: roomType.id, input: payload })
113
+ : await create.mutateAsync(payload);
114
+ onOpenChange(false);
115
+ onSuccess?.(saved);
116
+ };
117
+ const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
118
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Room Type" : "Add Room Type" }) }), _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: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Deluxe Double" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Code" }), _jsx(Input, { ...form.register("code"), placeholder: "DLX-DBL" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Description" }), _jsx(Textarea, { ...form.register("description") })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Inventory mode" }), _jsxs(Select, { items: INVENTORY_MODES.map((x) => ({ label: x.replace(/_/g, " "), value: x })), value: form.watch("inventoryMode"), onValueChange: (value) => form.setValue("inventoryMode", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: INVENTORY_MODES.map((mode) => (_jsx(SelectItem, { value: mode, className: "capitalize", children: mode }, mode))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sort order" }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Std. occupancy" }), _jsx(Input, { ...form.register("standardOccupancy"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Min occupancy" }), _jsx(Input, { ...form.register("minOccupancy"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Max occupancy" }), _jsx(Input, { ...form.register("maxOccupancy"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Max adults" }), _jsx(Input, { ...form.register("maxAdults"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Max children" }), _jsx(Input, { ...form.register("maxChildren"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Max infants" }), _jsx(Input, { ...form.register("maxInfants"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Bedrooms" }), _jsx(Input, { ...form.register("bedroomCount"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Bathrooms" }), _jsx(Input, { ...form.register("bathroomCount"), type: "number", min: "0" })] })] }), _jsxs("div", { className: "flex gap-6", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("smokingAllowed"), onCheckedChange: (checked) => form.setValue("smokingAllowed", checked) }), _jsx(Label, { children: "Smoking allowed" })] }), _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(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 Room Type"] })] })] })] }) }));
119
+ }
@@ -0,0 +1,5 @@
1
+ export interface RoomTypesTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function RoomTypesTab({ propertyId }: RoomTypesTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=room-types-tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-types-tab.d.ts","sourceRoot":"","sources":["../../src/components/room-types-tab.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,iBAAiB,2CAoH7D"}
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRoomTypeMutation, useRoomTypes } from "@voyantjs/hospitality-react";
4
+ import { Badge } from "@voyantjs/voyant-ui/components/badge";
5
+ import { Button } from "@voyantjs/voyant-ui/components/button";
6
+ import { Loader2, Pencil, Plus, Trash2 } from "lucide-react";
7
+ import * as React from "react";
8
+ import { PaginationFooter } from "./pagination-footer";
9
+ import { RoomTypeDialog } from "./room-type-dialog";
10
+ const PAGE_SIZE = 25;
11
+ export function RoomTypesTab({ propertyId }) {
12
+ const [dialogOpen, setDialogOpen] = React.useState(false);
13
+ const [editing, setEditing] = React.useState(undefined);
14
+ const [pageIndex, setPageIndex] = React.useState(0);
15
+ const { data, isPending } = useRoomTypes({
16
+ propertyId,
17
+ limit: PAGE_SIZE,
18
+ offset: pageIndex * PAGE_SIZE,
19
+ });
20
+ const { remove } = useRoomTypeMutation();
21
+ const rows = (data?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder);
22
+ 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: "Room categories sold as inventory." }), _jsxs(Button, { size: "sm", onClick: () => {
23
+ setEditing(undefined);
24
+ setDialogOpen(true);
25
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Room Type"] })] }), 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 types 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: "Name" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Code" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Mode" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Occupancy" }), _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-medium", children: row.name }), _jsx("td", { className: "p-3 font-mono text-xs text-muted-foreground", children: row.code ?? "—" }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: "outline", className: "capitalize", children: row.inventoryMode }) }), _jsxs("td", { className: "p-3 font-mono text-xs text-muted-foreground", children: [row.standardOccupancy ?? "—", " / ", row.maxOccupancy ?? "—"] }), _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: () => {
26
+ setEditing(row);
27
+ setDialogOpen(true);
28
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
29
+ if (confirm(`Delete room type "${row.name}"?`)) {
30
+ remove.mutate(row.id);
31
+ }
32
+ }, 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(RoomTypeDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, roomType: editing })] }));
33
+ }
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ propertyId: string;
3
+ value: string | null | undefined;
4
+ onChange: (value: string | null) => void;
5
+ placeholder?: string;
6
+ disabled?: boolean;
7
+ };
8
+ export declare function RoomUnitCombobox({ propertyId, value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=room-unit-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-unit-combobox.d.ts","sourceRoot":"","sources":["../../src/components/room-unit-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAID,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,WAAkC,EAClC,QAAQ,GACT,EAAE,KAAK,2CAoEP"}
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRoomUnit, useRoomUnits } 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 RoomUnitCombobox({ propertyId, value, onChange, placeholder = "Search room units…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useRoomUnits({
9
+ propertyId,
10
+ search: search || undefined,
11
+ limit: PAGE_SIZE,
12
+ enabled: !!propertyId,
13
+ });
14
+ const selectedQuery = useRoomUnit(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.roomNumber ?? selected.code ?? selected.id) : "";
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.roomNumber ?? item.code ?? item.id) : "";
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.roomNumber ?? item.code ?? item.id) : "");
44
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No room units found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
45
+ const item = itemMap.get(id);
46
+ if (!item)
47
+ return null;
48
+ return (_jsx(ComboboxItem, { value: item.id, children: item.roomNumber ?? item.code ?? item.id }, item.id));
49
+ } }) })] })] }));
50
+ }
@@ -0,0 +1,10 @@
1
+ import { type RoomUnitRecord } from "@voyantjs/hospitality-react";
2
+ export interface RoomUnitDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ propertyId: string;
6
+ unit?: RoomUnitRecord;
7
+ onSuccess?: (unit: RoomUnitRecord) => void;
8
+ }
9
+ export declare function RoomUnitDialog({ open, onOpenChange, propertyId, unit, onSuccess, }: RoomUnitDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=room-unit-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-unit-dialog.d.ts","sourceRoot":"","sources":["../../src/components/room-unit-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAA;AA4CtF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CAC3C;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,SAAS,GACV,EAAE,mBAAmB,2CA2KrB"}
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRoomUnitMutation } 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 { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect } from "react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod/v4";
9
+ import { RoomTypeCombobox } from "./room-type-combobox";
10
+ const STATUSES = ["active", "inactive", "out_of_order", "archived"];
11
+ const formSchema = z.object({
12
+ roomTypeId: z.string().min(1, "Room type is required"),
13
+ code: z.string().optional().nullable(),
14
+ roomNumber: z.string().optional().nullable(),
15
+ floor: z.string().optional().nullable(),
16
+ wing: z.string().optional().nullable(),
17
+ status: z.enum(STATUSES),
18
+ viewCode: z.string().optional().nullable(),
19
+ accessibilityCode: z.string().optional().nullable(),
20
+ genderRestriction: z.string().optional().nullable(),
21
+ notes: z.string().optional().nullable(),
22
+ });
23
+ export function RoomUnitDialog({ open, onOpenChange, propertyId, unit, onSuccess, }) {
24
+ const isEditing = Boolean(unit);
25
+ const { create, update } = useRoomUnitMutation();
26
+ const form = useForm({
27
+ resolver: zodResolver(formSchema),
28
+ defaultValues: {
29
+ roomTypeId: "",
30
+ code: "",
31
+ roomNumber: "",
32
+ floor: "",
33
+ wing: "",
34
+ status: "active",
35
+ viewCode: "",
36
+ accessibilityCode: "",
37
+ genderRestriction: "",
38
+ notes: "",
39
+ },
40
+ });
41
+ useEffect(() => {
42
+ if (open && unit) {
43
+ form.reset({
44
+ roomTypeId: unit.roomTypeId,
45
+ code: unit.code ?? "",
46
+ roomNumber: unit.roomNumber ?? "",
47
+ floor: unit.floor ?? "",
48
+ wing: unit.wing ?? "",
49
+ status: unit.status,
50
+ viewCode: unit.viewCode ?? "",
51
+ accessibilityCode: unit.accessibilityCode ?? "",
52
+ genderRestriction: unit.genderRestriction ?? "",
53
+ notes: unit.notes ?? "",
54
+ });
55
+ }
56
+ else if (open) {
57
+ form.reset({
58
+ roomTypeId: "",
59
+ code: "",
60
+ roomNumber: "",
61
+ floor: "",
62
+ wing: "",
63
+ status: "active",
64
+ viewCode: "",
65
+ accessibilityCode: "",
66
+ genderRestriction: "",
67
+ notes: "",
68
+ });
69
+ }
70
+ }, [form, open, unit]);
71
+ const onSubmit = async (values) => {
72
+ const payload = {
73
+ propertyId,
74
+ roomTypeId: values.roomTypeId,
75
+ code: values.code || null,
76
+ roomNumber: values.roomNumber || null,
77
+ floor: values.floor || null,
78
+ wing: values.wing || null,
79
+ status: values.status,
80
+ viewCode: values.viewCode || null,
81
+ accessibilityCode: values.accessibilityCode || null,
82
+ genderRestriction: values.genderRestriction || null,
83
+ notes: values.notes || null,
84
+ };
85
+ const saved = isEditing
86
+ ? await update.mutateAsync({ id: unit.id, input: payload })
87
+ : await create.mutateAsync(payload);
88
+ onOpenChange(false);
89
+ onSuccess?.(saved);
90
+ };
91
+ const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
92
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Room Unit" : "Add Room Unit" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid 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: !open })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Room number" }), _jsx(Input, { ...form.register("roomNumber"), placeholder: "412" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Code" }), _jsx(Input, { ...form.register("code"), placeholder: "DLX-412" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Floor" }), _jsx(Input, { ...form.register("floor"), placeholder: "4" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Wing" }), _jsx(Input, { ...form.register("wing"), placeholder: "North" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", 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.replace(/_/g, " ") }, status))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "View code" }), _jsx(Input, { ...form.register("viewCode"), placeholder: "sea" })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Accessibility" }), _jsx(Input, { ...form.register("accessibilityCode"), placeholder: "wheelchair" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Gender restriction" }), _jsx(Input, { ...form.register("genderRestriction"), placeholder: "female_only" })] })] }), _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 Room Unit"] })] })] })] }) }));
93
+ }
@@ -0,0 +1,5 @@
1
+ export interface RoomUnitsTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function RoomUnitsTab({ propertyId }: RoomUnitsTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=room-units-tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-units-tab.d.ts","sourceRoot":"","sources":["../../src/components/room-units-tab.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,iBAAiB,2CA0H7D"}
@@ -0,0 +1,40 @@
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, useRoomUnitMutation, useRoomUnits, 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 { RoomUnitDialog } from "./room-unit-dialog";
11
+ const PAGE_SIZE = 25;
12
+ export function RoomUnitsTab({ 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 } = useRoomUnits({
17
+ propertyId,
18
+ limit: PAGE_SIZE,
19
+ offset: pageIndex * PAGE_SIZE,
20
+ });
21
+ const { remove } = useRoomUnitMutation();
22
+ const rows = data?.data ?? [];
23
+ const { baseUrl, fetcher } = useVoyantHospitalityContext();
24
+ const roomTypeIds = Array.from(new Set(rows.map((row) => row.roomTypeId)));
25
+ const roomTypeQueries = useQueries({
26
+ queries: roomTypeIds.map((id) => getRoomTypeQueryOptions({ baseUrl, fetcher }, id)),
27
+ });
28
+ const roomTypeById = new Map(roomTypeQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
29
+ 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: "Physical rooms that belong to a room type." }), _jsxs(Button, { size: "sm", onClick: () => {
30
+ setEditing(undefined);
31
+ setDialogOpen(true);
32
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Room Unit"] })] }), 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 units 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: "Room #" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Room type" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Floor" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Wing" }), _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-medium", children: row.roomNumber ?? row.code ?? row.id }), _jsx("td", { className: "p-3 text-muted-foreground", children: roomTypeById.get(row.roomTypeId)?.name ?? row.roomTypeId }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.floor ?? "—" }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.wing ?? "—" }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: row.status === "active" ? "default" : "outline", className: "capitalize", children: row.status.replace(/_/g, " ") }) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
33
+ setEditing(row);
34
+ setDialogOpen(true);
35
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
36
+ if (confirm(`Delete room unit "${row.roomNumber ?? row.id}"?`)) {
37
+ remove.mutate(row.id);
38
+ }
39
+ }, 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(RoomUnitDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, unit: editing })] }));
40
+ }
@@ -0,0 +1,11 @@
1
+ import { type StayRuleRecord } from "@voyantjs/hospitality-react";
2
+ export type StayRuleData = StayRuleRecord;
3
+ export interface StayRuleDialogProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ propertyId: string;
7
+ rule?: StayRuleRecord;
8
+ onSuccess?: (rule: StayRuleRecord) => void;
9
+ }
10
+ export declare function StayRuleDialog({ open, onOpenChange, propertyId, rule, onSuccess, }: StayRuleDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=stay-rule-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stay-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stay-rule-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAA;AAuBtF,MAAM,MAAM,YAAY,GAAG,cAAc,CAAA;AA6BzC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CAC3C;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,IAAI,EACJ,SAAS,GACV,EAAE,mBAAmB,2CA4RrB"}