@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.
- package/dist/components/option-unit-form.d.ts.map +1 -1
- package/dist/components/option-unit-form.js +6 -4
- package/dist/components/product-detail/date-picker.d.ts +44 -0
- package/dist/components/product-detail/date-picker.d.ts.map +1 -0
- package/dist/components/product-detail/date-picker.js +125 -0
- package/dist/components/product-detail/host.d.ts +53 -0
- package/dist/components/product-detail/host.d.ts.map +1 -0
- package/dist/components/product-detail/host.js +24 -0
- package/dist/components/product-detail/index.d.ts +6 -0
- package/dist/components/product-detail/index.d.ts.map +1 -0
- package/dist/components/product-detail/index.js +5 -0
- package/dist/components/product-detail/product-activity-section.d.ts +4 -0
- package/dist/components/product-detail/product-activity-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-activity-section.js +37 -0
- package/dist/components/product-detail/product-day-sheet.d.ts +14 -0
- package/dist/components/product-detail/product-day-sheet.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-sheet.js +75 -0
- package/dist/components/product-detail/product-day-translation.d.ts +41 -0
- package/dist/components/product-detail/product-day-translation.d.ts.map +1 -0
- package/dist/components/product-detail/product-day-translation.js +111 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-departure-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-dialog.js +10 -0
- package/dist/components/product-detail/product-departure-form.d.ts +25 -0
- package/dist/components/product-detail/product-departure-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-form.js +217 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts +8 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-departure-pricing-override-dialog.js +125 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts +14 -0
- package/dist/components/product-detail/product-detail-day-row.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-day-row.js +43 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts +10 -0
- package/dist/components/product-detail/product-detail-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-dialog.js +10 -0
- package/dist/components/product-detail/product-detail-form.d.ts +19 -0
- package/dist/components/product-detail/product-detail-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-form.js +177 -0
- package/dist/components/product-detail/product-detail-header.d.ts +12 -0
- package/dist/components/product-detail/product-detail-header.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-header.js +19 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts +4 -0
- package/dist/components/product-detail/product-detail-itinerary-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-itinerary-section.js +201 -0
- package/dist/components/product-detail/product-detail-page.d.ts +4 -0
- package/dist/components/product-detail/product-detail-page.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-page.js +97 -0
- package/dist/components/product-detail/product-detail-sections.d.ts +63 -0
- package/dist/components/product-detail/product-detail-sections.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-sections.js +143 -0
- package/dist/components/product-detail/product-detail-shared.d.ts +264 -0
- package/dist/components/product-detail/product-detail-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-shared.js +157 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts +9 -0
- package/dist/components/product-detail/product-detail-skeleton.d.ts.map +1 -0
- package/dist/components/product-detail/product-detail-skeleton.js +53 -0
- package/dist/components/product-detail/product-extras-section.d.ts +4 -0
- package/dist/components/product-detail/product-extras-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-extras-section.js +141 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts +16 -0
- package/dist/components/product-detail/product-itinerary-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-itinerary-form.js +38 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts +6 -0
- package/dist/components/product-detail/product-market-rules-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-market-rules-section.js +81 -0
- package/dist/components/product-detail/product-media-gallery.d.ts +19 -0
- package/dist/components/product-detail/product-media-gallery.d.ts.map +1 -0
- package/dist/components/product-detail/product-media-gallery.js +114 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts +29 -0
- package/dist/components/product-detail/product-option-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-option-price-rule-form.js +125 -0
- package/dist/components/product-detail/product-options-pricing.d.ts +6 -0
- package/dist/components/product-detail/product-options-pricing.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-pricing.js +363 -0
- package/dist/components/product-detail/product-options-shared.d.ts +609 -0
- package/dist/components/product-detail/product-options-shared.d.ts.map +1 -0
- package/dist/components/product-detail/product-options-shared.js +34 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts +17 -0
- package/dist/components/product-detail/product-payment-policy-section.d.ts.map +1 -0
- package/dist/components/product-detail/product-payment-policy-section.js +58 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts +11 -0
- package/dist/components/product-detail/product-schedule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-dialog.js +10 -0
- package/dist/components/product-detail/product-schedule-form.d.ts +17 -0
- package/dist/components/product-detail/product-schedule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-schedule-form.js +222 -0
- package/dist/components/product-detail/product-service-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-service-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-dialog.js +10 -0
- package/dist/components/product-detail/product-service-form.d.ts +22 -0
- package/dist/components/product-detail/product-service-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-service-form.js +154 -0
- package/dist/components/product-detail/product-translation-popover.d.ts +91 -0
- package/dist/components/product-detail/product-translation-popover.d.ts.map +1 -0
- package/dist/components/product-detail/product-translation-popover.js +217 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts +12 -0
- package/dist/components/product-detail/product-unit-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-form.d.ts +26 -0
- package/dist/components/product-detail/product-unit-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-form.js +109 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts +16 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-dialog.js +10 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts +28 -0
- package/dist/components/product-detail/product-unit-price-rule-form.d.ts.map +1 -0
- package/dist/components/product-detail/product-unit-price-rule-form.js +126 -0
- package/dist/components/product-detail/timezone-options.d.ts +9 -0
- package/dist/components/product-detail/timezone-options.d.ts.map +1 -0
- package/dist/components/product-detail/timezone-options.js +28 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts +41 -0
- package/dist/components/product-detail/use-product-detail-data.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-data.js +143 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts +24 -0
- package/dist/components/product-detail/use-product-detail-dialogs.d.ts.map +1 -0
- package/dist/components/product-detail/use-product-detail-dialogs.js +40 -0
- package/dist/components/product-detail/zod-resolver.d.ts +4 -0
- package/dist/components/product-detail/zod-resolver.d.ts.map +1 -0
- package/dist/components/product-detail/zod-resolver.js +39 -0
- package/dist/components/product-option-form.js +1 -1
- package/dist/components/product-options-section.d.ts +3 -1
- package/dist/components/product-options-section.d.ts.map +1 -1
- package/dist/components/product-options-section.js +102 -5
- package/dist/i18n/en.d.ts +21 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +54 -33
- package/dist/i18n/messages.d.ts +21 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +42 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +21 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +53 -32
- 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
|
+
}
|
|
@@ -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":"
|
|
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
|
-
|
|
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" }),
|
|
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
|
};
|
package/dist/i18n/en.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0yBK,CAAA"}
|