@voyantjs/products-ui 0.30.6 → 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
@@ -0,0 +1,154 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
4
+ import { useProducts, } from "@voyantjs/products-react";
5
+ import { Badge } from "@voyantjs/ui/components/badge";
6
+ import { Button } from "@voyantjs/ui/components/button";
7
+ import { DateRangePicker } from "@voyantjs/ui/components/date-picker";
8
+ import { Input } from "@voyantjs/ui/components/input";
9
+ import { Label } from "@voyantjs/ui/components/label";
10
+ import { Popover, PopoverContent, PopoverTrigger } from "@voyantjs/ui/components/popover";
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
12
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
13
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
14
+ import { ArrowDown, ArrowUp, ArrowUpDown, ListFilter, Plus, Search, X } from "lucide-react";
15
+ import * as React from "react";
16
+ import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
17
+ import { ProductDialog } from "./product-dialog.js";
18
+ const STATUS_ALL = "__all__";
19
+ const PRODUCT_STATUSES = ["draft", "active", "archived"];
20
+ const SORTABLE_COLUMNS = {
21
+ name: "name",
22
+ status: "status",
23
+ sellAmount: "sellAmount",
24
+ pax: "pax",
25
+ startDate: "startDate",
26
+ };
27
+ const SKELETON_ROW_COUNT = 6;
28
+ const TABLE_COLUMN_COUNT = 5;
29
+ const statusVariant = {
30
+ draft: "outline",
31
+ active: "default",
32
+ archived: "secondary",
33
+ };
34
+ function formatAmount(cents, currency, fallback) {
35
+ if (cents == null)
36
+ return fallback;
37
+ return `${(cents / 100).toFixed(2)} ${currency}`;
38
+ }
39
+ export function ProductList({ pageSize = 25, onSelectProduct } = {}) {
40
+ const messages = useProductsUiMessagesOrDefault();
41
+ const productMessages = messages.productList;
42
+ const [search, setSearch] = React.useState("");
43
+ const [status, setStatus] = React.useState(STATUS_ALL);
44
+ const [dateRange, setDateRange] = React.useState(null);
45
+ const [paxMin, setPaxMin] = React.useState("");
46
+ const [paxMax, setPaxMax] = React.useState("");
47
+ const [sellAmountMin, setSellAmountMin] = React.useState("");
48
+ const [sellAmountMax, setSellAmountMax] = React.useState("");
49
+ const [sortBy, setSortBy] = React.useState("createdAt");
50
+ const [sortDir, setSortDir] = React.useState("desc");
51
+ const [offset, setOffset] = React.useState(0);
52
+ const [filterPopoverOpen, setFilterPopoverOpen] = React.useState(false);
53
+ const [dialogOpen, setDialogOpen] = React.useState(false);
54
+ const [editing, setEditing] = React.useState(undefined);
55
+ const paxMinNumber = paxMin === "" ? undefined : Number.parseInt(paxMin, 10);
56
+ const paxMaxNumber = paxMax === "" ? undefined : Number.parseInt(paxMax, 10);
57
+ const sellAmountMinCents = sellAmountMin === "" ? undefined : Math.round(Number.parseFloat(sellAmountMin) * 100);
58
+ const sellAmountMaxCents = sellAmountMax === "" ? undefined : Math.round(Number.parseFloat(sellAmountMax) * 100);
59
+ const { data, isPending, isFetching, isError } = useProducts({
60
+ search: search || undefined,
61
+ status: status === STATUS_ALL ? undefined : status,
62
+ dateFrom: dateRange?.from ?? undefined,
63
+ dateTo: dateRange?.to ?? undefined,
64
+ paxMin: Number.isFinite(paxMinNumber) ? paxMinNumber : undefined,
65
+ paxMax: Number.isFinite(paxMaxNumber) ? paxMaxNumber : undefined,
66
+ sellAmountMin: Number.isFinite(sellAmountMinCents) ? sellAmountMinCents : undefined,
67
+ sellAmountMax: Number.isFinite(sellAmountMaxCents) ? sellAmountMaxCents : undefined,
68
+ sortBy,
69
+ sortDir,
70
+ limit: pageSize,
71
+ offset,
72
+ });
73
+ const products = data?.data ?? [];
74
+ const total = data?.total ?? 0;
75
+ const page = Math.floor(offset / pageSize) + 1;
76
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
77
+ const showSkeleton = isPending || (isFetching && products.length === 0);
78
+ const resetOffset = () => setOffset(0);
79
+ const handleSort = (field) => {
80
+ setOffset(0);
81
+ if (sortBy !== field) {
82
+ setSortBy(field);
83
+ setSortDir("asc");
84
+ return;
85
+ }
86
+ if (sortDir === "asc") {
87
+ setSortDir("desc");
88
+ return;
89
+ }
90
+ setSortBy("createdAt");
91
+ setSortDir("desc");
92
+ };
93
+ const activeFilterCount = (status !== STATUS_ALL ? 1 : 0) +
94
+ (dateRange?.from || dateRange?.to ? 1 : 0) +
95
+ (paxMin !== "" || paxMax !== "" ? 1 : 0) +
96
+ (sellAmountMin !== "" || sellAmountMax !== "" ? 1 : 0);
97
+ const hasActiveFilters = activeFilterCount > 0 || search !== "";
98
+ const clearFilters = () => {
99
+ setSearch("");
100
+ setStatus(STATUS_ALL);
101
+ setDateRange(null);
102
+ setPaxMin("");
103
+ setPaxMax("");
104
+ setSellAmountMin("");
105
+ setSellAmountMax("");
106
+ resetOffset();
107
+ };
108
+ const handleEdit = (product) => {
109
+ if (onSelectProduct) {
110
+ onSelectProduct(product);
111
+ return;
112
+ }
113
+ setEditing(product);
114
+ setDialogOpen(true);
115
+ };
116
+ const handleCreate = () => {
117
+ setEditing(undefined);
118
+ setDialogOpen(true);
119
+ };
120
+ return (_jsxs("div", { "data-slot": "product-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative min-w-[14rem] flex-1", children: [_jsx(Label, { htmlFor: "products-search", className: "sr-only", children: productMessages.searchPlaceholder }), _jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { id: "products-search", placeholder: productMessages.searchPlaceholder, value: search, onChange: (event) => {
121
+ setSearch(event.target.value);
122
+ resetOffset();
123
+ }, className: "pl-9" })] }), _jsxs(Popover, { open: filterPopoverOpen, onOpenChange: setFilterPopoverOpen, children: [_jsx(PopoverTrigger, { render: _jsxs(Button, { variant: "outline", size: "default", children: [_jsx(ListFilter, { className: "mr-2 size-4", "aria-hidden": "true" }), productMessages.filters.button, activeFilterCount > 0 && (_jsx(Badge, { variant: "secondary", className: "ml-2 px-1.5", children: activeFilterCount }))] }) }), _jsx(PopoverContent, { align: "start", className: "w-[22rem] p-4", children: _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "products-filter-status", children: productMessages.filters.statusLabel }), _jsxs(Select, { value: status, onValueChange: (value) => {
124
+ setStatus(value ?? STATUS_ALL);
125
+ resetOffset();
126
+ }, children: [_jsx(SelectTrigger, { id: "products-filter-status", className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: STATUS_ALL, children: productMessages.filters.statusAll }), PRODUCT_STATUSES.map((value) => (_jsx(SelectItem, { value: value, children: messages.common.productStatusLabels[value] }, value)))] })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.filters.dateLabel }), _jsx(DateRangePicker, { value: dateRange, onChange: (value) => {
127
+ setDateRange(value);
128
+ resetOffset();
129
+ }, placeholder: productMessages.filters.datePlaceholder, clearable: true, className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.filters.paxLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, placeholder: productMessages.filters.min, value: paxMin, onChange: (event) => {
130
+ setPaxMin(event.target.value);
131
+ resetOffset();
132
+ }, className: "w-full", "aria-label": `${productMessages.filters.paxLabel} ${productMessages.filters.min}` }), _jsx("span", { className: "text-muted-foreground", children: "\u2013" }), _jsx(Input, { type: "number", min: 0, placeholder: productMessages.filters.max, value: paxMax, onChange: (event) => {
133
+ setPaxMax(event.target.value);
134
+ resetOffset();
135
+ }, className: "w-full", "aria-label": `${productMessages.filters.paxLabel} ${productMessages.filters.max}` })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: productMessages.filters.sellAmountLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { type: "number", min: 0, step: "0.01", placeholder: productMessages.filters.min, value: sellAmountMin, onChange: (event) => {
136
+ setSellAmountMin(event.target.value);
137
+ resetOffset();
138
+ }, className: "w-full", "aria-label": `${productMessages.filters.sellAmountLabel} ${productMessages.filters.min}` }), _jsx("span", { className: "text-muted-foreground", children: "\u2013" }), _jsx(Input, { type: "number", min: 0, step: "0.01", placeholder: productMessages.filters.max, value: sellAmountMax, onChange: (event) => {
139
+ setSellAmountMax(event.target.value);
140
+ resetOffset();
141
+ }, className: "w-full", "aria-label": `${productMessages.filters.sellAmountLabel} ${productMessages.filters.max}` })] })] })] }) })] }), hasActiveFilters && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: clearFilters, children: [_jsx(X, { className: "mr-1 size-4", "aria-hidden": "true" }), productMessages.filters.clear] })), _jsx("div", { className: "ml-auto", children: _jsxs(Button, { onClick: handleCreate, "data-slot": "product-list-create", children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), productMessages.newProduct] }) })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(SortHeader, { label: productMessages.columns.name, field: SORTABLE_COLUMNS.name, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: productMessages.columns.status, field: SORTABLE_COLUMNS.status, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: productMessages.columns.sellAmount, field: SORTABLE_COLUMNS.sellAmount, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: productMessages.columns.pax, field: SORTABLE_COLUMNS.pax, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) }), _jsx(TableHead, { children: _jsx(SortHeader, { label: productMessages.columns.startDate, field: SORTABLE_COLUMNS.startDate, sortBy: sortBy, sortDir: sortDir, onSort: handleSort }) })] }) }), _jsx(TableBody, { children: showSkeleton ? (_jsx(ProductTableSkeleton, { rows: SKELETON_ROW_COUNT })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-destructive", children: productMessages.loadFailed }) })) : products.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: TABLE_COLUMN_COUNT, className: "h-24 text-center text-sm text-muted-foreground", children: productMessages.empty }) })) : (products.map((product) => (_jsxs(TableRow, { onClick: () => handleEdit(product), className: "cursor-pointer", children: [_jsx(TableCell, { className: "font-medium", children: product.name }), _jsx(TableCell, { children: _jsx(Badge, { variant: statusVariant[product.status] ?? "secondary", children: messages.common.productStatusLabels[product.status] }) }), _jsx(TableCell, { children: formatAmount(product.sellAmountCents, product.sellCurrency, productMessages.noValue) }), _jsx(TableCell, { children: product.pax ?? productMessages.noValue }), _jsx(TableCell, { children: product.startDate ?? productMessages.noValue })] }, product.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: formatMessage(productMessages.paginationShowing, { count: products.length, total }) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((prev) => Math.max(0, prev - pageSize)), children: productMessages.paginationPrevious }), _jsx("span", { children: formatMessage(productMessages.paginationPage, { page, pageCount }) }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + pageSize >= total, onClick: () => setOffset((prev) => prev + pageSize), children: productMessages.paginationNext })] })] }), _jsx(ProductDialog, { open: dialogOpen, onOpenChange: setDialogOpen, product: editing, onSuccess: (product) => {
142
+ if (onSelectProduct) {
143
+ onSelectProduct(product);
144
+ }
145
+ } })] }));
146
+ }
147
+ function SortHeader({ label, field, sortBy, sortDir, onSort }) {
148
+ const active = sortBy === field;
149
+ const Icon = active ? (sortDir === "asc" ? ArrowUp : ArrowDown) : ArrowUpDown;
150
+ return (_jsxs("button", { type: "button", onClick: () => onSort(field), className: "-ml-2 inline-flex h-8 items-center gap-1 rounded-sm px-2 hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("span", { children: label }), _jsx(Icon, { className: `size-3.5 ${active ? "text-foreground" : "text-muted-foreground/60"}`, "aria-hidden": true })] }));
151
+ }
152
+ function ProductTableSkeleton({ rows }) {
153
+ return (_jsx(_Fragment, { children: Array.from({ length: rows }).map((_, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-48" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-5 w-16 rounded-full" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-24" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-8" }) }), _jsx(TableCell, { children: _jsx(Skeleton, { className: "h-4 w-24" }) })] }, `skeleton-${idx}`))) }));
154
+ }
@@ -0,0 +1,7 @@
1
+ import { type ProductTagListProps } from "./product-tag-list.js";
2
+ export interface ProductTagsPageProps {
3
+ pageSize?: ProductTagListProps["pageSize"];
4
+ className?: string;
5
+ }
6
+ export declare function ProductTagsPage({ pageSize, className }?: ProductTagsPageProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=product-tags-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-tags-page.d.ts","sourceRoot":"","sources":["../../src/components/product-tags-page.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAEhF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,CAAA;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAE,oBAAyB,2CAajF"}
@@ -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 { ProductTagList } from "./product-tag-list.js";
6
+ export function ProductTagsPage({ pageSize, className } = {}) {
7
+ const messages = useProductsUiMessagesOrDefault().productTagsPage;
8
+ return (_jsxs("div", { "data-slot": "product-tags-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(ProductTagList, { pageSize: pageSize })] }));
9
+ }
@@ -0,0 +1,6 @@
1
+ export interface ProductTypesPageProps {
2
+ pageSize?: number;
3
+ className?: string;
4
+ }
5
+ export declare function ProductTypesPage({ pageSize, className, }?: ProductTypesPageProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=product-types-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-types-page.d.ts","sourceRoot":"","sources":["../../src/components/product-types-page.tsx"],"names":[],"mappings":"AAuCA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAgBD,wBAAgB,gBAAgB,CAAC,EAC/B,QAA4B,EAC5B,SAAS,GACV,GAAE,qBAA0B,2CAyI5B"}
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useProductTypeMutation, useProductTypes, } from "@voyantjs/products-react";
4
+ import { Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Switch, Textarea, } from "@voyantjs/ui/components";
5
+ import { cn } from "@voyantjs/ui/lib/utils";
6
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
7
+ import { Loader2, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react";
8
+ import { useEffect, useMemo, useState } from "react";
9
+ import { useForm } from "react-hook-form";
10
+ import { z } from "zod/v4";
11
+ import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
12
+ const DEFAULT_PAGE_SIZE = 25;
13
+ function getFormSchema(messages) {
14
+ return z.object({
15
+ name: z.string().min(1, messages.validation.nameRequired).max(255),
16
+ code: z.string().min(1, messages.validation.codeRequired).max(100),
17
+ description: z.string().optional().nullable(),
18
+ sortOrder: z.coerce.number().int().default(0),
19
+ active: z.boolean().default(true),
20
+ });
21
+ }
22
+ export function ProductTypesPage({ pageSize = DEFAULT_PAGE_SIZE, className, } = {}) {
23
+ const messages = useProductsUiMessagesOrDefault();
24
+ const pageMessages = messages.productTypesPage;
25
+ const [sheetOpen, setSheetOpen] = useState(false);
26
+ const [editing, setEditing] = useState();
27
+ const [pageIndex, setPageIndex] = useState(0);
28
+ const { data, isPending, refetch } = useProductTypes({
29
+ limit: pageSize,
30
+ offset: pageIndex * pageSize,
31
+ });
32
+ const { remove } = useProductTypeMutation();
33
+ const items = data?.data ?? [];
34
+ const total = data?.total ?? 0;
35
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
36
+ return (_jsxs("div", { "data-slot": "product-types-page", className: cn("flex flex-col gap-6", className), children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: pageMessages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: pageMessages.description })] }), _jsxs(Button, { size: "sm", onClick: () => {
37
+ setEditing(undefined);
38
+ setSheetOpen(true);
39
+ }, children: [_jsx(Plus, { className: "mr-1.5 size-3.5" }), pageMessages.addType] })] }), isPending ? (_jsx(ProductTypesListLoading, { loadingLabel: messages.common.loading })) : (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: items.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: pageMessages.empty })) : (_jsx("div", { className: "flex flex-col divide-y", children: items.map((item) => (_jsxs("div", { className: "flex items-center justify-between gap-4 px-6 py-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: item.name }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: item.code }), !item.active ? (_jsx(Badge, { variant: "secondary", className: "text-xs", children: messages.common.inactive })) : null] }), item.description ? (_jsx("p", { className: "text-xs text-muted-foreground", children: item.description })) : null] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
40
+ setEditing(item);
41
+ setSheetOpen(true);
42
+ }, children: [_jsx(Pencil, { className: "size-4" }), pageMessages.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => {
43
+ if (confirm(pageMessages.deleteConfirm)) {
44
+ remove.mutate(item.id, { onSuccess: () => void refetch() });
45
+ }
46
+ }, children: [_jsx(Trash2, { className: "size-4" }), pageMessages.delete] })] })] })] }, item.id))) })) })), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsx("span", { children: pageMessages.showingSummary
47
+ .replace("{count}", String(items.length))
48
+ .replace("{total}", String(total)) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: pageIndex === 0, onClick: () => setPageIndex((current) => Math.max(0, current - 1)), children: messages.common.previous }), _jsxs("span", { children: [messages.common.page, " ", pageIndex + 1, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: (pageIndex + 1) * pageSize >= total, onClick: () => setPageIndex((current) => current + 1), children: messages.common.next })] })] }), _jsx(ProductTypeSheet, { open: sheetOpen, onOpenChange: setSheetOpen, item: editing, onSuccess: () => {
49
+ setSheetOpen(false);
50
+ setEditing(undefined);
51
+ void refetch();
52
+ } })] }));
53
+ }
54
+ function ProductTypesListLoading({ loadingLabel }) {
55
+ return (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: _jsxs("div", { className: "flex h-32 items-center justify-center text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "mr-2 size-4 animate-spin" }), loadingLabel] }) }));
56
+ }
57
+ function ProductTypeSheet({ open, onOpenChange, item, onSuccess, }) {
58
+ const messages = useProductsUiMessagesOrDefault().productTypesPage;
59
+ const { create, update } = useProductTypeMutation();
60
+ const formSchema = useMemo(() => getFormSchema(messages), [messages]);
61
+ const form = useForm({
62
+ resolver: zodResolver(formSchema),
63
+ defaultValues: {
64
+ name: "",
65
+ code: "",
66
+ description: "",
67
+ sortOrder: 0,
68
+ active: true,
69
+ },
70
+ });
71
+ useEffect(() => {
72
+ if (open && item) {
73
+ form.reset({
74
+ name: item.name,
75
+ code: item.code,
76
+ description: item.description ?? "",
77
+ sortOrder: item.sortOrder,
78
+ active: item.active,
79
+ });
80
+ }
81
+ else if (open) {
82
+ form.reset();
83
+ }
84
+ }, [open, item, form]);
85
+ const isSubmitting = create.isPending || update.isPending;
86
+ const onSubmit = async (values) => {
87
+ const payload = {
88
+ name: values.name,
89
+ code: values.code,
90
+ description: values.description || null,
91
+ sortOrder: values.sortOrder,
92
+ active: values.active,
93
+ };
94
+ if (item) {
95
+ await update.mutateAsync({ id: item.id, input: payload });
96
+ }
97
+ else {
98
+ await create.mutateAsync(payload);
99
+ }
100
+ onSuccess();
101
+ };
102
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: item ? messages.editSheetTitle : messages.newSheetTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(SheetBody, { 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: messages.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: messages.namePlaceholder, autoFocus: true }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.codeLabel }), _jsx(Input, { ...form.register("code"), placeholder: messages.codePlaceholder }), form.formState.errors.code ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.code.message })) : null] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.descriptionLabel }), _jsx(Textarea, { ...form.register("description"), placeholder: messages.descriptionPlaceholder })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.sortOrderLabel }), _jsx(Input, { ...form.register("sortOrder"), type: "number" })] }), _jsxs("div", { className: "flex items-center gap-2 pt-6", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { children: messages.activeLabel })] })] })] }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: messages.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting ? _jsx(Loader2, { className: "mr-2 size-4 animate-spin" }) : null, item ? messages.saveChanges : messages.createType] })] })] })] }) }));
103
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProductRecord } from "@voyantjs/products-react";
2
+ export interface ProductsPageProps {
3
+ pageSize?: number;
4
+ onProductOpen?: (product: ProductRecord) => void;
5
+ className?: string;
6
+ }
7
+ export declare function ProductsPage({ pageSize, onProductOpen, className }?: ProductsPageProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=products-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products-page.d.ts","sourceRoot":"","sources":["../../src/components/products-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAK7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,GAAE,iBAAsB,2CAa1F"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "@voyantjs/ui/lib/utils";
3
+ import { useProductsUiMessagesOrDefault } from "../i18n/index.js";
4
+ import { ProductList } from "./product-list.js";
5
+ export function ProductsPage({ pageSize, onProductOpen, className } = {}) {
6
+ const productMessages = useProductsUiMessagesOrDefault().productsPage;
7
+ return (_jsxs("div", { "data-slot": "products-page", className: cn("flex flex-col gap-6", className), children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: productMessages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: productMessages.description })] }), _jsx(ProductList, { pageSize: pageSize, onSelectProduct: onProductOpen })] }));
8
+ }
package/dist/i18n/en.d.ts CHANGED
@@ -29,6 +29,20 @@ export declare const productsUiEn: {
29
29
  active: string;
30
30
  archived: string;
31
31
  };
