@voyantjs/products-ui 0.101.1 → 0.102.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 (126) hide show
  1. package/dist/components/product-detail/date-picker.d.ts +44 -0
  2. package/dist/components/product-detail/date-picker.d.ts.map +1 -0
  3. package/dist/components/product-detail/date-picker.js +125 -0
  4. package/dist/components/product-detail/host.d.ts +53 -0
  5. package/dist/components/product-detail/host.d.ts.map +1 -0
  6. package/dist/components/product-detail/host.js +24 -0
  7. package/dist/components/product-detail/index.d.ts +6 -0
  8. package/dist/components/product-detail/index.d.ts.map +1 -0
  9. package/dist/components/product-detail/index.js +5 -0
  10. package/dist/components/product-detail/product-activity-section.d.ts +4 -0
  11. package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
  12. package/dist/components/product-detail/product-activity-section.js +37 -0
  13. package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
  14. package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
  15. package/dist/components/product-detail/product-day-sheet.js +75 -0
  16. package/dist/components/product-detail/product-day-translation.d.ts +41 -0
  17. package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
  18. package/dist/components/product-detail/product-day-translation.js +111 -0
  19. package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
  20. package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
  21. package/dist/components/product-detail/product-departure-dialog.js +10 -0
  22. package/dist/components/product-detail/product-departure-form.d.ts +25 -0
  23. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
  24. package/dist/components/product-detail/product-departure-form.js +237 -0
  25. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
  26. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
  27. package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
  28. package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
  29. package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
  30. package/dist/components/product-detail/product-detail-day-row.js +43 -0
  31. package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
  32. package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
  33. package/dist/components/product-detail/product-detail-dialog.js +10 -0
  34. package/dist/components/product-detail/product-detail-form.d.ts +19 -0
  35. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
  36. package/dist/components/product-detail/product-detail-form.js +180 -0
  37. package/dist/components/product-detail/product-detail-header.d.ts +12 -0
  38. package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
  39. package/dist/components/product-detail/product-detail-header.js +19 -0
  40. package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
  41. package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
  42. package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
  43. package/dist/components/product-detail/product-detail-page.d.ts +4 -0
  44. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
  45. package/dist/components/product-detail/product-detail-page.js +97 -0
  46. package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
  47. package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
  48. package/dist/components/product-detail/product-detail-sections.js +143 -0
  49. package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
  50. package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
  51. package/dist/components/product-detail/product-detail-shared.js +157 -0
  52. package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
  53. package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
  54. package/dist/components/product-detail/product-detail-skeleton.js +53 -0
  55. package/dist/components/product-detail/product-extras-section.d.ts +4 -0
  56. package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
  57. package/dist/components/product-detail/product-extras-section.js +141 -0
  58. package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
  59. package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
  60. package/dist/components/product-detail/product-itinerary-form.js +38 -0
  61. package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
  62. package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
  63. package/dist/components/product-detail/product-market-rules-section.js +81 -0
  64. package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
  65. package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
  66. package/dist/components/product-detail/product-media-gallery.js +114 -0
  67. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
  68. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
  69. package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
  70. package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
  71. package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
  72. package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
  73. package/dist/components/product-detail/product-option-pricing-grid.d.ts +16 -0
  74. package/dist/components/product-detail/product-option-pricing-grid.d.ts.map +1 -0
  75. package/dist/components/product-detail/product-option-pricing-grid.js +193 -0
  76. package/dist/components/product-detail/product-options-pricing.d.ts +34 -0
  77. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  78. package/dist/components/product-detail/product-options-pricing.js +385 -0
  79. package/dist/components/product-detail/product-options-shared.d.ts +623 -0
  80. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  81. package/dist/components/product-detail/product-options-shared.js +54 -0
  82. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  83. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  84. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  85. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  86. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  87. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  88. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  89. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  90. package/dist/components/product-detail/product-schedule-form.js +222 -0
  91. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  92. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  93. package/dist/components/product-detail/product-service-dialog.js +10 -0
  94. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  95. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  96. package/dist/components/product-detail/product-service-form.js +154 -0
  97. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  98. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  99. package/dist/components/product-detail/product-translation-popover.js +217 -0
  100. package/dist/components/product-detail/product-unit-dialog.d.ts +14 -0
  101. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  102. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  103. package/dist/components/product-detail/product-unit-form.d.ts +34 -0
  104. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  105. package/dist/components/product-detail/product-unit-form.js +139 -0
  106. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +17 -0
  107. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  108. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  109. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +29 -0
  110. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  111. package/dist/components/product-detail/product-unit-price-rule-form.js +145 -0
  112. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  113. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  114. package/dist/components/product-detail/timezone-options.js +28 -0
  115. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  116. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  117. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  118. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  119. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  120. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  121. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  122. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  123. package/dist/components/product-detail/zod-resolver.js +39 -0
  124. package/dist/components/product-options-section.d.ts.map +1 -1
  125. package/dist/components/product-options-section.js +31 -20
  126. package/package.json +38 -19
