@voyantjs/suppliers-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.
@@ -0,0 +1,2 @@
1
+ export declare function formatMessage(template: string, values: Record<string, string | number>): string;
2
+ //# sourceMappingURL=message-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-format.d.ts","sourceRoot":"","sources":["../../src/components/message-format.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,UAEtF"}
@@ -0,0 +1,3 @@
1
+ export function formatMessage(template, values) {
2
+ return template.replace(/\{(\w+)\}/g, (_match, key) => String(values[key] ?? ""));
3
+ }
@@ -0,0 +1,11 @@
1
+ import { type SupplierRate } from "@voyantjs/suppliers-react";
2
+ export type RateDialogProps = {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ supplierId: string;
6
+ serviceId: string;
7
+ rate?: SupplierRate;
8
+ onSuccess?: (rate: SupplierRate) => void;
9
+ };
10
+ export declare function RateDialog({ open, onOpenChange, supplierId, serviceId, rate, onSuccess, }: RateDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=rate-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-dialog.d.ts","sourceRoot":"","sources":["../../src/components/rate-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,YAAY,EAA2B,MAAM,2BAA2B,CAAA;AAwClG,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;CACzC,CAAA;AAED,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,eAAe,2CAsJjB"}
@@ -0,0 +1,82 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { RATE_UNITS, useSupplierRateMutation } from "@voyantjs/suppliers-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
5
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
6
+ import { Loader2 } from "lucide-react";
7
+ import * as React from "react";
8
+ import { useForm } from "react-hook-form";
9
+ import { z } from "zod/v4";
10
+ import { useSuppliersUiMessagesOrDefault } from "../i18n/index.js";
11
+ function getRateSchema(messages) {
12
+ const dialog = messages.dialogs.rate;
13
+ return z.object({
14
+ name: z.string().min(1, dialog.validationNameRequired),
15
+ currency: z.string().min(3, dialog.validationIsoCurrency).max(3, dialog.validationIsoCurrency),
16
+ amount: z.coerce.number().min(0, dialog.validationNonNegative),
17
+ unit: z.enum(["per_person", "per_group", "per_night", "per_vehicle", "flat"]),
18
+ validFrom: z.string().optional().nullable(),
19
+ validTo: z.string().optional().nullable(),
20
+ minPax: z.coerce.number().int().positive().optional().or(z.literal("")).nullable(),
21
+ maxPax: z.coerce.number().int().positive().optional().or(z.literal("")).nullable(),
22
+ notes: z.string().optional().nullable(),
23
+ });
24
+ }
25
+ export function RateDialog({ open, onOpenChange, supplierId, serviceId, rate, onSuccess, }) {
26
+ const messages = useSuppliersUiMessagesOrDefault();
27
+ const dialog = messages.dialogs.rate;
28
+ const schema = React.useMemo(() => getRateSchema(messages), [messages]);
29
+ const rateMutation = useSupplierRateMutation(supplierId);
30
+ const isEditing = !!rate;
31
+ const form = useForm({
32
+ resolver: zodResolver(schema),
33
+ defaultValues: {
34
+ name: "",
35
+ currency: "EUR",
36
+ amount: 0,
37
+ unit: "per_person",
38
+ validFrom: "",
39
+ validTo: "",
40
+ minPax: "",
41
+ maxPax: "",
42
+ notes: "",
43
+ },
44
+ });
45
+ React.useEffect(() => {
46
+ if (!open)
47
+ return;
48
+ form.reset({
49
+ name: rate?.name ?? "",
50
+ currency: rate?.currency ?? "EUR",
51
+ amount: rate ? rate.amountCents / 100 : 0,
52
+ unit: rate?.unit ?? "per_person",
53
+ validFrom: rate?.validFrom ?? "",
54
+ validTo: rate?.validTo ?? "",
55
+ minPax: rate?.minPax ?? "",
56
+ maxPax: rate?.maxPax ?? "",
57
+ notes: rate?.notes ?? "",
58
+ });
59
+ }, [form, open, rate]);
60
+ async function onSubmit(values) {
61
+ const input = {
62
+ name: values.name,
63
+ currency: values.currency.toUpperCase(),
64
+ amountCents: Math.round(values.amount * 100),
65
+ unit: values.unit,
66
+ validFrom: values.validFrom || null,
67
+ validTo: values.validTo || null,
68
+ minPax: values.minPax && typeof values.minPax === "number" ? values.minPax : null,
69
+ maxPax: values.maxPax && typeof values.maxPax === "number" ? values.maxPax : null,
70
+ notes: values.notes || null,
71
+ };
72
+ const saved = isEditing
73
+ ? await rateMutation.update.mutateAsync({ serviceId, rateId: rate.id, input })
74
+ : await rateMutation.create.mutateAsync({ serviceId, input });
75
+ onSuccess?.(saved);
76
+ onOpenChange(false);
77
+ }
78
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? dialog.editTitle : dialog.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(Field, { label: dialog.seasonNameLabel, error: form.formState.errors.name?.message, children: _jsx(Input, { ...form.register("name"), placeholder: dialog.seasonNamePlaceholder }) }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsx(Field, { label: dialog.currencyLabel, error: form.formState.errors.currency?.message, children: _jsx(Input, { ...form.register("currency"), maxLength: 3, placeholder: dialog.currencyPlaceholder, className: "uppercase" }) }), _jsx(Field, { label: dialog.amountLabel, error: form.formState.errors.amount?.message, children: _jsx(Input, { ...form.register("amount"), type: "number", min: "0", step: "0.01", placeholder: dialog.amountPlaceholder }) }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: dialog.unitLabel }), _jsxs(Select, { value: form.watch("unit"), onValueChange: (value) => form.setValue("unit", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: RATE_UNITS.map((unit) => (_jsx(SelectItem, { value: unit.value, children: messages.common.rateUnitLabels[unit.value] }, unit.value))) })] })] })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.validFromLabel, children: _jsx(Input, { ...form.register("validFrom"), type: "date" }) }), _jsx(Field, { label: dialog.validToLabel, children: _jsx(Input, { ...form.register("validTo"), type: "date" }) })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.minPaxLabel, children: _jsx(Input, { ...form.register("minPax"), type: "number", min: "1", placeholder: dialog.minPaxPlaceholder }) }), _jsx(Field, { label: dialog.maxPaxLabel, children: _jsx(Input, { ...form.register("maxPax"), type: "number", min: "1", placeholder: dialog.maxPaxPlaceholder }) })] }), _jsx(Field, { label: dialog.notesLabel, children: _jsx(Textarea, { ...form.register("notes"), placeholder: dialog.notesPlaceholder }) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "animate-spin" }), isEditing ? messages.common.save : messages.common.create] })] })] })] }) }));
79
+ }
80
+ function Field({ label, error, children, }) {
81
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: label }), children, error && _jsx("p", { className: "text-xs text-destructive", children: error })] }));
82
+ }
@@ -0,0 +1,10 @@
1
+ import { type SupplierService } from "@voyantjs/suppliers-react";
2
+ export type ServiceDialogProps = {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ supplierId: string;
6
+ service?: SupplierService;
7
+ onSuccess?: (service: SupplierService) => void;
8
+ };
9
+ export declare function ServiceDialog({ open, onOpenChange, supplierId, service, onSuccess, }: ServiceDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=service-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-dialog.d.ts","sourceRoot":"","sources":["../../src/components/service-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,2BAA2B,CAAA;AAqClC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;CAC/C,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,UAAU,EACV,OAAO,EACP,SAAS,GACV,EAAE,kBAAkB,2CAuHpB"}
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { SERVICE_TYPES, useSupplierServiceMutation, } from "@voyantjs/suppliers-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
5
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
6
+ import { Loader2 } from "lucide-react";
7
+ import * as React from "react";
8
+ import { useForm } from "react-hook-form";
9
+ import { z } from "zod/v4";
10
+ import { useSuppliersUiMessagesOrDefault } from "../i18n/index.js";
11
+ function getServiceSchema(messages) {
12
+ return z.object({
13
+ serviceType: z.enum(["accommodation", "transfer", "experience", "guide", "meal", "other"]),
14
+ name: z.string().min(1, messages.dialogs.service.validationNameRequired),
15
+ description: z.string().optional().nullable(),
16
+ duration: z.string().optional().nullable(),
17
+ capacity: z.coerce.number().int().positive().optional().or(z.literal("")).nullable(),
18
+ active: z.boolean().default(true),
19
+ });
20
+ }
21
+ export function ServiceDialog({ open, onOpenChange, supplierId, service, onSuccess, }) {
22
+ const messages = useSuppliersUiMessagesOrDefault();
23
+ const dialog = messages.dialogs.service;
24
+ const schema = React.useMemo(() => getServiceSchema(messages), [messages]);
25
+ const serviceMutation = useSupplierServiceMutation(supplierId);
26
+ const isEditing = !!service;
27
+ const form = useForm({
28
+ resolver: zodResolver(schema),
29
+ defaultValues: {
30
+ serviceType: "accommodation",
31
+ name: "",
32
+ description: "",
33
+ duration: "",
34
+ capacity: "",
35
+ active: true,
36
+ },
37
+ });
38
+ React.useEffect(() => {
39
+ if (!open)
40
+ return;
41
+ form.reset({
42
+ serviceType: service?.serviceType ?? "accommodation",
43
+ name: service?.name ?? "",
44
+ description: service?.description ?? "",
45
+ duration: service?.duration ?? "",
46
+ capacity: service?.capacity ?? "",
47
+ active: service?.active ?? true,
48
+ });
49
+ }, [form, open, service]);
50
+ async function onSubmit(values) {
51
+ const input = {
52
+ serviceType: values.serviceType,
53
+ name: values.name,
54
+ description: values.description || null,
55
+ duration: values.duration || null,
56
+ capacity: values.capacity && typeof values.capacity === "number" ? values.capacity : null,
57
+ active: values.active,
58
+ };
59
+ const saved = isEditing
60
+ ? await serviceMutation.update.mutateAsync({ serviceId: service.id, input })
61
+ : await serviceMutation.create.mutateAsync(input);
62
+ onSuccess?.(saved);
63
+ onOpenChange(false);
64
+ }
65
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? dialog.editTitle : dialog.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: dialog.serviceTypeLabel }), _jsxs(Select, { value: form.watch("serviceType"), onValueChange: (value) => form.setValue("serviceType", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SERVICE_TYPES.map((type) => (_jsx(SelectItem, { value: type.value, children: messages.common.serviceTypeLabels[type.value] }, type.value))) })] })] }), _jsx(Field, { label: dialog.nameLabel, error: form.formState.errors.name?.message, children: _jsx(Input, { ...form.register("name"), placeholder: dialog.namePlaceholder }) }), _jsx(Field, { label: dialog.descriptionLabel, children: _jsx(Textarea, { ...form.register("description"), placeholder: dialog.descriptionPlaceholder }) }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.durationLabel, children: _jsx(Input, { ...form.register("duration"), placeholder: dialog.durationPlaceholder }) }), _jsx(Field, { label: dialog.capacityLabel, children: _jsx(Input, { ...form.register("capacity"), type: "number", min: "1", placeholder: dialog.capacityPlaceholder }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (value) => form.setValue("active", value) }), _jsx(Label, { children: dialog.activeLabel })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "animate-spin" }), isEditing ? messages.common.save : messages.common.create] })] })] })] }) }));
66
+ }
67
+ function Field({ label, error, children, }) {
68
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: label }), children, error && _jsx("p", { className: "text-xs text-destructive", children: error })] }));
69
+ }
@@ -0,0 +1,16 @@
1
+ import { type Supplier, type UpdateSupplierInput } from "@voyantjs/suppliers-react";
2
+ import * as React from "react";
3
+ export type SupplierDetailPageProps = {
4
+ id: string;
5
+ locale?: string;
6
+ onBack?: () => void;
7
+ onDeleted?: () => void;
8
+ confirmAction?: (message: string) => boolean;
9
+ renderCustomerPaymentPolicy?: (args: {
10
+ supplier: Supplier;
11
+ updateSupplier: (input: UpdateSupplierInput) => Promise<Supplier>;
12
+ isUpdating: boolean;
13
+ }) => React.ReactNode;
14
+ };
15
+ export declare function SupplierDetailPage({ id, locale, onBack, onDeleted, confirmAction, renderCustomerPaymentPolicy, }: SupplierDetailPageProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=supplier-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supplier-detail-page.d.ts","sourceRoot":"","sources":["../../src/components/supplier-detail-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,QAAQ,EAIb,KAAK,mBAAmB,EAQzB,MAAM,2BAA2B,CAAA;AAWlC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IAC5C,2BAA2B,CAAC,EAAE,CAAC,IAAI,EAAE;QACnC,QAAQ,EAAE,QAAQ,CAAA;QAClB,cAAc,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;QACjE,UAAU,EAAE,OAAO,CAAA;KACpB,KAAK,KAAK,CAAC,SAAS,CAAA;CACtB,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EACjC,EAAE,EACF,MAAgB,EAChB,MAAM,EACN,SAAS,EACT,aAAkE,EAClE,2BAA2B,GAC5B,EAAE,uBAAuB,2CAmSzB"}
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { statusVariant, useSupplier, useSupplierMutation, useSupplierNoteMutation, useSupplierNotes, useSupplierRateMutation, useSupplierServiceMutation, useSupplierServices, } from "@voyantjs/suppliers-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Textarea, } from "@voyantjs/ui/components";
5
+ import { ArrowLeft, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
6
+ import * as React from "react";
7
+ import { useSuppliersUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { RateDialog } from "./rate-dialog.js";
9
+ import { ServiceDialog } from "./service-dialog.js";
10
+ import { SupplierDialog } from "./supplier-dialog.js";
11
+ import { SupplierServiceRow } from "./supplier-service-row.js";
12
+ export function SupplierDetailPage({ id, locale = "en-US", onBack, onDeleted, confirmAction = (message) => globalThis.confirm?.(message) ?? true, renderCustomerPaymentPolicy, }) {
13
+ const messages = useSuppliersUiMessagesOrDefault();
14
+ const detail = messages.supplierDetailPage;
15
+ const supplierQuery = useSupplier(id);
16
+ const servicesQuery = useSupplierServices(id);
17
+ const notesQuery = useSupplierNotes(id);
18
+ const supplierMutation = useSupplierMutation();
19
+ const serviceMutation = useSupplierServiceMutation(id);
20
+ const rateMutation = useSupplierRateMutation(id);
21
+ const noteMutation = useSupplierNoteMutation(id);
22
+ const [editOpen, setEditOpen] = React.useState(false);
23
+ const [serviceDialogOpen, setServiceDialogOpen] = React.useState(false);
24
+ const [editingService, setEditingService] = React.useState();
25
+ const [rateDialog, setRateDialog] = React.useState({ open: false, serviceId: "" });
26
+ const [expandedServiceId, setExpandedServiceId] = React.useState(null);
27
+ const [noteContent, setNoteContent] = React.useState("");
28
+ const supplier = supplierQuery.data?.data;
29
+ async function deleteSupplier() {
30
+ if (!supplier || !confirmAction(detail.deleteSupplierConfirm))
31
+ return;
32
+ await supplierMutation.remove.mutateAsync(supplier.id);
33
+ onDeleted?.();
34
+ }
35
+ async function deleteService(serviceId) {
36
+ if (!confirmAction(detail.deleteServiceConfirm))
37
+ return;
38
+ await serviceMutation.remove.mutateAsync(serviceId);
39
+ if (expandedServiceId === serviceId)
40
+ setExpandedServiceId(null);
41
+ }
42
+ async function deleteRate(serviceId, rateId) {
43
+ if (!confirmAction(detail.deleteRateConfirm))
44
+ return;
45
+ await rateMutation.remove.mutateAsync({ serviceId, rateId });
46
+ }
47
+ async function addNote() {
48
+ const content = noteContent.trim();
49
+ if (!content)
50
+ return;
51
+ await noteMutation.create.mutateAsync({ content });
52
+ setNoteContent("");
53
+ }
54
+ if (supplierQuery.isPending)
55
+ return _jsx(SupplierDetailSkeleton, {});
56
+ if (supplierQuery.isError) {
57
+ return (_jsx(EmptyState, { message: detail.loadFailed, onBack: onBack, backLabel: detail.backToSuppliers }));
58
+ }
59
+ if (!supplier) {
60
+ return (_jsx(EmptyState, { message: detail.notFound, onBack: onBack, backLabel: detail.backToSuppliers }));
61
+ }
62
+ const services = servicesQuery.data?.data ?? [];
63
+ const notes = notesQuery.data?.data ?? [];
64
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-4 md:flex-row md:items-start md:justify-between", children: [_jsxs("div", { className: "flex flex-col gap-3", children: [onBack && (_jsxs(Button, { type: "button", variant: "ghost", className: "w-fit px-0", onClick: onBack, children: [_jsx(ArrowLeft, {}), detail.backToSuppliers] })), _jsxs("div", { children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h1", { className: "text-3xl font-semibold tracking-tight", children: supplier.name }), _jsx(Badge, { variant: statusVariant[supplier.status], children: messages.common.supplierStatusLabels[supplier.status] })] }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: supplier.description })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", onClick: () => setEditOpen(true), children: [_jsx(Pencil, {}), messages.common.edit] }), _jsxs(Button, { type: "button", variant: "destructive", onClick: deleteSupplier, disabled: supplierMutation.remove.isPending, children: [_jsx(Trash2, {}), messages.common.delete] })] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.details }) }), _jsxs(CardContent, { className: "grid gap-3 text-sm", children: [_jsx(Detail, { label: detail.labels.type, children: messages.common.supplierTypeLabels[supplier.type] }), _jsx(Detail, { label: detail.labels.status, children: messages.common.supplierStatusLabels[supplier.status] }), _jsx(Detail, { label: detail.labels.city, children: supplier.city ?? messages.common.none }), _jsx(Detail, { label: detail.labels.country, children: supplier.country ?? messages.common.none }), _jsx(Detail, { label: detail.labels.currency, children: supplier.defaultCurrency ?? messages.common.none }), _jsx(Detail, { label: detail.labels.reservationTimeout, children: supplier.reservationTimeoutMinutes == null
65
+ ? messages.common.none
66
+ : String(supplier.reservationTimeoutMinutes) }), _jsx(Detail, { label: detail.labels.created, children: formatDate(supplier.createdAt, locale) }), _jsx(Detail, { label: detail.labels.updated, children: formatDate(supplier.updatedAt, locale) })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.contact }) }), _jsx(CardContent, { className: "grid gap-3 text-sm", children: !hasContactDetails(supplier) ? (_jsx("p", { className: "text-muted-foreground", children: detail.noContact })) : (_jsxs(_Fragment, { children: [_jsx(Detail, { label: detail.labels.email, children: supplier.email ?? messages.common.none }), _jsx(Detail, { label: detail.labels.phone, children: supplier.phone ?? messages.common.none }), _jsx(Detail, { label: detail.labels.website, children: supplier.website ? (_jsx("a", { href: supplier.website, className: "text-primary underline-offset-4 hover:underline", children: supplier.website })) : (messages.common.none) }), _jsx(Detail, { label: detail.labels.address, children: supplier.address ?? messages.common.none }), _jsx(Detail, { label: detail.labels.contactName, children: supplier.contactName ?? messages.common.none }), _jsx(Detail, { label: detail.labels.contactEmail, children: supplier.contactEmail ?? messages.common.none }), _jsx(Detail, { label: detail.labels.contactPhone, children: supplier.contactPhone ?? messages.common.none })] })) })] })] }), renderCustomerPaymentPolicy?.({
67
+ supplier,
68
+ updateSupplier: (input) => supplierMutation.update.mutateAsync({ id: supplier.id, input }),
69
+ isUpdating: supplierMutation.update.isPending,
70
+ }), _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-4", children: [_jsx(CardTitle, { children: detail.services }), _jsxs(Button, { type: "button", onClick: () => {
71
+ setEditingService(undefined);
72
+ setServiceDialogOpen(true);
73
+ }, children: [_jsx(Plus, {}), detail.addService] })] }), _jsx(CardContent, { className: "flex flex-col gap-3", children: servicesQuery.isPending ? (_jsx(LoadingLine, {})) : services.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: detail.noServices })) : (services.map((service) => (_jsx(SupplierServiceRow, { service: service, supplierId: supplier.id, expanded: expandedServiceId === service.id, onToggle: () => setExpandedServiceId((current) => (current === service.id ? null : service.id)), onEdit: () => {
74
+ setEditingService(service);
75
+ setServiceDialogOpen(true);
76
+ }, onDelete: () => void deleteService(service.id), onAddRate: () => setRateDialog({ open: true, serviceId: service.id }), onEditRate: (rate) => setRateDialog({ open: true, serviceId: service.id, rate }), onDeleteRate: (rateId) => void deleteRate(service.id, rateId) }, service.id)))) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: detail.notes }) }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Textarea, { value: noteContent, onChange: (event) => setNoteContent(event.target.value), placeholder: detail.notePlaceholder }), _jsxs(Button, { type: "button", className: "w-fit", onClick: () => void addNote(), disabled: !noteContent.trim() || noteMutation.create.isPending, children: [noteMutation.create.isPending && _jsx(Loader2, { className: "animate-spin" }), detail.addNote] })] }), notesQuery.isPending ? (_jsx(LoadingLine, {})) : notes.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: detail.noNotes })) : (_jsx("div", { className: "flex flex-col gap-3", children: notes.map((note) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("p", { className: "whitespace-pre-wrap text-sm", children: note.content }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: formatDate(note.createdAt, locale) })] }, note.id))) }))] })] }), _jsx(SupplierDialog, { open: editOpen, onOpenChange: setEditOpen, supplier: supplier, onSuccess: () => setEditOpen(false) }), _jsx(ServiceDialog, { open: serviceDialogOpen, onOpenChange: setServiceDialogOpen, supplierId: supplier.id, service: editingService, onSuccess: () => {
77
+ setServiceDialogOpen(false);
78
+ setEditingService(undefined);
79
+ } }), _jsx(RateDialog, { open: rateDialog.open, onOpenChange: (open) => setRateDialog((current) => ({ ...current, open })), supplierId: supplier.id, serviceId: rateDialog.serviceId, rate: rateDialog.rate, onSuccess: () => setRateDialog({ open: false, serviceId: "" }) })] }));
80
+ }
81
+ function Detail({ label, children }) {
82
+ return (_jsxs("div", { className: "grid grid-cols-[10rem_minmax(0,1fr)] gap-3", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { className: "min-w-0 break-words", children: children })] }));
83
+ }
84
+ function EmptyState({ message, onBack, backLabel, }) {
85
+ return (_jsxs("div", { className: "flex min-h-80 flex-col items-center justify-center gap-4 text-center", children: [_jsx("p", { className: "text-muted-foreground", children: message }), onBack && (_jsxs(Button, { type: "button", variant: "outline", onClick: onBack, children: [_jsx(ArrowLeft, {}), backLabel] }))] }));
86
+ }
87
+ function SupplierDetailSkeleton() {
88
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx("div", { className: "h-9 w-72 animate-pulse rounded bg-muted" }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsx("div", { className: "h-64 animate-pulse rounded-md bg-muted" }), _jsx("div", { className: "h-64 animate-pulse rounded-md bg-muted" })] }), _jsx("div", { className: "h-96 animate-pulse rounded-md bg-muted" })] }));
89
+ }
90
+ function LoadingLine() {
91
+ return _jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted" });
92
+ }
93
+ function hasContactDetails(supplier) {
94
+ return Boolean(supplier.email ||
95
+ supplier.phone ||
96
+ supplier.website ||
97
+ supplier.address ||
98
+ supplier.contactName ||
99
+ supplier.contactEmail ||
100
+ supplier.contactPhone);
101
+ }
102
+ function formatDate(value, locale) {
103
+ return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
104
+ }
@@ -0,0 +1,9 @@
1
+ import { type Supplier } from "@voyantjs/suppliers-react";
2
+ export type SupplierDialogProps = {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ supplier?: Supplier;
6
+ onSuccess?: (supplier: Supplier) => void;
7
+ };
8
+ export declare function SupplierDialog({ open, onOpenChange, supplier, onSuccess }: SupplierDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=supplier-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supplier-dialog.d.ts","sourceRoot":"","sources":["../../src/components/supplier-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,QAAQ,EAEd,MAAM,2BAA2B,CAAA;AAkDlC,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CACzC,CAAA;AAED,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,mBAAmB,2CAyO9F"}
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { SUPPLIER_STATUSES, SUPPLIER_TYPES, useSupplierMutation, } from "@voyantjs/suppliers-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
5
+ import { CountryCombobox } from "@voyantjs/ui/components/country-combobox";
6
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
7
+ import { Loader2 } from "lucide-react";
8
+ import * as React from "react";
9
+ import { useForm } from "react-hook-form";
10
+ import { z } from "zod/v4";
11
+ import { useSuppliersUiMessagesOrDefault } from "../i18n/index.js";
12
+ function getSupplierSchema(messages) {
13
+ const dialog = messages.dialogs.supplier;
14
+ return z.object({
15
+ name: z.string().min(1, dialog.validationNameRequired),
16
+ type: z.enum(["hotel", "transfer", "guide", "experience", "airline", "restaurant", "other"]),
17
+ status: z.enum(["active", "inactive", "pending"]),
18
+ description: z.string().optional().nullable(),
19
+ email: z.string().email().optional().or(z.literal("")).nullable(),
20
+ phone: z.string().optional().nullable(),
21
+ website: z.string().url().optional().or(z.literal("")).nullable(),
22
+ address: z.string().optional().nullable(),
23
+ city: z.string().optional().nullable(),
24
+ country: z.string().optional().nullable(),
25
+ defaultCurrency: z.string().max(3, dialog.validationIsoCurrency).optional().nullable(),
26
+ reservationTimeoutMinutes: z
27
+ .union([z.literal(""), z.coerce.number().int().min(0, dialog.validationReservationTimeout)])
28
+ .optional()
29
+ .nullable(),
30
+ contactName: z.string().optional().nullable(),
31
+ contactEmail: z.string().email().optional().or(z.literal("")).nullable(),
32
+ contactPhone: z.string().optional().nullable(),
33
+ });
34
+ }
35
+ export function SupplierDialog({ open, onOpenChange, supplier, onSuccess }) {
36
+ const messages = useSuppliersUiMessagesOrDefault();
37
+ const dialog = messages.dialogs.supplier;
38
+ const schema = React.useMemo(() => getSupplierSchema(messages), [messages]);
39
+ const supplierMutation = useSupplierMutation();
40
+ const isEditing = !!supplier;
41
+ const form = useForm({
42
+ resolver: zodResolver(schema),
43
+ defaultValues: {
44
+ name: "",
45
+ type: "hotel",
46
+ status: "active",
47
+ description: "",
48
+ email: "",
49
+ phone: "",
50
+ website: "",
51
+ address: "",
52
+ city: "",
53
+ country: "",
54
+ defaultCurrency: "",
55
+ reservationTimeoutMinutes: "",
56
+ contactName: "",
57
+ contactEmail: "",
58
+ contactPhone: "",
59
+ },
60
+ });
61
+ React.useEffect(() => {
62
+ if (!open)
63
+ return;
64
+ form.reset({
65
+ name: supplier?.name ?? "",
66
+ type: supplier?.type ?? "hotel",
67
+ status: supplier?.status ?? "active",
68
+ description: supplier?.description ?? "",
69
+ email: supplier?.email ?? "",
70
+ phone: supplier?.phone ?? "",
71
+ website: supplier?.website ?? "",
72
+ address: supplier?.address ?? "",
73
+ city: supplier?.city ?? "",
74
+ country: supplier?.country ?? "",
75
+ defaultCurrency: supplier?.defaultCurrency ?? "",
76
+ reservationTimeoutMinutes: supplier?.reservationTimeoutMinutes == null
77
+ ? ""
78
+ : String(supplier.reservationTimeoutMinutes),
79
+ contactName: supplier?.contactName ?? "",
80
+ contactEmail: supplier?.contactEmail ?? "",
81
+ contactPhone: supplier?.contactPhone ?? "",
82
+ });
83
+ }, [form, open, supplier]);
84
+ async function onSubmit(values) {
85
+ const input = {
86
+ ...values,
87
+ description: values.description || null,
88
+ email: values.email || null,
89
+ phone: values.phone || null,
90
+ website: values.website || null,
91
+ address: values.address || null,
92
+ city: values.city || null,
93
+ country: values.country || null,
94
+ defaultCurrency: values.defaultCurrency || null,
95
+ reservationTimeoutMinutes: values.reservationTimeoutMinutes === "" ||
96
+ values.reservationTimeoutMinutes === null ||
97
+ values.reservationTimeoutMinutes === undefined
98
+ ? null
99
+ : values.reservationTimeoutMinutes,
100
+ contactName: values.contactName || null,
101
+ contactEmail: values.contactEmail || null,
102
+ contactPhone: values.contactPhone || null,
103
+ };
104
+ const saved = isEditing
105
+ ? await supplierMutation.update.mutateAsync({ id: supplier.id, input })
106
+ : await supplierMutation.create.mutateAsync(input);
107
+ onSuccess?.(saved);
108
+ onOpenChange(false);
109
+ }
110
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? dialog.editTitle : dialog.newTitle }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: dialog.typeLabel }), _jsxs(Select, { value: form.watch("type"), onValueChange: (value) => form.setValue("type", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SUPPLIER_TYPES.map((type) => (_jsx(SelectItem, { value: type.value, children: messages.common.supplierTypeLabels[type.value] }, type.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: dialog.statusLabel }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: SUPPLIER_STATUSES.map((status) => (_jsx(SelectItem, { value: status.value, children: messages.common.supplierStatusLabels[status.value] }, status.value))) })] })] })] }), _jsx(Field, { label: dialog.nameLabel, error: form.formState.errors.name?.message, children: _jsx(Input, { ...form.register("name"), placeholder: dialog.namePlaceholder }) }), _jsx(Field, { label: dialog.descriptionLabel, children: _jsx(Textarea, { ...form.register("description"), placeholder: dialog.descriptionPlaceholder }) }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.emailLabel, error: form.formState.errors.email?.message, children: _jsx(Input, { ...form.register("email"), type: "email", placeholder: dialog.emailPlaceholder }) }), _jsx(Field, { label: dialog.phoneLabel, children: _jsx(Input, { ...form.register("phone"), placeholder: dialog.phonePlaceholder }) })] }), _jsx(Field, { label: dialog.websiteLabel, error: form.formState.errors.website?.message, children: _jsx(Input, { ...form.register("website"), placeholder: dialog.websitePlaceholder }) }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.cityLabel, children: _jsx(Input, { ...form.register("city"), placeholder: dialog.cityPlaceholder }) }), _jsx(Field, { label: dialog.countryLabel, children: _jsx(CountryCombobox, { value: form.watch("country"), onChange: (value) => form.setValue("country", value ?? ""), placeholder: dialog.countryPlaceholder }) })] }), _jsx(Field, { label: dialog.addressLabel, children: _jsx(Textarea, { ...form.register("address"), placeholder: dialog.addressPlaceholder }) }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [_jsx(Field, { label: dialog.defaultCurrencyLabel, error: form.formState.errors.defaultCurrency?.message, children: _jsx(Input, { ...form.register("defaultCurrency"), maxLength: 3, placeholder: dialog.defaultCurrencyPlaceholder, className: "uppercase" }) }), _jsx(Field, { label: dialog.reservationTimeoutLabel, error: form.formState.errors.reservationTimeoutMinutes?.message, children: _jsx(Input, { ...form.register("reservationTimeoutMinutes"), type: "number", min: "0", placeholder: dialog.reservationTimeoutPlaceholder }) })] }), _jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [_jsx(Field, { label: dialog.contactNameLabel, children: _jsx(Input, { ...form.register("contactName"), placeholder: dialog.contactNamePlaceholder }) }), _jsx(Field, { label: dialog.contactEmailLabel, error: form.formState.errors.contactEmail?.message, children: _jsx(Input, { ...form.register("contactEmail"), type: "email", placeholder: dialog.contactEmailPlaceholder }) }), _jsx(Field, { label: dialog.contactPhoneLabel, children: _jsx(Input, { ...form.register("contactPhone"), placeholder: dialog.contactPhonePlaceholder }) })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "animate-spin" }), isEditing ? messages.common.save : messages.common.create] })] })] })] }) }));
111
+ }
112
+ function Field({ label, error, children, }) {
113
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: label }), children, error && _jsx("p", { className: "text-xs text-destructive", children: error })] }));
114
+ }
@@ -1,11 +1,9 @@
1
- import type { Supplier } from "@voyantjs/suppliers-react";
2
- export declare function SuppliersPage({ search, onSearchChange, onCreate, onRowClick, rows, total, isPending, }: {
3
- search: string;
4
- onSearchChange: (value: string) => void;
5
- onCreate: () => void;
6
- onRowClick: (supplier: Supplier) => void;
7
- rows: Supplier[];
8
- total: number;
9
- isPending?: boolean;
10
- }): import("react/jsx-runtime").JSX.Element;
1
+ import { type Supplier } from "@voyantjs/suppliers-react";
2
+ export type SuppliersPageProps = {
3
+ pageSize?: number;
4
+ onSupplierOpen?: (supplier: Supplier) => void;
5
+ onSupplierCreated?: (supplier: Supplier) => void;
6
+ initialSearch?: string;
7
+ };
8
+ export declare function SuppliersPage({ pageSize, onSupplierOpen, onSupplierCreated, initialSearch, }?: SuppliersPageProps): import("react/jsx-runtime").JSX.Element;
11
9
  //# sourceMappingURL=suppliers-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"suppliers-page.d.ts","sourceRoot":"","sources":["../../src/components/suppliers-page.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AA+DzD,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,cAAc,EACd,QAAQ,EACR,UAAU,EACV,IAAI,EACJ,KAAK,EACL,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;IACxC,IAAI,EAAE,QAAQ,EAAE,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,2CA0CA"}
1
+ {"version":3,"file":"suppliers-page.d.ts","sourceRoot":"","sources":["../../src/components/suppliers-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,QAAQ,EAKd,MAAM,2BAA2B,CAAA;AAmBlC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;IAC7C,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAa,EACb,cAAc,EACd,iBAAiB,EACjB,aAAkB,GACnB,GAAE,kBAAuB,2CAoPzB"}