32
+ productStatusLabels: {
33
+ draft: string;
34
+ active: string;
35
+ archived: string;
36
+ };
37
+ productBookingModeLabels: {
38
+ date: string;
39
+ date_time: string;
40
+ open: string;
41
+ stay: string;
42
+ transfer: string;
43
+ itinerary: string;
44
+ other: string;
45
+ };
32
46
  };
33
47
  comboboxes: {
34
48
  productCategory: {
@@ -40,6 +54,161 @@ export declare const productsUiEn: {
40
54
  empty: string;
41
55
  };
42
56
  };
57
+ productCategoriesPage: {
58
+ title: string;
59
+ description: string;
60
+ };
61
+ productsPage: {
62
+ title: string;
63
+ description: string;
64
+ };
65
+ productDetailPage: {
66
+ actions: {
67
+ back: string;
68
+ edit: string;
69
+ delete: string;
70
+ createBooking: string;
71
+ addItinerary: string;
72
+ editItinerary: string;
73
+ deleteItinerary: string;
74
+ addDay: string;
75
+ };
76
+ tabs: {
77
+ overview: string;
78
+ media: string;
79
+ itinerary: string;
80
+ options: string;
81
+ versions: string;
82
+ };
83
+ sections: {
84
+ overview: {
85
+ title: string;
86
+ description: string;
87
+ };
88
+ details: {
89
+ title: string;
90
+ description: string;
91
+ };
92
+ commercial: {
93
+ title: string;
94
+ description: string;
95
+ };
96
+ itinerary: {
97
+ title: string;
98
+ description: string;
99
+ };
100
+ sidebar: {
101
+ title: string;
102
+ description: string;
103
+ };
104
+ };
105
+ fields: {
106
+ status: string;
107
+ bookingMode: string;
108
+ visibility: string;
109
+ capacityMode: string;
110
+ timezone: string;
111
+ productType: string;
112
+ facility: string;
113
+ taxClass: string;
114
+ sellAmount: string;
115
+ costAmount: string;
116
+ margin: string;
117
+ pax: string;
118
+ startDate: string;
119
+ endDate: string;
120
+ reservationTimeout: string;
121
+ tags: string;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ };
125
+ states: {
126
+ loading: string;
127
+ loadFailed: string;
128
+ notFoundTitle: string;
129
+ notFoundDescription: string;
130
+ noDescription: string;
131
+ noItineraries: string;
132
+ noDays: string;
133
+ deleteConfirm: string;
134
+ deleteItineraryConfirm: string;
135
+ deleteDayConfirm: string;
136
+ deleteFailed: string;
137
+ minutes: string;
138
+ };
139
+ };
140
+ productDialog: {
141
+ titles: {
142
+ create: string;
143
+ edit: string;
144
+ };
145
+ descriptions: {
146
+ create: string;
147
+ edit: string;
148
+ };
149
+ };
150
+ productForm: {
151
+ fields: {
152
+ name: string;
153
+ description: string;
154
+ tags: string;
155
+ status: string;
156
+ bookingMode: string;
157
+ productType: string;
158
+ sellCurrency: string;
159
+ sellAmount: string;
160
+ costAmount: string;
161
+ };
162
+ placeholders: {
163
+ name: string;
164
+ description: string;
165
+ tagInput: string;
166
+ productTypeSearch: string;
167
+ currencySearch: string;
168
+ amount: string;
169
+ };
170
+ validation: {
171
+ nameRequired: string;
172
+ sellCurrencyInvalid: string;
173
+ saveFailed: string;
174
+ };
175
+ actions: {
176
+ cancel: string;
177
+ saving: string;
178
+ create: string;
179
+ saveChanges: string;
180
+ };
181
+ };
182
+ productList: {
183
+ searchPlaceholder: string;
184
+ newProduct: string;
185
+ filters: {
186
+ button: string;
187
+ statusLabel: string;
188
+ statusAll: string;
189
+ dateLabel: string;
190
+ datePlaceholder: string;
191
+ paxLabel: string;
192
+ sellAmountLabel: string;
193
+ min: string;
194
+ max: string;
195
+ clear: string;
196
+ };
197
+ columns: {
198
+ name: string;
199
+ status: string;
200
+ sellAmount: string;
201
+ pax: string;
202
+ startDate: string;
203
+ };
204
+ loadFailed: string;
205
+ empty: string;
206
+ noValue: string;
207
+ paginationShowing: string;
208
+ paginationPage: string;
209
+ paginationPrevious: string;
210
+ paginationNext: string;
211
+ };
43
212
  productCategoryDialog: {
44
213
  titles: {
45
214
  create: string;
@@ -130,6 +299,37 @@ export declare const productsUiEn: {
130
299
  deleteConfirm: string;
131
300
  showingSummary: string;
132
301
  };
302
+ productTagsPage: {
303
+ title: string;
304
+ description: string;
305
+ };
306
+ productTypesPage: {
307
+ title: string;
308
+ description: string;
309
+ addType: string;
310
+ empty: string;
311
+ edit: string;
312
+ delete: string;
313
+ deleteConfirm: string;
314
+ showingSummary: string;
315
+ editSheetTitle: string;
316
+ newSheetTitle: string;
317
+ nameLabel: string;
318
+ namePlaceholder: string;
319
+ codeLabel: string;
320
+ codePlaceholder: string;
321
+ descriptionLabel: string;
322
+ descriptionPlaceholder: string;
323
+ sortOrderLabel: string;
324
+ activeLabel: string;
325
+ cancel: string;
326
+ saveChanges: string;
327
+ createType: string;
328
+ validation: {
329
+ nameRequired: string;
330
+ codeRequired: string;
331
+ };
332
+ };
133
333
  productMediaDialog: {
134
334
  titles: {
135
335
  create: string;
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAocK,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6oBK,CAAA"}