@voyantjs/products-ui 0.38.1 → 0.40.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.
@@ -0,0 +1,14 @@
1
+ import type { ProductDayServiceRecord } from "@voyantjs/products-react";
2
+ import { type ProductDayServiceFormProps } from "./product-day-service-form.js";
3
+ export interface ProductDayServiceDialogProps {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ productId: string;
7
+ dayId: string;
8
+ service?: ProductDayServiceRecord;
9
+ onSuccess?: (service: ProductDayServiceRecord) => void;
10
+ renderSupplierServiceField?: ProductDayServiceFormProps["renderSupplierServiceField"];
11
+ onSupplierServiceSelected?: ProductDayServiceFormProps["onSupplierServiceSelected"];
12
+ }
13
+ export declare function ProductDayServiceDialog({ open, onOpenChange, productId, dayId, service, onSuccess, renderSupplierServiceField, onSupplierServiceSelected, }: ProductDayServiceDialogProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=product-day-service-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-day-service-dialog.d.ts","sourceRoot":"","sources":["../../src/components/product-day-service-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAWvE,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,+BAA+B,CAAA;AAEtC,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,uBAAuB,CAAA;IACjC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,CAAA;IACtD,0BAA0B,CAAC,EAAE,0BAA0B,CAAC,4BAA4B,CAAC,CAAA;IACrF,yBAAyB,CAAC,EAAE,0BAA0B,CAAC,2BAA2B,CAAC,CAAA;CACpF;AAED,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,KAAK,EACL,OAAO,EACP,SAAS,EACT,0BAA0B,EAC1B,yBAAyB,GAC1B,EAAE,4BAA4B,2CAoC9B"}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@voyantjs/ui/components/dialog";
4
+ import { useProductsUiMessagesOrDefault } from "../i18n/provider.js";
5
+ import { ProductDayServiceForm, } from "./product-day-service-form.js";
6
+ export function ProductDayServiceDialog({ open, onOpenChange, productId, dayId, service, onSuccess, renderSupplierServiceField, onSupplierServiceSelected, }) {
7
+ const isEdit = Boolean(service);
8
+ const messages = useProductsUiMessagesOrDefault();
9
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "product-day-service-dialog", className: "sm:max-w-[720px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit
10
+ ? messages.productDayServiceDialog.titles.edit
11
+ : messages.productDayServiceDialog.titles.create }), _jsx(DialogDescription, { children: isEdit
12
+ ? messages.productDayServiceDialog.descriptions.edit
13
+ : messages.productDayServiceDialog.descriptions.create })] }), _jsx(ProductDayServiceForm, { mode: service
14
+ ? { kind: "edit", productId, dayId, service }
15
+ : { kind: "create", productId, dayId }, renderSupplierServiceField: renderSupplierServiceField, onSupplierServiceSelected: onSupplierServiceSelected, onSuccess: (savedService) => {
16
+ onSuccess?.(savedService);
17
+ onOpenChange(false);
18
+ }, onCancel: () => onOpenChange(false) })] }) }));
19
+ }
@@ -1,5 +1,6 @@
1
1
  import { type ProductDayServiceRecord, type ProductRecord } from "@voyantjs/products-react";
2
2
  import * as React from "react";
3
+ import type { ProductDayServiceFormProps } from "./product-day-service-form.js";
3
4
  import { type ProductItineraryDayRowRenderContext } from "./product-itinerary-day-row.js";
4
5
  import { type ProductMediaSectionProps } from "./product-media-section.js";
5
6
  import { type ProductOptionsSectionProps } from "./product-options-section.js";
@@ -24,9 +25,11 @@ export interface ProductDetailPageProps {
24
25
  renderOptionDetails?: ProductOptionsSectionProps["renderOptionDetails"];
25
26
  renderItineraryDayDetails?: (context: ProductItineraryDayRowRenderContext) => React.ReactNode;
26
27
  renderItineraryServiceActions?: (service: ProductDayServiceRecord) => React.ReactNode;
28
+ renderSupplierServiceField?: ProductDayServiceFormProps["renderSupplierServiceField"];
29
+ onSupplierServiceSelected?: ProductDayServiceFormProps["onSupplierServiceSelected"];
27
30
  slots?: ProductDetailPageSlots;
28
31
  }
