@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,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 @@
|
|
|
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 @@
|
|
|
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"}
|