@@ -0,0 +1,17 @@
1
+ import { type ProductRecord } from "@voyantjs/products-react";
2
+ /**
3
+ * Per-listing customer payment policy override for a product.
4
+ *
5
+ * Wins over the product's category and supplier policies in the
6
+ * cascade — use this when a single product has stricter / looser
7
+ * terms than the rest of its catalog group (a luxury-tier offering,
8
+ * a flash sale, etc.).
9
+ *
10
+ * Inherit by default; flipping the toggle off saves an explicit
11
+ * policy on the product row.
12
+ */
13
+ export declare function ProductPaymentPolicySection({ product, onSuccess, }: {
14
+ product: ProductRecord;
15
+ onSuccess?: () => void;
16
+ }): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=product-payment-policy-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-payment-policy-section.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-payment-policy-section.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,0BAA0B,CAAA;AAgBjC;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,EAC1C,OAAO,EACP,SAAS,GACV,EAAE;IACD,OAAO,EAAE,aAAa,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB,2CAwFA"}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { PaymentPolicyForm, PaymentPolicyPreview } from "@voyantjs/finance-ui";
4
+ import { useProductMutation, } from "@voyantjs/products-react";
5
+ import { Badge, Button, Label, Switch } from "@voyantjs/ui/components";
6
+ import { Separator } from "@voyantjs/ui/components/separator";
7
+ import { Loader2 } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
+ import { toast } from "sonner";
10
+ import { useProductDetailMessages } from "./host.js";
11
+ import { Section } from "./product-detail-sections.js";
12
+ const DEFAULT_POLICY = {
13
+ deposit: { kind: "percent", percent: 50 },
14
+ minDaysBeforeDepartureForDeposit: 30,
15
+ balanceDueDaysBeforeDeparture: 30,
16
+ balanceDueMinDaysFromNow: 7,
17
+ };
18
+ /**
19
+ * Per-listing customer payment policy override for a product.
20
+ *
21
+ * Wins over the product's category and supplier policies in the
22
+ * cascade — use this when a single product has stricter / looser
23
+ * terms than the rest of its catalog group (a luxury-tier offering,
24
+ * a flash sale, etc.).
25
+ *
26
+ * Inherit by default; flipping the toggle off saves an explicit
27
+ * policy on the product row.
28
+ */
29
+ export function ProductPaymentPolicySection({ product, onSuccess, }) {
30
+ const t = useProductDetailMessages().products.operations.paymentPolicy;
31
+ const persisted = product.customerPaymentPolicy ?? null;
32
+ const [draft, setDraft] = useState(persisted);
33
+ const { update } = useProductMutation();
34
+ // One-way sync: when the persisted policy reference changes (after
35
+ // a save → query invalidation, or external edit), refresh the
36
+ // draft. Mid-flight typing is preserved because we don't depend on
37
+ // setState callbacks running on every render.
38
+ useEffect(() => {
39
+ setDraft(persisted);
40
+ }, [persisted]);
41
+ const isInheriting = draft === null;
42
+ const isDirty = JSON.stringify(draft) !== JSON.stringify(persisted);
43
+ const save = () => {
44
+ update.mutate({
45
+ id: product.id,
46
+ input: { customerPaymentPolicy: draft ?? null },
47
+ }, {
48
+ onSuccess: () => {
49
+ toast.success(t.savedToast);
50
+ onSuccess?.();
51
+ },
52
+ onError: (err) => toast.error(err instanceof Error ? err.message : t.saveFailed),
53
+ });
54
+ };
55
+ return (_jsx(Section, { title: t.title, actions: _jsxs(Button, { size: "sm", disabled: !isDirty || update.isPending, onClick: save, children: [update.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, t.save] }), children: _jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { htmlFor: "payment-policy-inherit", className: "text-sm font-medium", children: t.inheritLabel }), _jsx(Badge, { variant: isInheriting ? "secondary" : "outline", className: "text-[10px]", children: isInheriting ? t.inheritingBadge : t.customBadge })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t.inheritHint })] }), _jsx(Switch, { id: "payment-policy-inherit", checked: isInheriting, onCheckedChange: (checked) => {
56
+ setDraft(checked ? null : (draft ?? DEFAULT_POLICY));
57
+ }, disabled: update.isPending })] }), isInheriting ? null : (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(PaymentPolicyForm, { value: draft, onChange: setDraft, inheritable: false, currency: product.sellCurrency, disabled: update.isPending }), _jsx(Separator, {}), _jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: t.previewHeading }), _jsx(PaymentPolicyPreview, { policy: draft, currency: product.sellCurrency })] })] }))] }) }));
58
+ }
@@ -0,0 +1,11 @@
1
+ import { type AvailabilityRule } from "./product-schedule-form.js";
2
+ export type { AvailabilityRule };
3
+ type ScheduleDialogProps = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ productId: string;
7
+ rule?: AvailabilityRule;
8
+ onSuccess: () => void;
9
+ };
10
+ export declare function ScheduleDialog({ open, onOpenChange, productId, rule, onSuccess, }: ScheduleDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=product-schedule-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-schedule-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-schedule-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,4BAA4B,CAAA;AAEhF,YAAY,EAAE,gBAAgB,EAAE,CAAA;AAEhC,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAA;IACvB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,mBAAmB,2CAwBrB"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
+ import { useProductDetailMessages } from "./host.js";
4
+ import { ScheduleForm } from "./product-schedule-form.js";
5
+ export function ScheduleDialog({ open, onOpenChange, productId, rule, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const scheduleMessages = messages.products.operations.schedules;
8
+ const isEditing = !!rule;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? scheduleMessages.editTitle : scheduleMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(ScheduleForm, { productId: productId, rule: rule, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,17 @@
1
+ export type AvailabilityRule = {
2
+ id: string;
3
+ productId: string;
4
+ timezone: string;
5
+ recurrenceRule: string;
6
+ maxCapacity: number;
7
+ cutoffMinutes: number | null;
8
+ active: boolean;
9
+ };
10
+ export interface ScheduleFormProps {
11
+ productId: string;
12
+ rule?: AvailabilityRule;
13
+ onSuccess: () => void;
14
+ onCancel?: () => void;
15
+ }
16
+ export declare function ScheduleForm({ productId, rule, onSuccess, onCancel }: ScheduleFormProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=product-schedule-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-schedule-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-schedule-form.tsx"],"names":[],"mappings":"AAuEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAA;IACvB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAgID,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAgSvF"}
@@ -0,0 +1,222 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formatMessage } from "@voyantjs/i18n";
3
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from "@voyantjs/ui/components";
4
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
5
+ import { ToggleGroup, ToggleGroupItem } from "@voyantjs/ui/components/toggle-group";
6
+ import { Loader2 } from "lucide-react";
7
+ import { useEffect, useMemo } from "react";
8
+ import { useForm } from "react-hook-form";
9
+ import { z } from "zod/v4";
10
+ import { useProductDetailApi, useProductDetailMessages } from "./host.js";
11
+ import { getTimezoneLabel, TIMEZONE_IDS, TIMEZONE_OPTIONS } from "./timezone-options.js";
12
+ import { zodResolver } from "./zod-resolver.js";
13
+ const WEEKDAY_VALUES = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
14
+ const FREQUENCY_OPTIONS = ["DAILY", "WEEKLY", "MONTHLY"];
15
+ const buildScheduleFormSchema = (messages) => z
16
+ .object({
17
+ timezone: z.string().min(1, messages.validationTimezoneRequired),
18
+ frequency: z.enum(FREQUENCY_OPTIONS),
19
+ interval: z.coerce.number().int().min(1).max(365),
20
+ byWeekdays: z.array(z.enum(WEEKDAY_VALUES)),
21
+ byMonthDays: z.array(z.coerce.number().int().min(1).max(31)),
22
+ maxCapacity: z.coerce.number().int().min(0),
23
+ cutoffMinutes: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
24
+ active: z.boolean(),
25
+ })
26
+ .refine((v) => {
27
+ if (v.frequency !== "WEEKLY")
28
+ return true;
29
+ return v.byWeekdays.length > 0;
30
+ }, { message: messages.validationWeekdayRequired, path: ["byWeekdays"] })
31
+ .refine((v) => {
32
+ if (v.frequency !== "MONTHLY")
33
+ return true;
34
+ return v.byMonthDays.length > 0;
35
+ }, { message: messages.validationMonthdayRequired, path: ["byMonthDays"] });
36
+ function parseRRule(rrule) {
37
+ const parts = rrule
38
+ .split(";")
39
+ .map((p) => p.trim())
40
+ .filter(Boolean);
41
+ const map = new Map();
42
+ for (const part of parts) {
43
+ const [key, value] = part.split("=");
44
+ if (key && value !== undefined)
45
+ map.set(key.toUpperCase(), value);
46
+ }
47
+ const rawFreq = (map.get("FREQ") ?? "DAILY").toUpperCase();
48
+ const frequency = rawFreq === "WEEKLY" || rawFreq === "MONTHLY" ? rawFreq : "DAILY";
49
+ const interval = Number.parseInt(map.get("INTERVAL") ?? "1", 10) || 1;
50
+ const byday = map.get("BYDAY") ?? "";
51
+ const byWeekdays = byday
52
+ .split(",")
53
+ .map((d) => d.trim().toUpperCase())
54
+ .filter((d) => WEEKDAY_VALUES.includes(d));
55
+ const bymonthday = map.get("BYMONTHDAY") ?? "";
56
+ const byMonthDays = bymonthday
57
+ .split(",")
58
+ .map((d) => Number.parseInt(d.trim(), 10))
59
+ .filter((n) => Number.isFinite(n) && n >= 1 && n <= 31);
60
+ return { frequency, interval, byWeekdays, byMonthDays };
61
+ }
62
+ function buildRRule(values) {
63
+ const parts = [`FREQ=${values.frequency}`];
64
+ if (values.interval > 1)
65
+ parts.push(`INTERVAL=${values.interval}`);
66
+ if (values.frequency === "WEEKLY" && values.byWeekdays.length > 0) {
67
+ const ordered = WEEKDAY_VALUES.filter((d) => values.byWeekdays.includes(d));
68
+ parts.push(`BYDAY=${ordered.join(",")}`);
69
+ }
70
+ if (values.frequency === "MONTHLY" && values.byMonthDays.length > 0) {
71
+ const ordered = [...values.byMonthDays].sort((a, b) => a - b);
72
+ parts.push(`BYMONTHDAY=${ordered.join(",")}`);
73
+ }
74
+ return parts.join(";");
75
+ }
76
+ function describeRRule(values, messages, weekdayOptions) {
77
+ const { frequency, interval, byWeekdays, byMonthDays } = values;
78
+ const cadence = frequency === "DAILY"
79
+ ? interval > 1
80
+ ? formatMessage(messages.previewEveryDays, { interval })
81
+ : messages.previewEveryDay
82
+ : frequency === "WEEKLY"
83
+ ? interval > 1
84
+ ? formatMessage(messages.previewEveryWeeks, { interval })
85
+ : messages.previewEveryWeek
86
+ : interval > 1
87
+ ? formatMessage(messages.previewEveryMonths, { interval })
88
+ : messages.previewEveryMonth;
89
+ if (frequency === "WEEKLY") {
90
+ if (byWeekdays.length === 0)
91
+ return `${cadence}${messages.previewPickWeekday}`;
92
+ const labels = weekdayOptions.filter((d) => byWeekdays.includes(d.value)).map((d) => d.label);
93
+ return formatMessage(messages.previewWeekly, { cadence, days: labels.join(", ") });
94
+ }
95
+ if (frequency === "MONTHLY") {
96
+ if (byMonthDays.length === 0)
97
+ return `${cadence}${messages.previewPickMonthday}`;
98
+ const ordered = [...byMonthDays].sort((a, b) => a - b);
99
+ return formatMessage(messages.previewMonthly, {
100
+ cadence,
101
+ suffix: ordered.length === 1 ? "" : messages.previewSuffixDays,
102
+ days: ordered.join(", "),
103
+ });
104
+ }
105
+ return cadence;
106
+ }
107
+ const MONTH_DAYS = Array.from({ length: 31 }, (_, i) => i + 1);
108
+ function initialValues(rule, defaultTz) {
109
+ if (rule) {
110
+ const parsed = parseRRule(rule.recurrenceRule);
111
+ return {
112
+ timezone: rule.timezone,
113
+ frequency: parsed.frequency,
114
+ interval: parsed.interval,
115
+ byWeekdays: parsed.byWeekdays,
116
+ byMonthDays: parsed.byMonthDays,
117
+ maxCapacity: rule.maxCapacity,
118
+ cutoffMinutes: rule.cutoffMinutes ?? "",
119
+ active: rule.active,
120
+ };
121
+ }
122
+ return {
123
+ timezone: defaultTz,
124
+ frequency: "WEEKLY",
125
+ interval: 1,
126
+ byWeekdays: ["MO"],
127
+ byMonthDays: [],
128
+ maxCapacity: 0,
129
+ cutoffMinutes: "",
130
+ active: true,
131
+ };
132
+ }
133
+ export function ScheduleForm({ productId, rule, onSuccess, onCancel }) {
134
+ const messages = useProductDetailMessages();
135
+ const api = useProductDetailApi();
136
+ const productMessages = messages.products.core;
137
+ const scheduleMessages = messages.products.operations.schedules;
138
+ const isEditing = !!rule;
139
+ const scheduleFormSchema = buildScheduleFormSchema(scheduleMessages);
140
+ const weekdayOptions = [
141
+ { value: "MO", label: scheduleMessages.weekdayMon },
142
+ { value: "TU", label: scheduleMessages.weekdayTue },
143
+ { value: "WE", label: scheduleMessages.weekdayWed },
144
+ { value: "TH", label: scheduleMessages.weekdayThu },
145
+ { value: "FR", label: scheduleMessages.weekdayFri },
146
+ { value: "SA", label: scheduleMessages.weekdaySat },
147
+ { value: "SU", label: scheduleMessages.weekdaySun },
148
+ ];
149
+ const frequencyOptions = [
150
+ { value: "DAILY", label: scheduleMessages.frequencyDaily },
151
+ { value: "WEEKLY", label: scheduleMessages.frequencyWeekly },
152
+ { value: "MONTHLY", label: scheduleMessages.frequencyMonthly },
153
+ ];
154
+ const defaultTz = typeof Intl !== "undefined"
155
+ ? (Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC")
156
+ : "UTC";
157
+ const form = useForm({
158
+ resolver: zodResolver(scheduleFormSchema),
159
+ defaultValues: initialValues(rule, defaultTz),
160
+ });
161
+ const active = form.watch("active");
162
+ const timezone = form.watch("timezone");
163
+ const frequency = form.watch("frequency");
164
+ const interval = form.watch("interval");
165
+ const byWeekdays = form.watch("byWeekdays");
166
+ const byMonthDays = form.watch("byMonthDays");
167
+ const preview = useMemo(() => describeRRule({
168
+ frequency,
169
+ interval: typeof interval === "number" ? interval : Number(interval) || 1,
170
+ byWeekdays,
171
+ byMonthDays,
172
+ }, scheduleMessages, weekdayOptions), [byMonthDays, byWeekdays, frequency, interval, scheduleMessages, weekdayOptions]);
173
+ useEffect(() => {
174
+ form.reset(initialValues(rule, defaultTz));
175
+ }, [rule, form, defaultTz]);
176
+ const onSubmit = async (values) => {
177
+ const cutoffMinutes = typeof values.cutoffMinutes === "number" ? values.cutoffMinutes : null;
178
+ const recurrenceRule = buildRRule({
179
+ frequency: values.frequency,
180
+ interval: values.interval,
181
+ byWeekdays: values.byWeekdays,
182
+ byMonthDays: values.byMonthDays,
183
+ });
184
+ const payload = {
185
+ productId,
186
+ timezone: values.timezone,
187
+ recurrenceRule,
188
+ maxCapacity: values.maxCapacity,
189
+ cutoffMinutes,
190
+ active: values.active,
191
+ };
192
+ if (isEditing) {
193
+ await api.patch(`/v1/availability/rules/${rule.id}`, payload);
194
+ }
195
+ else {
196
+ await api.post("/v1/availability/rules", payload);
197
+ }
198
+ onSuccess();
199
+ };
200
+ const unitLabel = frequency === "DAILY"
201
+ ? scheduleMessages.unitDays
202
+ : frequency === "WEEKLY"
203
+ ? scheduleMessages.unitWeeks
204
+ : scheduleMessages.unitMonths;
205
+ return (_jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col gap-4 overflow-hidden", children: [_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-[160px_1fr] gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.repeatsLabel }), _jsxs(Select, { value: frequency, onValueChange: (v) => form.setValue("frequency", v, {
206
+ shouldValidate: true,
207
+ shouldDirty: true,
208
+ }), items: frequencyOptions, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: frequencyOptions.map((f) => (_jsx(SelectItem, { value: f.value, children: f.label }, f.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.everyLabel }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { ...form.register("interval"), type: "number", min: "1", max: "365", step: "1", className: "w-24" }), _jsx("span", { className: "text-sm text-muted-foreground", children: unitLabel })] }), form.formState.errors.interval && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.interval.message }))] })] }), frequency === "WEEKLY" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.weeklyDaysLabel }), _jsx(ToggleGroup, { multiple: true, value: byWeekdays, onValueChange: (next) => form.setValue("byWeekdays", next, {
209
+ shouldValidate: true,
210
+ shouldDirty: true,
211
+ }), variant: "outline", spacing: 1, children: weekdayOptions.map((d) => (_jsx(ToggleGroupItem, { value: d.value, className: "w-14", children: d.label }, d.value))) }), form.formState.errors.byWeekdays && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.byWeekdays.message }))] })), frequency === "MONTHLY" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.monthlyDaysLabel }), _jsx(ToggleGroup, { multiple: true, value: byMonthDays.map(String), onValueChange: (next) => form.setValue("byMonthDays", next.map((n) => Number.parseInt(n, 10)), { shouldValidate: true, shouldDirty: true }), variant: "outline", spacing: 1, className: "flex-wrap", children: MONTH_DAYS.map((d) => (_jsx(ToggleGroupItem, { value: String(d), className: "w-10", children: d }, d))) }), form.formState.errors.byMonthDays && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.byMonthDays.message }))] })), _jsx("p", { className: "text-xs text-muted-foreground", children: preview }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.timezoneLabel }), _jsxs(Combobox, { items: TIMEZONE_IDS, value: timezone || null, autoHighlight: true, itemToStringValue: (id) => getTimezoneLabel(id), onValueChange: (next) => {
212
+ if (typeof next === "string") {
213
+ form.setValue("timezone", next, {
214
+ shouldValidate: true,
215
+ shouldDirty: true,
216
+ });
217
+ }
218
+ }, children: [_jsx(ComboboxInput, { placeholder: scheduleMessages.timezoneSearchPlaceholder }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: scheduleMessages.timezoneEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
219
+ const tz = TIMEZONE_OPTIONS.find((t) => t.id === id);
220
+ return (_jsxs(ComboboxItem, { value: id, children: [_jsx("span", { className: "font-mono text-xs", children: id }), tz ? (_jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: tz.label })) : null] }, id));
221
+ } }) })] })] }), form.formState.errors.timezone && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.timezone.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.maxCapacityLabel }), _jsx(Input, { ...form.register("maxCapacity"), type: "number", min: "0", step: "1" }), form.formState.errors.maxCapacity && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.maxCapacity.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: scheduleMessages.cutoffLabel }), _jsx(Input, { ...form.register("cutoffMinutes"), type: "number", min: "0", step: "1", placeholder: "0" })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "schedule-active", checked: active, onCheckedChange: (c) => form.setValue("active", c) }), _jsx(Label, { htmlFor: "schedule-active", className: "font-normal cursor-pointer", children: scheduleMessages.activeDescription })] })] }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancel, children: productMessages.cancel })) : null, _jsxs(Button, { type: "submit", size: "sm", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? productMessages.saveChanges : scheduleMessages.create] })] })] }));
222
+ }
@@ -0,0 +1,12 @@
1
+ import { type DayServiceData } from "./product-service-form.js";
2
+ export type { DayServiceData };
3
+ type ServiceDialogProps = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ productId: string;
7
+ dayId: string;
8
+ service?: DayServiceData;
9
+ onSuccess: () => void;
10
+ };
11
+ export declare function ServiceDialog({ open, onOpenChange, productId, dayId, service, onSuccess, }: ServiceDialogProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=product-service-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-service-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-service-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,2BAA2B,CAAA;AAE5E,YAAY,EAAE,cAAc,EAAE,CAAA;AAE9B,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,KAAK,EACL,OAAO,EACP,SAAS,GACV,EAAE,kBAAkB,2CAyBpB"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle } from "@voyantjs/ui/components";
3
+ import { useProductDetailMessages } from "./host.js";
4
+ import { ServiceForm } from "./product-service-form.js";
5
+ export function ServiceDialog({ open, onOpenChange, productId, dayId, service, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const serviceMessages = messages.products.operations.services;
8
+ const isEditing = !!service;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? serviceMessages.editTitle : serviceMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(ServiceForm, { productId: productId, dayId: dayId, service: service, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,22 @@
1
+ export type DayServiceData = {
2
+ id: string;
3
+ serviceType: "accommodation" | "transfer" | "experience" | "guide" | "meal" | "other";
4
+ name: string;
5
+ description: string | null;
6
+ countryCode: string | null;
7
+ supplierServiceId: string | null;
8
+ costCurrency: string;
9
+ costAmountCents: number;
10
+ quantity: number;
11
+ sortOrder: number | null;
12
+ notes: string | null;
13
+ };
14
+ export interface ServiceFormProps {
15
+ productId: string;
16
+ dayId: string;
17
+ service?: DayServiceData;
18
+ onSuccess: () => void;
19
+ onCancel?: () => void;
20
+ }
21
+ export declare function ServiceForm({ productId, dayId, service, onSuccess, onCancel }: ServiceFormProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=product-service-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-service-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-service-form.tsx"],"names":[],"mappings":"AAgDA,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,eAAe,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;IACrF,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAUD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,cAAc,CAAA;IACxB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAkDD,wBAAgB,WAAW,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,gBAAgB,2CAwP/F"}
@@ -0,0 +1,154 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/ui/components";
4
+ import { CurrencyCombobox } from "@voyantjs/ui/components/currency-combobox";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect } from "react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod/v4";
9
+ import { useProductDetailApi, useProductDetailMessages } from "./host.js";
10
+ import { zodResolver } from "./zod-resolver.js";
11
+ const buildServiceFormSchema = (messages) => z.object({
12
+ serviceType: z.enum(["accommodation", "transfer", "experience", "guide", "meal", "other"]),
13
+ name: z.string().min(1, messages.validationNameRequired),
14
+ description: z.string().optional().nullable(),
15
+ countryCode: z
16
+ .string()
17
+ .trim()
18
+ .max(2, messages.validationCountryCode)
19
+ .optional()
20
+ .or(z.literal(""))
21
+ .nullable(),
22
+ supplierServiceId: z.string().optional().nullable(),
23
+ costCurrency: z.string().min(3).max(3, messages.validationIsoCurrency),
24
+ costAmount: z.coerce.number().min(0, messages.validationCostNonNegative),
25
+ quantity: z.coerce.number().int().positive().default(1),
26
+ sortOrder: z.coerce.number().int().optional().or(z.literal("")).nullable(),
27
+ notes: z.string().optional().nullable(),
28
+ });
29
+ function initialValues(service) {
30
+ if (service) {
31
+ return {
32
+ serviceType: service.serviceType,
33
+ name: service.name,
34
+ description: service.description ?? "",
35
+ countryCode: service.countryCode ?? "",
36
+ supplierServiceId: service.supplierServiceId ?? "",
37
+ costCurrency: service.costCurrency,
38
+ costAmount: service.costAmountCents / 100,
39
+ quantity: service.quantity,
40
+ sortOrder: service.sortOrder ?? "",
41
+ notes: service.notes ?? "",
42
+ };
43
+ }
44
+ return {
45
+ serviceType: "accommodation",
46
+ name: "",
47
+ description: "",
48
+ countryCode: "",
49
+ supplierServiceId: "",
50
+ costCurrency: "EUR",
51
+ costAmount: 0,
52
+ quantity: 1,
53
+ sortOrder: "",
54
+ notes: "",
55
+ };
56
+ }
57
+ function getServiceTypeLabel(type, messages) {
58
+ switch (type) {
59
+ case "accommodation":
60
+ return messages.serviceTypeAccommodation;
61
+ case "transfer":
62
+ return messages.serviceTypeTransfer;
63
+ case "experience":
64
+ return messages.serviceTypeExperience;
65
+ case "guide":
66
+ return messages.serviceTypeGuide;
67
+ case "meal":
68
+ return messages.serviceTypeMeal;
69
+ case "other":
70
+ return messages.serviceTypeOther;
71
+ default:
72
+ return type;
73
+ }
74
+ }
75
+ export function ServiceForm({ productId, dayId, service, onSuccess, onCancel }) {
76
+ const messages = useProductDetailMessages();
77
+ const api = useProductDetailApi();
78
+ const productMessages = messages.products.core;
79
+ const serviceMessages = messages.products.operations.services;
80
+ const isEditing = !!service;
81
+ const serviceFormSchema = buildServiceFormSchema(serviceMessages);
82
+ const serviceTypes = [
83
+ { value: "accommodation", label: serviceMessages.serviceTypeAccommodation },
84
+ { value: "transfer", label: serviceMessages.serviceTypeTransfer },
85
+ { value: "experience", label: serviceMessages.serviceTypeExperience },
86
+ { value: "guide", label: serviceMessages.serviceTypeGuide },
87
+ { value: "meal", label: serviceMessages.serviceTypeMeal },
88
+ { value: "other", label: serviceMessages.serviceTypeOther },
89
+ ];
90
+ const { data: suppliersData } = useQuery({
91
+ queryKey: ["suppliers-for-picker"],
92
+ queryFn: async () => {
93
+ const res = await api.get("/v1/suppliers?limit=100");
94
+ const options = [];
95
+ for (const supplier of res.data) {
96
+ const servicesRes = await api.get(`/v1/suppliers/${supplier.id}/services`);
97
+ for (const svc of servicesRes.data) {
98
+ options.push({
99
+ id: svc.id,
100
+ supplierId: supplier.id,
101
+ supplierName: supplier.name,
102
+ serviceType: svc.serviceType,
103
+ name: svc.name,
104
+ });
105
+ }
106
+ }
107
+ return options;
108
+ },
109
+ });
110
+ const form = useForm({
111
+ resolver: zodResolver(serviceFormSchema),
112
+ defaultValues: initialValues(service),
113
+ });
114
+ useEffect(() => {
115
+ form.reset(initialValues(service));
116
+ }, [service, form]);
117
+ const handleSupplierServiceSelect = (supplierServiceId) => {
118
+ const nextSupplierServiceId = supplierServiceId ?? "";
119
+ form.setValue("supplierServiceId", nextSupplierServiceId);
120
+ const option = suppliersData?.find((o) => o.id === nextSupplierServiceId);
121
+ if (option) {
122
+ form.setValue("name", option.name);
123
+ form.setValue("serviceType", option.serviceType);
124
+ }
125
+ };
126
+ const onSubmit = async (values) => {
127
+ const payload = {
128
+ serviceType: values.serviceType,
129
+ name: values.name,
130
+ description: values.description || null,
131
+ countryCode: values.countryCode?.trim().toUpperCase() || null,
132
+ supplierServiceId: values.supplierServiceId || null,
133
+ costCurrency: values.costCurrency,
134
+ costAmountCents: Math.round(values.costAmount * 100),
135
+ quantity: values.quantity,
136
+ sortOrder: values.sortOrder && typeof values.sortOrder === "number" ? values.sortOrder : null,
137
+ notes: values.notes || null,
138
+ };
139
+ if (isEditing) {
140
+ await api.patch(`/v1/products/${productId}/days/${dayId}/services/${service.id}`, payload);
141
+ }
142
+ else {
143
+ await api.post(`/v1/products/${productId}/days/${dayId}/services`, payload);
144
+ }
145
+ onSuccess();
146
+ };
147
+ return (_jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col gap-4 overflow-hidden", children: [_jsxs("div", { className: "grid gap-4", children: [suppliersData && suppliersData.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.supplierServiceLabel }), _jsxs(Select, { value: form.watch("supplierServiceId") ?? "", onValueChange: handleSupplierServiceSelect, items: suppliersData.map((opt) => ({
148
+ value: opt.id,
149
+ label: `${opt.supplierName} — ${opt.name} (${getServiceTypeLabel(opt.serviceType, serviceMessages)})`,
150
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: serviceMessages.supplierServicePlaceholder }) }), _jsx(SelectContent, { children: suppliersData.map((opt) => (_jsxs(SelectItem, { value: opt.id, children: [opt.supplierName, " \u2014 ", opt.name, " (", getServiceTypeLabel(opt.serviceType, serviceMessages), ")"] }, opt.id))) })] })] })), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.serviceTypeLabel }), _jsxs(Select, { value: form.watch("serviceType"), onValueChange: (v) => form.setValue("serviceType", v), items: serviceTypes, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: serviceTypes.map((t) => (_jsx(SelectItem, { value: t.value, children: t.label }, t.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: serviceMessages.namePlaceholder }), form.formState.errors.name && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message }))] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.descriptionLabel }), _jsx(Textarea, { ...form.register("description"), placeholder: serviceMessages.descriptionPlaceholder })] }), _jsx("div", { className: "grid grid-cols-2 gap-4", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.countryCodeLabel }), _jsx(Input, { ...form.register("countryCode"), placeholder: serviceMessages.countryCodePlaceholder, maxLength: 2, className: "uppercase" }), form.formState.errors.countryCode && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.countryCode.message }))] }) }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.costCurrencyLabel }), _jsx(CurrencyCombobox, { value: form.watch("costCurrency") || null, onChange: (next) => form.setValue("costCurrency", next ?? "", {
151
+ shouldValidate: true,
152
+ shouldDirty: true,
153
+ }), placeholder: serviceMessages.costCurrencyPlaceholder }), form.formState.errors.costCurrency && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.costCurrency.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.costAmountLabel }), _jsx(Input, { ...form.register("costAmount"), type: "number", step: "0.01", min: "0", placeholder: serviceMessages.costAmountPlaceholder }), form.formState.errors.costAmount && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.costAmount.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.quantityLabel }), _jsx(Input, { ...form.register("quantity"), type: "number", min: "1", placeholder: serviceMessages.quantityPlaceholder })] })] }), _jsx("div", { className: "grid grid-cols-2 gap-4", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.sortOrderLabel }), _jsx(Input, { ...form.register("sortOrder"), type: "number", placeholder: serviceMessages.sortOrderPlaceholder })] }) }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: serviceMessages.notesLabel }), _jsx(Textarea, { ...form.register("notes"), placeholder: serviceMessages.notesPlaceholder })] })] }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [onCancel ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onCancel, children: productMessages.cancel })) : null, _jsxs(Button, { type: "submit", size: "sm", disabled: form.formState.isSubmitting, children: [form.formState.isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? productMessages.saveChanges : serviceMessages.create] })] })] }));
154
+ }