29
- export declare function ProductDetailPage({ id, className, onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, slots, }: ProductDetailPageProps): import("react/jsx-runtime").JSX.Element;
32
+ export declare function ProductDetailPage({ id, className, onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, renderSupplierServiceField, onSupplierServiceSelected, slots, }: ProductDetailPageProps): import("react/jsx-runtime").JSX.Element;
30
33
  export interface ProductDetailHeaderProps {
31
34
  product: ProductRecord;
32
35
  onBack?: () => void;
@@ -55,7 +58,9 @@ export interface ProductItinerarySectionProps {
55
58
  description?: string;
56
59
  renderDayDetails?: (context: ProductItineraryDayRowRenderContext) => React.ReactNode;
57
60
  renderServiceActions?: (service: ProductDayServiceRecord) => React.ReactNode;
61
+ renderSupplierServiceField?: ProductDayServiceFormProps["renderSupplierServiceField"];
62
+ onSupplierServiceSelected?: ProductDayServiceFormProps["onSupplierServiceSelected"];
58
63
  className?: string;
59
64
  }
60
- export declare function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, className, }: ProductItinerarySectionProps): import("react/jsx-runtime").JSX.Element;
65
+ export declare function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, renderSupplierServiceField, onSupplierServiceSelected, className, }: ProductItinerarySectionProps): import("react/jsx-runtime").JSX.Element;
61
66
  //# sourceMappingURL=product-detail-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"product-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/product-detail-page.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"product-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/product-detail-page.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useProduct, useProductDayMutation, useProductItineraries, useProductItineraryDays, useProductItineraryMutation, useProductMutation, } from "@voyantjs/products-react";
3
+ import { useProduct, useProductDayMutation, useProductDayServiceMutation, useProductItineraries, useProductItineraryDays, useProductItineraryMutation, useProductMutation, } from "@voyantjs/products-react";
4
4
  import { Badge } from "@voyantjs/ui/components/badge";
5
5
  import { Button } from "@voyantjs/ui/components/button";
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@voyantjs/ui/components/card";
@@ -10,13 +10,14 @@ import { ArrowLeft, Edit, FileText, Loader2, Plus, ReceiptText, Trash2 } from "l
10
10
  import * as React from "react";
11
11
  import { useProductsUiI18nOrDefault, useProductsUiMessagesOrDefault } from "../i18n/provider.js";
12
12
  import { ProductDayDialog } from "./product-day-dialog.js";
13
+ import { ProductDayServiceDialog } from "./product-day-service-dialog.js";
13
14
  import { ProductDialog } from "./product-dialog.js";
14
15
  import { ProductItineraryDayRow, } from "./product-itinerary-day-row.js";
15
16
  import { ProductItineraryDialog } from "./product-itinerary-dialog.js";
16
17
  import { ProductMediaSection } from "./product-media-section.js";
17
18
  import { ProductOptionsSection, } from "./product-options-section.js";
18
19
  import { ProductVersionsSection } from "./product-versions-section.js";
19
- export function ProductDetailPage({ id, className, onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, slots, }) {
20
+ export function ProductDetailPage({ id, className, onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, renderSupplierServiceField, onSupplierServiceSelected, slots, }) {
20
21
  const messages = useProductsUiMessagesOrDefault();
21
22
  const pageMessages = messages.productDetailPage;
22
23
  const productQuery = useProduct(id);
@@ -45,7 +46,7 @@ export function ProductDetailPage({ id, className, onBack, onBookingCreate, onDe
45
46
  setDeleteError(error instanceof Error ? error.message : pageMessages.states.deleteFailed);
46
47
  }
47
48
  };
48
- return (_jsxs("div", { "data-slot": "product-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsx(ProductDetailHeader, { product: product, onBack: onBack, onEdit: () => setEditOpen(true), onDelete: () => void handleDelete(), onBookingCreate: onBookingCreate ? () => onBookingCreate(product) : undefined, deleting: remove.isPending, actionsSlot: slots?.header }), deleteError ? _jsx("p", { className: "text-sm text-destructive", children: deleteError }) : null, slots?.afterHeader, _jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [slots?.overviewStart, _jsx(ProductOverviewCard, { product: product }), _jsx(ProductCommercialCard, { product: product }), slots?.overviewEnd, _jsx(ProductMediaSection, { productId: product.id, uploadMedia: uploadMedia }), slots?.mediaEnd, _jsx(ProductItinerarySection, { productId: product.id, renderDayDetails: renderItineraryDayDetails, renderServiceActions: renderItineraryServiceActions }), slots?.itineraryEnd, _jsx(ProductOptionsSection, { productId: product.id, renderOptionDetails: renderOptionDetails }), slots?.optionsEnd, _jsx(ProductVersionsSection, { productId: product.id }), slots?.versionsEnd] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [_jsx(ProductDetailSidebar, { product: product }), slots?.sidebar] })] }), _jsx(ProductDialog, { open: editOpen, onOpenChange: setEditOpen, product: product, onSuccess: () => setEditOpen(false) })] }));
49
+ return (_jsxs("div", { "data-slot": "product-detail-page", className: cn("flex flex-col gap-6 p-6", className), children: [_jsx(ProductDetailHeader, { product: product, onBack: onBack, onEdit: () => setEditOpen(true), onDelete: () => void handleDelete(), onBookingCreate: onBookingCreate ? () => onBookingCreate(product) : undefined, deleting: remove.isPending, actionsSlot: slots?.header }), deleteError ? _jsx("p", { className: "text-sm text-destructive", children: deleteError }) : null, slots?.afterHeader, _jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [slots?.overviewStart, _jsx(ProductOverviewCard, { product: product }), _jsx(ProductCommercialCard, { product: product }), slots?.overviewEnd, _jsx(ProductMediaSection, { productId: product.id, uploadMedia: uploadMedia }), slots?.mediaEnd, _jsx(ProductItinerarySection, { productId: product.id, renderDayDetails: renderItineraryDayDetails, renderServiceActions: renderItineraryServiceActions, renderSupplierServiceField: renderSupplierServiceField, onSupplierServiceSelected: onSupplierServiceSelected }), slots?.itineraryEnd, _jsx(ProductOptionsSection, { productId: product.id, renderOptionDetails: renderOptionDetails }), slots?.optionsEnd, _jsx(ProductVersionsSection, { productId: product.id }), slots?.versionsEnd] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-6", children: [_jsx(ProductDetailSidebar, { product: product }), slots?.sidebar] })] }), _jsx(ProductDialog, { open: editOpen, onOpenChange: setEditOpen, product: product, onSuccess: () => setEditOpen(false) })] }));
49
50
  }
50
51
  export function ProductDetailHeader({ product, onBack, onEdit, onDelete, onBookingCreate, deleting = false, actionsSlot, className, }) {
51
52
  const messages = useProductsUiMessagesOrDefault();
@@ -74,7 +75,7 @@ export function ProductDetailSidebar({ product, className }) {
74
75
  const { formatDateTime } = useProductsUiI18nOrDefault();
75
76
  return (_jsxs(Card, { "data-slot": "product-detail-sidebar", className: className, children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: pageMessages.sections.sidebar.title }), _jsx(CardDescription, { children: pageMessages.sections.sidebar.description })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Badge, { children: messages.common.productStatusLabels[product.status] }), _jsx(Badge, { variant: "outline", children: messages.common.productBookingModeLabels[product.bookingMode] }), product.activated ? _jsx(Badge, { variant: "secondary", children: messages.common.active }) : null] }), _jsx(ProductField, { label: pageMessages.fields.tags, value: product.tags.join(", ") }), _jsx(ProductField, { label: pageMessages.fields.createdAt, value: formatDateTime(product.createdAt) }), _jsx(ProductField, { label: pageMessages.fields.updatedAt, value: formatDateTime(product.updatedAt) })] })] }));
