@voyantjs/products-ui 0.101.0 → 0.101.2

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 (137) hide show
  1. package/dist/components/option-unit-form.d.ts.map +1 -1
  2. package/dist/components/option-unit-form.js +6 -4
  3. package/dist/components/product-detail/date-picker.d.ts +44 -0
  4. package/dist/components/product-detail/date-picker.d.ts.map +1 -0
  5. package/dist/components/product-detail/date-picker.js +125 -0
  6. package/dist/components/product-detail/host.d.ts +53 -0
  7. package/dist/components/product-detail/host.d.ts.map +1 -0
  8. package/dist/components/product-detail/host.js +24 -0
  9. package/dist/components/product-detail/index.d.ts +6 -0
  10. package/dist/components/product-detail/index.d.ts.map +1 -0
  11. package/dist/components/product-detail/index.js +5 -0
  12. package/dist/components/product-detail/product-activity-section.d.ts +4 -0
  13. package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
  14. package/dist/components/product-detail/product-activity-section.js +37 -0
  15. package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
  16. package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
  17. package/dist/components/product-detail/product-day-sheet.js +75 -0
  18. package/dist/components/product-detail/product-day-translation.d.ts +41 -0
  19. package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
  20. package/dist/components/product-detail/product-day-translation.js +111 -0
  21. package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
  22. package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
  23. package/dist/components/product-detail/product-departure-dialog.js +10 -0
  24. package/dist/components/product-detail/product-departure-form.d.ts +25 -0
  25. package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
  26. package/dist/components/product-detail/product-departure-form.js +217 -0
  27. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
  28. package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
  29. package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
  30. package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
  31. package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
  32. package/dist/components/product-detail/product-detail-day-row.js +43 -0
  33. package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
  34. package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
  35. package/dist/components/product-detail/product-detail-dialog.js +10 -0
  36. package/dist/components/product-detail/product-detail-form.d.ts +19 -0
  37. package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
  38. package/dist/components/product-detail/product-detail-form.js +177 -0
  39. package/dist/components/product-detail/product-detail-header.d.ts +12 -0
  40. package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
  41. package/dist/components/product-detail/product-detail-header.js +19 -0
  42. package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
  43. package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
  44. package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
  45. package/dist/components/product-detail/product-detail-page.d.ts +4 -0
  46. package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
  47. package/dist/components/product-detail/product-detail-page.js +97 -0
  48. package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
  49. package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
  50. package/dist/components/product-detail/product-detail-sections.js +143 -0
  51. package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
  52. package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
  53. package/dist/components/product-detail/product-detail-shared.js +157 -0
  54. package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
  55. package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
  56. package/dist/components/product-detail/product-detail-skeleton.js +53 -0
  57. package/dist/components/product-detail/product-extras-section.d.ts +4 -0
  58. package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
  59. package/dist/components/product-detail/product-extras-section.js +141 -0
  60. package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
  61. package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
  62. package/dist/components/product-detail/product-itinerary-form.js +38 -0
  63. package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
  64. package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
  65. package/dist/components/product-detail/product-market-rules-section.js +81 -0
  66. package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
  67. package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
  68. package/dist/components/product-detail/product-media-gallery.js +114 -0
  69. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
  70. package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
  71. package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
  72. package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
  73. package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
  74. package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
  75. package/dist/components/product-detail/product-options-pricing.d.ts +6 -0
  76. package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
  77. package/dist/components/product-detail/product-options-pricing.js +363 -0
  78. package/dist/components/product-detail/product-options-shared.d.ts +609 -0
  79. package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
  80. package/dist/components/product-detail/product-options-shared.js +34 -0
  81. package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
  82. package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
  83. package/dist/components/product-detail/product-payment-policy-section.js +58 -0
  84. package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
  85. package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
  86. package/dist/components/product-detail/product-schedule-dialog.js +10 -0
  87. package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
  88. package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
  89. package/dist/components/product-detail/product-schedule-form.js +222 -0
  90. package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
  91. package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
  92. package/dist/components/product-detail/product-service-dialog.js +10 -0
  93. package/dist/components/product-detail/product-service-form.d.ts +22 -0
  94. package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
  95. package/dist/components/product-detail/product-service-form.js +154 -0
  96. package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
  97. package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
  98. package/dist/components/product-detail/product-translation-popover.js +217 -0
  99. package/dist/components/product-detail/product-unit-dialog.d.ts +12 -0
  100. package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
  101. package/dist/components/product-detail/product-unit-dialog.js +10 -0
  102. package/dist/components/product-detail/product-unit-form.d.ts +26 -0
  103. package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
  104. package/dist/components/product-detail/product-unit-form.js +109 -0
  105. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +16 -0
  106. package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
  107. package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
  108. package/dist/components/product-detail/product-unit-price-rule-form.d.ts +28 -0
  109. package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
  110. package/dist/components/product-detail/product-unit-price-rule-form.js +126 -0
  111. package/dist/components/product-detail/timezone-options.d.ts +9 -0
  112. package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
  113. package/dist/components/product-detail/timezone-options.js +28 -0
  114. package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
  115. package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
  116. package/dist/components/product-detail/use-product-detail-data.js +143 -0
  117. package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
  118. package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
  119. package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
  120. package/dist/components/product-detail/zod-resolver.d.ts +4 -0
  121. package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
  122. package/dist/components/product-detail/zod-resolver.js +39 -0
  123. package/dist/components/product-option-form.js +1 -1
  124. package/dist/components/product-options-section.d.ts +3 -1
  125. package/dist/components/product-options-section.d.ts.map +1 -1
  126. package/dist/components/product-options-section.js +102 -5
  127. package/dist/i18n/en.d.ts +21 -0
  128. package/dist/i18n/en.d.ts.map +1 -1
  129. package/dist/i18n/en.js +54 -33
  130. package/dist/i18n/messages.d.ts +21 -0
  131. package/dist/i18n/messages.d.ts.map +1 -1
  132. package/dist/i18n/provider.d.ts +42 -0
  133. package/dist/i18n/provider.d.ts.map +1 -1
  134. package/dist/i18n/ro.d.ts +21 -0
  135. package/dist/i18n/ro.d.ts.map +1 -1
  136. package/dist/i18n/ro.js +53 -32
  137. package/package.json +38 -19
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-departure-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-departure-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAE/E,YAAY,EAAE,aAAa,EAAE,CAAA;AAE7B,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,oBAAoB,2CAwBtB"}
@@ -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 { DepartureForm } from "./product-departure-form.js";
5
+ export function DepartureDialog({ open, onOpenChange, productId, slot, onSuccess, }) {
6
+ const messages = useProductDetailMessages();
7
+ const departureMessages = messages.products.operations.departures;
8
+ const isEditing = !!slot;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? departureMessages.editTitle : departureMessages.newTitle }) }), _jsx(SheetBody, { children: _jsx(DepartureForm, { productId: productId, slot: slot, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,25 @@
1
+ export type DepartureSlot = {
2
+ id: string;
3
+ productId: string;
4
+ optionId: string | null;
5
+ itineraryId: string | null;
6
+ dateLocal: string;
7
+ startsAt: string;
8
+ endsAt: string | null;
9
+ timezone: string;
10
+ status: "open" | "closed" | "sold_out" | "cancelled";
11
+ unlimited: boolean;
12
+ initialPax: number | null;
13
+ remainingPax: number | null;
14
+ nights: number | null;
15
+ days: number | null;
16
+ notes: string | null;
17
+ };
18
+ export interface DepartureFormProps {
19
+ productId: string;
20
+ slot?: DepartureSlot;
21
+ onSuccess: () => void;
22
+ onCancel?: () => void;
23
+ }
24
+ export declare function DepartureForm({ productId, slot, onSuccess, onCancel }: DepartureFormProps): import("react/jsx-runtime").JSX.Element;
25
+ //# sourceMappingURL=product-departure-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-departure-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-departure-form.tsx"],"names":[],"mappings":"AA8EA,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,CAAA;IACpD,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAuDD,wBAAgB,aAAa,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,kBAAkB,2CAwWzF"}
@@ -0,0 +1,217 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { formatMessage } from "@voyantjs/i18n";
3
+ import { useProductItineraries } from "@voyantjs/products-react";
4
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea, } from "@voyantjs/ui/components";
5
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
+ import { Loader2 } from "lucide-react";
7
+ import { useEffect } from "react";
8
+ import { useForm } from "react-hook-form";
9
+ import { z } from "zod/v4";
10
+ import { DatePicker } from "./date-picker.js";
11
+ import { useProductDetailApi, useProductDetailMessages } from "./host.js";
12
+ import { getTimezoneLabel, TIMEZONE_IDS, TIMEZONE_OPTIONS } from "./timezone-options.js";
13
+ import { zodResolver } from "./zod-resolver.js";
14
+ const buildDepartureFormSchema = (messages) => z
15
+ .object({
16
+ startDate: z.string().min(1, messages.validationStartDateRequired),
17
+ startTime: z.string().min(1, messages.validationStartTimeRequired),
18
+ endDate: z.string().optional().nullable(),
19
+ endTime: z.string().optional().nullable(),
20
+ itineraryId: z.string().optional().nullable(),
21
+ timezone: z.string().min(1, messages.validationTimezoneRequired),
22
+ status: z.enum(["open", "closed", "sold_out", "cancelled"]),
23
+ unlimited: z.boolean(),
24
+ initialPax: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
25
+ nights: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
26
+ days: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
27
+ notes: z.string().optional().nullable(),
28
+ })
29
+ .refine((v) => {
30
+ if (!v.endDate || typeof v.endDate !== "string" || v.endDate.length === 0)
31
+ return true;
32
+ return v.endDate >= v.startDate;
33
+ }, { message: messages.validationEndDateOrder, path: ["endDate"] })
34
+ .refine((v) => {
35
+ const endDate = v.endDate && typeof v.endDate === "string" && v.endDate.length > 0
36
+ ? v.endDate
37
+ : v.startDate;
38
+ const endTime = v.endTime && typeof v.endTime === "string" && v.endTime.length > 0 ? v.endTime : null;
39
+ if (!endTime)
40
+ return true;
41
+ if (endDate > v.startDate)
42
+ return true;
43
+ return endTime >= v.startTime;
44
+ }, { message: messages.validationEndTimeOrder, path: ["endTime"] });
45
+ function combineLocalToIso(date, time) {
46
+ const iso = new Date(`${date}T${time}:00Z`).toISOString();
47
+ return iso;
48
+ }
49
+ function isoToLocalDate(iso) {
50
+ const d = new Date(iso);
51
+ const y = d.getUTCFullYear();
52
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
53
+ const day = String(d.getUTCDate()).padStart(2, "0");
54
+ return `${y}-${m}-${day}`;
55
+ }
56
+ function isoToLocalTime(iso) {
57
+ const d = new Date(iso);
58
+ const hh = String(d.getUTCHours()).padStart(2, "0");
59
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
60
+ return `${hh}:${mm}`;
61
+ }
62
+ function initialValues(slot, defaultTz) {
63
+ if (slot) {
64
+ return {
65
+ startDate: slot.dateLocal,
66
+ startTime: isoToLocalTime(slot.startsAt),
67
+ endDate: slot.endsAt ? isoToLocalDate(slot.endsAt) : "",
68
+ endTime: slot.endsAt ? isoToLocalTime(slot.endsAt) : "",
69
+ itineraryId: slot.itineraryId ?? "",
70
+ timezone: slot.timezone,
71
+ status: slot.status,
72
+ unlimited: slot.unlimited,
73
+ initialPax: slot.initialPax != null ? slot.initialPax : "",
74
+ nights: slot.nights != null ? slot.nights : "",
75
+ days: slot.days != null ? slot.days : "",
76
+ notes: slot.notes ?? "",
77
+ };
78
+ }
79
+ return {
80
+ startDate: "",
81
+ startTime: "09:00",
82
+ endDate: "",
83
+ endTime: "",
84
+ itineraryId: "",
85
+ timezone: defaultTz,
86
+ status: "open",
87
+ unlimited: false,
88
+ initialPax: "",
89
+ nights: "",
90
+ days: "",
91
+ notes: "",
92
+ };
93
+ }
94
+ export function DepartureForm({ productId, slot, onSuccess, onCancel }) {
95
+ const messages = useProductDetailMessages();
96
+ const api = useProductDetailApi();
97
+ const productMessages = messages.products.core;
98
+ const departureMessages = messages.products.operations.departures;
99
+ const itineraryMessages = messages.products.operations.itineraries;
100
+ const isEditing = !!slot;
101
+ const departureFormSchema = buildDepartureFormSchema(departureMessages);
102
+ const slotStatuses = [
103
+ { value: "open", label: productMessages.departureStatusOpen },
104
+ { value: "closed", label: productMessages.departureStatusClosed },
105
+ { value: "sold_out", label: productMessages.departureStatusSoldOut },
106
+ { value: "cancelled", label: productMessages.departureStatusCancelled },
107
+ ];
108
+ const defaultTz = typeof Intl !== "undefined"
109
+ ? (Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC")
110
+ : "UTC";
111
+ const form = useForm({
112
+ resolver: zodResolver(departureFormSchema),
113
+ defaultValues: initialValues(slot, defaultTz),
114
+ });
115
+ const unlimited = form.watch("unlimited");
116
+ const startDate = form.watch("startDate");
117
+ const endDate = form.watch("endDate");
118
+ const timezone = form.watch("timezone");
119
+ const { data: itineraryData } = useProductItineraries(productId);
120
+ const itineraries = itineraryData?.data ?? [];
121
+ const defaultItinerary = itineraries.find((itinerary) => itinerary.isDefault) ?? itineraries[0];
122
+ const nights = (() => {
123
+ if (!startDate || !endDate || typeof endDate !== "string" || endDate.length === 0)
124
+ return 0;
125
+ const start = new Date(`${startDate}T00:00:00Z`).getTime();
126
+ const end = new Date(`${endDate}T00:00:00Z`).getTime();
127
+ const diffDays = Math.round((end - start) / 86_400_000);
128
+ return diffDays > 0 ? diffDays : 0;
129
+ })();
130
+ useEffect(() => {
131
+ form.reset(initialValues(slot, defaultTz));
132
+ }, [slot, form, defaultTz]);
133
+ const onSubmit = async (values) => {
134
+ const startsAt = combineLocalToIso(values.startDate, values.startTime);
135
+ const effectiveEndDate = values.endDate && typeof values.endDate === "string" && values.endDate.length > 0
136
+ ? values.endDate
137
+ : values.startDate;
138
+ const hasEndTime = values.endTime && typeof values.endTime === "string" && values.endTime.length > 0;
139
+ const hasExplicitEndDate = values.endDate && typeof values.endDate === "string" && values.endDate.length > 0;
140
+ const endsAt = hasEndTime || hasExplicitEndDate
141
+ ? combineLocalToIso(effectiveEndDate, hasEndTime ? values.endTime : "18:00")
142
+ : null;
143
+ const initialPax = !values.unlimited && typeof values.initialPax === "number" ? values.initialPax : null;
144
+ // Treat blank / zero overrides as `null` so the slot card doesn't show
145
+ // "0 nights / 0 days" after the operator clears the override (#1087 side
146
+ // bug). The schema accepts `null` for both; sending `0` was the bug.
147
+ const nightsOverride = typeof values.nights === "number" && values.nights > 0 ? values.nights : null;
148
+ const daysOverride = typeof values.days === "number" && values.days > 0 ? values.days : null;
149
+ // `remainingPax` is intentionally omitted on edit — the slot service is
150
+ // the source of truth for that field. Concurrent flows (holds, bookings,
151
+ // refunds) mutate it atomically while a form is open, so any snapshot
152
+ // we computed in JS would be stale by save time (#1087, Codex review on
153
+ // #1088). The backend's `updateSlot` recomputes remaining_pax in the
154
+ // same UPDATE statement when initialPax / unlimited change.
155
+ const baseFields = {
156
+ productId,
157
+ itineraryId: values.itineraryId ? values.itineraryId : null,
158
+ dateLocal: values.startDate,
159
+ startsAt,
160
+ endsAt,
161
+ timezone: values.timezone,
162
+ status: values.status,
163
+ unlimited: values.unlimited,
164
+ initialPax,
165
+ nights: nightsOverride,
166
+ days: daysOverride,
167
+ notes: values.notes || null,
168
+ };
169
+ if (isEditing) {
170
+ await api.patch(`/v1/availability/slots/${slot.id}`, baseFields);
171
+ }
172
+ else {
173
+ // New slots haven't been booked against yet, so seeding remainingPax
174
+ // from initialPax is correct on create.
175
+ await api.post("/v1/availability/slots", { ...baseFields, remainingPax: initialPax });
176
+ }
177
+ onSuccess();
178
+ };
179
+ return (_jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col gap-6 overflow-hidden", children: [_jsxs("fieldset", { className: "grid gap-3", children: [_jsx("legend", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: departureMessages.scheduleLegend }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.startDateLabel }), _jsx(DatePicker, { value: startDate || null, onChange: (v) => form.setValue("startDate", v ?? "", {
180
+ shouldValidate: true,
181
+ shouldDirty: true,
182
+ }), placeholder: departureMessages.datePlaceholder }), form.formState.errors.startDate && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.startDate.message }))] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.startTimeLabel }), _jsx(Input, { ...form.register("startTime"), type: "time" }), form.formState.errors.startTime && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.startTime.message }))] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { children: [departureMessages.endDateLabel, " ", _jsx("span", { className: "text-muted-foreground font-normal", children: departureMessages.endDateOptional })] }), _jsx(DatePicker, { value: typeof endDate === "string" && endDate.length > 0 ? endDate : null, onChange: (v) => form.setValue("endDate", v ?? "", {
183
+ shouldValidate: true,
184
+ shouldDirty: true,
185
+ }), placeholder: departureMessages.datePlaceholder, clearable: true, dateDisabled: startDate ? { before: new Date(`${startDate}T00:00:00`) } : undefined }), form.formState.errors.endDate && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.endDate.message }))] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { children: [departureMessages.endTimeLabel, " ", _jsx("span", { className: "text-muted-foreground font-normal", children: departureMessages.endTimeOptional })] }), _jsx(Input, { ...form.register("endTime"), type: "time" }), form.formState.errors.endTime && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.endTime.message }))] })] }), nights > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-xs text-muted-foreground", children: formatMessage(departureMessages.multiDayHint, {
186
+ nights,
187
+ nightSuffix: nights === 1 ? "" : "s",
188
+ days: nights + 1,
189
+ }) }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.nightsOverrideLabel }), _jsx(Input, { ...form.register("nights"), type: "number", min: "0", step: "1", placeholder: String(nights) })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.daysOverrideLabel }), _jsx(Input, { ...form.register("days"), type: "number", min: "0", step: "1", placeholder: String(nights + 1) })] })] })] })), itineraries.length > 1 ? (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: itineraryMessages.formLabel }), _jsxs(Select, { items: [
190
+ {
191
+ label: defaultItinerary
192
+ ? formatMessage(itineraryMessages.defaultWithName, {
193
+ name: defaultItinerary.name,
194
+ })
195
+ : itineraryMessages.defaultBadge,
196
+ value: "",
197
+ },
198
+ ...itineraries.map((itinerary) => ({
199
+ label: itinerary.name,
200
+ value: itinerary.id,
201
+ })),
202
+ ], value: form.watch("itineraryId") ?? "", onValueChange: (value) => form.setValue("itineraryId", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "", children: defaultItinerary
203
+ ? formatMessage(itineraryMessages.defaultWithName, {
204
+ name: defaultItinerary.name,
205
+ })
206
+ : itineraryMessages.defaultBadge }), itineraries.map((itinerary) => (_jsx(SelectItem, { value: itinerary.id, children: itinerary.name }, itinerary.id)))] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: itineraryMessages.overrideHint })] })) : null, _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.timezoneLabel }), _jsxs(Combobox, { items: TIMEZONE_IDS, value: timezone || null, autoHighlight: true, itemToStringValue: (id) => getTimezoneLabel(id), onValueChange: (next) => {
207
+ if (typeof next === "string") {
208
+ form.setValue("timezone", next, {
209
+ shouldValidate: true,
210
+ shouldDirty: true,
211
+ });
212
+ }
213
+ }, children: [_jsx(ComboboxInput, { placeholder: departureMessages.timezoneSearchPlaceholder, className: "w-full" }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: departureMessages.timezoneEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
214
+ const tz = TIMEZONE_OPTIONS.find((t) => t.id === id);
215
+ 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));
216
+ } }) })] })] }), form.formState.errors.timezone && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.timezone.message }))] })] }), _jsxs("fieldset", { className: "grid gap-3", children: [_jsx("legend", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: departureMessages.availabilityLegend }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.statusLabel }), _jsxs(Select, { value: form.watch("status"), onValueChange: (v) => form.setValue("status", v), items: slotStatuses, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: slotStatuses.map((s) => (_jsx(SelectItem, { value: s.value, children: s.label }, s.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.capacityLabel }), _jsx(Input, { ...form.register("initialPax"), type: "number", min: "0", step: "1", placeholder: "0", disabled: unlimited })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "unlimited", checked: unlimited, onCheckedChange: (c) => form.setValue("unlimited", c) }), _jsx(Label, { htmlFor: "unlimited", className: "font-normal cursor-pointer", children: departureMessages.unlimitedLabel })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: departureMessages.notesLabel }), _jsx(Textarea, { ...form.register("notes"), placeholder: departureMessages.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 : departureMessages.create] })] })] }));
217
+ }
@@ -0,0 +1,8 @@
1
+ export declare function DeparturePricingOverrideDialog({ open, onOpenChange, departureId, optionId, onSuccess, }: {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ departureId: string | null;
5
+ optionId: string | null;
6
+ onSuccess: () => void;
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=product-departure-pricing-override-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-departure-pricing-override-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-departure-pricing-override-dialog.tsx"],"names":[],"mappings":"AAmDA,wBAAgB,8BAA8B,CAAC,EAC7C,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,SAAS,GACV,EAAE;IACD,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,2CAwNA"}
@@ -0,0 +1,125 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useDeparturePriceOverrideMutation } from "@voyantjs/pricing-react";
5
+ import { useVoyantProductsContext } from "@voyantjs/products-react";
6
+ import { Button, Input, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, Switch, } from "@voyantjs/ui/components";
7
+ import { Trash2 } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
+ import { useProductDetailMessages } from "./host.js";
10
+ import { getDeparturePriceOverridesQueryOptions, getOptionUnitsQueryOptions, getPriceCatalogsQueryOptions, } from "./product-options-shared.js";
11
+ function centsToInput(value) {
12
+ if (value === null || value === undefined)
13
+ return "";
14
+ return (value / 100).toFixed(2);
15
+ }
16
+ function inputToCents(value) {
17
+ const trimmed = value.trim();
18
+ if (trimmed === "")
19
+ return null;
20
+ const parsed = Number.parseFloat(trimmed);
21
+ if (!Number.isFinite(parsed) || parsed < 0)
22
+ return null;
23
+ return Math.round(parsed * 100);
24
+ }
25
+ export function DeparturePricingOverrideDialog({ open, onOpenChange, departureId, optionId, onSuccess, }) {
26
+ const messages = useProductDetailMessages();
27
+ const productMessages = messages.products.core;
28
+ const client = useVoyantProductsContext();
29
+ const enabled = open && !!departureId && !!optionId;
30
+ const { data: unitsData } = useQuery({
31
+ ...getOptionUnitsQueryOptions(client, optionId ?? ""),
32
+ enabled: enabled && !!optionId,
33
+ });
34
+ const { data: overridesData, refetch: refetchOverrides } = useQuery({
35
+ ...getDeparturePriceOverridesQueryOptions(client, departureId ?? ""),
36
+ enabled: enabled && !!departureId,
37
+ });
38
+ const { data: catalogsData } = useQuery({
39
+ ...getPriceCatalogsQueryOptions(client),
40
+ enabled,
41
+ });
42
+ const { create, update, remove } = useDeparturePriceOverrideMutation();
43
+ const catalog = catalogsData?.data.find((c) => c.catalogType === "public" && c.isDefault) ??
44
+ catalogsData?.data.find((c) => c.catalogType === "public") ??
45
+ null;
46
+ const [rows, setRows] = useState([]);
47
+ const [saving, setSaving] = useState(false);
48
+ useEffect(() => {
49
+ if (!open)
50
+ return;
51
+ const units = (unitsData?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder);
52
+ const overrides = overridesData?.data ?? [];
53
+ const next = units.map((unit) => {
54
+ const existing = overrides.find((o) => o.optionUnitId === unit.id) ?? null;
55
+ return {
56
+ unitId: unit.id,
57
+ unitName: unit.name,
58
+ sortOrder: unit.sortOrder,
59
+ overrideId: existing?.id ?? null,
60
+ sellInput: existing ? centsToInput(existing.sellAmountCents) : "",
61
+ costInput: existing ? centsToInput(existing.costAmountCents) : "",
62
+ active: existing ? existing.active : true,
63
+ dirty: false,
64
+ };
65
+ });
66
+ setRows(next);
67
+ }, [open, unitsData, overridesData]);
68
+ if (!enabled) {
69
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsx(SheetContent, { side: "right", children: _jsx(SheetHeader, { children: _jsx(SheetTitle, { children: productMessages.departureOverrideTitle }) }) }) }));
70
+ }
71
+ const updateRow = (unitId, patch) => {
72
+ setRows((prev) => prev.map((r) => (r.unitId === unitId ? { ...r, ...patch, dirty: true } : r)));
73
+ };
74
+ const handleSave = async () => {
75
+ if (!catalog || !departureId || !optionId)
76
+ return;
77
+ setSaving(true);
78
+ try {
79
+ for (const row of rows) {
80
+ const sellCents = inputToCents(row.sellInput);
81
+ const costCents = inputToCents(row.costInput);
82
+ if (row.overrideId && sellCents === null) {
83
+ await remove.mutateAsync(row.overrideId);
84
+ continue;
85
+ }
86
+ if (sellCents === null)
87
+ continue;
88
+ if (row.overrideId) {
89
+ if (!row.dirty)
90
+ continue;
91
+ await update.mutateAsync({
92
+ id: row.overrideId,
93
+ input: {
94
+ sellAmountCents: sellCents,
95
+ costAmountCents: costCents,
96
+ active: row.active,
97
+ },
98
+ });
99
+ }
100
+ else {
101
+ await create.mutateAsync({
102
+ departureId,
103
+ optionId,
104
+ optionUnitId: row.unitId,
105
+ priceCatalogId: catalog.id,
106
+ sellAmountCents: sellCents,
107
+ costAmountCents: costCents,
108
+ active: row.active,
109
+ });
110
+ }
111
+ }
112
+ await refetchOverrides();
113
+ onSuccess();
114
+ }
115
+ finally {
116
+ setSaving(false);
117
+ }
118
+ };
119
+ const handleClearRow = (unitId) => {
120
+ updateRow(unitId, { sellInput: "", costInput: "" });
121
+ };
122
+ const noUnits = rows.length === 0;
123
+ const noCatalog = !catalog;
124
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: productMessages.departureOverrideTitle }) }), _jsxs(SheetBody, { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: productMessages.departureOverrideDescription }), noCatalog ? (_jsx("p", { className: "rounded border border-destructive/40 bg-destructive/10 p-3 text-xs text-destructive", children: productMessages.departureOverrideNoCatalog })) : null, noUnits ? (_jsx("p", { className: "rounded border bg-muted/30 p-3 text-xs text-muted-foreground", children: productMessages.departureOverrideNoUnits })) : (_jsx("div", { className: "overflow-x-auto rounded border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50 text-xs text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "p-2 text-left font-medium", children: productMessages.departureOverrideUnitColumn }), _jsx("th", { className: "p-2 text-left font-medium", children: productMessages.departureOverrideSellColumn }), _jsx("th", { className: "p-2 text-left font-medium", children: productMessages.departureOverrideCostColumn }), _jsx("th", { className: "p-2 text-left font-medium", children: productMessages.departureOverrideActiveColumn }), _jsx("th", { className: "w-10 p-2" })] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-2 font-medium", children: row.unitName }), _jsx("td", { className: "p-2", children: _jsx(Input, { type: "number", step: "0.01", min: "0", inputMode: "decimal", value: row.sellInput, onChange: (e) => updateRow(row.unitId, { sellInput: e.target.value }), className: "h-8 w-28", disabled: noCatalog }) }), _jsx("td", { className: "p-2", children: _jsx(Input, { type: "number", step: "0.01", min: "0", inputMode: "decimal", value: row.costInput, onChange: (e) => updateRow(row.unitId, { costInput: e.target.value }), className: "h-8 w-28", disabled: noCatalog }) }), _jsx("td", { className: "p-2", children: _jsx(Switch, { checked: row.active, onCheckedChange: (active) => updateRow(row.unitId, { active }), disabled: noCatalog }) }), _jsx("td", { className: "p-2", children: row.sellInput ? (_jsx(Button, { variant: "ghost", size: "icon", onClick: () => handleClearRow(row.unitId), "aria-label": productMessages.departureOverrideClear, children: _jsx(Trash2, { className: "h-4 w-4" }) })) : null })] }, row.unitId))) })] }) })), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: handleSave, disabled: noUnits || noCatalog || saving, children: productMessages.departureOverrideSave }) })] })] }) }));
125
+ }
@@ -0,0 +1,14 @@
1
+ import { type DayService, type ProductDay } from "./product-detail-shared.js";
2
+ export interface ProductDetailDayRowProps {
3
+ day: ProductDay;
4
+ productId: string;
5
+ expanded: boolean;
6
+ onToggle: () => void;
7
+ onEdit: () => void;
8
+ onDelete: () => void;
9
+ onAddService: () => void;
10
+ onEditService: (service: DayService) => void;
11
+ onDeleteService: (serviceId: string) => void;
12
+ }
13
+ export declare function ProductDetailDayRow({ day, productId, expanded, onToggle, onEdit, onDelete, onAddService, onEditService, onDeleteService, }: ProductDetailDayRowProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=product-detail-day-row.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-day-row.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-detail-day-row.tsx"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,UAAU,EAGf,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,UAAU,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAA;IAC5C,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7C;AAwBD,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,eAAe,GAChB,EAAE,wBAAwB,2CAgI1B"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { formatMessage } from "@voyantjs/i18n";
4
+ import { Badge, DropdownMenuItem, DropdownMenuSeparator } from "@voyantjs/ui/components";
5
+ import { ChevronDown, ChevronRight, Image as ImageIcon, Pencil, Plus, Trash2 } from "lucide-react";
6
+ import { useProductDetailApi, useProductDetailMessages } from "./host.js";
7
+ import { ActionMenu } from "./product-detail-sections.js";
8
+ import { getProductDayMediaQueryOptions, getProductDayServicesQueryOptions, } from "./product-detail-shared.js";
9
+ function getServiceTypeLabel(serviceType, messages) {
10
+ switch (serviceType) {
11
+ case "accommodation":
12
+ return messages.serviceTypeAccommodation;
13
+ case "transfer":
14
+ return messages.serviceTypeTransfer;
15
+ case "experience":
16
+ return messages.serviceTypeExperience;
17
+ case "guide":
18
+ return messages.serviceTypeGuide;
19
+ case "meal":
20
+ return messages.serviceTypeMeal;
21
+ case "other":
22
+ return messages.serviceTypeOther;
23
+ default:
24
+ return serviceType;
25
+ }
26
+ }
27
+ export function ProductDetailDayRow({ day, productId, expanded, onToggle, onEdit, onDelete, onAddService, onEditService, onDeleteService, }) {
28
+ const api = useProductDetailApi();
29
+ const messages = useProductDetailMessages();
30
+ const dayRowMessages = messages.products.operations.dayRows;
31
+ const serviceMessages = messages.products.operations.services;
32
+ const { data: servicesData } = useQuery({
33
+ ...getProductDayServicesQueryOptions(api, productId, day.id),
34
+ enabled: expanded,
35
+ });
36
+ const { data: dayMediaData } = useQuery(getProductDayMediaQueryOptions(api, productId, day.id));
37
+ const mediaCount = dayMediaData?.data.length ?? 0;
38
+ const cover = dayMediaData?.data.find((m) => m.isCover) ?? dayMediaData?.data[0];
39
+ return (_jsxs("div", { className: "rounded-lg border", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx("button", { type: "button", onClick: onToggle, className: "text-muted-foreground transition-colors hover:text-foreground", children: expanded ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronRight, { className: "h-4 w-4" }) }), cover?.mediaType === "image" ? (_jsx("img", { src: cover.url, alt: cover.altText ?? cover.name, className: "h-10 w-14 flex-shrink-0 rounded object-cover" })) : (_jsx("div", { className: "flex h-10 w-14 flex-shrink-0 items-center justify-center rounded border bg-muted/50 text-muted-foreground", children: _jsx(ImageIcon, { className: "h-4 w-4" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "text-sm font-medium", children: [formatMessage(dayRowMessages.title, { dayNumber: day.dayNumber }), day.title ? `: ${day.title}` : ""] }), day.location ? (_jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: day.location })) : null] }), mediaCount > 0 ? (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: formatMessage(dayRowMessages.photoCount, {
40
+ count: mediaCount,
41
+ suffix: mediaCount === 1 ? "" : "s",
42
+ }) })) : null, _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: onEdit, children: [_jsx(Pencil, { className: "h-4 w-4" }), dayRowMessages.editAction] }), _jsxs(DropdownMenuItem, { onClick: onAddService, children: [_jsx(Plus, { className: "h-4 w-4" }), dayRowMessages.addServiceAction] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: onDelete, children: [_jsx(Trash2, { className: "h-4 w-4" }), dayRowMessages.deleteAction] })] })] }), expanded ? (_jsx("div", { className: "border-t", children: !servicesData?.data || servicesData.data.length === 0 ? (_jsx("p", { className: "py-4 text-center text-xs text-muted-foreground", children: dayRowMessages.emptyServices })) : (_jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b bg-muted/30 text-muted-foreground", children: [_jsx("th", { className: "py-2 pl-4 pr-3 text-left font-medium", children: dayRowMessages.tableName }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: dayRowMessages.tableType }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: dayRowMessages.tableCost }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: dayRowMessages.tableQuantity }), _jsx("th", { className: "w-10 px-3 py-2" })] }) }), _jsx("tbody", { children: servicesData.data.map((service) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "py-2 pl-4 pr-3", children: service.name }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: "text-xs capitalize", children: getServiceTypeLabel(service.serviceType, serviceMessages) }) }), _jsxs("td", { className: "px-3 py-2 font-mono", children: [(service.costAmountCents / 100).toFixed(2), " ", service.costCurrency] }), _jsx("td", { className: "px-3 py-2", children: service.quantity }), _jsx("td", { className: "px-3 py-2", children: _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: () => onEditService(service), children: [_jsx(Pencil, { className: "h-4 w-4" }), dayRowMessages.editAction] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => onDeleteService(service.id), children: [_jsx(Trash2, { className: "h-4 w-4" }), dayRowMessages.deleteAction] })] }) })] }, service.id))) })] })) })) : null] }));
43
+ }
@@ -0,0 +1,10 @@
1
+ import { type ProductData } from "./product-detail-form.js";
2
+ export type { ProductData };
3
+ type ProductDialogProps = {
4
+ open: boolean;
5
+ onOpenChange: (open: boolean) => void;
6
+ product?: ProductData;
7
+ onSuccess: (id?: string) => void;
8
+ };
9
+ export declare function ProductDialog({ open, onOpenChange, product, onSuccess }: ProductDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=product-detail-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-dialog.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-detail-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,0BAA0B,CAAA;AAE9E,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,CAAA;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,kBAAkB,2CAuB3F"}
@@ -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 { ProductDetailForm } from "./product-detail-form.js";
5
+ export function ProductDialog({ open, onOpenChange, product, onSuccess }) {
6
+ const messages = useProductDetailMessages();
7
+ const productMessages = messages.products.core;
8
+ const isEditing = !!product;
9
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? productMessages.detailSheetEditTitle : productMessages.detailSheetNewTitle }) }), _jsx(SheetBody, { children: _jsx(ProductDetailForm, { product: product, onSuccess: onSuccess, onCancel: () => onOpenChange(false) }) })] }) }));
10
+ }
@@ -0,0 +1,19 @@
1
+ export type ProductData = {
2
+ id: string;
3
+ name: string;
4
+ status: "draft" | "active" | "archived";
5
+ description: string | null;
6
+ bookingMode: "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary" | "other";
7
+ productTypeId: string | null;
8
+ taxClassId: string | null;
9
+ sellCurrency: string;
10
+ tags: string[];
11
+ defaultLanguageTag?: string | null;
12
+ };
13
+ export interface ProductDetailFormProps {
14
+ product?: ProductData;
15
+ onSuccess: (id?: string) => void;
16
+ onCancel?: () => void;
17
+ }
18
+ export declare function ProductDetailForm({ product, onSuccess, onCancel }: ProductDetailFormProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=product-detail-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-form.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-detail-form.tsx"],"names":[],"mappings":"AAuCA,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAA;IACvC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,OAAO,CAAA;IACxF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACnC,CAAA;AAgBD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AA6BD,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CAqWzF"}