@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
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @voyantjs/hospitality-ui
2
+
3
+ Importable React UI components for Voyant hospitality. Bundler-consumed (Vite, Next.js, webpack, etc.).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @voyantjs/hospitality-ui @voyantjs/hospitality-react @voyantjs/voyant-ui @tanstack/react-query react react-dom
9
+ ```
10
+
11
+ `@voyantjs/voyant-ui` provides the design-system primitives. `@voyantjs/hospitality-react` provides the data-layer hooks. Both are required peers.
12
+
13
+ All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function CancellationPolicyCombobox({ value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=cancellation-policy-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancellation-policy-combobox.d.ts","sourceRoot":"","sources":["../../src/components/cancellation-policy-combobox.tsx"],"names":[],"mappings":"AAgBA,KAAK,KAAK,GAAG;IACX,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,0BAA0B,CAAC,EACzC,KAAK,EACL,QAAQ,EACR,WAA6C,EAC7C,QAAQ,GACT,EAAE,KAAK,2CAoEP"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCancellationPolicies, useCancellationPolicy, } from "@voyantjs/pricing-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 CancellationPolicyCombobox({ value, onChange, placeholder = "Search cancellation policies…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useCancellationPolicies({ search: search || undefined, limit: PAGE_SIZE });
9
+ const selectedQuery = useCancellationPolicy(value, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? [])
13
+ map.set(item.id, item);
14
+ if (selectedQuery.data)
15
+ map.set(selectedQuery.data.id, selectedQuery.data);
16
+ return Array.from(map.values());
17
+ }, [listQuery.data?.data, selectedQuery.data]);
18
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
19
+ const selected = value ? itemMap.get(value) : undefined;
20
+ const selectedLabel = selected
21
+ ? `${selected.name}${selected.code ? ` · ${selected.code}` : ""}`
22
+ : "";
23
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
24
+ React.useEffect(() => {
25
+ if (selectedLabel)
26
+ setInputValue(selectedLabel);
27
+ }, [selectedLabel]);
28
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => {
29
+ const item = itemMap.get(id);
30
+ return item ? `${item.name}${item.code ? ` · ${item.code}` : ""}` : "";
31
+ }, 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
+ const item = id ? itemMap.get(id) : null;
40
+ setInputValue(item ? `${item.name}${item.code ? ` · ${item.code}` : ""}` : "");
41
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending
42
+ ? "Loading…"
43
+ : "No cancellation policies found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
44
+ const item = itemMap.get(id);
45
+ if (!item)
46
+ return null;
47
+ return (_jsxs(ComboboxItem, { value: item.id, children: [item.name, item.code ? ` · ${item.code}` : ""] }, item.id));
48
+ } }) })] })] }));
49
+ }
@@ -0,0 +1,11 @@
1
+ import { type MaintenanceBlockRecord } from "@voyantjs/hospitality-react";
2
+ export type MaintenanceBlockData = MaintenanceBlockRecord;
3
+ export interface MaintenanceBlockDialogProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ propertyId: string;
7
+ block?: MaintenanceBlockRecord;
8
+ onSuccess?: (block: MaintenanceBlockRecord) => void;
9
+ }
10
+ export declare function MaintenanceBlockDialog({ open, onOpenChange, propertyId, block, onSuccess, }: MaintenanceBlockDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=maintenance-block-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance-block-dialog.d.ts","sourceRoot":"","sources":["../../src/components/maintenance-block-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,sBAAsB,EAE5B,MAAM,6BAA6B,CAAA;AA2BpC,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAkBzD,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,sBAAsB,CAAA;IAC9B,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;CACpD;AAED,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,KAAK,EACL,SAAS,GACV,EAAE,2BAA2B,2CA2K7B"}
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMaintenanceBlockMutation, } from "@voyantjs/hospitality-react";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
4
+ import { DatePicker } from "@voyantjs/voyant-ui/components/date-picker";
5
+ import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
6
+ import { Loader2 } from "lucide-react";
7
+ import { useEffect } from "react";
8
+ import { useForm } from "react-hook-form";
9
+ import { z } from "zod/v4";
10
+ import { RoomTypeCombobox } from "./room-type-combobox";
11
+ import { RoomUnitCombobox } from "./room-unit-combobox";
12
+ const STATUSES = ["open", "in_progress", "resolved", "cancelled"];
13
+ const formSchema = z.object({
14
+ roomTypeId: z.string().optional().nullable(),
15
+ roomUnitId: z.string().optional().nullable(),
16
+ startsOn: z.string().min(1, "Start date is required"),
17
+ endsOn: z.string().min(1, "End date is required"),
18
+ status: z.enum(STATUSES),
19
+ reason: z.string().optional().nullable(),
20
+ notes: z.string().optional().nullable(),
21
+ });
22
+ export function MaintenanceBlockDialog({ open, onOpenChange, propertyId, block, onSuccess, }) {
23
+ const isEditing = Boolean(block);
24
+ const { create, update } = useMaintenanceBlockMutation();
25
+ const form = useForm({
26
+ resolver: zodResolver(formSchema),
27
+ defaultValues: {
28
+ roomTypeId: "",
29
+ roomUnitId: "",
30
+ startsOn: "",
31
+ endsOn: "",
32
+ status: "open",
33
+ reason: "",
34
+ notes: "",
35
+ },
36
+ });
37
+ useEffect(() => {
38
+ if (open && block) {
39
+ form.reset({
40
+ roomTypeId: block.roomTypeId ?? "",
41
+ roomUnitId: block.roomUnitId ?? "",
42
+ startsOn: block.startsOn,
43
+ endsOn: block.endsOn,
44
+ status: block.status,
45
+ reason: block.reason ?? "",
46
+ notes: block.notes ?? "",
47
+ });
48
+ }
49
+ else if (open) {
50
+ form.reset({
51
+ roomTypeId: "",
52
+ roomUnitId: "",
53
+ startsOn: "",
54
+ endsOn: "",
55
+ status: "open",
56
+ reason: "",
57
+ notes: "",
58
+ });
59
+ }
60
+ }, [block, form, open]);
61
+ const onSubmit = async (values) => {
62
+ const payload = {
63
+ propertyId,
64
+ roomTypeId: values.roomTypeId || null,
65
+ roomUnitId: values.roomUnitId || null,
66
+ startsOn: values.startsOn,
67
+ endsOn: values.endsOn,
68
+ status: values.status,
69
+ reason: values.reason || null,
70
+ notes: values.notes || null,
71
+ };
72
+ const saved = isEditing
73
+ ? await update.mutateAsync({ id: block.id, input: payload })
74
+ : await create.mutateAsync(payload);
75
+ onOpenChange(false);
76
+ onSuccess?.(saved);
77
+ };
78
+ const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
79
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Maintenance Block" : "Add Maintenance Block" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Room type (optional)" }), _jsx(RoomTypeCombobox, { propertyId: propertyId, value: form.watch("roomTypeId"), onChange: (value) => form.setValue("roomTypeId", value ?? ""), placeholder: "None", disabled: !open })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Room unit (optional)" }), _jsx(RoomUnitCombobox, { propertyId: propertyId, value: form.watch("roomUnitId"), onChange: (value) => form.setValue("roomUnitId", value ?? ""), placeholder: "None", disabled: !open })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Starts on" }), _jsx(DatePicker, { value: form.watch("startsOn") || null, onChange: (next) => form.setValue("startsOn", next ?? "", {
80
+ shouldValidate: true,
81
+ shouldDirty: true,
82
+ }), placeholder: "Select start date", className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Ends on" }), _jsx(DatePicker, { value: form.watch("endsOn") || null, onChange: (next) => form.setValue("endsOn", next ?? "", {
83
+ shouldValidate: true,
84
+ shouldDirty: true,
85
+ }), placeholder: "Select end date", className: "w-full" })] })] }), _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: "Reason" }), _jsx(Input, { ...form.register("reason"), placeholder: "HVAC failure" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes") })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Add Block"] })] })] })] }) }));
86
+ }
@@ -0,0 +1,5 @@
1
+ export interface MaintenanceBlocksTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function MaintenanceBlocksTab({ propertyId }: MaintenanceBlocksTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=maintenance-blocks-tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maintenance-blocks-tab.d.ts","sourceRoot":"","sources":["../../src/components/maintenance-blocks-tab.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,oBAAoB,CAAC,EAAE,UAAU,EAAE,EAAE,yBAAyB,2CAuI7E"}
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQueries } from "@tanstack/react-query";
4
+ import { getRoomTypeQueryOptions, getRoomUnitQueryOptions, useMaintenanceBlockMutation, useMaintenanceBlocks, 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 { MaintenanceBlockDialog } from "./maintenance-block-dialog";
10
+ import { PaginationFooter } from "./pagination-footer";
11
+ const PAGE_SIZE = 25;
12
+ export function MaintenanceBlocksTab({ 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 } = useMaintenanceBlocks({
17
+ propertyId,
18
+ limit: PAGE_SIZE,
19
+ offset: pageIndex * PAGE_SIZE,
20
+ });
21
+ const { remove } = useMaintenanceBlockMutation();
22
+ const rows = data?.data ?? [];
23
+ const { baseUrl, fetcher } = useVoyantHospitalityContext();
24
+ const roomTypeIds = Array.from(new Set(rows.map((row) => row.roomTypeId).filter(Boolean)));
25
+ const roomUnitIds = Array.from(new Set(rows.map((row) => row.roomUnitId).filter(Boolean)));
26
+ const roomTypeQueries = useQueries({
27
+ queries: roomTypeIds.map((id) => getRoomTypeQueryOptions({ baseUrl, fetcher }, id)),
28
+ });
29
+ const roomUnitQueries = useQueries({
30
+ queries: roomUnitIds.map((id) => getRoomUnitQueryOptions({ baseUrl, fetcher }, id)),
31
+ });
32
+ const roomTypeById = new Map(roomTypeQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
33
+ const roomUnitById = new Map(roomUnitQueries.flatMap((query) => (query.data ? [[query.data.id, query.data]] : [])));
34
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: "Take rooms out of service for repairs or upkeep." }), _jsxs(Button, { size: "sm", onClick: () => {
35
+ setEditing(undefined);
36
+ setDialogOpen(true);
37
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Block"] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No maintenance blocks yet." }) })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-3 text-left font-medium", children: "Dates" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Room type / unit" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Reason" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Status" }), _jsx("th", { className: "w-20 p-3" })] }) }), _jsx("tbody", { children: rows.map((row) => {
38
+ const roomType = row.roomTypeId ? roomTypeById.get(row.roomTypeId)?.name : null;
39
+ const roomUnit = row.roomUnitId
40
+ ? roomUnitById.get(row.roomUnitId)?.roomNumber
41
+ : null;
42
+ return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsxs("td", { className: "p-3 font-mono text-xs", children: [row.startsOn, " \u2192 ", row.endsOn] }), _jsx("td", { className: "p-3 text-muted-foreground", children: roomType ?? roomUnit ?? row.roomTypeId ?? row.roomUnitId ?? "—" }), _jsx("td", { className: "p-3 text-muted-foreground", children: row.reason ?? "—" }), _jsx("td", { className: "p-3", children: _jsx(Badge, { variant: "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: () => {
43
+ setEditing(row);
44
+ setDialogOpen(true);
45
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
46
+ if (confirm("Delete block?")) {
47
+ remove.mutate(row.id);
48
+ }
49
+ }, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, row.id));
50
+ }) })] }) })), _jsx(PaginationFooter, { pageIndex: pageIndex, pageSize: PAGE_SIZE, total: data?.total ?? 0, onPageIndexChange: setPageIndex }), _jsx(MaintenanceBlockDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, block: editing })] }));
51
+ }
@@ -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 MealPlanCombobox({ propertyId, value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=meal-plan-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meal-plan-combobox.d.ts","sourceRoot":"","sources":["../../src/components/meal-plan-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 { useMealPlan, useMealPlans } 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 MealPlanCombobox({ propertyId, value, onChange, placeholder = "Search meal plans…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = useMealPlans({
9
+ propertyId,
10
+ search: search || undefined,
11
+ limit: PAGE_SIZE,
12
+ enabled: !!propertyId,
13
+ });
14
+ const selectedQuery = useMealPlan(value, { enabled: !!value });
15
+ const items = React.useMemo(() => {
16
+ const map = new Map();
17
+ for (const item of listQuery.data?.data ?? [])
18
+ map.set(item.id, item);
19
+ if (selectedQuery.data)
20
+ map.set(selectedQuery.data.id, selectedQuery.data);
21
+ return Array.from(map.values());
22
+ }, [listQuery.data?.data, selectedQuery.data]);
23
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
24
+ const selected = value ? itemMap.get(value) : undefined;
25
+ const selectedLabel = selected ? `${selected.name} · ${selected.code}` : "";
26
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
27
+ React.useEffect(() => {
28
+ if (selectedLabel)
29
+ setInputValue(selectedLabel);
30
+ }, [selectedLabel]);
31
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => {
32
+ const item = itemMap.get(id);
33
+ return item ? `${item.name} · ${item.code}` : "";
34
+ }, onInputValueChange: (next) => {
35
+ setInputValue(next);
36
+ setSearch(next);
37
+ if (!next)
38
+ onChange(null);
39
+ }, onValueChange: (next) => {
40
+ const id = next ?? null;
41
+ onChange(id);
42
+ const item = id ? itemMap.get(id) : null;
43
+ setInputValue(item ? `${item.name} · ${item.code}` : "");
44
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No meal plans found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
45
+ const item = itemMap.get(id);
46
+ if (!item)
47
+ return null;
48
+ return (_jsxs(ComboboxItem, { value: item.id, children: [item.name, " \u00B7 ", item.code] }, item.id));
49
+ } }) })] })] }));
50
+ }
@@ -0,0 +1,10 @@
1
+ import { type MealPlanRecord } from "@voyantjs/hospitality-react";
2
+ export interface MealPlanDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ propertyId: string;
6
+ mealPlan?: MealPlanRecord;
7
+ onSuccess?: (mealPlan: MealPlanRecord) => void;
8
+ }
9
+ export declare function MealPlanDialog({ open, onOpenChange, propertyId, mealPlan, onSuccess, }: MealPlanDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=meal-plan-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meal-plan-dialog.d.ts","sourceRoot":"","sources":["../../src/components/meal-plan-dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAuB,MAAM,6BAA6B,CAAA;AAmCtF,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,2CAoJrB"}
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMealPlanMutation } from "@voyantjs/hospitality-react";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, 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 formSchema = z.object({
10
+ code: z.string().min(1, "Code is required").max(50),
11
+ name: z.string().min(1, "Name is required").max(255),
12
+ description: z.string().optional().nullable(),
13
+ includesBreakfast: z.boolean(),
14
+ includesLunch: z.boolean(),
15
+ includesDinner: z.boolean(),
16
+ includesDrinks: z.boolean(),
17
+ active: z.boolean(),
18
+ sortOrder: z.coerce.number().int(),
19
+ });
20
+ export function MealPlanDialog({ open, onOpenChange, propertyId, mealPlan, onSuccess, }) {
21
+ const isEditing = Boolean(mealPlan);
22
+ const { create, update } = useMealPlanMutation();
23
+ const form = useForm({
24
+ resolver: zodResolver(formSchema),
25
+ defaultValues: {
26
+ code: "",
27
+ name: "",
28
+ description: "",
29
+ includesBreakfast: false,
30
+ includesLunch: false,
31
+ includesDinner: false,
32
+ includesDrinks: false,
33
+ active: true,
34
+ sortOrder: 0,
35
+ },
36
+ });
37
+ useEffect(() => {
38
+ if (open && mealPlan) {
39
+ form.reset({
40
+ code: mealPlan.code,
41
+ name: mealPlan.name,
42
+ description: mealPlan.description ?? "",
43
+ includesBreakfast: mealPlan.includesBreakfast,
44
+ includesLunch: mealPlan.includesLunch,
45
+ includesDinner: mealPlan.includesDinner,
46
+ includesDrinks: mealPlan.includesDrinks,
47
+ active: mealPlan.active,
48
+ sortOrder: mealPlan.sortOrder,
49
+ });
50
+ }
51
+ else if (open) {
52
+ form.reset({
53
+ code: "",
54
+ name: "",
55
+ description: "",
56
+ includesBreakfast: false,
57
+ includesLunch: false,
58
+ includesDinner: false,
59
+ includesDrinks: false,
60
+ active: true,
61
+ sortOrder: 0,
62
+ });
63
+ }
64
+ }, [form, mealPlan, open]);
65
+ const onSubmit = async (values) => {
66
+ const payload = {
67
+ propertyId,
68
+ code: values.code,
69
+ name: values.name,
70
+ description: values.description || null,
71
+ includesBreakfast: values.includesBreakfast,
72
+ includesLunch: values.includesLunch,
73
+ includesDinner: values.includesDinner,
74
+ includesDrinks: values.includesDrinks,
75
+ active: values.active,
76
+ sortOrder: values.sortOrder,
77
+ };
78
+ const saved = isEditing
79
+ ? await update.mutateAsync({ id: mealPlan.id, input: payload })
80
+ : await create.mutateAsync(payload);
81
+ onOpenChange(false);
82
+ onSuccess?.(saved);
83
+ };
84
+ const isSubmitting = form.formState.isSubmitting || create.isPending || update.isPending;
85
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Meal Plan" : "Add Meal Plan" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Code" }), _jsx(Input, { ...form.register("code"), placeholder: "BB" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Bed & Breakfast" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Description" }), _jsx(Textarea, { ...form.register("description") })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sort order" }), _jsx(Input, { ...form.register("sortOrder"), type: "number", className: "w-32" })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("includesBreakfast"), onCheckedChange: (checked) => form.setValue("includesBreakfast", checked) }), _jsx(Label, { children: "Breakfast" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("includesLunch"), onCheckedChange: (checked) => form.setValue("includesLunch", checked) }), _jsx(Label, { children: "Lunch" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("includesDinner"), onCheckedChange: (checked) => form.setValue("includesDinner", checked) }), _jsx(Label, { children: "Dinner" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.watch("includesDrinks"), onCheckedChange: (checked) => form.setValue("includesDrinks", checked) }), _jsx(Label, { children: "Drinks" })] })] }), _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 Meal Plan"] })] })] })] }) }));
86
+ }
@@ -0,0 +1,5 @@
1
+ export interface MealPlansTabProps {
2
+ propertyId: string;
3
+ }
4
+ export declare function MealPlansTab({ propertyId }: MealPlansTabProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=meal-plans-tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meal-plans-tab.d.ts","sourceRoot":"","sources":["../../src/components/meal-plans-tab.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB;AAGD,wBAAgB,YAAY,CAAC,EAAE,UAAU,EAAE,EAAE,iBAAiB,2CAkI7D"}
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMealPlanMutation, useMealPlans } 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 { MealPlanDialog } from "./meal-plan-dialog";
9
+ import { PaginationFooter } from "./pagination-footer";
10
+ const PAGE_SIZE = 25;
11
+ export function MealPlansTab({ 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 } = useMealPlans({
16
+ propertyId,
17
+ limit: PAGE_SIZE,
18
+ offset: pageIndex * PAGE_SIZE,
19
+ });
20
+ const { remove } = useMealPlanMutation();
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: "Meal inclusions like BB, HB, FB, all-inclusive, and room-only." }), _jsxs(Button, { size: "sm", onClick: () => {
23
+ setEditing(undefined);
24
+ setDialogOpen(true);
25
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Meal Plan"] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : rows.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No meal plans yet." }) })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-3 text-left font-medium", children: "Code" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Name" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Includes" }), _jsx("th", { className: "p-3 text-left font-medium", children: "Status" }), _jsx("th", { className: "w-20 p-3" })] }) }), _jsx("tbody", { children: rows.map((row) => {
26
+ const includes = [];
27
+ if (row.includesBreakfast)
28
+ includes.push("Breakfast");
29
+ if (row.includesLunch)
30
+ includes.push("Lunch");
31
+ if (row.includesDinner)
32
+ includes.push("Dinner");
33
+ if (row.includesDrinks)
34
+ includes.push("Drinks");
35
+ return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-3 font-mono text-xs", children: row.code }), _jsx("td", { className: "p-3 font-medium", children: row.name }), _jsx("td", { className: "p-3", children: _jsx("div", { className: "flex flex-wrap gap-1", children: includes.length === 0 ? (_jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" })) : (includes.map((label) => (_jsx(Badge, { variant: "secondary", children: label }, label)))) }) }), _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: () => {
36
+ setEditing(row);
37
+ setDialogOpen(true);
38
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
39
+ if (confirm(`Delete meal plan "${row.name}"?`)) {
40
+ remove.mutate(row.id);
41
+ }
42
+ }, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, row.id));
43
+ }) })] }) })), _jsx(PaginationFooter, { pageIndex: pageIndex, pageSize: PAGE_SIZE, total: data?.total ?? 0, onPageIndexChange: setPageIndex }), _jsx(MealPlanDialog, { open: dialogOpen, onOpenChange: setDialogOpen, propertyId: propertyId, mealPlan: editing })] }));
44
+ }
@@ -0,0 +1,9 @@
1
+ type PaginationFooterProps = {
2
+ pageIndex: number;
3
+ pageSize: number;
4
+ total: number;
5
+ onPageIndexChange: (pageIndex: number) => void;
6
+ };
7
+ export declare function PaginationFooter({ pageIndex, pageSize, total, onPageIndexChange, }: PaginationFooterProps): import("react/jsx-runtime").JSX.Element | null;
8
+ export {};
9
+ //# sourceMappingURL=pagination-footer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination-footer.d.ts","sourceRoot":"","sources":["../../src/components/pagination-footer.tsx"],"names":[],"mappings":"AAIA,KAAK,qBAAqB,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/C,CAAA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,QAAQ,EACR,KAAK,EACL,iBAAiB,GAClB,EAAE,qBAAqB,kDAmCvB"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { Button } from "@voyantjs/voyant-ui/components/button";
4
+ export function PaginationFooter({ pageIndex, pageSize, total, onPageIndexChange, }) {
5
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
6
+ const canPreviousPage = pageIndex > 0;
7
+ const canNextPage = pageIndex + 1 < pageCount;
8
+ if (total <= pageSize)
9
+ return null;
10
+ return (_jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsxs("span", { children: ["Showing ", pageIndex * pageSize + 1, "-", Math.min((pageIndex + 1) * pageSize, total), " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: !canPreviousPage, onClick: () => onPageIndexChange(pageIndex - 1), children: "Previous" }), _jsxs("span", { children: ["Page ", pageIndex + 1, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: !canNextPage, onClick: () => onPageIndexChange(pageIndex + 1), children: "Next" })] })] }));
11
+ }
@@ -0,0 +1,9 @@
1
+ type Props = {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ disabled?: boolean;
6
+ };
7
+ export declare function PriceCatalogCombobox({ value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=price-catalog-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price-catalog-combobox.d.ts","sourceRoot":"","sources":["../../src/components/price-catalog-combobox.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,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,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,WAAsC,EACtC,QAAQ,GACT,EAAE,KAAK,2CA+DP"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { usePriceCatalog, usePriceCatalogs } from "@voyantjs/pricing-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 PriceCatalogCombobox({ value, onChange, placeholder = "Search price catalogs…", disabled, }) {
7
+ const [search, setSearch] = React.useState("");
8
+ const listQuery = usePriceCatalogs({ search: search || undefined, limit: PAGE_SIZE });
9
+ const selectedQuery = usePriceCatalog(value, { enabled: !!value });
10
+ const items = React.useMemo(() => {
11
+ const map = new Map();
12
+ for (const item of listQuery.data?.data ?? [])
13
+ map.set(item.id, item);
14
+ if (selectedQuery.data)
15
+ map.set(selectedQuery.data.id, selectedQuery.data);
16
+ return Array.from(map.values());
17
+ }, [listQuery.data?.data, selectedQuery.data]);
18
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
19
+ const selected = value ? itemMap.get(value) : undefined;
20
+ const selectedLabel = selected ? `${selected.name} · ${selected.code}` : "";
21
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
22
+ React.useEffect(() => {
23
+ if (selectedLabel)
24
+ setInputValue(selectedLabel);
25
+ }, [selectedLabel]);
26
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => {
27
+ const item = itemMap.get(id);
28
+ return item ? `${item.name} · ${item.code}` : "";
29
+ }, onInputValueChange: (next) => {
30
+ setInputValue(next);
31
+ setSearch(next);
32
+ if (!next)
33
+ onChange(null);
34
+ }, onValueChange: (next) => {
35
+ const id = next ?? null;
36
+ onChange(id);
37
+ const item = id ? itemMap.get(id) : null;
38
+ setInputValue(item ? `${item.name} · ${item.code}` : "");
39
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending ? "Loading…" : "No price catalogs found." }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
40
+ const item = itemMap.get(id);
41
+ if (!item)
42
+ return null;
43
+ return (_jsxs(ComboboxItem, { value: item.id, children: [item.name, " \u00B7 ", item.code] }, item.id));
44
+ } }) })] })] }));
45
+ }
@@ -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 RatePlanCombobox({ propertyId, value, onChange, placeholder, disabled, }: Props): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=rate-plan-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-plan-combobox.d.ts","sourceRoot":"","sources":["../../src/components/rate-plan-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"}