76
77
  }
77
- export function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, className, }) {
78
+ export function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, renderSupplierServiceField, onSupplierServiceSelected, className, }) {
78
79
  const messages = useProductsUiMessagesOrDefault();
79
80
  const pageMessages = messages.productDetailPage;
80
81
  const itinerariesQuery = useProductItineraries(productId);
@@ -87,10 +88,14 @@ export function ProductItinerarySection({ productId, title, description, renderD
87
88
  const days = React.useMemo(() => (daysQuery.data?.data ?? []).slice().sort((a, b) => a.dayNumber - b.dayNumber), [daysQuery.data?.data]);
88
89
  const itineraryMutation = useProductItineraryMutation();
89
90
  const dayMutation = useProductDayMutation();
91
+ const serviceMutation = useProductDayServiceMutation();
90
92
  const [itineraryDialogOpen, setItineraryDialogOpen] = React.useState(false);
91
93
  const [editingItinerary, setEditingItinerary] = React.useState(null);
92
94
  const [dayDialogOpen, setDayDialogOpen] = React.useState(false);
93
95
  const [editingDay, setEditingDay] = React.useState(null);
96
+ const [serviceDialogOpen, setServiceDialogOpen] = React.useState(false);
97
+ const [serviceDayId, setServiceDayId] = React.useState(null);
98
+ const [editingService, setEditingService] = React.useState(null);
94
99
  const [expandedDayId, setExpandedDayId] = React.useState(null);
95
100
  React.useEffect(() => {
96
101
  if (itineraries.length === 0) {
@@ -133,10 +138,34 @@ export function ProductItinerarySection({ productId, title, description, renderD
133
138
  dayId: day.id,
134
139
  itineraryId: day.itineraryId,
135
140
  });
141
+ }, onAddService: () => {
142
+ setServiceDayId(day.id);
143
+ setEditingService(null);
144
+ setExpandedDayId(day.id);
145
+ setServiceDialogOpen(true);
146
+ }, onEditService: (service) => {
147
+ setServiceDayId(day.id);
148
+ setEditingService(service);
149
+ setExpandedDayId(day.id);
150
+ setServiceDialogOpen(true);
151
+ }, onDeleteService: (service) => {
152
+ if (!confirm(pageMessages.states.deleteServiceConfirm.replace("{name}", service.name))) {
153
+ return;
154
+ }
155
+ void serviceMutation.remove.mutateAsync({
156
+ productId,
157
+ dayId: day.id,
158
+ serviceId: service.id,
159
+ });
136
160
  }, renderDayDetails: renderDayDetails, renderServiceActions: renderServiceActions }, day.id))))] })) }), _jsx(ProductItineraryDialog, { open: itineraryDialogOpen, onOpenChange: setItineraryDialogOpen, productId: productId, itinerary: editingItinerary ?? undefined, itineraryCount: itineraries.length, onSuccess: (itineraryId) => setSelectedItineraryId(itineraryId) }), selectedItineraryId ? (_jsx(ProductDayDialog, { open: dayDialogOpen, onOpenChange: setDayDialogOpen, productId: productId, itineraryId: selectedItineraryId, day: editingDay ?? undefined, nextDayNumber: nextDayNumber, onSuccess: (day) => {
137
161
  setExpandedDayId(day.id);
138
162
  setEditingDay(null);
139
- } })) : null] }));
163
+ } })) : null, serviceDayId ? (_jsx(ProductDayServiceDialog, { open: serviceDialogOpen, onOpenChange: (open) => {
164
+ setServiceDialogOpen(open);
165
+ if (!open) {
166
+ setEditingService(null);
167
+ }
168
+ }, productId: productId, dayId: serviceDayId, service: editingService ?? undefined, renderSupplierServiceField: renderSupplierServiceField, onSupplierServiceSelected: onSupplierServiceSelected, onSuccess: () => setEditingService(null) })) : null] }));
140
169
  }
