@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,143 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { describeRRule } from "@voyantjs/availability/rrule";
4
+ import { formatMessage } from "@voyantjs/i18n";
5
+ import { useProductTranslations } from "@voyantjs/products-react";
6
+ import { Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, ToggleGroup, ToggleGroupItem, } from "@voyantjs/ui/components";
7
+ import { Separator } from "@voyantjs/ui/components/separator";
8
+ import { CalendarRange, DollarSign, Download, FileText, MoreHorizontal, Pencil, Plus, RefreshCw, Trash2, } from "lucide-react";
9
+ import { useEffect, useState } from "react";
10
+ import { useProductDetailApi, useProductDetailMessages, useProductLocale, } from "./host.js";
11
+ import { formatAmount, formatCapacityLabel, formatDuration, formatSlotDate, formatSlotTime, getDepartureStatusLabel, getProductBookingModeLabel, slotStatusVariant, } from "./product-detail-shared.js";
12
+ import { ProductMediaGallery } from "./product-media-gallery.js";
13
+ export function Section({ title, actions, children, contentClassName, }) {
14
+ return (_jsxs("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: [_jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [_jsx("h2", { className: "font-semibold leading-none tracking-tight", children: title }), actions] }), _jsx(Separator, {}), _jsx("div", { className: contentClassName ?? "px-6 py-4", children: children })] }));
15
+ }
16
+ export function DetailRow({ label, value }) {
17
+ return (_jsxs("div", { className: "flex items-center justify-between py-3 text-sm [&:not(:last-child)]:border-b", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("span", { children: value })] }));
18
+ }
19
+ export function ActionMenu({ children }) {
20
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: children })] }));
21
+ }
22
+ export function EmptyState({ message }) {
23
+ return _jsx("p", { className: "py-6 text-center text-sm text-muted-foreground", children: message });
24
+ }
25
+ async function getProductStartingFromCents(api, productId) {
26
+ const rules = await api.get(`/v1/pricing/option-price-rules?productId=${encodeURIComponent(productId)}&limit=100&active=true`);
27
+ const ruleIds = rules.data.filter((rule) => rule.active).map((rule) => rule.id);
28
+ if (ruleIds.length === 0)
29
+ return null;
30
+ const unitPriceResponses = await Promise.all(ruleIds.map((ruleId) => api.get(`/v1/pricing/option-unit-price-rules?optionPriceRuleId=${encodeURIComponent(ruleId)}&limit=100&active=true`)));
31
+ const prices = unitPriceResponses
32
+ .flatMap((response) => response.data)
33
+ .filter((rule) => rule.active)
34
+ .map((rule) => rule.sellAmountCents)
35
+ .filter((amount) => amount != null && amount > 0);
36
+ return prices.length > 0 ? Math.min(...prices) : null;
37
+ }
38
+ // Legacy CMS imports occasionally store rich HTML in the plain-text description
39
+ // field. Detect it so we can render it as markup instead of dumping raw tags.
40
+ function looksLikeHtml(value) {
41
+ return /<\/?[a-z][\s\S]*>/i.test(value);
42
+ }
43
+ export function ProductDetailsSection({ product, onEdit, }) {
44
+ const api = useProductDetailApi();
45
+ const messages = useProductDetailMessages();
46
+ const productMessages = messages.products.core;
47
+ const resolvedLocale = useProductLocale();
48
+ const startingFromQuery = useQuery({
49
+ queryKey: ["product-starting-from", product.id],
50
+ queryFn: () => getProductStartingFromCents(api, product.id),
51
+ });
52
+ const startingFromCents = startingFromQuery.data ?? null;
53
+ const usesOptionUnitPricing = startingFromQuery.isPending || startingFromCents != null;
54
+ const translationsQuery = useProductTranslations(product.id, { limit: 100 });
55
+ const translations = translationsQuery.data?.data ?? [];
56
+ const [selectedLanguageTag, setSelectedLanguageTag] = useState("");
57
+ // Default the toggle to the operator's current locale (or its base language),
58
+ // falling back to the first available translation.
59
+ useEffect(() => {
60
+ if (translations.length === 0)
61
+ return;
62
+ if (translations.some((translation) => translation.languageTag === selectedLanguageTag))
63
+ return;
64
+ const baseLocale = resolvedLocale.toLowerCase().split("-")[0];
65
+ const preferred = translations.find((translation) => translation.languageTag.toLowerCase() === resolvedLocale.toLowerCase()) ??
66
+ translations.find((translation) => translation.languageTag.toLowerCase().split("-")[0] === baseLocale) ??
67
+ translations[0];
68
+ setSelectedLanguageTag(preferred?.languageTag ?? "");
69
+ }, [translations, selectedLanguageTag, resolvedLocale]);
70
+ const selectedTranslation = translations.find((translation) => translation.languageTag === selectedLanguageTag) ?? null;
71
+ const description = selectedTranslation?.description ?? product.description ?? null;
72
+ return (_jsxs(Section, { title: productMessages.detailsTitle, actions: _jsxs("div", { className: "flex items-center gap-2", children: [translations.length > 0 ? (_jsx(ToggleGroup, { value: selectedLanguageTag ? [selectedLanguageTag] : [], onValueChange: (values) => {
73
+ const next = values[values.length - 1];
74
+ if (next)
75
+ setSelectedLanguageTag(next);
76
+ }, variant: "outline", size: "sm", "aria-label": productMessages.descriptionLanguageLabel, children: translations.map((translation) => (_jsx(ToggleGroupItem, { value: translation.languageTag, className: "px-2 text-xs uppercase", children: translation.languageTag }, translation.id))) })) : null, _jsx(ActionMenu, { children: _jsxs(DropdownMenuItem, { onClick: onEdit, children: [_jsx(Pencil, { className: "h-4 w-4" }), productMessages.edit] }) })] }), children: [description ? (looksLikeHtml(description) ? (_jsx("div", { className: "border-b pb-4 text-sm leading-relaxed text-muted-foreground [&_a]:font-medium [&_a]:text-foreground [&_a]:underline [&_em]:italic [&_h2]:mt-4 [&_h2]:mb-1.5 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:text-foreground [&_h3]:mt-4 [&_h3]:mb-1.5 [&_h3]:text-sm [&_h3]:font-semibold [&_h3]:text-foreground [&_li]:mb-0.5 [&_ol]:mb-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p]:mb-2 [&_p:last-child]:mb-0 [&_strong]:font-semibold [&_strong]:text-foreground [&_ul]:mb-2 [&_ul]:list-disc [&_ul]:pl-5",
77
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: operator-authored localized rich text from the products module
78
+ dangerouslySetInnerHTML: { __html: description } })) : (_jsx("div", { className: "border-b pb-3 text-sm whitespace-pre-line text-muted-foreground", children: description }))) : null, usesOptionUnitPricing ? (_jsx(DetailRow, { label: productMessages.startingFromLabel, value: _jsx("span", { className: "font-mono", children: startingFromCents != null
79
+ ? formatAmount(startingFromCents, product.sellCurrency)
80
+ : productMessages.noValue }) })) : null, !usesOptionUnitPricing && product.sellAmountCents != null ? (_jsx(DetailRow, { label: productMessages.sellLabel, value: _jsx("span", { className: "font-mono", children: formatAmount(product.sellAmountCents, product.sellCurrency) }) })) : null, !usesOptionUnitPricing && product.costAmountCents != null ? (_jsx(DetailRow, { label: productMessages.costLabel, value: _jsx("span", { className: "font-mono", children: formatAmount(product.costAmountCents, product.sellCurrency) }) })) : null] }));
81
+ }
82
+ export function ProductDeparturesSection({ slots, itineraryNameById, slotIdsWithOverrides, onCreate, onEdit, onOverridePrice, onManageAvailability, onDelete, }) {
83
+ const messages = useProductDetailMessages();
84
+ const productMessages = messages.products.core;
85
+ return (_jsx(Section, { title: productMessages.departuresTitle, actions: _jsx(ActionMenu, { children: _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(Plus, { className: "h-4 w-4" }), productMessages.newDeparture] }) }), contentClassName: "", children: slots.length === 0 ? (_jsx(EmptyState, { message: productMessages.departuresEmpty })) : (_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "py-2.5 pl-6 pr-3 text-left font-medium", children: productMessages.departureStartColumn }), _jsx("th", { className: "px-3 py-2.5 text-left font-medium", children: productMessages.departureEndColumn }), _jsx("th", { className: "px-3 py-2.5 text-left font-medium", children: "Itinerary" }), _jsx("th", { className: "px-3 py-2.5 text-left font-medium", children: productMessages.departureDurationColumn }), _jsx("th", { className: "px-3 py-2.5 text-left font-medium", children: productMessages.departureStatusColumn }), _jsx("th", { className: "px-3 py-2.5 text-left font-medium", children: productMessages.departureCapacityColumn }), _jsx("th", { className: "w-10 px-3 py-2.5" })] }) }), _jsx("tbody", { children: slots.map((slot) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsxs("td", { className: "py-2.5 pl-6 pr-3", children: [_jsx("div", { className: "font-mono text-xs", children: slot.dateLocal }), _jsx("div", { className: "text-xs text-muted-foreground", children: formatSlotTime(slot.startsAt) })] }), _jsx("td", { className: "px-3 py-2.5", children: slot.endsAt ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "font-mono text-xs", children: formatSlotDate(slot.endsAt) }), _jsx("div", { className: "text-xs text-muted-foreground", children: formatSlotTime(slot.endsAt) })] })) : (_jsx("span", { className: "text-muted-foreground", children: productMessages.noValue })) }), _jsx("td", { className: "px-3 py-2.5 text-xs", children: slot.itineraryId
86
+ ? (itineraryNameById.get(slot.itineraryId) ??
87
+ messages.products.operations.itineraries.customOverride)
88
+ : messages.products.operations.itineraries.defaultBadge }), _jsx("td", { className: "px-3 py-2.5 text-xs", children: formatDuration(slot) }), _jsx("td", { className: "px-3 py-2.5", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Badge, { variant: slotStatusVariant[slot.status], className: "text-xs", children: getDepartureStatusLabel(slot.status, messages) }), slotIdsWithOverrides?.has(slot.id) ? (_jsx(Badge, { variant: "outline", className: "text-xs", children: productMessages.departureOverrideBadge })) : null] }) }), _jsx("td", { className: "px-3 py-2.5 font-mono text-xs", children: formatCapacityLabel(slot, messages) }), _jsx("td", { className: "px-3 py-2.5", children: _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: () => onEdit(slot), children: [_jsx(Pencil, { className: "h-4 w-4" }), productMessages.edit] }), onManageAvailability ? (_jsxs(DropdownMenuItem, { onClick: () => onManageAvailability(slot), children: [_jsx(CalendarRange, { className: "h-4 w-4" }), productMessages.departureManageAvailability] })) : null, onOverridePrice ? (_jsxs(DropdownMenuItem, { onClick: () => onOverridePrice(slot), children: [_jsx(DollarSign, { className: "h-4 w-4" }), productMessages.departureOverridePricing] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => onDelete(slot.id), children: [_jsx(Trash2, { className: "h-4 w-4" }), productMessages.delete] })] }) })] }, slot.id))) })] })) }));
89
+ }
90
+ export function ProductSchedulesSection({ rules, onCreate, onEdit, onDelete, }) {
91
+ const messages = useProductDetailMessages();
92
+ const productMessages = messages.products.core;
93
+ return (_jsx(Section, { title: productMessages.schedulesTitle, actions: _jsx(ActionMenu, { children: _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(Plus, { className: "h-4 w-4" }), productMessages.newSchedule] }) }), children: rules.length === 0 ? (_jsx(EmptyState, { message: productMessages.schedulesEmpty })) : (_jsx("div", { className: "flex flex-col divide-y", children: rules.map((rule) => (_jsxs("div", { className: "flex items-center justify-between py-3 first:pt-0 last:pb-0", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: describeRRule(rule.recurrenceRule) }), !rule.active ? (_jsx(Badge, { variant: "outline", className: "text-xs", children: productMessages.inactiveBadge })) : null] }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: formatMessage(productMessages.scheduleSummary, {
94
+ maxCapacity: rule.maxCapacity,
95
+ timezone: rule.timezone,
96
+ cutoff: rule.cutoffMinutes != null
97
+ ? formatMessage(productMessages.scheduleCutoffSuffix, {
98
+ minutes: rule.cutoffMinutes,
99
+ })
100
+ : "",
101
+ }) })] }), _jsxs(ActionMenu, { children: [_jsxs(DropdownMenuItem, { onClick: () => onEdit(rule), children: [_jsx(Pencil, { className: "h-4 w-4" }), productMessages.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => onDelete(rule.id), children: [_jsx(Trash2, { className: "h-4 w-4" }), productMessages.delete] })] })] }, rule.id))) })) }));
102
+ }
103
+ export function ProductChannelsSection({ allChannels, mappings, onAddChannel, onRemoveChannel, }) {
104
+ const messages = useProductDetailMessages();
105
+ const productMessages = messages.products.core;
106
+ const assignedChannelIds = new Set(mappings.map((mapping) => mapping.channelId));
107
+ const assignedChannels = allChannels.filter((channel) => assignedChannelIds.has(channel.id));
108
+ const unassignedChannels = allChannels.filter((channel) => !assignedChannelIds.has(channel.id) && channel.status === "active");
109
+ return (_jsx(Section, { title: productMessages.channelsTitle, children: _jsxs("div", { className: "flex flex-col gap-3", children: [assignedChannels.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: productMessages.channelsEmpty })) : (_jsx("div", { className: "flex flex-col divide-y", children: assignedChannels.map((channel) => {
110
+ const mapping = mappings.find((entry) => entry.channelId === channel.id);
111
+ return (_jsxs("div", { className: "flex items-center justify-between py-2 first:pt-0 last:pb-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm", children: channel.name }), _jsx(Badge, { variant: "outline", className: "text-[10px] capitalize", children: channel.kind.replace("_", " ") })] }), mapping ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-destructive", onClick: () => onRemoveChannel(mapping.id), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })) : null] }, channel.id));
112
+ }) })), unassignedChannels.length > 0 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), productMessages.addChannel] }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: unassignedChannels.map((channel) => (_jsxs(DropdownMenuItem, { onClick: () => onAddChannel(channel.id), children: [channel.name, _jsx("span", { className: "ml-auto text-xs capitalize text-muted-foreground", children: channel.kind.replace("_", " ") })] }, channel.id))) })] })) : null, allChannels.length === 0 ? (_jsxs("p", { className: "text-xs text-muted-foreground", children: [productMessages.noChannelsDefined, " ", _jsx("a", { href: "/settings/channels", className: "underline", children: productMessages.createChannelsInSettings })] })) : null] }) }));
113
+ }
114
+ export function ProductOrganizeSection({ product, onEdit, }) {
115
+ const api = useProductDetailApi();
116
+ const messages = useProductDetailMessages();
117
+ const productMessages = messages.products.core;
118
+ const taxClassQuery = useQuery({
119
+ queryKey: ["tax-class", product.taxClassId],
120
+ enabled: !!product.taxClassId,
121
+ queryFn: () => api.get(`/v1/admin/finance/tax-classes/${product.taxClassId}`),
122
+ });
123
+ return (_jsxs(Section, { title: productMessages.organizeTitle, actions: _jsx(ActionMenu, { children: _jsxs(DropdownMenuItem, { onClick: onEdit, children: [_jsx(Pencil, { className: "h-4 w-4" }), productMessages.edit] }) }), children: [_jsx(DetailRow, { label: productMessages.tagsLabel, value: product.tags.length > 0 ? (_jsx("div", { className: "flex flex-wrap justify-end gap-1", children: product.tags.map((tag) => (_jsx(Badge, { variant: "secondary", className: "text-xs", children: tag }, tag))) })) : (_jsx("span", { className: "text-muted-foreground", children: productMessages.noValue })) }), _jsx(DetailRow, { label: productMessages.typeLabel, value: _jsx("span", { children: getProductBookingModeLabel(product.bookingMode, messages) }) }), _jsx(DetailRow, { label: productMessages.taxClassLabel, value: taxClassQuery.data?.data.label ? (_jsx("span", { children: taxClassQuery.data.data.label })) : (_jsx("span", { className: "text-muted-foreground", children: productMessages.taxClassNone })) })] }));
124
+ }
125
+ export function ProductBrochureSection({ brochure, isGenerating, onGenerate, }) {
126
+ const messages = useProductDetailMessages();
127
+ const productMessages = messages.products.core;
128
+ return (_jsx(Section, { title: productMessages.brochureTitle, children: _jsxs("div", { className: "flex flex-col gap-3", children: [brochure ? (_jsxs("div", { className: "flex items-start gap-3 rounded-md border bg-muted/20 p-3", children: [_jsx("div", { className: "mt-0.5 rounded-md bg-background p-2 text-muted-foreground", children: _jsx(FileText, { className: "h-4 w-4" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: brochure.name }), _jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: formatMessage(productMessages.brochureMeta, {
129
+ version: brochure.brochureVersion ?? 1,
130
+ size: formatFileSize(brochure.fileSize),
131
+ }) })] })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: productMessages.brochureEmpty })), _jsxs("div", { className: "flex gap-2", children: [brochure ? (_jsx("a", { href: brochure.url, target: "_blank", rel: "noreferrer", className: "flex-1", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(Download, { className: "mr-1.5 h-3.5 w-3.5" }), productMessages.downloadBrochure] }) })) : null, _jsxs(Button, { variant: brochure ? "secondary" : "default", size: "sm", className: "flex-1", disabled: isGenerating, onClick: onGenerate, children: [_jsx(RefreshCw, { className: `mr-1.5 h-3.5 w-3.5 ${isGenerating ? "animate-spin" : ""}` }), brochure ? productMessages.regenerateBrochure : productMessages.generateBrochure] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: productMessages.brochureSizeHint })] }) }));
132
+ }
133
+ export function ProductMediaSection({ productId, media, isUploading, onUpload, onSetCover, onDelete, }) {
134
+ const messages = useProductDetailMessages();
135
+ return (_jsx(Section, { title: messages.products.core.mediaTitle, children: _jsx("div", { className: "flex flex-col gap-4", children: _jsx(ProductMediaGallery, { productId: productId, media: media, isUploading: isUploading, onUpload: onUpload, onSetCover: onSetCover, onDelete: onDelete }) }) }));
136
+ }
137
+ function formatFileSize(value) {
138
+ if (value == null)
139
+ return "-";
140
+ if (value < 1024 * 1024)
141
+ return `${Math.max(1, Math.round(value / 1024))} KB`;
142
+ return `${(value / (1024 * 1024)).toFixed(1)} MB`;
143
+ }
@@ -0,0 +1,264 @@
1
+ import type { ProductRecord } from "@voyantjs/products-react";
2
+ import type { ProductDetailApi, ProductMessagesRoot } from "./host.js";
3
+ import type { DepartureSlot } from "./product-departure-dialog.js";
4
+ import type { AvailabilityRule } from "./product-schedule-dialog.js";
5
+ export type { AvailabilityRule, DepartureSlot, ProductRecord };
6
+ export type ProductDay = {
7
+ id: string;
8
+ itineraryId: string;
9
+ dayNumber: number;
10
+ title: string | null;
11
+ description: string | null;
12
+ location: string | null;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ };
16
+ export type DayService = {
17
+ id: string;
18
+ dayId: string;
19
+ supplierServiceId: string | null;
20
+ serviceType: "accommodation" | "transfer" | "experience" | "guide" | "meal" | "other";
21
+ name: string;
22
+ description: string | null;
23
+ countryCode: string | null;
24
+ costCurrency: string;
25
+ costAmountCents: number;
26
+ quantity: number;
27
+ sortOrder: number | null;
28
+ notes: string | null;
29
+ createdAt: string;
30
+ };
31
+ export type ChannelInfo = {
32
+ id: string;
33
+ name: string;
34
+ kind: string;
35
+ status: string;
36
+ };
37
+ export type ChannelProductMapping = {
38
+ id: string;
39
+ channelId: string;
40
+ productId: string;
41
+ active: boolean;
42
+ };
43
+ export type ProductMediaItem = {
44
+ id: string;
45
+ productId: string;
46
+ dayId: string | null;
47
+ mediaType: "image" | "video" | "document";
48
+ name: string;
49
+ url: string;
50
+ storageKey: string | null;
51
+ mimeType: string | null;
52
+ fileSize: number | null;
53
+ altText: string | null;
54
+ sortOrder: number;
55
+ isCover: boolean;
56
+ isBrochure: boolean;
57
+ isBrochureCurrent: boolean;
58
+ brochureVersion: number | null;
59
+ createdAt: string;
60
+ updatedAt: string;
61
+ };
62
+ export declare function getProductDaysQueryOptions(api: ProductDetailApi, id: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
63
+ data: ProductDay[];
64
+ }, Error, {
65
+ data: ProductDay[];
66
+ }, string[]>, "queryFn"> & {
67
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
68
+ data: ProductDay[];
69
+ }, string[], never> | undefined;
70
+ } & {
71
+ queryKey: string[] & {
72
+ [dataTagSymbol]: {
73
+ data: ProductDay[];
74
+ };
75
+ [dataTagErrorSymbol]: Error;
76
+ };
77
+ };
78
+ export declare function getProductSlotsQueryOptions(api: ProductDetailApi, id: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
79
+ data: DepartureSlot[];
80
+ }, Error, {
81
+ data: DepartureSlot[];
82
+ }, string[]>, "queryFn"> & {
83
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
84
+ data: DepartureSlot[];
85
+ }, string[], never> | undefined;
86
+ } & {
87
+ queryKey: string[] & {
88
+ [dataTagSymbol]: {
89
+ data: DepartureSlot[];
90
+ };
91
+ [dataTagErrorSymbol]: Error;
92
+ };
93
+ };
94
+ export declare function getProductRulesQueryOptions(api: ProductDetailApi, id: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
95
+ data: AvailabilityRule[];
96
+ }, Error, {
97
+ data: AvailabilityRule[];
98
+ }, string[]>, "queryFn"> & {
99
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
100
+ data: AvailabilityRule[];
101
+ }, string[], never> | undefined;
102
+ } & {
103
+ queryKey: string[] & {
104
+ [dataTagSymbol]: {
105
+ data: AvailabilityRule[];
106
+ };
107
+ [dataTagErrorSymbol]: Error;
108
+ };
109
+ };
110
+ export declare function getProductDayServicesQueryOptions(api: ProductDetailApi, productId: string, dayId: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
111
+ data: DayService[];
112
+ }, Error, {
113
+ data: DayService[];
114
+ }, string[]>, "queryFn"> & {
115
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
116
+ data: DayService[];
117
+ }, string[], never> | undefined;
118
+ } & {
119
+ queryKey: string[] & {
120
+ [dataTagSymbol]: {
121
+ data: DayService[];
122
+ };
123
+ [dataTagErrorSymbol]: Error;
124
+ };
125
+ };
126
+ export declare function getChannelsQueryOptions(api: ProductDetailApi): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
127
+ data: ChannelInfo[];
128
+ }, Error, {
129
+ data: ChannelInfo[];
130
+ }, string[]>, "queryFn"> & {
131
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
132
+ data: ChannelInfo[];
133
+ }, string[], never> | undefined;
134
+ } & {
135
+ queryKey: string[] & {
136
+ [dataTagSymbol]: {
137
+ data: ChannelInfo[];
138
+ };
139
+ [dataTagErrorSymbol]: Error;
140
+ };
141
+ };
142
+ export declare function getProductChannelMappingsQueryOptions(api: ProductDetailApi, id: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
143
+ data: ChannelProductMapping[];
144
+ }, Error, {
145
+ data: ChannelProductMapping[];
146
+ }, string[]>, "queryFn"> & {
147
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
148
+ data: ChannelProductMapping[];
149
+ }, string[], never> | undefined;
150
+ } & {
151
+ queryKey: string[] & {
152
+ [dataTagSymbol]: {
153
+ data: ChannelProductMapping[];
154
+ };
155
+ [dataTagErrorSymbol]: Error;
156
+ };
157
+ };
158
+ export declare function getProductMediaQueryOptions(api: ProductDetailApi, id: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
159
+ data: ProductMediaItem[];
160
+ total: number;
161
+ }, Error, {
162
+ data: ProductMediaItem[];
163
+ total: number;
164
+ }, string[]>, "queryFn"> & {
165
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
166
+ data: ProductMediaItem[];
167
+ total: number;
168
+ }, string[], never> | undefined;
169
+ } & {
170
+ queryKey: string[] & {
171
+ [dataTagSymbol]: {
172
+ data: ProductMediaItem[];
173
+ total: number;
174
+ };
175
+ [dataTagErrorSymbol]: Error;
176
+ };
177
+ };
178
+ export declare function getProductDayMediaQueryOptions(api: ProductDetailApi, productId: string, dayId: string): import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<{
179
+ data: ProductMediaItem[];
180
+ total: number;
181
+ }, Error, {
182
+ data: ProductMediaItem[];
183
+ total: number;
184
+ }, string[]>, "queryFn"> & {
185
+ queryFn?: import("@tanstack/react-query").QueryFunction<{
186
+ data: ProductMediaItem[];
187
+ total: number;
188
+ }, string[], never> | undefined;
189
+ } & {
190
+ queryKey: string[] & {
191
+ [dataTagSymbol]: {
192
+ data: ProductMediaItem[];
193
+ total: number;
194
+ };
195
+ [dataTagErrorSymbol]: Error;
196
+ };
197
+ };
198
+ export declare const statusVariant: Record<string, "default" | "secondary" | "outline" | "destructive">;
199
+ export declare const slotStatusVariant: Record<DepartureSlot["status"], "default" | "secondary" | "outline" | "destructive">;
200
+ export declare function formatAmount(cents: number | null, currency: string): string;
201
+ export declare function formatMargin(percent: number | null): string;
202
+ export declare function formatSlotTime(iso: string): string;
203
+ export declare function formatSlotDate(iso: string): string;
204
+ export declare function formatDuration(slot: DepartureSlot): string;
205
+ export declare function getProductStatusLabel(status: ProductRecord["status"], messages: ProductMessagesRoot): string;
206
+ export declare function getProductBookingModeLabel(bookingMode: ProductRecord["bookingMode"], messages: ProductMessagesRoot): string;
207
+ export declare function getDepartureStatusLabel(status: DepartureSlot["status"], messages: ProductMessagesRoot): string;
208
+ export declare function formatCapacityLabel(slot: DepartureSlot, messages: ProductMessagesRoot): string;
209
+ export interface ProductSourcedContentResponse {
210
+ data: {
211
+ content: {
212
+ product: {
213
+ id: string;
214
+ name: string;
215
+ description?: string | null;
216
+ highlights?: string[];
217
+ hero_image_url?: string | null;
218
+ duration_days?: number | null;
219
+ sell_currency?: string | null;
220
+ supplier?: string | null;
221
+ country?: string | null;
222
+ };
223
+ options: Array<{
224
+ id: string;
225
+ name: string;
226
+ description?: string | null;
227
+ }>;
228
+ days: Array<{
229
+ day_number: number;
230
+ title?: string | null;
231
+ description?: string | null;
232
+ location?: string | null;
233
+ hero_image_url?: string | null;
234
+ }>;
235
+ media: Array<{
236
+ url: string;
237
+ type: string;
238
+ caption?: string | null;
239
+ }>;
240
+ policies: Array<{
241
+ kind: string;
242
+ body: string;
243
+ }>;
244
+ departures?: Array<{
245
+ id: string;
246
+ starts_at: string;
247
+ ends_at?: string | null;
248
+ status?: string | null;
249
+ capacity?: number | null;
250
+ remaining?: number | null;
251
+ lowest_price_cents?: number | null;
252
+ currency?: string | null;
253
+ note?: string | null;
254
+ }>;
255
+ };
256
+ served_locale: string;
257
+ match_kind: "exact" | "language_match" | "fallback_chain" | "any";
258
+ source: "sourced-cache" | "sourced-fresh" | "synthesized" | "owned";
259
+ served_stale: boolean;
260
+ synthesized: boolean;
261
+ machine_translated: boolean;
262
+ };
263
+ }
264
+ //# sourceMappingURL=product-detail-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-shared.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-detail-shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ import { queryOptions } from "@tanstack/react-query";
2
+ export function getProductDaysQueryOptions(api, id) {
3
+ return queryOptions({
4
+ queryKey: ["product-days", id],
5
+ queryFn: () => api.get(`/v1/products/${id}/days`),
6
+ });
7
+ }
8
+ export function getProductSlotsQueryOptions(api, id) {
9
+ return queryOptions({
10
+ queryKey: ["product-slots", id],
11
+ queryFn: () => api.get(`/v1/availability/slots?productId=${id}&limit=25`),
12
+ });
13
+ }
14
+ export function getProductRulesQueryOptions(api, id) {
15
+ return queryOptions({
16
+ queryKey: ["product-rules", id],
17
+ queryFn: () => api.get(`/v1/availability/rules?productId=${id}&limit=50`),
18
+ });
19
+ }
20
+ export function getProductDayServicesQueryOptions(api, productId, dayId) {
21
+ return queryOptions({
22
+ queryKey: ["product-day-services", productId, dayId],
23
+ queryFn: () => api.get(`/v1/products/${productId}/days/${dayId}/services`),
24
+ });
25
+ }
26
+ export function getChannelsQueryOptions(api) {
27
+ return queryOptions({
28
+ queryKey: ["channels"],
29
+ queryFn: () => api.get("/v1/distribution/channels?limit=25"),
30
+ });
31
+ }
32
+ export function getProductChannelMappingsQueryOptions(api, id) {
33
+ return queryOptions({
34
+ queryKey: ["product-channel-mappings", id],
35
+ queryFn: () => api.get(`/v1/distribution/product-mappings?productId=${id}&limit=25`),
36
+ });
37
+ }
38
+ export function getProductMediaQueryOptions(api, id) {
39
+ return queryOptions({
40
+ queryKey: ["product-media", id],
41
+ queryFn: () => api.get(`/v1/products/${id}/media?limit=50`),
42
+ });
43
+ }
44
+ export function getProductDayMediaQueryOptions(api, productId, dayId) {
45
+ return queryOptions({
46
+ queryKey: ["day-media", productId, dayId],
47
+ queryFn: () => api.get(`/v1/products/${productId}/days/${dayId}/media?limit=50`),
48
+ });
49
+ }
50
+ export const statusVariant = {
51
+ draft: "outline",
52
+ active: "default",
53
+ archived: "secondary",
54
+ };
55
+ export const slotStatusVariant = {
56
+ open: "default",
57
+ closed: "secondary",
58
+ sold_out: "outline",
59
+ cancelled: "destructive",
60
+ };
61
+ export function formatAmount(cents, currency) {
62
+ if (cents == null)
63
+ return "-";
64
+ return `${(cents / 100).toFixed(2)} ${currency}`;
65
+ }
66
+ export function formatMargin(percent) {
67
+ if (percent == null)
68
+ return "-";
69
+ return `${percent.toFixed(0)}%`;
70
+ }
71
+ export function formatSlotTime(iso) {
72
+ const date = new Date(iso);
73
+ return `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}`;
74
+ }
75
+ export function formatSlotDate(iso) {
76
+ const date = new Date(iso);
77
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
78
+ }
79
+ export function formatDuration(slot) {
80
+ if (slot.nights != null || slot.days != null) {
81
+ const parts = [];
82
+ if (slot.days != null)
83
+ parts.push(`${slot.days} day${slot.days === 1 ? "" : "s"}`);
84
+ if (slot.nights != null)
85
+ parts.push(`${slot.nights} night${slot.nights === 1 ? "" : "s"}`);
86
+ return parts.join(" / ");
87
+ }
88
+ if (!slot.endsAt)
89
+ return "-";
90
+ const startMs = new Date(slot.startsAt).getTime();
91
+ const endMs = new Date(slot.endsAt).getTime();
92
+ const diffMs = endMs - startMs;
93
+ if (diffMs <= 0)
94
+ return "-";
95
+ const hours = diffMs / 3_600_000;
96
+ if (hours < 24)
97
+ return `${hours.toFixed(hours % 1 === 0 ? 0 : 1)}h`;
98
+ const startDate = formatSlotDate(slot.startsAt);
99
+ const endDate = formatSlotDate(slot.endsAt);
100
+ const nights = Math.round((new Date(`${endDate}T00:00:00Z`).getTime() - new Date(`${startDate}T00:00:00Z`).getTime()) /
101
+ 86_400_000);
102
+ return `${nights} night${nights === 1 ? "" : "s"}`;
103
+ }
104
+ export function getProductStatusLabel(status, messages) {
105
+ switch (status) {
106
+ case "draft":
107
+ return messages.products.core.statusDraft;
108
+ case "active":
109
+ return messages.products.core.statusActive;
110
+ case "archived":
111
+ return messages.products.core.statusArchived;
112
+ default:
113
+ return status;
114
+ }
115
+ }
116
+ export function getProductBookingModeLabel(bookingMode, messages) {
117
+ switch (bookingMode) {
118
+ case "date":
119
+ return messages.products.core.bookingModeDate;
120
+ case "date_time":
121
+ return messages.products.core.bookingModeDateTime;
122
+ case "open":
123
+ return messages.products.core.bookingModeOpen;
124
+ case "stay":
125
+ return messages.products.core.bookingModeStay;
126
+ case "transfer":
127
+ return messages.products.core.bookingModeTransfer;
128
+ case "itinerary":
129
+ return messages.products.core.bookingModeItinerary;
130
+ case "other":
131
+ return messages.products.core.bookingModeOther;
132
+ default:
133
+ return bookingMode;
134
+ }
135
+ }
136
+ export function getDepartureStatusLabel(status, messages) {
137
+ switch (status) {
138
+ case "open":
139
+ return messages.products.core.departureStatusOpen;
140
+ case "closed":
141
+ return messages.products.core.departureStatusClosed;
142
+ case "sold_out":
143
+ return messages.products.core.departureStatusSoldOut;
144
+ case "cancelled":
145
+ return messages.products.core.departureStatusCancelled;
146
+ default:
147
+ return status;
148
+ }
149
+ }
150
+ export function formatCapacityLabel(slot, messages) {
151
+ if (slot.unlimited)
152
+ return messages.products.core.unlimitedCapacity;
153
+ if (slot.initialPax == null)
154
+ return messages.products.core.noValue;
155
+ const remaining = slot.remainingPax ?? slot.initialPax;
156
+ return `${remaining} / ${slot.initialPax}`;
157
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Layout-matched placeholder for ProductDetailPage. Mirrors the real shell:
3
+ * - Header: title + status + action buttons
4
+ * - 2-column body (1fr + 320px)
5
+ * - Left stack: Details, Departures, Schedules, Itinerary (3 days), Options
6
+ * - Right stack: Channels, Organize, Media (3x3 grid)
7
+ */
8
+ export declare function ProductDetailSkeleton(): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=product-detail-skeleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-detail-skeleton.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/product-detail-skeleton.tsx"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,wBAAgB,qBAAqB,4CAwBpC"}