@voyantjs/products-ui 0.30.7 → 0.31.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 (39) hide show
  1. package/README.md +11 -0
  2. package/dist/components/product-categories-page.d.ts +7 -0
  3. package/dist/components/product-categories-page.d.ts.map +1 -0
  4. package/dist/components/product-categories-page.js +9 -0
  5. package/dist/components/product-detail-page.d.ts +63 -0
  6. package/dist/components/product-detail-page.d.ts.map +1 -0
  7. package/dist/components/product-detail-page.js +159 -0
  8. package/dist/components/product-dialog.d.ts +9 -0
  9. package/dist/components/product-dialog.d.ts.map +1 -0
  10. package/dist/components/product-dialog.js +13 -0
  11. package/dist/components/product-form.d.ts +14 -0
  12. package/dist/components/product-form.d.ts.map +1 -0
  13. package/dist/components/product-form.js +121 -0
  14. package/dist/components/product-list.d.ts +7 -0
  15. package/dist/components/product-list.d.ts.map +1 -0
  16. package/dist/components/product-list.js +154 -0
  17. package/dist/components/product-tags-page.d.ts +7 -0
  18. package/dist/components/product-tags-page.d.ts.map +1 -0
  19. package/dist/components/product-tags-page.js +9 -0
  20. package/dist/components/product-types-page.d.ts +6 -0
  21. package/dist/components/product-types-page.d.ts.map +1 -0
  22. package/dist/components/product-types-page.js +103 -0
  23. package/dist/components/products-page.d.ts +8 -0
  24. package/dist/components/products-page.d.ts.map +1 -0
  25. package/dist/components/products-page.js +8 -0
  26. package/dist/i18n/en.d.ts +200 -0
  27. package/dist/i18n/en.d.ts.map +1 -1
  28. package/dist/i18n/en.js +200 -0
  29. package/dist/i18n/messages.d.ts +191 -0
  30. package/dist/i18n/messages.d.ts.map +1 -1
  31. package/dist/i18n/provider.d.ts +400 -0
  32. package/dist/i18n/provider.d.ts.map +1 -1
  33. package/dist/i18n/ro.d.ts +200 -0
  34. package/dist/i18n/ro.d.ts.map +1 -1
  35. package/dist/i18n/ro.js +200 -0
  36. package/dist/index.d.ts +8 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +8 -0
  39. package/package.json +23 -19