141
170
  function ProductDetailPageLoading({ className }) {
142
171
  const messages = useProductsUiMessagesOrDefault();
@@ -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 ProductFacilityCombobox({ value, onChange, placeholder, disabled }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=product-facility-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-facility-combobox.d.ts","sourceRoot":"","sources":["../../src/components/product-facility-combobox.tsx"],"names":[],"mappings":"AAkBA,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;AA6DD,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAyExF"}
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { fetchWithValidation, useVoyantProductsContext } from "@voyantjs/products-react";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
+ import * as React from "react";
7
+ import { z } from "zod";
8
+ import { useProductsUiMessagesOrDefault } from "../i18n/provider.js";
9
+ const PAGE_SIZE = 25;
10
+ const facilityRecordSchema = z
11
+ .object({
12
+ id: z.string(),
13
+ name: z.string(),
14
+ kind: z.string().nullable().optional(),
15
+ code: z.string().nullable().optional(),
16
+ country: z.string().nullable().optional(),
17
+ })
18
+ .passthrough();
19
+ const facilityListResponse = z.object({
20
+ data: z.array(facilityRecordSchema),
21
+ total: z.number().int(),
22
+ limit: z.number().int(),
23
+ offset: z.number().int(),
24
+ });
25
+ const facilitySingleResponse = z.object({ data: facilityRecordSchema });
26
+ function useFacilityOptions(search) {
27
+ const { baseUrl, fetcher } = useVoyantProductsContext();
28
+ return useQuery({
29
+ queryKey: ["voyant", "products-ui", "facility-combobox", "list", { search }],
30
+ queryFn: () => {
31
+ const params = new URLSearchParams();
32
+ params.set("limit", String(PAGE_SIZE));
33
+ params.set("status", "active");
34
+ if (search)
35
+ params.set("search", search);
36
+ return fetchWithValidation(`/v1/facilities/facilities?${params}`, facilityListResponse, {
37
+ baseUrl,
38
+ fetcher,
39
+ });
40
+ },
41
+ });
42
+ }
43
+ function useSelectedFacility(id) {
44
+ const { baseUrl, fetcher } = useVoyantProductsContext();
45
+ return useQuery({
46
+ queryKey: ["voyant", "products-ui", "facility-combobox", "detail", id],
47
+ queryFn: async () => {
48
+ if (!id)
49
+ throw new Error("useSelectedFacility requires an id");
50
+ const { data } = await fetchWithValidation(`/v1/facilities/facilities/${id}`, facilitySingleResponse, { baseUrl, fetcher });
51
+ return data;
52
+ },
53
+ enabled: Boolean(id),
54
+ });
55
+ }
56
+ export function ProductFacilityCombobox({ value, onChange, placeholder, disabled }) {
57
+ const messages = useProductsUiMessagesOrDefault();
58
+ const [search, setSearch] = React.useState("");
59
+ const listQuery = useFacilityOptions(search);
60
+ const selectedQuery = useSelectedFacility(value);
61
+ const items = React.useMemo(() => {
62
+ const map = new Map();
63
+ for (const item of listQuery.data?.data ?? [])
64
+ map.set(item.id, item);
65
+ if (selectedQuery.data)
66
+ map.set(selectedQuery.data.id, selectedQuery.data);
67
+ return Array.from(map.values());
68
+ }, [listQuery.data?.data, selectedQuery.data]);
69
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
70
+ const selected = value ? itemMap.get(value) : undefined;
71
+ const selectedLabel = selected ? selected.name : "";
72
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
73
+ React.useEffect(() => {
74
+ setInputValue(selectedLabel);
75
+ }, [selectedLabel]);
76
+ 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) => {
77
+ setInputValue(next);
78
+ setSearch(next);
79
+ if (!next)
80
+ onChange(null);
81
+ }, onValueChange: (next) => {
82
+ const id = next ?? null;
83
+ onChange(id);
84
+ setInputValue(id ? (itemMap.get(id)?.name ?? "") : "");
85
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder ?? messages.comboboxes.facility.placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending
86
+ ? messages.common.loading
87
+ : messages.comboboxes.facility.empty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
88
+ const item = itemMap.get(id);
89
+ if (!item)
90
+ return null;
91
+ const subtitle = [item.kind, item.code, item.country].filter(Boolean).join(" · ");
92
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.name }), subtitle ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: subtitle })) : null] }) }, item.id));
93
+ } }) })] })] }));
94
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"product-form.d.ts","sourceRoot":"","sources":["../../src/components/product-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAA;AAmBjC,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3F,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,eAAe,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAiED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CA+O1E"}
1
+ {"version":3,"file":"product-form.d.ts","sourceRoot":"","sources":["../../src/components/product-form.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAA;AAqBjC,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAA;AAE3F,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,eAAe,CAAA;IACrB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AA2GD,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAsX1E"}
@@ -11,6 +11,8 @@ import { Textarea } from "@voyantjs/ui/components/textarea";
11
11
  import { Loader2, X } from "lucide-react";
12
12
  import * as React from "react";
13
13
  import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
14
+ import { ProductFacilityCombobox } from "./product-facility-combobox.js";
15
+ import { ProductTaxClassCombobox } from "./product-tax-class-combobox.js";
14
16
  import { ProductTypeCombobox } from "./product-type-combobox.js";
15
17
  function initialState(mode) {
16
18
  if (mode.kind === "edit") {
@@ -20,10 +22,17 @@ function initialState(mode) {
20
22
  description: product.description ?? "",
21
23
  status: product.status,
22
24
  bookingMode: product.bookingMode,
25
+ capacityMode: product.capacityMode,
26
+ visibility: product.visibility,
27
+ timezone: product.timezone ?? "",
28
+ facilityId: product.facilityId ?? "__none__",
23
29
  productTypeId: product.productTypeId ?? "__none__",
30
+ taxClassId: product.taxClassId ?? "__none__",
24
31
  sellCurrency: product.sellCurrency,
25
32
  sellAmount: product.sellAmountCents != null ? String(product.sellAmountCents / 100) : "",
26
33
  costAmount: product.costAmountCents != null ? String(product.costAmountCents / 100) : "",
34
+ pax: product.pax != null ? String(product.pax) : "",
35
+ reservationTimeoutMinutes: product.reservationTimeoutMinutes != null ? String(product.reservationTimeoutMinutes) : "",
27
36
  tags: product.tags ?? [],
28
37
  };
29
38
  }
@@ -32,10 +41,17 @@ function initialState(mode) {
32
41
  description: "",
33
42
  status: "draft",
34
43
  bookingMode: "itinerary",
44
+ capacityMode: "limited",
45
+ visibility: "private",
46
+ timezone: "",
47
+ facilityId: "__none__",
35
48
  productTypeId: "__none__",
49
+ taxClassId: "__none__",
36
50
  sellCurrency: "EUR", // i18n-literal-ok ISO default currency
37
51
  sellAmount: "",
38
52
  costAmount: "",
53
+ pax: "",
54
+ reservationTimeoutMinutes: "",
39
55
  tags: [],
40
56
  };
41
57
  }
@@ -48,16 +64,36 @@ function toAmountCents(value) {
48
64
  return null;
49
65
  return Math.round(parsed * 100);
50
66
  }
67
+ function toIntegerOrNull(value) {
68
+ const trimmed = value.trim();
69
+ if (!trimmed)
70
+ return null;
71
+ const parsed = Number(trimmed);
72
+ if (!Number.isInteger(parsed) || parsed < 0)
73
+ return null;
74
+ return parsed;
75
+ }
76
+ function toPositiveIntegerOrNull(value) {
77
+ const parsed = toIntegerOrNull(value);
78
+ return parsed == null || parsed < 1 ? null : parsed;
79
+ }
51
80
  function toPayload(state) {
52
81
  return {
53
82
  name: state.name.trim(),
54
83
  description: state.description.trim() || null,
55
84
  status: state.status,
56
85
  bookingMode: state.bookingMode,
86
+ capacityMode: state.capacityMode,
87
+ visibility: state.visibility,
88
+ timezone: state.timezone.trim() || null,
89
+ facilityId: state.facilityId === "__none__" ? null : state.facilityId,
57
90
  productTypeId: state.productTypeId === "__none__" ? null : state.productTypeId,
91
+ taxClassId: state.taxClassId === "__none__" ? null : state.taxClassId,
58
92
  sellCurrency: state.sellCurrency.trim().toUpperCase(),
59
93
  sellAmountCents: toAmountCents(state.sellAmount),
60
94
  costAmountCents: toAmountCents(state.costAmount),
95
+ pax: toPositiveIntegerOrNull(state.pax),
96
+ reservationTimeoutMinutes: toIntegerOrNull(state.reservationTimeoutMinutes),
61
97
  tags: state.tags,
62
98
  };
63
99
  }
@@ -83,6 +119,16 @@ export function ProductForm({ mode, onSuccess, onCancel }) {
83
119
  { value: "itinerary", label: messages.common.productBookingModeLabels.itinerary },
84
120
  { value: "other", label: messages.common.productBookingModeLabels.other },
85
121
  ], [messages]);
