@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,28 @@
1
+ import { timezones } from "@voyantjs/utils/timezones";
2
+ function buildTimezoneOptions() {
3
+ const seen = new Map();
4
+ for (const tz of timezones) {
5
+ for (const id of tz.utc) {
6
+ if (seen.has(id))
7
+ continue;
8
+ seen.set(id, { id, label: tz.text, offset: tz.offset });
9
+ }
10
+ }
11
+ // Include the browser-resolved zone if not already present
12
+ const browserZone = typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : null;
13
+ if (browserZone && !seen.has(browserZone)) {
14
+ seen.set(browserZone, { id: browserZone, label: browserZone, offset: 0 });
15
+ }
16
+ return Array.from(seen.values()).sort((a, b) => {
17
+ if (a.offset !== b.offset)
18
+ return a.offset - b.offset;
19
+ return a.id.localeCompare(b.id);
20
+ });
21
+ }
22
+ export const TIMEZONE_OPTIONS = buildTimezoneOptions();
23
+ export const TIMEZONE_IDS = TIMEZONE_OPTIONS.map((t) => t.id);
24
+ const TIMEZONE_BY_ID = new Map(TIMEZONE_OPTIONS.map((t) => [t.id, t]));
25
+ export function getTimezoneLabel(id) {
26
+ const tz = TIMEZONE_BY_ID.get(id);
27
+ return tz ? `${id} — ${tz.label}` : id;
28
+ }
@@ -0,0 +1,41 @@
1
+ import { useMutation } from "@tanstack/react-query";
2
+ import { useProduct } from "@voyantjs/products-react";
3
+ import { type AvailabilityRule, type ChannelInfo, type ChannelProductMapping, type DepartureSlot, type ProductMediaItem } from "./product-detail-shared.js";
4
+ export interface UseProductDetailDataResult {
5
+ product: ReturnType<typeof useProduct>["data"];
6
+ isPending: boolean;
7
+ slots: DepartureSlot[];
8
+ rules: AvailabilityRule[];
9
+ channels: ChannelInfo[];
10
+ mappings: ChannelProductMapping[];
11
+ media: ProductMediaItem[];
12
+ itineraryNameById: Map<string, string>;
13
+ refetch: {
14
+ slots: () => void;
15
+ rules: () => void;
16
+ mappings: () => void;
17
+ media: () => void;
18
+ };
19
+ mutations: {
20
+ addChannelMapping: ReturnType<typeof useMutation<unknown, Error, string>>;
21
+ removeChannelMapping: ReturnType<typeof useMutation<unknown, Error, string>>;
22
+ duplicateProduct: ReturnType<typeof useMutation<{
23
+ data: {
24
+ id: string;
25
+ };
26
+ }, Error, void>>;
27
+ deleteProduct: ReturnType<typeof useMutation<unknown, Error, void>>;
28
+ deleteSlot: ReturnType<typeof useMutation<unknown, Error, string>>;
29
+ deleteRule: ReturnType<typeof useMutation<unknown, Error, string>>;
30
+ uploadMedia: ReturnType<typeof useMutation<unknown, Error, {
31
+ file: File;
32
+ dayId?: string;
33
+ }>>;
34
+ deleteMedia: ReturnType<typeof useMutation<unknown, Error, string>>;
35
+ setCover: ReturnType<typeof useMutation<unknown, Error, string>>;
36
+ generateBrochure: ReturnType<typeof useMutation<unknown, Error, void>>;
37
+ };
38
+ invalidateProduct: () => void;
39
+ }
40
+ export declare function useProductDetailData(productId: string): UseProductDetailDataResult;
41
+ //# sourceMappingURL=use-product-detail-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-product-detail-data.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/use-product-detail-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA4B,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EAAqB,UAAU,EAAyB,MAAM,0BAA0B,CAAA;AAK/F,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAMlB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,QAAQ,EAAE,qBAAqB,EAAE,CAAA;IACjC,KAAK,EAAE,gBAAgB,EAAE,CAAA;IACzB,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,IAAI,CAAA;QACjB,KAAK,EAAE,MAAM,IAAI,CAAA;QACjB,QAAQ,EAAE,MAAM,IAAI,CAAA;QACpB,KAAK,EAAE,MAAM,IAAI,CAAA;KAClB,CAAA;IACD,SAAS,EAAE;QACT,iBAAiB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QACzE,oBAAoB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAC5E,gBAAgB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC;YAAE,IAAI,EAAE;gBAAE,EAAE,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QACvF,aAAa,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QACnE,UAAU,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAClE,UAAU,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAClE,WAAW,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE;YAAE,IAAI,EAAE,IAAI,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAA;QAC3F,WAAW,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QACnE,QAAQ,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAA;QAChE,gBAAgB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;KACvE,CAAA;IACD,iBAAiB,EAAE,MAAM,IAAI,CAAA;CAC9B;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,0BAA0B,CAiKlF"}
@@ -0,0 +1,143 @@
1
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { productsQueryKeys, useProduct, useProductItineraries } from "@voyantjs/products-react";
3
+ import { useMemo } from "react";
4
+ import { useProductDetailHost, useProductDetailMessages } from "./host.js";
5
+ import { getChannelsQueryOptions, getProductChannelMappingsQueryOptions, getProductMediaQueryOptions, getProductRulesQueryOptions, getProductSlotsQueryOptions, } from "./product-detail-shared.js";
6
+ export function useProductDetailData(productId) {
7
+ const queryClient = useQueryClient();
8
+ const host = useProductDetailHost();
9
+ const api = host.api;
10
+ const messages = useProductDetailMessages();
11
+ const productMessages = messages.products.core;
12
+ const productQuery = useProduct(productId);
13
+ const itinerariesQuery = useProductItineraries(productId);
14
+ const productActionLedgerQueryKey = [...productsQueryKeys.product(productId), "action-ledger"];
15
+ const slotsQuery = useQuery(getProductSlotsQueryOptions(api, productId));
16
+ const rulesQuery = useQuery(getProductRulesQueryOptions(api, productId));
17
+ const channelsQuery = useQuery(getChannelsQueryOptions(api));
18
+ const mappingsQuery = useQuery(getProductChannelMappingsQueryOptions(api, productId));
19
+ const mediaQuery = useQuery(getProductMediaQueryOptions(api, productId));
20
+ const addChannelMapping = useMutation({
21
+ mutationFn: (channelId) => api.post("/v1/distribution/product-mappings", {
22
+ channelId,
23
+ productId,
24
+ active: true,
25
+ }),
26
+ onSuccess: () => {
27
+ void mappingsQuery.refetch();
28
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
29
+ },
30
+ });
31
+ const removeChannelMapping = useMutation({
32
+ mutationFn: (mappingId) => api.delete(`/v1/distribution/product-mappings/${mappingId}`),
33
+ onSuccess: () => {
34
+ void mappingsQuery.refetch();
35
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
36
+ },
37
+ });
38
+ const deleteProduct = useMutation({
39
+ mutationFn: () => api.delete(`/v1/products/${productId}`),
40
+ onSuccess: () => {
41
+ void queryClient.invalidateQueries({ queryKey: ["products"] });
42
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
43
+ },
44
+ });
45
+ const duplicateProduct = useMutation({
46
+ mutationFn: () => api.post(`/v1/admin/products/${productId}/duplicate`),
47
+ onSuccess: () => {
48
+ void queryClient.invalidateQueries({ queryKey: productsQueryKeys.products() });
49
+ },
50
+ });
51
+ const deleteSlot = useMutation({
52
+ mutationFn: (slotId) => api.delete(`/v1/availability/slots/${slotId}`),
53
+ onSuccess: () => {
54
+ void slotsQuery.refetch();
55
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
56
+ },
57
+ });
58
+ const deleteRule = useMutation({
59
+ mutationFn: (ruleId) => api.delete(`/v1/availability/rules/${ruleId}`),
60
+ onSuccess: () => {
61
+ void rulesQuery.refetch();
62
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
63
+ },
64
+ });
65
+ const uploadMedia = useMutation({
66
+ mutationFn: async ({ file, dayId }) => {
67
+ if (!host.uploadMedia)
68
+ throw new Error(productMessages.uploadFailed);
69
+ const result = await host.uploadMedia(file, { productId, dayId });
70
+ const endpoint = dayId
71
+ ? `/v1/products/${productId}/days/${dayId}/media`
72
+ : `/v1/products/${productId}/media`;
73
+ return api.post(endpoint, {
74
+ mediaType: result.mediaType,
75
+ name: result.name,
76
+ url: result.url,
77
+ storageKey: result.storageKey,
78
+ mimeType: result.mimeType,
79
+ fileSize: result.fileSize,
80
+ });
81
+ },
82
+ onSuccess: () => {
83
+ void mediaQuery.refetch();
84
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
85
+ },
86
+ });
87
+ const deleteMedia = useMutation({
88
+ mutationFn: (mediaId) => api.delete(`/v1/products/media/${mediaId}`),
89
+ onSuccess: () => {
90
+ void mediaQuery.refetch();
91
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
92
+ },
93
+ });
94
+ const setCover = useMutation({
95
+ mutationFn: (mediaId) => api.patch(`/v1/products/media/${mediaId}/set-cover`, {}),
96
+ onSuccess: () => {
97
+ void mediaQuery.refetch();
98
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
99
+ },
100
+ });
101
+ const generateBrochure = useMutation({
102
+ mutationFn: () => api.post(`/v1/admin/products/${productId}/brochure/generate`, {}),
103
+ onSuccess: () => {
104
+ void mediaQuery.refetch();
105
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
106
+ },
107
+ });
108
+ const itineraryNameById = useMemo(() => new Map((itinerariesQuery.data?.data ?? []).map((itinerary) => [itinerary.id, itinerary.name])), [itinerariesQuery.data]);
109
+ const invalidateProduct = () => {
110
+ void queryClient.invalidateQueries({ queryKey: productsQueryKeys.product(productId) });
111
+ void queryClient.invalidateQueries({ queryKey: productsQueryKeys.products() });
112
+ void queryClient.invalidateQueries({ queryKey: productActionLedgerQueryKey });
113
+ };
114
+ return {
115
+ product: productQuery.data,
116
+ isPending: productQuery.isPending,
117
+ slots: slotsQuery.data?.data ?? [],
118
+ rules: rulesQuery.data?.data ?? [],
119
+ channels: channelsQuery.data?.data ?? [],
120
+ mappings: mappingsQuery.data?.data ?? [],
121
+ media: mediaQuery.data?.data ?? [],
122
+ itineraryNameById,
123
+ refetch: {
124
+ slots: () => void slotsQuery.refetch(),
125
+ rules: () => void rulesQuery.refetch(),
126
+ mappings: () => void mappingsQuery.refetch(),
127
+ media: () => void mediaQuery.refetch(),
128
+ },
129
+ mutations: {
130
+ addChannelMapping,
131
+ removeChannelMapping,
132
+ duplicateProduct,
133
+ deleteProduct,
134
+ deleteSlot,
135
+ deleteRule,
136
+ uploadMedia,
137
+ deleteMedia,
138
+ setCover,
139
+ generateBrochure,
140
+ },
141
+ invalidateProduct,
142
+ };
143
+ }
@@ -0,0 +1,24 @@
1
+ import type { AvailabilityRule, DepartureSlot } from "./product-detail-shared.js";
2
+ export interface Toggle {
3
+ open: boolean;
4
+ setOpen: (open: boolean) => void;
5
+ openNow: () => void;
6
+ close: () => void;
7
+ }
8
+ export interface EditingToggle<T> {
9
+ open: boolean;
10
+ setOpen: (open: boolean) => void;
11
+ editing: T | undefined;
12
+ openNew: () => void;
13
+ openEdit: (item: T) => void;
14
+ close: () => void;
15
+ }
16
+ export interface UseProductDetailDialogsResult {
17
+ edit: Toggle;
18
+ bookingCreate: Toggle;
19
+ departure: EditingToggle<DepartureSlot>;
20
+ departureOverride: EditingToggle<DepartureSlot>;
21
+ schedule: EditingToggle<AvailabilityRule>;
22
+ }
23
+ export declare function useProductDetailDialogs(): UseProductDetailDialogsResult;
24
+ //# sourceMappingURL=use-product-detail-dialogs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-product-detail-dialogs.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/use-product-detail-dialogs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAEjF,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,OAAO,EAAE,CAAC,GAAG,SAAS,CAAA;IACtB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IACvC,iBAAiB,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;IAC/C,QAAQ,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAA;CAC1C;AAkCD,wBAAgB,uBAAuB,IAAI,6BAA6B,CAQvE"}
@@ -0,0 +1,40 @@
1
+ import { useState } from "react";
2
+ function useToggle() {
3
+ const [open, setOpen] = useState(false);
4
+ return {
5
+ open,
6
+ setOpen,
7
+ openNow: () => setOpen(true),
8
+ close: () => setOpen(false),
9
+ };
10
+ }
11
+ function useEditingToggle() {
12
+ const [open, setOpen] = useState(false);
13
+ const [editing, setEditing] = useState();
14
+ return {
15
+ open,
16
+ setOpen,
17
+ editing,
18
+ openNew: () => {
19
+ setEditing(undefined);
20
+ setOpen(true);
21
+ },
22
+ openEdit: (item) => {
23
+ setEditing(item);
24
+ setOpen(true);
25
+ },
26
+ close: () => {
27
+ setOpen(false);
28
+ setEditing(undefined);
29
+ },
30
+ };
31
+ }
32
+ export function useProductDetailDialogs() {
33
+ return {
34
+ edit: useToggle(),
35
+ bookingCreate: useToggle(),
36
+ departure: useEditingToggle(),
37
+ departureOverride: useEditingToggle(),
38
+ schedule: useEditingToggle(),
39
+ };
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { FieldValues, Resolver } from "react-hook-form";
2
+ import type { z } from "zod/v4";
3
+ export declare function zodResolver<TSchema extends z.ZodType<FieldValues, FieldValues>>(schema: TSchema): Resolver<z.input<TSchema>, unknown, z.output<TSchema>>;
4
+ //# sourceMappingURL=zod-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-resolver.d.ts","sourceRoot":"","sources":["../../../src/components/product-detail/zod-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,WAAW,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AA+B/B,wBAAgB,WAAW,CAAC,OAAO,SAAS,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC,EAC7E,MAAM,EAAE,OAAO,GACd,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CA6BxD"}
@@ -0,0 +1,39 @@
1
+ function setFieldError(target, path, error) {
2
+ let current = target;
3
+ for (let index = 0; index < path.length; index += 1) {
4
+ const key = String(path[index] ?? "root");
5
+ if (index === path.length - 1) {
6
+ current[key] = error;
7
+ return;
8
+ }
9
+ const next = current[key];
10
+ if (typeof next !== "object" || next === null) {
11
+ current[key] = {};
12
+ }
13
+ current = current[key];
14
+ }
15
+ }
16
+ export function zodResolver(schema) {
17
+ return async (values) => {
18
+ const result = await schema.safeParseAsync(values);
19
+ if (result.success) {
20
+ return {
21
+ values: result.data,
22
+ errors: {},
23
+ };
24
+ }
25
+ const errors = {};
26
+ for (const issue of result.error.issues) {
27
+ const path = issue.path.filter((segment) => typeof segment !== "symbol");
28
+ const normalizedPath = path.length > 0 ? path : ["root"];
29
+ setFieldError(errors, normalizedPath, {
30
+ type: issue.code,
31
+ message: issue.message,
32
+ });
33
+ }
34
+ return {
35
+ values: {},
36
+ errors: errors,
37
+ };
38
+ };
39
+ }
@@ -29,7 +29,7 @@ function initialState(mode) {
29
29
  name: "",
30
30
  code: "",
31
31
  description: "",
32
- status: "draft",
32
+ status: "active",
33
33
  isDefault: false,
34
34
  sortOrder: String(mode.sortOrder ?? 0),
35
35
  availableFrom: "",
@@ -1,5 +1,7 @@
1
- import { type ProductOptionRecord } from "@voyantjs/products-react";
1
+ import { type OptionUnitRecord, type ProductOptionRecord } from "@voyantjs/products-react";
2
2
  import * as React from "react";
3
+ export declare function optionLooksLikeRoomArrangementLabel(option: Pick<ProductOptionRecord, "code" | "name">): boolean;
4
+ export declare function getRoomArrangementOptionNames(options: ReadonlyArray<Pick<ProductOptionRecord, "code" | "id" | "name" | "status">>, unitsByOptionId: ReadonlyMap<string, readonly Pick<OptionUnitRecord, "unitType">[]>): string[];
3
5
  export interface ProductOptionsSectionProps {
4
6
  productId: string;
5
7
  pageSize?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"product-options-section.d.ts","sourceRoot":"","sources":["../../src/components/product-options-section.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,mBAAmB,EAMzB,MAAM,0BAA0B,CAAA;AAmBjC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,KAAK,CAAC,SAAS,CAAA;CACvE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAc,EACd,KAAK,EACL,WAAW,EACX,mBAAmB,GACpB,EAAE,0BAA0B,2CAoH5B"}
1
+ {"version":3,"file":"product-options-section.d.ts","sourceRoot":"","sources":["../../src/components/product-options-section.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EAOzB,MAAM,0BAA0B,CAAA;AA6BjC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqC9B,wBAAgB,mCAAmC,CACjD,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,MAAM,CAAC,GACjD,OAAO,CAIT;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,EACpF,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,EAAE,CAAC,GAClF,MAAM,EAAE,CASV;AA0CD,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,KAAK,CAAC,SAAS,CAAA;CACvE;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAc,EACd,KAAK,EACL,WAAW,EACX,mBAAmB,GACpB,EAAE,0BAA0B,2CAsJ5B"}
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQueries } from "@tanstack/react-query";
3
4
  import { useDuplicateOptionPricingMutation } from "@voyantjs/pricing-react";
4
- import { useDuplicateProductOptionMutation, useOptionUnitMutation, useOptionUnits, useProductOptionMutation, useProductOptions, } from "@voyantjs/products-react";
5
+ import { getOptionUnitsQueryOptions, useDuplicateProductOptionMutation, useOptionUnitMutation, useOptionUnits, useProductOptionMutation, useProductOptions, useVoyantProductsContext, } from "@voyantjs/products-react";
6
+ import { Alert, AlertDescription, AlertTitle } from "@voyantjs/ui/components/alert";
5
7
  import { Badge } from "@voyantjs/ui/components/badge";
6
8
  import { Button } from "@voyantjs/ui/components/button";
7
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@voyantjs/ui/components/card";
8
10
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
9
- import { ChevronDown, ChevronRight, Copy, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
11
+ import { ChevronDown, ChevronRight, Copy, Loader2, Pencil, Plus, Trash2, TriangleAlert, } from "lucide-react";
10
12
  import * as React from "react";
11
13
  import { useProductsUiMessagesOrDefault } from "../i18n/provider.js";
12
14
  import { OptionUnitDialog } from "./option-unit-dialog.js";
@@ -22,8 +24,60 @@ function formatRange(min, max) {
22
24
  }
23
25
  return `${min ?? 0}–${max ?? "∞"}`;
24
26
  }
27
+ function formatMessage(template, replacements) {
28
+ return Object.entries(replacements).reduce((message, [key, value]) => message.replaceAll(`{${key}}`, String(value)), template);
29
+ }
30
+ const ROOM_ARRANGEMENT_LABEL_PATTERN = /\b(single|sgl|double|dbl|twin|triple|tpl|quad|dubla|tripla|camera)\b/i;
31
+ function normalizeConfigurationLabel(value) {
32
+ return (value ?? "")
33
+ .normalize("NFKD")
34
+ .replace(/[\u0300-\u036f]/g, "")
35
+ .toLowerCase();
36
+ }
37
+ export function optionLooksLikeRoomArrangementLabel(option) {
38
+ return [option.name, option.code].some((value) => ROOM_ARRANGEMENT_LABEL_PATTERN.test(normalizeConfigurationLabel(value)));
39
+ }
40
+ export function getRoomArrangementOptionNames(options, unitsByOptionId) {
41
+ return options
42
+ .filter((option) => option.status !== "archived")
43
+ .filter(optionLooksLikeRoomArrangementLabel)
44
+ .filter((option) => {
45
+ const units = unitsByOptionId.get(option.id) ?? [];
46
+ return units.length > 0 && units.every((unit) => unit.unitType === "room");
47
+ })
48
+ .map((option) => option.name);
49
+ }
50
+ function formatInventory(unit, messages) {
51
+ if (unit.unitType === "room") {
52
+ if (unit.maxQuantity != null && unit.maxQuantity > 0) {
53
+ return formatMessage(messages.unitSummaries.roomsWithCount, { count: unit.maxQuantity });
54
+ }
55
+ return messages.unitSummaries.rooms;
56
+ }
57
+ if (unit.unitType === "vehicle") {
58
+ if (unit.maxQuantity != null && unit.maxQuantity > 0) {
59
+ return formatMessage(messages.unitSummaries.vehiclesWithCount, { count: unit.maxQuantity });
60
+ }
61
+ return messages.unitSummaries.vehicles;
62
+ }
63
+ return formatMessage(messages.unitSummaries.range, {
64
+ range: formatRange(unit.minQuantity, unit.maxQuantity),
65
+ });
66
+ }
67
+ function formatOccupancyText(unit, messages) {
68
+ if (unit.occupancyMin == null && unit.occupancyMax == null) {
69
+ return "—";
70
+ }
71
+ if (unit.occupancyMin === unit.occupancyMax) {
72
+ return formatMessage(messages.unitSummaries.sleeps, { count: unit.occupancyMin ?? 0 });
73
+ }
74
+ return formatMessage(messages.unitSummaries.sleepsRange, {
75
+ range: `${unit.occupancyMin ?? 0}–${unit.occupancyMax ?? "∞"}`,
76
+ });
77
+ }
25
78
  export function ProductOptionsSection({ productId, pageSize = 100, title, description, renderOptionDetails, }) {
26
79
  const messages = useProductsUiMessagesOrDefault();
80
+ const productsClient = useVoyantProductsContext();
27
81
  const [expandedOptionId, setExpandedOptionId] = React.useState(null);
28
82
  const [dialogOpen, setDialogOpen] = React.useState(false);
29
83
  const [editingOption, setEditingOption] = React.useState(undefined);
@@ -35,13 +89,32 @@ export function ProductOptionsSection({ productId, pageSize = 100, title, descri
35
89
  const duplicateOption = useDuplicateProductOptionMutation();
36
90
  const duplicatePricing = useDuplicateOptionPricingMutation();
37
91
  const options = React.useMemo(() => (data?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder), [data?.data]);
92
+ const optionUnitQueries = useQueries({
93
+ queries: options.map((option) => ({
94
+ ...getOptionUnitsQueryOptions(productsClient, {
95
+ optionId: option.id,
96
+ limit: 100,
97
+ }),
98
+ enabled: options.length > 1,
99
+ })),
100
+ });
101
+ const roomArrangementOptionNames = React.useMemo(() => {
102
+ const unitsByOptionId = new Map();
103
+ options.forEach((option, index) => {
104
+ const units = optionUnitQueries[index]?.data?.data;
105
+ if (units)
106
+ unitsByOptionId.set(option.id, units);
107
+ });
108
+ return getRoomArrangementOptionNames(options, unitsByOptionId);
109
+ }, [options, optionUnitQueries]);
110
+ const showRoomArrangementWarning = roomArrangementOptionNames.length >= 2;
38
111
  const nextSortOrder = options.length > 0 ? Math.max(...options.map((option) => option.sortOrder)) + 1 : 0;
39
112
  const resolvedTitle = title ?? messages.productOptionsSection.titles.default;
40
113
  const resolvedDescription = description ?? messages.productOptionsSection.descriptions.default;
41
114
  return (_jsxs(Card, { "data-slot": "product-options-section", children: [_jsxs(CardHeader, { className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(CardTitle, { children: resolvedTitle }), _jsx(CardDescription, { children: resolvedDescription })] }), _jsxs(Button, { onClick: () => {
42
115
  setEditingOption(undefined);
43
116
  setDialogOpen(true);
44
- }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productOptionsSection.actions.addOption] })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.options })) : options.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.productOptionsSection.empty.options })) : (options.map((option) => (_jsx(OptionRow, { option: option, expanded: expandedOptionId === option.id, onToggle: () => setExpandedOptionId((current) => (current === option.id ? null : option.id)), onEdit: () => {
117
+ }, children: [_jsx(Plus, { className: "mr-2 size-4", "aria-hidden": "true" }), messages.productOptionsSection.actions.addOption] })] }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [showRoomArrangementWarning ? (_jsxs(Alert, { className: "border-amber-500/40 bg-amber-500/10", children: [_jsx(TriangleAlert, { className: "size-4 text-amber-600", "aria-hidden": "true" }), _jsx(AlertTitle, { children: messages.productOptionsSection.configurationWarnings.roomOptionsTitle }), _jsx(AlertDescription, { children: formatMessage(messages.productOptionsSection.configurationWarnings.roomOptionsDescription, { options: roomArrangementOptionNames.join(", ") }) })] })) : null, isPending ? (_jsx("div", { className: "flex min-h-24 items-center justify-center", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.options })) : options.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.productOptionsSection.empty.options })) : (options.map((option) => (_jsx(OptionRow, { option: option, expanded: expandedOptionId === option.id, onToggle: () => setExpandedOptionId((current) => (current === option.id ? null : option.id)), onEdit: () => {
45
118
  setEditingOption(option);
46
119
  setDialogOpen(true);
47
120
  }, onDuplicate: () => {
@@ -74,10 +147,34 @@ function UnitsPanel({ optionId, messages, }) {
74
147
  const { remove } = useOptionUnitMutation();
75
148
  const units = React.useMemo(() => (data?.data ?? []).slice().sort((a, b) => a.sortOrder - b.sortOrder), [data?.data]);
76
149
  const nextSortOrder = units.length > 0 ? Math.max(...units.map((unit) => unit.sortOrder)) + 1 : 0;
77
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: messages.productOptionsSection.titles.units }), _jsx("p", { className: "text-xs text-muted-foreground", children: messages.productOptionsSection.descriptions.units })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
150
+ const isPersonOnly = units.length > 0 && units.every((unit) => unit.unitType === "person");
151
+ const showAge = units.some((unit) => unit.unitType === "person");
152
+ const hasRoomUnits = units.some((unit) => unit.unitType === "room");
153
+ const showOccupancy = units.some((unit) => unit.unitType === "room" || unit.occupancyMin != null || unit.occupancyMax != null);
154
+ const unitsTitle = isPersonOnly
155
+ ? messages.productOptionsSection.titles.personUnits
156
+ : hasRoomUnits
157
+ ? messages.productOptionsSection.titles.roomUnits
158
+ : messages.productOptionsSection.titles.units;
159
+ const unitsDescription = isPersonOnly
160
+ ? messages.productOptionsSection.descriptions.personUnits
161
+ : hasRoomUnits
162
+ ? messages.productOptionsSection.descriptions.roomUnits
163
+ : messages.productOptionsSection.descriptions.units;
164
+ const addUnitLabel = isPersonOnly
165
+ ? messages.productOptionsSection.actions.addPersonUnit
166
+ : hasRoomUnits
167
+ ? messages.productOptionsSection.actions.addRoomUnit
168
+ : messages.productOptionsSection.actions.addUnit;
169
+ const quantityColumnLabel = isPersonOnly
170
+ ? messages.productOptionsSection.columns.personQuantity
171
+ : hasRoomUnits
172
+ ? messages.productOptionsSection.columns.roomQuantity
173
+ : messages.productOptionsSection.columns.quantity;
174
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: unitsTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: unitsDescription })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => {
78
175
  setEditingUnit(undefined);
79
176
  setDialogOpen(true);
80
- }, children: [_jsx(Plus, { className: "mr-2 size-3.5", "aria-hidden": "true" }), messages.productOptionsSection.actions.addUnit] })] }), isPending ? (_jsx("div", { className: "flex min-h-20 items-center justify-center rounded-md border bg-background", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.units })) : units.length === 0 ? (_jsx("p", { className: "rounded-md border bg-background px-3 py-4 text-sm text-muted-foreground", children: messages.productOptionsSection.empty.units })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.productOptionsSection.columns.unitType }), _jsx(TableHead, { children: messages.productOptionsSection.columns.unitName }), _jsx(TableHead, { children: messages.productOptionsSection.columns.quantity }), _jsx(TableHead, { children: messages.productOptionsSection.columns.age }), _jsx(TableHead, { children: messages.productOptionsSection.columns.occupancy }), _jsx(TableHead, { className: "w-[88px] text-right", children: messages.productOptionsSection.columns.actions })] }) }), _jsx(TableBody, { children: units.map((unit) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Badge, { variant: "outline", children: messages.common.optionUnitTypeLabels[unit.unitType] }) }), _jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: unit.name }), unit.code ? (_jsx("div", { className: "font-mono text-xs text-muted-foreground", children: unit.code })) : null] }), _jsx(TableCell, { className: "font-mono text-xs", children: formatRange(unit.minQuantity, unit.maxQuantity) }), _jsx(TableCell, { className: "font-mono text-xs", children: formatRange(unit.minAge, unit.maxAge) }), _jsx(TableCell, { className: "font-mono text-xs", children: formatRange(unit.occupancyMin, unit.occupancyMax) }), _jsx(TableCell, { className: "text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => {
177
+ }, children: [_jsx(Plus, { className: "mr-2 size-3.5", "aria-hidden": "true" }), addUnitLabel] })] }), isPending ? (_jsx("div", { className: "flex min-h-20 items-center justify-center rounded-md border bg-background", children: _jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" }) })) : isError ? (_jsx("p", { className: "text-sm text-destructive", children: messages.productOptionsSection.loadingError.units })) : units.length === 0 ? (_jsx("p", { className: "rounded-md border bg-background px-3 py-4 text-sm text-muted-foreground", children: messages.productOptionsSection.empty.units })) : (_jsx("div", { className: "rounded-md border bg-background", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.productOptionsSection.columns.unitType }), _jsx(TableHead, { children: messages.productOptionsSection.columns.unitName }), _jsx(TableHead, { children: quantityColumnLabel }), showAge ? (_jsx(TableHead, { children: messages.productOptionsSection.columns.age })) : null, showOccupancy ? (_jsx(TableHead, { children: messages.productOptionsSection.columns.occupancy })) : null, _jsx(TableHead, { className: "w-[88px] text-right", children: messages.productOptionsSection.columns.actions })] }) }), _jsx(TableBody, { children: units.map((unit) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: _jsx(Badge, { variant: "outline", children: messages.common.optionUnitTypeLabels[unit.unitType] }) }), _jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: unit.name }), unit.code ? (_jsx("div", { className: "font-mono text-xs text-muted-foreground", children: unit.code })) : null] }), _jsx(TableCell, { children: _jsx("div", { className: "text-xs", children: formatInventory(unit, messages.productOptionsSection) }) }), showAge ? (_jsx(TableCell, { className: "font-mono text-xs", children: formatRange(unit.minAge, unit.maxAge) })) : null, showOccupancy ? (_jsx(TableCell, { children: _jsx("div", { className: "text-xs", children: formatOccupancyText(unit, messages.productOptionsSection) }) })) : null, _jsx(TableCell, { className: "text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => {
81
178
  setEditingUnit(unit);
82
179
  setDialogOpen(true);
83
180
  }, "aria-label": messages.productOptionsSection.actions.edit, children: _jsx(Pencil, { className: "size-4", "aria-hidden": "true" }) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => {
package/dist/i18n/en.d.ts CHANGED
@@ -742,14 +742,20 @@ export declare const productsUiEn: {
742
742
  titles: {
743
743
  default: string;
744
744
  units: string;
745
+ personUnits: string;
746
+ roomUnits: string;
745
747
  };
746
748
  descriptions: {
747
749
  default: string;
748
750
  units: string;
751
+ personUnits: string;
752
+ roomUnits: string;
749
753
  };
750
754
  actions: {
751
755
  addOption: string;
752
756
  addUnit: string;
757
+ addPersonUnit: string;
758
+ addRoomUnit: string;
753
759
  duplicate: string;
754
760
  edit: string;
755
761
  delete: string;
@@ -762,6 +768,10 @@ export declare const productsUiEn: {
762
768
  options: string;
763
769
  units: string;
764
770
  };
771
+ configurationWarnings: {
772
+ roomOptionsTitle: string;
773
+ roomOptionsDescription: string;
774
+ };
765
775
  deleteConfirm: {
766
776
  option: string;
767
777
  unit: string;
@@ -770,10 +780,21 @@ export declare const productsUiEn: {
770
780
  unitType: string;
771
781
  unitName: string;
772
782
  quantity: string;
783
+ personQuantity: string;
784
+ roomQuantity: string;
773
785
  age: string;
774
786
  occupancy: string;
775
787
  actions: string;
776
788
  };
789
+ unitSummaries: {
790
+ range: string;
791
+ rooms: string;
792
+ roomsWithCount: string;
793
+ vehicles: string;
794
+ vehiclesWithCount: string;
795
+ sleeps: string;
796
+ sleepsRange: string;
797
+ };
777
798
  badges: {
778
799
  defaultOption: string;
779
800
  };
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+wBK,CAAA"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0yBK,CAAA"}