package/README.md CHANGED
@@ -9,9 +9,20 @@ pnpm add @voyantjs/products-ui @voyantjs/products-react @voyantjs/ui @tanstack/r
9
9
  ```
10
10
 
11
11
  `@voyantjs/ui` provides the design-system primitives. `@voyantjs/products-react` provides the data-layer hooks. Both are required peers.
12
+ `ProductTypesPage` also uses `react-hook-form` and `zod` for sheet validation.
12
13
 
13
14
  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.
14
15
 
16
+ ## Components
17
+
18
+ - `ProductsPage` publishes the product list composition.
19
+ - `ProductDetailPage` publishes the complete product detail workspace with
20
+ overview, media, itinerary, option, and version tabs.
21
+ - `ProductDetailHeader`, `ProductOverviewCard`, `ProductCommercialCard`,
22
+ `ProductDetailSidebar`, and `ProductItinerarySection` remain exported for
23
+ consumers that need to compose the detail page manually.
24
+ - Product category, type, and tag pages publish reusable list-management compositions.
25
+
15
26
  ## I18n
16
27
 
17
28
  Components render English by default. To localize them, wrap your UI in
@@ -0,0 +1,7 @@
1
+ import { type ProductCategoryListProps } from "./product-category-list.js";
2
+ export interface ProductCategoriesPageProps {
3
+ pageSize?: ProductCategoryListProps["pageSize"];
4
+ className?: string;
5
+ }
6
+ export declare function ProductCategoriesPage({ pageSize, className }?: ProductCategoriesPageProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=product-categories-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-categories-page.d.ts","sourceRoot":"","sources":["../../src/components/product-categories-page.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAuB,KAAK,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AAE/F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,EAAE,wBAAwB,CAAC,UAAU,CAAC,CAAA;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAE,0BAA+B,2CAa7F"}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "@voyantjs/ui/lib/utils";
4
+ import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
5
+ import { ProductCategoryList } from "./product-category-list.js";
6
+ export function ProductCategoriesPage({ pageSize, className } = {}) {
7
+ const messages = useProductsUiMessagesOrDefault().productCategoriesPage;
8
+ return (_jsxs("div", { "data-slot": "product-categories-page", className: cn("flex flex-col gap-6", className), children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: messages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.description })] }), _jsx(ProductCategoryList, { pageSize: pageSize })] }));
9
+ }
@@ -0,0 +1,63 @@
1
+ import { type ProductDayServiceRecord, type ProductRecord } from "@voyantjs/products-react";
2
+ import * as React from "react";
3
+ import { type ProductItineraryDayRowRenderContext } from "./product-itinerary-day-row.js";
4
+ import { type ProductMediaSectionProps } from "./product-media-section.js";
5
+ import { type ProductOptionsSectionProps } from "./product-options-section.js";
6
+ export type ProductDetailPageTab = "overview" | "media" | "itinerary" | "options" | "versions";
7
+ export interface ProductDetailPageSlots {
8
+ header?: React.ReactNode;
9
+ afterHeader?: React.ReactNode;
10
+ overviewStart?: React.ReactNode;
11
+ overviewEnd?: React.ReactNode;
12
+ sidebar?: React.ReactNode;
13
+ mediaEnd?: React.ReactNode;
14
+ itineraryEnd?: React.ReactNode;
15
+ optionsEnd?: React.ReactNode;
16
+ versionsEnd?: React.ReactNode;
17
+ }
18
+ export interface ProductDetailPageProps {
19
+ id: string;
20
+ className?: string;
21
+ defaultTab?: ProductDetailPageTab;
22
+ onBack?: () => void;
23
+ onBookingCreate?: (product: ProductRecord) => void;
24
+ onDeleted?: () => void;
25
+ uploadMedia?: ProductMediaSectionProps["uploadMedia"];
26
+ renderOptionDetails?: ProductOptionsSectionProps["renderOptionDetails"];
27
+ renderItineraryDayDetails?: (context: ProductItineraryDayRowRenderContext) => React.ReactNode;
28
+ renderItineraryServiceActions?: (service: ProductDayServiceRecord) => React.ReactNode;
29
+ slots?: ProductDetailPageSlots;
30
+ }
31
+ export declare function ProductDetailPage({ id, className, defaultTab, onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, slots, }: ProductDetailPageProps): import("react/jsx-runtime").JSX.Element;
32
+ export interface ProductDetailHeaderProps {
33
+ product: ProductRecord;
34
+ onBack?: () => void;
35
+ onEdit?: () => void;
36
+ onDelete?: () => void;
37
+ onBookingCreate?: () => void;
38
+ deleting?: boolean;
39
+ actionsSlot?: React.ReactNode;
40
+ className?: string;
41
+ }
42
+ export declare function ProductDetailHeader({ product, onBack, onEdit, onDelete, onBookingCreate, deleting, actionsSlot, className, }: ProductDetailHeaderProps): import("react/jsx-runtime").JSX.Element;
43
+ export interface ProductOverviewCardProps {
44
+ product: ProductRecord;
45
+ className?: string;
46
+ }
47
+ export declare function ProductOverviewCard({ product, className }: ProductOverviewCardProps): import("react/jsx-runtime").JSX.Element;
48
+ export declare function ProductCommercialCard({ product, className }: ProductOverviewCardProps): import("react/jsx-runtime").JSX.Element;
49
+ export interface ProductDetailSidebarProps {
50
+ product: ProductRecord;
51
+ className?: string;
52
+ }
53
+ export declare function ProductDetailSidebar({ product, className }: ProductDetailSidebarProps): import("react/jsx-runtime").JSX.Element;
54
+ export interface ProductItinerarySectionProps {
55
+ productId: string;
56
+ title?: string;
57
+ description?: string;
58
+ renderDayDetails?: (context: ProductItineraryDayRowRenderContext) => React.ReactNode;
59
+ renderServiceActions?: (service: ProductDayServiceRecord) => React.ReactNode;
60
+ className?: string;
61
+ }
62
+ export declare function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, className, }: ProductItinerarySectionProps): import("react/jsx-runtime").JSX.Element;
63
+ //# sourceMappingURL=product-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/product-detail-page.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ "use client";
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";
4
+ import { Badge } from "@voyantjs/ui/components/badge";
5
+ import { Button } from "@voyantjs/ui/components/button";
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@voyantjs/ui/components/card";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
8
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
9
+ import { cn } from "@voyantjs/ui/lib/utils";
10
+ import { ArrowLeft, Edit, FileText, Loader2, Plus, ReceiptText, Trash2 } from "lucide-react";
11
+ import * as React from "react";
12
+ import { useProductsUiI18nOrDefault, useProductsUiMessagesOrDefault } from "../i18n/provider.js";
13
+ import { ProductDayDialog } from "./product-day-dialog.js";
14
+ import { ProductDialog } from "./product-dialog.js";
15
+ import { ProductItineraryDayRow, } from "./product-itinerary-day-row.js";
16
+ import { ProductItineraryDialog } from "./product-itinerary-dialog.js";
17
+ import { ProductMediaSection } from "./product-media-section.js";
18
+ import { ProductOptionsSection, } from "./product-options-section.js";
19
+ import { ProductVersionsSection } from "./product-versions-section.js";
20
+ export function ProductDetailPage({ id, className, defaultTab = "overview", onBack, onBookingCreate, onDeleted, uploadMedia, renderOptionDetails, renderItineraryDayDetails, renderItineraryServiceActions, slots, }) {
21
+ const messages = useProductsUiMessagesOrDefault();
22
+ const pageMessages = messages.productDetailPage;
23
+ const productQuery = useProduct(id);
24
+ const { remove } = useProductMutation();
25
+ const [editOpen, setEditOpen] = React.useState(false);
26
+ const [deleteError, setDeleteError] = React.useState(null);
27
+ if (productQuery.isPending) {
28
+ return _jsx(ProductDetailPageLoading, { className: className });
29
+ }
30
+ if (productQuery.isError) {
31
+ return (_jsx(ProductDetailPageState, { className: className, title: pageMessages.states.loadFailed, description: productQuery.error instanceof Error ? productQuery.error.message : undefined, onBack: onBack }));
32
+ }
33
+ const product = productQuery.data;
34
+ if (!product) {
35
+ return (_jsx(ProductDetailPageState, { className: className, title: pageMessages.states.notFoundTitle, description: pageMessages.states.notFoundDescription, onBack: onBack }));
36
+ }
37
+ const handleDelete = async () => {
38
+ setDeleteError(null);
39
+ if (!confirm(pageMessages.states.deleteConfirm.replace("{name}", product.name)))
40
+ return;
41
+ try {
42
+ await remove.mutateAsync(product.id);
43
+ onDeleted?.();
44
+ }
45
+ catch (error) {
46
+ setDeleteError(error instanceof Error ? error.message : pageMessages.states.deleteFailed);
47
+ }
48
+ };
49
+ return (_jsxs("div", { "data-slot": "product-detail-page", className: cn("flex flex-col gap-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(Tabs, { defaultValue: defaultTab, children: [_jsxs(TabsList, { className: "w-full justify-start overflow-x-auto", children: [_jsx(TabsTrigger, { value: "overview", children: pageMessages.tabs.overview }), _jsx(TabsTrigger, { value: "media", children: pageMessages.tabs.media }), _jsx(TabsTrigger, { value: "itinerary", children: pageMessages.tabs.itinerary }), _jsx(TabsTrigger, { value: "options", children: pageMessages.tabs.options }), _jsx(TabsTrigger, { value: "versions", children: pageMessages.tabs.versions })] }), _jsx(TabsContent, { value: "overview", className: "mt-4", children: _jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [slots?.overviewStart, _jsx(ProductOverviewCard, { product: product }), _jsx(ProductCommercialCard, { product: product }), slots?.overviewEnd] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(ProductDetailSidebar, { product: product }), slots?.sidebar] })] }) }), _jsxs(TabsContent, { value: "media", className: "mt-4 space-y-4", children: [_jsx(ProductMediaSection, { productId: product.id, uploadMedia: uploadMedia }), slots?.mediaEnd] }), _jsxs(TabsContent, { value: "itinerary", className: "mt-4 space-y-4", children: [_jsx(ProductItinerarySection, { productId: product.id, renderDayDetails: renderItineraryDayDetails, renderServiceActions: renderItineraryServiceActions }), slots?.itineraryEnd] }), _jsxs(TabsContent, { value: "options", className: "mt-4 space-y-4", children: [_jsx(ProductOptionsSection, { productId: product.id, renderOptionDetails: renderOptionDetails }), slots?.optionsEnd] }), _jsxs(TabsContent, { value: "versions", className: "mt-4 space-y-4", children: [_jsx(ProductVersionsSection, { productId: product.id }), slots?.versionsEnd] })] }), _jsx(ProductDialog, { open: editOpen, onOpenChange: setEditOpen, product: product, onSuccess: () => setEditOpen(false) })] }));
50
+ }
51
+ export function ProductDetailHeader({ product, onBack, onEdit, onDelete, onBookingCreate, deleting = false, actionsSlot, className, }) {
52
+ const messages = useProductsUiMessagesOrDefault();
53
+ return (_jsxs("div", { "data-slot": "product-detail-header", className: cn("flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between", className), children: [_jsxs("div", { className: "min-w-0 space-y-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [onBack ? (_jsxs(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: onBack, children: [_jsx(ArrowLeft, { className: "size-4", "aria-hidden": "true" }), _jsx("span", { className: "sr-only", children: messages.productDetailPage.actions.back })] })) : null, _jsx(Badge, { variant: product.status === "active"
54
+ ? "default"
55
+ : product.status === "archived"
56
+ ? "secondary"
57
+ : "outline", children: messages.common.productStatusLabels[product.status] }), _jsx(Badge, { variant: "outline", children: messages.common.productBookingModeLabels[product.bookingMode] })] }), _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: product.name }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: product.id })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [actionsSlot, onBookingCreate ? (_jsxs(Button, { type: "button", variant: "outline", onClick: onBookingCreate, children: [_jsx(ReceiptText, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productDetailPage.actions.createBooking] })) : null, onEdit ? (_jsxs(Button, { type: "button", variant: "outline", onClick: onEdit, children: [_jsx(Edit, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productDetailPage.actions.edit] })) : null, onDelete ? (_jsxs(Button, { type: "button", variant: "destructive", disabled: deleting, onClick: onDelete, children: [deleting ? (_jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": "true" })) : (_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" })), messages.productDetailPage.actions.delete] })) : null] })] }));
58
+ }
59
+ export function ProductOverviewCard({ product, className }) {
60
+ const messages = useProductsUiMessagesOrDefault();
61
+ const pageMessages = messages.productDetailPage;
62
+ return (_jsxs(Card, { "data-slot": "product-overview-card", className: className, children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: pageMessages.sections.overview.title }), _jsx(CardDescription, { children: pageMessages.sections.overview.description })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsx("p", { className: "whitespace-pre-wrap text-sm leading-6 text-muted-foreground", children: product.description || pageMessages.states.noDescription }), _jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [_jsx(ProductField, { label: pageMessages.fields.visibility, value: product.visibility }), _jsx(ProductField, { label: pageMessages.fields.capacityMode, value: product.capacityMode }), _jsx(ProductField, { label: pageMessages.fields.timezone, value: product.timezone }), _jsx(ProductField, { label: pageMessages.fields.productType, value: product.productTypeId }), _jsx(ProductField, { label: pageMessages.fields.facility, value: product.facilityId }), _jsx(ProductField, { label: pageMessages.fields.taxClass, value: product.taxClassId }), _jsx(ProductField, { label: pageMessages.fields.startDate, value: product.startDate }), _jsx(ProductField, { label: pageMessages.fields.endDate, value: product.endDate })] })] })] }));
63
+ }
64
+ export function ProductCommercialCard({ product, className }) {
65
+ const messages = useProductsUiMessagesOrDefault();
66
+ const pageMessages = messages.productDetailPage;
67
+ const { formatCurrency, formatNumber } = useProductsUiI18nOrDefault();
68
+ return (_jsxs(Card, { "data-slot": "product-commercial-card", className: className, children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: pageMessages.sections.commercial.title }), _jsx(CardDescription, { children: pageMessages.sections.commercial.description })] }), _jsxs(CardContent, { className: "grid gap-3 sm:grid-cols-2", children: [_jsx(ProductField, { label: pageMessages.fields.sellAmount, value: formatMoney(product.sellAmountCents, product.sellCurrency, formatCurrency) }), _jsx(ProductField, { label: pageMessages.fields.costAmount, value: formatMoney(product.costAmountCents, product.sellCurrency, formatCurrency) }), _jsx(ProductField, { label: pageMessages.fields.margin, value: product.marginPercent == null ? null : `${formatNumber(product.marginPercent)}%` }), _jsx(ProductField, { label: pageMessages.fields.pax, value: product.pax == null ? null : formatNumber(product.pax) }), _jsx(ProductField, { label: pageMessages.fields.reservationTimeout, value: product.reservationTimeoutMinutes == null
69
+ ? null
70
+ : pageMessages.states.minutes.replace("{count}", formatNumber(product.reservationTimeoutMinutes)) })] })] }));
71
+ }
72
+ export function ProductDetailSidebar({ product, className }) {
73
+ const messages = useProductsUiMessagesOrDefault();
74
+ const pageMessages = messages.productDetailPage;
75
+ const { formatDateTime } = useProductsUiI18nOrDefault();
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) })] })] }));
77
+ }
78
+ export function ProductItinerarySection({ productId, title, description, renderDayDetails, renderServiceActions, className, }) {
79
+ const messages = useProductsUiMessagesOrDefault();
80
+ const pageMessages = messages.productDetailPage;
81
+ const itinerariesQuery = useProductItineraries(productId);
82
+ const itineraries = React.useMemo(() => (itinerariesQuery.data?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder), [itinerariesQuery.data?.data]);
83
+ const [selectedItineraryId, setSelectedItineraryId] = React.useState(null);
84
+ const selectedItinerary = itineraries.find((itinerary) => itinerary.id === selectedItineraryId) ?? null;
85
+ const daysQuery = useProductItineraryDays(productId, selectedItineraryId, {
86
+ enabled: Boolean(selectedItineraryId),
87
+ });
88
+ const days = React.useMemo(() => (daysQuery.data?.data ?? []).slice().sort((a, b) => a.dayNumber - b.dayNumber), [daysQuery.data?.data]);
89
+ const itineraryMutation = useProductItineraryMutation();
90
+ const dayMutation = useProductDayMutation();
91
+ const [itineraryDialogOpen, setItineraryDialogOpen] = React.useState(false);
92
+ const [editingItinerary, setEditingItinerary] = React.useState(null);
93
+ const [dayDialogOpen, setDayDialogOpen] = React.useState(false);
94
+ const [editingDay, setEditingDay] = React.useState(null);
95
+ const [expandedDayId, setExpandedDayId] = React.useState(null);
96
+ React.useEffect(() => {
97
+ if (itineraries.length === 0) {
98
+ setSelectedItineraryId(null);
99
+ return;
100
+ }
101
+ setSelectedItineraryId((current) => {
102
+ if (current && itineraries.some((itinerary) => itinerary.id === current))
103
+ return current;
104
+ return itineraries.find((itinerary) => itinerary.isDefault)?.id ?? itineraries[0]?.id ?? null;
105
+ });
106
+ }, [itineraries]);
107
+ const nextDayNumber = days.length > 0 ? Math.max(...days.map((day) => day.dayNumber)) + 1 : 1;
108
+ return (_jsxs(Card, { "data-slot": "product-itinerary-section", className: className, children: [_jsxs(CardHeader, { className: "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(CardTitle, { children: title ?? pageMessages.sections.itinerary.title }), _jsx(CardDescription, { children: description ?? pageMessages.sections.itinerary.description })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [itineraries.length > 0 ? (_jsxs(Select, { value: selectedItineraryId ?? undefined, onValueChange: (value) => setSelectedItineraryId(value), children: [_jsx(SelectTrigger, { className: "w-[220px]", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: itineraries.map((itinerary) => (_jsx(SelectItem, { value: itinerary.id, children: itinerary.name }, itinerary.id))) })] })) : null, _jsxs(Button, { type: "button", variant: "outline", onClick: () => {
109
+ setEditingItinerary(null);
110
+ setItineraryDialogOpen(true);
111
+ }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.actions.addItinerary] }), selectedItinerary ? (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", onClick: () => {
112
+ setEditingItinerary(selectedItinerary);
113
+ setItineraryDialogOpen(true);
114
+ }, children: [_jsx(Edit, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.actions.editItinerary] }), _jsxs(Button, { type: "button", variant: "outline", onClick: () => {
115
+ if (!confirm(pageMessages.states.deleteItineraryConfirm.replace("{name}", selectedItinerary.name))) {
116
+ return;
117
+ }
118
+ void itineraryMutation.remove.mutateAsync({
119
+ productId,
120
+ itineraryId: selectedItinerary.id,
121
+ });
122
+ }, children: [_jsx(Trash2, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.actions.deleteItinerary] })] })) : null] })] }), _jsx(CardContent, { className: "flex flex-col gap-3", children: itinerariesQuery.isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground", "aria-hidden": "true" }) })) : itinerariesQuery.isError ? (_jsx("p", { className: "text-sm text-destructive", children: pageMessages.states.loadFailed })) : itineraries.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: pageMessages.states.noItineraries })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", onClick: () => {
123
+ setEditingDay(null);
124
+ setDayDialogOpen(true);
125
+ }, disabled: !selectedItineraryId, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), pageMessages.actions.addDay] }) }), daysQuery.isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground", "aria-hidden": "true" }) })) : daysQuery.isError ? (_jsx("p", { className: "text-sm text-destructive", children: pageMessages.states.loadFailed })) : days.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: pageMessages.states.noDays })) : (days.map((day) => (_jsx(ProductItineraryDayRow, { productId: productId, day: day, expanded: expandedDayId === day.id, onToggle: () => setExpandedDayId((current) => (current === day.id ? null : day.id)), onEdit: () => {
126
+ setEditingDay(day);
127
+ setDayDialogOpen(true);
128
+ }, onDelete: () => {
129
+ if (!confirm(pageMessages.states.deleteDayConfirm.replace("{dayNumber}", String(day.dayNumber)))) {
130
+ return;
131
+ }
132
+ void dayMutation.remove.mutateAsync({
133
+ productId,
134
+ dayId: day.id,
135
+ itineraryId: day.itineraryId,
136
+ });
137
+ }, 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) => {
138
+ setExpandedDayId(day.id);
139
+ setEditingDay(null);
140
+ } })) : null] }));
141
+ }
142
+ function ProductDetailPageLoading({ className }) {
143
+ const messages = useProductsUiMessagesOrDefault();
144
+ return (_jsx("div", { "data-slot": "product-detail-page-loading", className: cn("flex min-h-48 items-center justify-center", className), children: _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" }), messages.productDetailPage.states.loading] }) }));
145
+ }
146
+ function ProductDetailPageState({ className, title, description, onBack, }) {
147
+ const messages = useProductsUiMessagesOrDefault();
148
+ return (_jsxs("div", { "data-slot": "product-detail-page-state", className: cn("flex flex-col gap-4", className), children: [onBack ? (_jsxs(Button, { type: "button", variant: "ghost", className: "w-fit", onClick: onBack, children: [_jsx(ArrowLeft, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productDetailPage.actions.back] })) : null, _jsx(Card, { children: _jsxs(CardContent, { className: "flex min-h-40 flex-col items-center justify-center gap-2 text-center", children: [_jsx(FileText, { className: "size-5 text-muted-foreground", "aria-hidden": "true" }), _jsx("h2", { className: "text-lg font-semibold", children: title }), description ? _jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null] }) })] }));
149
+ }
150
+ function ProductField({ label, value }) {
151
+ const messages = useProductsUiMessagesOrDefault();
152
+ const hasValue = value !== null &&
153
+ value !== undefined &&
154
+ !(typeof value === "string" && value.trim().length === 0);
155
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs font-medium uppercase text-muted-foreground", children: label }), _jsx("div", { className: "break-words text-sm", children: hasValue ? value : messages.common.none })] }));
156
+ }
157
+ function formatMoney(amountCents, currency, formatCurrency) {
158
+ return amountCents == null ? null : formatCurrency(amountCents / 100, currency);
159
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProductRecord } from "@voyantjs/products-react";
2
+ export interface ProductDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ product?: ProductRecord;
6
+ onSuccess?: (product: ProductRecord) => void;
7
+ }
8
+ export declare function ProductDialog({ open, onOpenChange, product, onSuccess }: ProductDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=product-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-dialog.d.ts","sourceRoot":"","sources":["../../src/components/product-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAa7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CAC7C;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,kBAAkB,2CA0B3F"}
@@ -0,0 +1,13 @@
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/index.js";
5
+ import { ProductForm } from "./product-form.js";
6
+ export function ProductDialog({ open, onOpenChange, product, onSuccess }) {
7
+ const productMessages = useProductsUiMessagesOrDefault().productDialog;
8
+ const isEdit = Boolean(product);
9
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "product-dialog", className: "sm:max-w-[720px]", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: isEdit ? productMessages.titles.edit : productMessages.titles.create }), _jsx(DialogDescription, { children: isEdit ? productMessages.descriptions.edit : productMessages.descriptions.create })] }), _jsx(ProductForm, { mode: product ? { kind: "edit", product } : { kind: "create" }, onSuccess: (saved) => {
10
+ onSuccess?.(saved);
11
+ onOpenChange(false);
12
+ }, onCancel: () => onOpenChange(false) })] }) }));
13
+ }
@@ -0,0 +1,14 @@
1
+ import { type ProductRecord } from "@voyantjs/products-react";
2
+ export type ProductFormMode = {
3
+ kind: "create";
4
+ } | {
5
+ kind: "edit";
6
+ product: ProductRecord;
7
+ };
8
+ export interface ProductFormProps {
9
+ mode: ProductFormMode;
10
+ onSuccess?: (product: ProductRecord) => void;
11
+ onCancel?: () => void;
12
+ }
13
+ export declare function ProductForm({ mode, onSuccess, onCancel }: ProductFormProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=product-form.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,121 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useProductMutation, } from "@voyantjs/products-react";
4
+ import { Badge } from "@voyantjs/ui/components/badge";
5
+ import { Button } from "@voyantjs/ui/components/button";
6
+ import { CurrencyCombobox } from "@voyantjs/ui/components/currency-combobox";
7
+ import { Input } from "@voyantjs/ui/components/input";
8
+ import { Label } from "@voyantjs/ui/components/label";
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
10
+ import { Textarea } from "@voyantjs/ui/components/textarea";
11
+ import { Loader2, X } from "lucide-react";
12
+ import * as React from "react";
13
+ import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
14
+ import { ProductTypeCombobox } from "./product-type-combobox.js";
15
+ function initialState(mode) {
16
+ if (mode.kind === "edit") {
17
+ const product = mode.product;
18
+ return {
19
+ name: product.name,
20
+ description: product.description ?? "",
21
+ status: product.status,
22
+ bookingMode: product.bookingMode,
23
+ productTypeId: product.productTypeId ?? "__none__",
24
+ sellCurrency: product.sellCurrency,
25
+ sellAmount: product.sellAmountCents != null ? String(product.sellAmountCents / 100) : "",
26
+ costAmount: product.costAmountCents != null ? String(product.costAmountCents / 100) : "",
27
+ tags: product.tags ?? [],
28
+ };
29
+ }
30
+ return {
31
+ name: "",
32
+ description: "",
33
+ status: "draft",
34
+ bookingMode: "itinerary",
35
+ productTypeId: "__none__",
36
+ sellCurrency: "EUR",
37
+ sellAmount: "",
38
+ costAmount: "",
39
+ tags: [],
40
+ };
41
+ }
42
+ function toAmountCents(value) {
43
+ const trimmed = value.trim();
44
+ if (!trimmed)
45
+ return null;
46
+ const parsed = Number(trimmed);
47
+ if (!Number.isFinite(parsed) || parsed < 0)
48
+ return null;
49
+ return Math.round(parsed * 100);
50
+ }
51
+ function toPayload(state) {
52
+ return {
53
+ name: state.name.trim(),
54
+ description: state.description.trim() || null,
55
+ status: state.status,
56
+ bookingMode: state.bookingMode,
57
+ productTypeId: state.productTypeId === "__none__" ? null : state.productTypeId,
58
+ sellCurrency: state.sellCurrency.trim().toUpperCase(),
59
+ sellAmountCents: toAmountCents(state.sellAmount),
60
+ costAmountCents: toAmountCents(state.costAmount),
61
+ tags: state.tags,
62
+ };
63
+ }
64
+ export function ProductForm({ mode, onSuccess, onCancel }) {
65
+ const messages = useProductsUiMessagesOrDefault();
66
+ const productMessages = messages.productForm;
67
+ const [state, setState] = React.useState(() => initialState(mode));
68
+ const [tagInput, setTagInput] = React.useState("");
69
+ const [error, setError] = React.useState(null);
70
+ const { create, update } = useProductMutation();
71
+ const isSubmitting = create.isPending || update.isPending;
72
+ const productStatuses = React.useMemo(() => [
73
+ { value: "draft", label: messages.common.productStatusLabels.draft },
74
+ { value: "active", label: messages.common.productStatusLabels.active },
75
+ { value: "archived", label: messages.common.productStatusLabels.archived },
76
+ ], [messages]);
77
+ const bookingModes = React.useMemo(() => [
78
+ { value: "date", label: messages.common.productBookingModeLabels.date },
79
+ { value: "date_time", label: messages.common.productBookingModeLabels.date_time },
80
+ { value: "open", label: messages.common.productBookingModeLabels.open },
81
+ { value: "stay", label: messages.common.productBookingModeLabels.stay },
82
+ { value: "transfer", label: messages.common.productBookingModeLabels.transfer },
83
+ { value: "itinerary", label: messages.common.productBookingModeLabels.itinerary },
84
+ { value: "other", label: messages.common.productBookingModeLabels.other },
85
+ ], [messages]);
86
+ const field = (key) => (value) => {
87
+ setState((prev) => ({ ...prev, [key]: value }));
88
+ };
89
+ const handleSubmit = async (event) => {
90
+ event.preventDefault();
91
+ setError(null);
92
+ if (!state.name.trim()) {
93
+ setError(productMessages.validation.nameRequired);
94
+ return;
95
+ }
96
+ if (state.sellCurrency.trim().length !== 3) {
97
+ setError(productMessages.validation.sellCurrencyInvalid);
98
+ return;
99
+ }
100
+ const payload = toPayload(state);
101
+ try {
102
+ const product = mode.kind === "create"
103
+ ? await create.mutateAsync(payload)
104
+ : await update.mutateAsync({ id: mode.product.id, input: payload });
105
+ onSuccess?.(product);
106
+ }
107
+ catch (err) {
108
+ setError(err instanceof Error ? err.message : productMessages.validation.saveFailed);
109
+ }
110
+ };
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) => {
112
+ if (event.key === "Enter" || event.key === ",") {
113
+ event.preventDefault();
114
+ const value = tagInput.trim().replace(/,+$/, "");
115
+ if (value && !state.tags.includes(value)) {
116
+ field("tags")([...state.tags, value]);
117
+ }
118
+ setTagInput("");
119
+ }
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) })] })] }));
121
+ }
@@ -0,0 +1,7 @@
1
+ import { type ProductRecord } from "@voyantjs/products-react";
2
+ export interface ProductListProps {
3
+ pageSize?: number;
4
+ onSelectProduct?: (product: ProductRecord) => void;
5
+ }
6
+ export declare function ProductList({ pageSize, onSelectProduct }?: ProductListProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=product-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-list.d.ts","sourceRoot":"","sources":["../../src/components/product-list.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AA4BjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACnD;AA8BD,wBAAgB,WAAW,CAAC,EAAE,QAAa,EAAE,eAAe,EAAE,GAAE,gBAAqB,2CA2YpF"}