122
+ const capacityModes = React.useMemo(() => [
123
+ { value: "free_sale", label: messages.common.productCapacityModeLabels.free_sale },
124
+ { value: "limited", label: messages.common.productCapacityModeLabels.limited },
125
+ { value: "on_request", label: messages.common.productCapacityModeLabels.on_request },
126
+ ], [messages]);
127
+ const visibilityOptions = React.useMemo(() => [
128
+ { value: "public", label: messages.common.productVisibilityLabels.public },
129
+ { value: "private", label: messages.common.productVisibilityLabels.private },
130
+ { value: "hidden", label: messages.common.productVisibilityLabels.hidden },
131
+ ], [messages]);
86
132
  const field = (key) => (value) => {
87
133
  setState((prev) => ({ ...prev, [key]: value }));
88
134
  };
@@ -97,6 +143,15 @@ export function ProductForm({ mode, onSuccess, onCancel }) {
97
143
  setError(productMessages.validation.sellCurrencyInvalid);
98
144
  return;
99
145
  }
146
+ if (state.pax.trim() && toPositiveIntegerOrNull(state.pax) == null) {
147
+ setError(productMessages.validation.paxInvalid);
148
+ return;
149
+ }
150
+ if (state.reservationTimeoutMinutes.trim() &&
151
+ toIntegerOrNull(state.reservationTimeoutMinutes) == null) {
152
+ setError(productMessages.validation.reservationTimeoutInvalid);
153
+ return;
154
+ }
100
155
  const payload = toPayload(state);
101
156
  try {
102
157
  const product = mode.kind === "create"
@@ -108,7 +163,7 @@ export function ProductForm({ mode, onSuccess, onCancel }) {
108
163
  setError(err instanceof Error ? err.message : productMessages.validation.saveFailed);
109
164
  }
110
165
  };
111
- return (_jsxs("form", { "data-slot": "product-form", onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-name", children: productMessages.fields.name }), _jsx(Input, { id: "product-name", required: true, autoFocus: true, value: state.name, onChange: (event) => field("name")(event.target.value), placeholder: productMessages.placeholders.name })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-description", children: productMessages.fields.description }), _jsx(Textarea, { id: "product-description", value: state.description, onChange: (event) => field("description")(event.target.value), placeholder: productMessages.placeholders.description })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.tags }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: state.tags.map((tag) => (_jsxs(Badge, { variant: "secondary", className: "gap-1 text-xs", children: [tag, _jsx("button", { type: "button", className: "ml-0.5 rounded-full hover:text-destructive", onClick: () => field("tags")(state.tags.filter((value) => value !== tag)), children: _jsx(X, { className: "size-3", "aria-hidden": "true" }) })] }, tag))) }), _jsx(Input, { value: tagInput, onChange: (event) => setTagInput(event.target.value), onKeyDown: (event) => {
166
+ return (_jsxs("form", { "data-slot": "product-form", onSubmit: handleSubmit, className: "flex min-h-0 flex-col gap-4 overflow-y-auto pr-1", children: [_jsxs("div", { className: "grid grid-cols-1 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-name", children: productMessages.fields.name }), _jsx(Input, { id: "product-name", required: true, autoFocus: true, value: state.name, onChange: (event) => field("name")(event.target.value), placeholder: productMessages.placeholders.name })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-description", children: productMessages.fields.description }), _jsx(Textarea, { id: "product-description", value: state.description, onChange: (event) => field("description")(event.target.value), placeholder: productMessages.placeholders.description })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.tags }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: state.tags.map((tag) => (_jsxs(Badge, { variant: "secondary", className: "gap-1 text-xs", children: [tag, _jsx("button", { type: "button", className: "ml-0.5 rounded-full hover:text-destructive", onClick: () => field("tags")(state.tags.filter((value) => value !== tag)), children: _jsx(X, { className: "size-3", "aria-hidden": "true" }) })] }, tag))) }), _jsx(Input, { value: tagInput, onChange: (event) => setTagInput(event.target.value), onKeyDown: (event) => {
112
167
  if (event.key === "Enter" || event.key === ",") {
113
168
  event.preventDefault();
114
169
  const value = tagInput.trim().replace(/,+$/, "");
@@ -117,5 +172,5 @@ export function ProductForm({ mode, onSuccess, onCancel }) {
117
172
  }
118
173
  setTagInput("");
119
174
  }
120
- }, placeholder: productMessages.placeholders.tagInput })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.status }), _jsxs(Select, { value: state.status, onValueChange: (value) => value && field("status")(value), items: productStatuses, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: productStatuses.map((status) => (_jsx(SelectItem, { value: status.value, children: status.label }, status.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.bookingMode }), _jsxs(Select, { items: bookingModes, value: state.bookingMode, onValueChange: (value) => value && field("bookingMode")(value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: bookingModes.map((modeOption) => (_jsx(SelectItem, { value: modeOption.value, children: modeOption.label }, modeOption.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.productType }), _jsx(ProductTypeCombobox, { value: state.productTypeId === "__none__" ? null : state.productTypeId, onChange: (value) => field("productTypeId")(value ?? "__none__"), placeholder: productMessages.placeholders.productTypeSearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.sellCurrency }), _jsx(CurrencyCombobox, { value: state.sellCurrency, onChange: (value) => field("sellCurrency")(value ?? ""), placeholder: productMessages.placeholders.currencySearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-sell-amount", children: productMessages.fields.sellAmount }), _jsx(Input, { id: "product-sell-amount", type: "number", min: "0", step: "0.01", value: state.sellAmount, onChange: (event) => field("sellAmount")(event.target.value), placeholder: productMessages.placeholders.amount })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-cost-amount", children: productMessages.fields.costAmount }), _jsx(Input, { id: "product-cost-amount", type: "number", min: "0", step: "0.01", value: state.costAmount, onChange: (event) => field("costAmount")(event.target.value), placeholder: productMessages.placeholders.amount })] })] })] }), error ? (_jsx("p", { "data-slot": "product-form-error", className: "text-sm text-destructive", children: error })) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: isSubmitting, children: productMessages.actions.cancel })) : null, _jsx(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" }), productMessages.actions.saving] })) : mode.kind === "create" ? (productMessages.actions.create) : (productMessages.actions.saveChanges) })] })] }));
175
+ }, placeholder: productMessages.placeholders.tagInput })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.status }), _jsxs(Select, { value: state.status, onValueChange: (value) => value && field("status")(value), items: productStatuses, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: productStatuses.map((status) => (_jsx(SelectItem, { value: status.value, children: status.label }, status.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.bookingMode }), _jsxs(Select, { items: bookingModes, value: state.bookingMode, onValueChange: (value) => value && field("bookingMode")(value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: bookingModes.map((modeOption) => (_jsx(SelectItem, { value: modeOption.value, children: modeOption.label }, modeOption.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.productType }), _jsx(ProductTypeCombobox, { value: state.productTypeId === "__none__" ? null : state.productTypeId, onChange: (value) => field("productTypeId")(value ?? "__none__"), placeholder: productMessages.placeholders.productTypeSearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.facility }), _jsx(ProductFacilityCombobox, { value: state.facilityId === "__none__" ? null : state.facilityId, onChange: (value) => field("facilityId")(value ?? "__none__"), placeholder: productMessages.placeholders.facilitySearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.taxClass }), _jsx(ProductTaxClassCombobox, { value: state.taxClassId === "__none__" ? null : state.taxClassId, onChange: (value) => field("taxClassId")(value ?? "__none__"), placeholder: productMessages.placeholders.taxClassSearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.visibility }), _jsxs(Select, { items: visibilityOptions, value: state.visibility, onValueChange: (value) => value && field("visibility")(value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: visibilityOptions.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.capacityMode }), _jsxs(Select, { items: capacityModes, value: state.capacityMode, onValueChange: (value) => value && field("capacityMode")(value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: capacityModes.map((option) => (_jsx(SelectItem, { value: option.value, children: option.label }, option.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-timezone", children: productMessages.fields.timezone }), _jsx(Input, { id: "product-timezone", value: state.timezone, onChange: (event) => field("timezone")(event.target.value), placeholder: productMessages.placeholders.timezone })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-pax", children: productMessages.fields.pax }), _jsx(Input, { id: "product-pax", type: "number", min: "1", step: "1", value: state.pax, onChange: (event) => field("pax")(event.target.value), placeholder: productMessages.placeholders.pax })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-reservation-timeout", children: productMessages.fields.reservationTimeout }), _jsx(Input, { id: "product-reservation-timeout", type: "number", min: "0", step: "1", value: state.reservationTimeoutMinutes, onChange: (event) => field("reservationTimeoutMinutes")(event.target.value), placeholder: productMessages.placeholders.reservationTimeout })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.fields.sellCurrency }), _jsx(CurrencyCombobox, { value: state.sellCurrency, onChange: (value) => field("sellCurrency")(value ?? ""), placeholder: productMessages.placeholders.currencySearch })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-sell-amount", children: productMessages.fields.sellAmount }), _jsx(Input, { id: "product-sell-amount", type: "number", min: "0", step: "0.01", value: state.sellAmount, onChange: (event) => field("sellAmount")(event.target.value), placeholder: productMessages.placeholders.amount })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "product-cost-amount", children: productMessages.fields.costAmount }), _jsx(Input, { id: "product-cost-amount", type: "number", min: "0", step: "0.01", value: state.costAmount, onChange: (event) => field("costAmount")(event.target.value), placeholder: productMessages.placeholders.amount })] })] })] }), error ? (_jsx("p", { "data-slot": "product-form-error", className: "text-sm text-destructive", children: error })) : null, _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", onClick: onCancel, disabled: isSubmitting, children: productMessages.actions.cancel })) : null, _jsx(Button, { type: "submit", disabled: isSubmitting, children: isSubmitting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" }), productMessages.actions.saving] })) : mode.kind === "create" ? (productMessages.actions.create) : (productMessages.actions.saveChanges) })] })] }));
121
176
  }
@@ -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 ProductTaxClassCombobox({ value, onChange, placeholder, disabled }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=product-tax-class-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-tax-class-combobox.d.ts","sourceRoot":"","sources":["../../src/components/product-tax-class-combobox.tsx"],"names":[],"mappings":"AAkBA,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;AA4DD,wBAAgB,uBAAuB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,EAAE,KAAK,2CAiFxF"}
@@ -0,0 +1,100 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { fetchWithValidation, useVoyantProductsContext } from "@voyantjs/products-react";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
+ import * as React from "react";
7
+ import { z } from "zod";
8
+ import { useProductsUiMessagesOrDefault } from "../i18n/provider.js";
9
+ const PAGE_SIZE = 100;
10
+ const taxClassRecordSchema = z
11
+ .object({
12
+ id: z.string(),
13
+ code: z.string(),
14
+ label: z.string(),
15
+ description: z.string().nullable().optional(),
16
+ active: z.boolean(),
17
+ })
18
+ .passthrough();
19
+ const taxClassListResponse = z.object({
20
+ data: z.array(taxClassRecordSchema),
21
+ total: z.number().int(),
22
+ limit: z.number().int(),
23
+ offset: z.number().int(),
24
+ });
25
+ const taxClassSingleResponse = z.object({ data: taxClassRecordSchema });
26
+ function useTaxClassOptions() {
27
+ const { baseUrl, fetcher } = useVoyantProductsContext();
28
+ return useQuery({
29
+ queryKey: ["voyant", "products-ui", "tax-class-combobox", "list"],
30
+ queryFn: () => {
31
+ const params = new URLSearchParams();
32
+ params.set("limit", String(PAGE_SIZE));
33
+ params.set("active", "true");
34
+ return fetchWithValidation(`/v1/finance/tax-classes?${params}`, taxClassListResponse, {
35
+ baseUrl,
36
+ fetcher,
37
+ });
38
+ },
39
+ });
40
+ }
41
+ function useSelectedTaxClass(id) {
42
+ const { baseUrl, fetcher } = useVoyantProductsContext();
43
+ return useQuery({
44
+ queryKey: ["voyant", "products-ui", "tax-class-combobox", "detail", id],
45
+ queryFn: async () => {
46
+ if (!id)
47
+ throw new Error("useSelectedTaxClass requires an id");
48
+ const { data } = await fetchWithValidation(`/v1/finance/tax-classes/${id}`, taxClassSingleResponse, { baseUrl, fetcher });
49
+ return data;
50
+ },
51
+ enabled: Boolean(id),
52
+ });
53
+ }
54
+ export function ProductTaxClassCombobox({ value, onChange, placeholder, disabled }) {
55
+ const messages = useProductsUiMessagesOrDefault();
56
+ const [search, setSearch] = React.useState("");
57
+ const listQuery = useTaxClassOptions();
58
+ const selectedQuery = useSelectedTaxClass(value);
59
+ const items = React.useMemo(() => {
60
+ const map = new Map();
61
+ for (const item of listQuery.data?.data ?? [])
62
+ map.set(item.id, item);
63
+ if (selectedQuery.data)
64
+ map.set(selectedQuery.data.id, selectedQuery.data);
65
+ const normalizedSearch = search.trim().toLowerCase();
66
+ const values = Array.from(map.values());
67
+ if (!normalizedSearch)
68
+ return values;
69
+ return values.filter((item) => item.label.toLowerCase().includes(normalizedSearch) ||
70
+ item.code.toLowerCase().includes(normalizedSearch));
71
+ }, [listQuery.data?.data, search, selectedQuery.data]);
72
+ const itemMap = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
73
+ const selected = value ? itemMap.get(value) : selectedQuery.data;
74
+ const selectedLabel = selected ? `${selected.label} · ${selected.code}` : "";
75
+ const [inputValue, setInputValue] = React.useState(selectedLabel);
76
+ React.useEffect(() => {
77
+ setInputValue(selectedLabel);
78
+ }, [selectedLabel]);
79
+ return (_jsxs(Combobox, { items: items.map((item) => item.id), value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringValue: (id) => {
80
+ const item = itemMap.get(id);
81
+ return item ? `${item.label} · ${item.code}` : "";
82
+ }, onInputValueChange: (next) => {
83
+ setInputValue(next);
84
+ setSearch(next);
85
+ if (!next)
86
+ onChange(null);
87
+ }, onValueChange: (next) => {
88
+ const id = next ?? null;
89
+ onChange(id);
90
+ const item = id ? itemMap.get(id) : null;
91
+ setInputValue(item ? `${item.label} · ${item.code}` : "");
92
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder ?? messages.comboboxes.taxClass.placeholder, showClear: !!value }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: listQuery.isPending || selectedQuery.isPending
93
+ ? messages.common.loading
94
+ : messages.comboboxes.taxClass.empty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
95
+ const item = itemMap.get(id);
96
+ if (!item)
97
+ return null;
98
+ return (_jsx(ComboboxItem, { value: item.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: item.label }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: item.code })] }) }, item.id));
99
+ } }) })] })] }));
100
+ }