@escapenavigator/hooks 2.0.49 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/players-and-variation-picker/index.d.ts +50 -49
- package/dist/players-and-variation-picker/index.js +267 -219
- package/dist/players-and-variation-picker/ticket-row.d.ts +2 -0
- package/dist/players-and-variation-picker/ticket-row.js +55 -4
- package/dist/use-players-and-variation-picker/types.d.ts +8 -0
- package/package.json +5 -5
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { ProfileCurrencyEnum } from '@escapenavigator/types/dist/profile/enum/profile-currency';
|
|
2
2
|
import { SlotDiscountTypeEnum } from '@escapenavigator/types/dist/slot/enum/slot-discount-type.enum';
|
|
3
3
|
import { TariffReducedRateModeEnum } from '@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum';
|
|
4
|
+
import { OrderComposition, TariffCategory } from '@escapenavigator/types/dist/tariff/tariff-category';
|
|
4
5
|
import React from 'react';
|
|
5
6
|
import { usePlayersAndVariationPicker } from '../use-players-and-variation-picker';
|
|
6
7
|
import { PickerSlotDetails, PickerSlotInfo, PickerSlotVariation, PickerVariationChange } from '../use-players-and-variation-picker/types';
|
|
7
8
|
type LoadSlotDetailsApi = Parameters<typeof usePlayersAndVariationPicker>[0]['loadSlotDetails'];
|
|
9
|
+
/** Денормализованный состав для хостов, которые ещё живут на старых полях. */
|
|
10
|
+
export type LegacyCounts = {
|
|
11
|
+
players: number;
|
|
12
|
+
children: number;
|
|
13
|
+
reducedPlayers: number;
|
|
14
|
+
};
|
|
8
15
|
export type PlayersAndVariationPickerProps = {
|
|
9
16
|
/**
|
|
10
17
|
* Minimal slot identity used to fetch enriched details. Pass
|
|
@@ -25,9 +32,7 @@ export type PlayersAndVariationPickerProps = {
|
|
|
25
32
|
onVariationChange?: (change: PickerVariationChange) => void;
|
|
26
33
|
/**
|
|
27
34
|
* Notifies the host whenever the embedded picker resolves the currently
|
|
28
|
-
* highlighted variation (initial load or user click).
|
|
29
|
-
* host needs the variation's tariff/duration outside of the picker UI
|
|
30
|
-
* (e.g. to render a price list or seed Formik initial values).
|
|
35
|
+
* highlighted variation (initial load or user click).
|
|
31
36
|
*/
|
|
32
37
|
onVariationResolved?: (variation: PickerSlotVariation | undefined) => void;
|
|
33
38
|
/**
|
|
@@ -36,6 +41,20 @@ export type PlayersAndVariationPickerProps = {
|
|
|
36
41
|
* persisted `slotId` (fix) or rely on coordinate-based booking (flex).
|
|
37
42
|
*/
|
|
38
43
|
onDetailsLoaded?: (details: PickerSlotDetails | null | undefined) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Состав по категориям. Ведущее поле: когда передано непустым, теми же
|
|
46
|
+
* ключами и рисуются строки. Пусто — состав собирается из `players` /
|
|
47
|
+
* `child` / `reducedPlayers`, чтобы не мигрировать всех хостов сразу.
|
|
48
|
+
*/
|
|
49
|
+
composition?: OrderComposition;
|
|
50
|
+
/**
|
|
51
|
+
* Категории тарифа. По умолчанию берутся из выбранной вариации
|
|
52
|
+
* (`/slots/details`). Явно передают там, где условия нужно взять со
|
|
53
|
+
* снимка на брони, а не с текущего тарифа, — карточка оформленной игры.
|
|
54
|
+
*/
|
|
55
|
+
categories?: TariffCategory[];
|
|
56
|
+
/** Fires on every composition change, вместе с денормом старых полей. */
|
|
57
|
+
onCompositionChange?: (composition: OrderComposition, legacy: LegacyCounts) => void;
|
|
39
58
|
/** Players form field value. */
|
|
40
59
|
players: number;
|
|
41
60
|
/** Children form field value (defaults to 0). */
|
|
@@ -44,9 +63,8 @@ export type PlayersAndVariationPickerProps = {
|
|
|
44
63
|
reducedPlayers?: number;
|
|
45
64
|
/**
|
|
46
65
|
* Льготная ставка и минимальная стоимость игры. По умолчанию берутся из
|
|
47
|
-
* выбранной
|
|
48
|
-
*
|
|
49
|
-
* оформленной брони.
|
|
66
|
+
* выбранной вариации. Явно передают там, где условия нужно взять со
|
|
67
|
+
* снимка на заказе, а не с текущего тарифа.
|
|
50
68
|
*/
|
|
51
69
|
reduced?: {
|
|
52
70
|
minGameAmount?: number | null;
|
|
@@ -58,9 +76,8 @@ export type PlayersAndVariationPickerProps = {
|
|
|
58
76
|
/** Fires when the guest changes the number of reduced seats. */
|
|
59
77
|
onReducedChange?: (value: number) => void;
|
|
60
78
|
/**
|
|
61
|
-
* Override for the price map used to render
|
|
62
|
-
* the selected variation's tariff
|
|
63
|
-
* already maintains its own tariff state (e.g. legacy flows).
|
|
79
|
+
* Override for the price map used to render the tariff grid. Defaults to
|
|
80
|
+
* the selected variation's tariff.
|
|
64
81
|
*/
|
|
65
82
|
tariff?: Record<string, number>;
|
|
66
83
|
currency: ProfileCurrencyEnum;
|
|
@@ -68,67 +85,51 @@ export type PlayersAndVariationPickerProps = {
|
|
|
68
85
|
minPlayers?: number;
|
|
69
86
|
/**
|
|
70
87
|
* Capacity cap (questroom max, ticket-system `numSeatsAvailable`, etc.).
|
|
71
|
-
* Player/children buttons exceeding `min(maxFromTariff, maxPlayersLimit)`
|
|
72
|
-
* are filtered out (not just disabled).
|
|
73
88
|
*/
|
|
74
89
|
maxPlayersLimit?: number;
|
|
75
90
|
/**
|
|
76
91
|
* Slot discount: basis points (1% = 100) for `percent`, an amount in
|
|
77
|
-
* minor units for `fixed`.
|
|
78
|
-
* mirroring the widget behaviour.
|
|
92
|
+
* minor units for `fixed`.
|
|
79
93
|
*/
|
|
80
94
|
slotDiscount?: number;
|
|
81
95
|
/** Defaults to `percent` for slots created before fixed discounts. */
|
|
82
96
|
slotDiscountType?: SlotDiscountTypeEnum;
|
|
83
97
|
/** External validation error label (controlled by the host form). */
|
|
84
98
|
error?: string;
|
|
85
|
-
/**
|
|
86
|
-
* Translator. Required for the embedded `usePlayersAndVariationPicker`
|
|
87
|
-
* (it composes the default flex duration label).
|
|
88
|
-
*/
|
|
99
|
+
/** Translator. */
|
|
89
100
|
t: (key: string, options?: Record<string, unknown>) => string;
|
|
90
|
-
/** Fires on every player/children change. */
|
|
101
|
+
/** Fires on every player/children change. Денорм из состава по категориям. */
|
|
91
102
|
onChange?: (field: 'players' | 'children', value: number) => void;
|
|
92
103
|
/**
|
|
93
|
-
* When provided, renders a Save button inside the picker. The
|
|
94
|
-
*
|
|
104
|
+
* When provided, renders a Save button inside the picker. The composition
|
|
105
|
+
* is buffered locally until the user saves.
|
|
95
106
|
*/
|
|
96
|
-
onSave?: (data: {
|
|
97
|
-
|
|
98
|
-
children: number;
|
|
99
|
-
reducedPlayers: number;
|
|
107
|
+
onSave?: (data: LegacyCounts & {
|
|
108
|
+
composition: OrderComposition;
|
|
100
109
|
}) => void;
|
|
101
110
|
/**
|
|
102
111
|
* When editing an existing order, pass the order id here so the backend
|
|
103
112
|
* excludes the current booking from occupancy calculations.
|
|
104
113
|
*/
|
|
105
114
|
orderId?: number;
|
|
115
|
+
/** Fires whenever the internal loading flag changes. */
|
|
116
|
+
onLoadingChange?: (loading: boolean) => void;
|
|
106
117
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* having to duplicate the hook call themselves.
|
|
112
|
-
*
|
|
113
|
-
* Starts `true` and flips to `false` once the first `/slots/details`
|
|
114
|
-
* resolves (success or error).
|
|
118
|
+
* Состав можно оформить: гостей хватает на минимальную игру, цена на
|
|
119
|
+
* такой состав задана и все влезают в комнату. Хост гасит по нему свою
|
|
120
|
+
* кнопку «дальше» — пикер намеренно не правит состав за гостя, поэтому
|
|
121
|
+
* непригодное состояние он показывает, а не прячет.
|
|
115
122
|
*/
|
|
116
|
-
|
|
123
|
+
onValidityChange?: (bookable: boolean) => void;
|
|
117
124
|
/**
|
|
118
|
-
* Прячет встроенные ценовые
|
|
119
|
-
*
|
|
120
|
-
* команду / за человека». Нужен консьюмерам, которые показывают эти
|
|
121
|
-
* данные консолидированно в своём месте (например виджет — в пунктирной
|
|
122
|
-
* сводке рядом с «Итого»). По умолчанию `false` — поведение не меняется.
|
|
125
|
+
* Прячет встроенные ценовые подсказки и показывает вместо них одну
|
|
126
|
+
* пунктирную сводку. Нужен виджету, который сводит цену в одно место.
|
|
123
127
|
*/
|
|
124
128
|
hidePriceHints?: boolean;
|
|
125
129
|
/**
|
|
126
130
|
* Опция «Определимся на месте» (questroom.playersSelectMode=OPTIONAL).
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* детей скрыта (и дети сброшены в 0), ценовые подсказки пикера не
|
|
130
|
-
* показываются — хост рендерит «базовую стоимость от» сам. Клик по
|
|
131
|
-
* числу автоматически снимает опцию (onSelect(false)).
|
|
131
|
+
* Пока выбрана — строки состава скрыты и состав сброшен, ценовые
|
|
132
|
+
* подсказки не показываются: хост рендерит «базовую стоимость от» сам.
|
|
132
133
|
*/
|
|
133
134
|
flexPlayersOption?: {
|
|
134
135
|
selected: boolean;
|
|
@@ -137,13 +138,13 @@ export type PlayersAndVariationPickerProps = {
|
|
|
137
138
|
};
|
|
138
139
|
};
|
|
139
140
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
141
|
+
* Единый пикер «вариация + состав», который используют админские модалки
|
|
142
|
+
* заказа, виджет и пакет orders.
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
144
|
+
* Состав описывается категориями участников тарифа: взрослые, дети до 7,
|
|
145
|
+
* студенты, пенсионеры — все одинаковыми строками со степпером. Цена
|
|
146
|
+
* категории считается от доли групповой цены на текущий состав, поэтому она
|
|
147
|
+
* живая: гость в команде из шести дешевле, чем в команде из двух.
|
|
147
148
|
*/
|
|
148
149
|
export declare const PlayersAndVariationPicker: React.FC<PlayersAndVariationPickerProps>;
|
|
149
150
|
export {};
|
|
@@ -48,9 +48,10 @@ const skeleton_1 = require("@alphakits/ui/dist/skeleton");
|
|
|
48
48
|
const tag_1 = require("@alphakits/ui/dist/tag");
|
|
49
49
|
const toast_plate_1 = require("@alphakits/ui/dist/toast-plate");
|
|
50
50
|
const typography_1 = require("@alphakits/ui/dist/typography");
|
|
51
|
+
const tariff_category_mode_enum_1 = require("@escapenavigator/types/dist/tariff/enum/tariff-category-mode.enum");
|
|
52
|
+
const composition_limits_1 = require("@escapenavigator/utils/dist/composition-limits");
|
|
53
|
+
const composition_price_1 = require("@escapenavigator/utils/dist/composition-price");
|
|
51
54
|
const format_amount_1 = require("@escapenavigator/utils/dist/format-amount");
|
|
52
|
-
const get_players_price_1 = require("@escapenavigator/utils/dist/get-players-price");
|
|
53
|
-
const get_prices_1 = require("@escapenavigator/utils/dist/get-prices");
|
|
54
55
|
const get_slot_discount_amount_1 = require("@escapenavigator/utils/dist/get-slot-discount-amount");
|
|
55
56
|
const reduced_price_1 = require("@escapenavigator/utils/dist/reduced-price");
|
|
56
57
|
const react_1 = __importStar(require("react"));
|
|
@@ -58,30 +59,20 @@ const react_i18next_1 = require("react-i18next");
|
|
|
58
59
|
const use_players_and_variation_picker_1 = require("../use-players-and-variation-picker");
|
|
59
60
|
const ticket_row_1 = require("./ticket-row");
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
62
|
+
* Единый пикер «вариация + состав», который используют админские модалки
|
|
63
|
+
* заказа, виджет и пакет orders.
|
|
63
64
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Состав описывается категориями участников тарифа: взрослые, дети до 7,
|
|
66
|
+
* студенты, пенсионеры — все одинаковыми строками со степпером. Цена
|
|
67
|
+
* категории считается от доли групповой цены на текущий состав, поэтому она
|
|
68
|
+
* живая: гость в команде из шести дешевле, чем в команде из двух.
|
|
68
69
|
*/
|
|
69
|
-
const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loadSlotDetails, onVariationChange, onVariationResolved, onDetailsLoaded, players = 0, child = 0, reducedPlayers = 0, reduced: reducedOverride, onReducedChange, tariff: tariffOverride, currency, minPlayers, maxPlayersLimit, slotDiscount, slotDiscountType, error, t, onChange, onSave, orderId, onLoadingChange, hidePriceHints = false, flexPlayersOption, }) => {
|
|
70
|
-
var _a
|
|
70
|
+
const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loadSlotDetails, onVariationChange, onVariationResolved, onDetailsLoaded, composition: compositionProp, categories: categoriesOverride, onCompositionChange, players = 0, child = 0, reducedPlayers = 0, reduced: reducedOverride, onReducedChange, tariff: tariffOverride, currency, minPlayers, maxPlayersLimit, slotDiscount, slotDiscountType, error, t, onChange, onSave, orderId, onLoadingChange, onValidityChange, hidePriceHints = false, flexPlayersOption, }) => {
|
|
71
|
+
var _a;
|
|
71
72
|
/**
|
|
72
|
-
* Страховка от «голых ключей»
|
|
73
|
-
* `
|
|
74
|
-
*
|
|
75
|
-
* грузится лениво — если юзер открыл карточку игры раньше, чем
|
|
76
|
-
* какой-либо экран задекларировал этот ns, бандл никогда не
|
|
77
|
-
* запрашивался и вместо переводов светились ключи/фолбэки.
|
|
78
|
-
*
|
|
79
|
-
* Декларируем namespace'ы прямо здесь: react-i18next сам догрузит
|
|
80
|
-
* недостающие бандлы и перерендерит компонент, когда они приедут.
|
|
81
|
-
* `useSuspense: false` — чтобы не требовать Suspense-границу у всех
|
|
82
|
-
* хостов; вместо этого до готовности показываем скелетон ниже.
|
|
83
|
-
* При ошибке загрузки i18next помечает ns как «loaded (failed)»,
|
|
84
|
-
* так что вечного скелетона не будет.
|
|
73
|
+
* Страховка от «голых ключей» перевода: хост часто берёт `t` из
|
|
74
|
+
* `useTranslation()` без нужных namespace'ов, а в CRM `prices` грузится
|
|
75
|
+
* лениво. Декларируем их здесь — i18next догрузит бандлы и перерендерит.
|
|
85
76
|
*/
|
|
86
77
|
const { ready: translationsReady } = (0, react_i18next_1.useTranslation)(['prices', 'common'], {
|
|
87
78
|
useSuspense: false,
|
|
@@ -94,11 +85,6 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
94
85
|
t: t,
|
|
95
86
|
orderId,
|
|
96
87
|
});
|
|
97
|
-
/**
|
|
98
|
-
* Прокидываем loading-сигнал наверх через ref'нутый колбэк, чтобы
|
|
99
|
-
* не зависеть от референциальной стабильности `onLoadingChange`
|
|
100
|
-
* со стороны хоста.
|
|
101
|
-
*/
|
|
102
88
|
const onLoadingChangeRef = (0, react_1.useRef)(onLoadingChange);
|
|
103
89
|
onLoadingChangeRef.current = onLoadingChange;
|
|
104
90
|
(0, react_1.useEffect)(() => {
|
|
@@ -132,132 +118,149 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
132
118
|
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.duration,
|
|
133
119
|
selectedVariation,
|
|
134
120
|
]);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const [_reduced, setReduced] = (0, react_1.useState)(reducedPlayers);
|
|
138
|
-
(0, react_1.useEffect)(() => {
|
|
139
|
-
setPlayers(players);
|
|
140
|
-
}, [players]);
|
|
141
|
-
(0, react_1.useEffect)(() => {
|
|
142
|
-
setChildren(child);
|
|
143
|
-
}, [child]);
|
|
144
|
-
(0, react_1.useEffect)(() => {
|
|
145
|
-
setReduced(reducedPlayers);
|
|
146
|
-
}, [reducedPlayers]);
|
|
147
|
-
const flexSelected = !!(flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.selected);
|
|
148
|
-
const handleChangePlayers = (value) => {
|
|
149
|
-
setPlayers(value);
|
|
150
|
-
onChange === null || onChange === void 0 ? void 0 : onChange('players', value);
|
|
151
|
-
// Явный выбор числа снимает «Определимся на месте».
|
|
152
|
-
if (flexSelected)
|
|
153
|
-
flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.onSelect(false);
|
|
154
|
-
};
|
|
155
|
-
const handleChangeChildren = (value) => {
|
|
156
|
-
setChildren(value);
|
|
157
|
-
onChange === null || onChange === void 0 ? void 0 : onChange('children', value);
|
|
158
|
-
};
|
|
159
|
-
const handleSelectFlex = () => {
|
|
160
|
-
if (flexSelected)
|
|
161
|
-
return;
|
|
162
|
-
// Состав не определён — детей тоже сбрасываем (их выбор скрывается).
|
|
163
|
-
if (_children)
|
|
164
|
-
handleChangeChildren(0);
|
|
165
|
-
flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.onSelect(true);
|
|
166
|
-
};
|
|
167
|
-
// Цена 0 за число игроков = «это кол-во недоступно для выбора». Такие
|
|
168
|
-
// ключи не рендерим кнопками и не учитываем в availablePlayerKeys/clamp.
|
|
169
|
-
const playersOptions = (0, react_1.useMemo)(() => Object.entries(tariff)
|
|
170
|
-
.filter(([key, price]) => key !== 'children' && !Number.isNaN(+key) && +price > 0)
|
|
171
|
-
.map(([key, price]) => ({
|
|
172
|
-
key: +key,
|
|
173
|
-
price,
|
|
174
|
-
})), [tariff]);
|
|
175
|
-
const playersArray = (0, get_prices_1.getPricesPlayersArray)(tariff);
|
|
176
|
-
const maxFromTariff = playersArray.length ? +playersArray[playersArray.length - 1] : 0;
|
|
177
|
-
const minFromTariff = playersArray.length ? +playersArray[0] : 0;
|
|
178
|
-
// Верхняя граница = пересечение тарифа и questroom-капа (с учётом
|
|
179
|
-
// numSeatsAvailable). За пределы тарифа всё равно нельзя — нет цены.
|
|
180
|
-
const effectiveMax = typeof maxPlayersLimit === 'number' && Number.isFinite(maxPlayersLimit)
|
|
181
|
-
? Math.min(maxFromTariff, maxPlayersLimit)
|
|
182
|
-
: maxFromTariff;
|
|
183
|
-
// Нижняя граница — приоритет questroom.playersMin (canonical), но не
|
|
184
|
-
// ниже минимального ключа тарифа (иначе не на что кликать). Если
|
|
185
|
-
// questroom.playersMin шире тарифа — берём тарифный минимум.
|
|
186
|
-
const effectiveMin = typeof minPlayers === 'number' && Number.isFinite(minPlayers)
|
|
187
|
-
? Math.max(minFromTariff, minPlayers)
|
|
188
|
-
: minFromTariff;
|
|
189
|
-
// Единственный источник истины «что реально можно выбрать»: ключи
|
|
190
|
-
// прайса тарифа, попадающие в [effectiveMin, effectiveMax] с учётом
|
|
191
|
-
// детей. Ровно те же числа рендерятся кнопками ниже.
|
|
192
|
-
const availablePlayerKeys = (0, react_1.useMemo)(() => playersOptions
|
|
193
|
-
.map((p) => p.key)
|
|
194
|
-
.filter((key) => {
|
|
195
|
-
const total = key + (_children || 0);
|
|
196
|
-
return total >= effectiveMin && total <= effectiveMax;
|
|
197
|
-
}), [playersOptions, effectiveMin, effectiveMax, _children]);
|
|
198
|
-
// Если уже выбранное (или засеянное хостом) число игроков выпало из
|
|
199
|
-
// доступного набора — например, после смены вариации/тарифа на более
|
|
200
|
-
// узкий диапазон — подтягиваем его к ближайшему валидному. Без этого
|
|
201
|
-
// `values.players` остаётся «залипшим» на count, которого в новом
|
|
202
|
-
// тарифе нет, и на бэке `tariff.price[players]` = undefined → NaN →
|
|
203
|
-
// 500 при создании заказа. `_players === 0/undefined` трактуем как
|
|
204
|
-
// «ещё не выбрано» и не трогаем (валидация формы заставит выбрать).
|
|
205
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: однонаправленный clamp, зависим только от набора и значения
|
|
206
|
-
(0, react_1.useEffect)(() => {
|
|
207
|
-
if (!availablePlayerKeys.length)
|
|
208
|
-
return;
|
|
209
|
-
if (!_players)
|
|
210
|
-
return;
|
|
211
|
-
if (availablePlayerKeys.includes(_players))
|
|
212
|
-
return;
|
|
213
|
-
const nearest = availablePlayerKeys.reduce((acc, key) => Math.abs(key - _players) < Math.abs(acc - _players) ? key : acc);
|
|
214
|
-
setPlayers(nearest);
|
|
215
|
-
onChange === null || onChange === void 0 ? void 0 : onChange('players', nearest);
|
|
216
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
217
|
-
}, [availablePlayerKeys, _players]);
|
|
218
|
-
const childrenOptions = (0, react_1.useMemo)(() => {
|
|
219
|
-
if (!tariff.child || !maxFromTariff)
|
|
220
|
-
return [];
|
|
221
|
-
const maxChildren = maxFromTariff + 1;
|
|
222
|
-
return Array.from({ length: maxChildren }, (_, i) => i);
|
|
223
|
-
}, [tariff, maxFromTariff]);
|
|
224
|
-
// Условия льготной ставки: снимок с брони (если хост его передал) важнее
|
|
225
|
-
// текущего тарифа — иначе повышение цен переоценило бы открытую карточку.
|
|
121
|
+
// Условия игры: снимок с брони (если хост его передал) важнее текущего
|
|
122
|
+
// тарифа — иначе повышение цен переоценило бы открытую карточку.
|
|
226
123
|
const reducedConfig = (0, react_1.useMemo)(() => {
|
|
227
|
-
var _a, _b, _c, _d, _e, _f;
|
|
124
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
228
125
|
return ({
|
|
229
126
|
minGameAmount: (_b = (_a = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.minGameAmount) !== null && _a !== void 0 ? _a : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.minGameAmount) !== null && _b !== void 0 ? _b : 0,
|
|
230
127
|
reducedRateMode: (_d = (_c = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRateMode) !== null && _c !== void 0 ? _c : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRateMode) !== null && _d !== void 0 ? _d : null,
|
|
231
128
|
reducedRate: (_f = (_e = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRate) !== null && _e !== void 0 ? _e : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRate) !== null && _f !== void 0 ? _f : 0,
|
|
129
|
+
reducedTitle: (_h = (_g = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedTitle) !== null && _g !== void 0 ? _g : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedTitle) !== null && _h !== void 0 ? _h : null,
|
|
130
|
+
reducedPriceDescription: (_k = (_j = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedPriceDescription) !== null && _j !== void 0 ? _j : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedPriceDescription) !== null && _k !== void 0 ? _k : null,
|
|
131
|
+
childPriceDescription: (_l = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription) !== null && _l !== void 0 ? _l : null,
|
|
232
132
|
});
|
|
233
133
|
}, [
|
|
234
134
|
reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.minGameAmount,
|
|
235
135
|
reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRateMode,
|
|
236
136
|
reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRate,
|
|
137
|
+
reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedTitle,
|
|
138
|
+
reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedPriceDescription,
|
|
237
139
|
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.minGameAmount,
|
|
238
140
|
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRateMode,
|
|
239
141
|
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRate,
|
|
142
|
+
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedTitle,
|
|
143
|
+
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedPriceDescription,
|
|
144
|
+
selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription,
|
|
240
145
|
]);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Категории тарифа. Бэкенд отдаёт их заполненными, но резолвер оставлен
|
|
148
|
+
* ради хостов, которые ещё передают только прайс и льготную ставку.
|
|
149
|
+
*/
|
|
150
|
+
const categories = (0, react_1.useMemo)(() => (0, composition_price_1.resolveCategories)({
|
|
151
|
+
price: tariff,
|
|
152
|
+
categories: categoriesOverride !== null && categoriesOverride !== void 0 ? categoriesOverride : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.categories,
|
|
153
|
+
legacy: reducedConfig,
|
|
154
|
+
}), [tariff, categoriesOverride, selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.categories, reducedConfig]);
|
|
155
|
+
const [_composition, setComposition] = (0, react_1.useState)(() => compositionProp && Object.keys(compositionProp).length
|
|
156
|
+
? compositionProp
|
|
157
|
+
: (0, composition_price_1.compositionFromLegacy)({ players, children: child, reducedPlayers }, categories));
|
|
158
|
+
/**
|
|
159
|
+
* Денорм, который пикер сам отправил наверх последним. Хост кладёт его в
|
|
160
|
+
* свою форму и возвращает пропсами — и без этой отметки эхо собственного
|
|
161
|
+
* ответа схлопнуло бы несколько скидочных категорий в одну (в старых
|
|
162
|
+
* полях льготные места — одно число).
|
|
163
|
+
*/
|
|
164
|
+
const emittedLegacy = (0, react_1.useRef)(null);
|
|
165
|
+
/**
|
|
166
|
+
* Синхронизация сверху вниз. Состав по категориям приоритетнее старых
|
|
167
|
+
* полей: их читаем, только пока хост на него не перешёл.
|
|
168
|
+
*/
|
|
169
|
+
(0, react_1.useEffect)(() => {
|
|
170
|
+
if (compositionProp && Object.keys(compositionProp).length) {
|
|
171
|
+
setComposition(compositionProp);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const echo = emittedLegacy.current;
|
|
175
|
+
if (echo &&
|
|
176
|
+
echo.players === players &&
|
|
177
|
+
echo.children === child &&
|
|
178
|
+
echo.reducedPlayers === reducedPlayers) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setComposition((0, composition_price_1.compositionFromLegacy)({ players, children: child, reducedPlayers }, categories));
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, [compositionProp, players, child, reducedPlayers, categories]);
|
|
184
|
+
const flexSelected = !!(flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.selected);
|
|
185
|
+
/**
|
|
186
|
+
* Границы выбора и пригодность состава. Живут в утилите, а не здесь:
|
|
187
|
+
* арифметика двух независимых потолков (вместимость комнаты против сетки
|
|
188
|
+
* `price[N]`) — ровно то место, где раньше добавленный малыш отнимал
|
|
189
|
+
* место у взрослых, а «плюс» на закрытом уровне отскакивал назад.
|
|
190
|
+
*/
|
|
191
|
+
const limits = (0, react_1.useMemo)(() => (0, composition_limits_1.getCompositionLimits)({
|
|
192
|
+
price: tariff,
|
|
193
|
+
categories,
|
|
194
|
+
composition: _composition,
|
|
195
|
+
minPlayers,
|
|
196
|
+
maxPlayersLimit,
|
|
197
|
+
}), [tariff, categories, _composition, minPlayers, maxPlayersLimit]);
|
|
198
|
+
const { capacityMax, pricingMax, effectiveMin, pricingHeads, totalHeads, levelUnavailable, belowMin, capacityReached, maxByCategory, baseLevelOptions, } = limits;
|
|
199
|
+
const baseCategory = categories.find((category) => category.mode === tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE);
|
|
200
|
+
const baseCategoryKey = baseCategory === null || baseCategory === void 0 ? void 0 : baseCategory.key;
|
|
201
|
+
const applyComposition = (next) => {
|
|
202
|
+
setComposition(next);
|
|
203
|
+
const legacy = (0, composition_price_1.legacyCountsFromComposition)(next, categories);
|
|
204
|
+
emittedLegacy.current = legacy;
|
|
205
|
+
onCompositionChange === null || onCompositionChange === void 0 ? void 0 : onCompositionChange(next, legacy);
|
|
206
|
+
// Денорм для хостов на старых полях: их формы всё ещё хранят
|
|
207
|
+
// `players`/`children`/`reducedPlayers` и валидируются по ним.
|
|
208
|
+
onChange === null || onChange === void 0 ? void 0 : onChange('players', legacy.players);
|
|
209
|
+
onChange === null || onChange === void 0 ? void 0 : onChange('children', legacy.children);
|
|
210
|
+
onReducedChange === null || onReducedChange === void 0 ? void 0 : onReducedChange(legacy.reducedPlayers);
|
|
211
|
+
};
|
|
212
|
+
const handleChangeCategory = (key, value) => {
|
|
213
|
+
applyComposition(Object.assign(Object.assign({}, _composition), { [key]: Math.max(0, value) }));
|
|
214
|
+
// Явный выбор состава снимает «Определимся на месте».
|
|
215
|
+
if (flexSelected)
|
|
216
|
+
flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.onSelect(false);
|
|
250
217
|
};
|
|
251
|
-
|
|
252
|
-
|
|
218
|
+
const handleSelectFlex = () => {
|
|
219
|
+
if (flexSelected)
|
|
220
|
+
return;
|
|
221
|
+
// Состав не определён: `players` остаётся минимумом квеста (по нему
|
|
222
|
+
// считается «базовая стоимость от»), а надбавочные категории сбрасываем
|
|
223
|
+
// — платить за детей, которых ещё не посчитали, гость не должен.
|
|
224
|
+
const addOns = categories.filter((category) => !category.countsInPrice && _composition[category.key]);
|
|
225
|
+
if (addOns.length) {
|
|
226
|
+
applyComposition(Object.assign(Object.assign({}, _composition), Object.fromEntries(addOns.map((category) => [category.key, 0]))));
|
|
227
|
+
}
|
|
228
|
+
flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.onSelect(true);
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Единственная автоправка состава: смена вариации сузила сетку, и состав
|
|
232
|
+
* торчит выше нового потолка — иначе на бэке `price[N]` = undefined →
|
|
233
|
+
* NaN → 500. Внутри сетки состав не трогаем даже на закрытом уровне.
|
|
234
|
+
*/
|
|
253
235
|
// biome-ignore lint/correctness/useExhaustiveDependencies: односторонний clamp
|
|
254
236
|
(0, react_1.useEffect)(() => {
|
|
255
|
-
if (
|
|
256
|
-
|
|
237
|
+
if (flexSelected)
|
|
238
|
+
return;
|
|
239
|
+
const clamped = (0, composition_limits_1.clampCompositionToGrid)({
|
|
240
|
+
price: tariff,
|
|
241
|
+
categories,
|
|
242
|
+
composition: _composition,
|
|
243
|
+
minPlayers,
|
|
244
|
+
maxPlayersLimit,
|
|
245
|
+
});
|
|
246
|
+
if (clamped)
|
|
247
|
+
applyComposition(clamped);
|
|
257
248
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
258
|
-
}, [
|
|
259
|
-
const
|
|
260
|
-
const
|
|
249
|
+
}, [pricingMax, pricingHeads, flexSelected]);
|
|
250
|
+
const perPerson = (0, composition_price_1.getPerPersonAmount)(tariff, pricingHeads);
|
|
251
|
+
const bookable = flexSelected || limits.bookable;
|
|
252
|
+
const onValidityChangeRef = (0, react_1.useRef)(onValidityChange);
|
|
253
|
+
onValidityChangeRef.current = onValidityChange;
|
|
254
|
+
(0, react_1.useEffect)(() => {
|
|
255
|
+
var _a;
|
|
256
|
+
(_a = onValidityChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onValidityChangeRef, bookable);
|
|
257
|
+
}, [bookable]);
|
|
258
|
+
const teamPriceBase = (0, composition_price_1.getCompositionPrice)({
|
|
259
|
+
price: tariff,
|
|
260
|
+
categories,
|
|
261
|
+
composition: _composition,
|
|
262
|
+
minGameAmount: reducedConfig.minGameAmount,
|
|
263
|
+
});
|
|
261
264
|
const teamPrice = teamPriceBase -
|
|
262
265
|
(0, get_slot_discount_amount_1.getSlotDiscountAmount)({
|
|
263
266
|
price: teamPriceBase,
|
|
@@ -265,20 +268,42 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
265
268
|
discountType: slotDiscountType,
|
|
266
269
|
rounding: 'floor',
|
|
267
270
|
});
|
|
268
|
-
const totalHeads = _players + (_children || 0);
|
|
269
271
|
const personPrice = totalHeads > 0 ? Math.round(teamPrice / totalHeads / 100) * 100 : 0;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Пол выручки достигнут: следующая скидочная голова уже не снижает цену.
|
|
274
|
+
* Считаем сравнением с составом без скидок — так работает для любого
|
|
275
|
+
* набора категорий, а не только для одной льготной ставки.
|
|
276
|
+
*/
|
|
277
|
+
const priceWithoutDiscounts = (0, composition_price_1.getCompositionPrice)({
|
|
278
|
+
price: tariff,
|
|
279
|
+
categories: categories.map((category) => category.countsInPrice ? Object.assign(Object.assign({}, category), { mode: tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE }) : category),
|
|
280
|
+
composition: _composition,
|
|
281
|
+
minGameAmount: reducedConfig.minGameAmount,
|
|
282
|
+
});
|
|
283
|
+
const minAmount = (0, reduced_price_1.getMinGameAmount)(reducedConfig);
|
|
284
|
+
const floorReached = minAmount > 0 && teamPriceBase <= minAmount && priceWithoutDiscounts > minAmount;
|
|
285
|
+
const minAmountLabel = (0, format_amount_1.formatAmount)(minAmount, currency);
|
|
286
|
+
/**
|
|
287
|
+
* Есть ли в тарифе строки кроме базовых взрослых — льготные места или
|
|
288
|
+
* надбавка сверх группы. Только тогда состав рисуется степперами: гость
|
|
289
|
+
* должен разложить группу по строкам. Без них раскладывать нечего, и
|
|
290
|
+
* работает прежний выбор числа тегами — он компактнее и привычнее.
|
|
291
|
+
*/
|
|
292
|
+
const hasExtraRows = (0, composition_price_1.hasExtraCategories)(categories);
|
|
293
|
+
/** Категории сверх групповой цены — они всегда строка со степпером. */
|
|
294
|
+
const addOnCategories = categories.filter((category) => !category.countsInPrice);
|
|
295
|
+
const handleSelectLevel = (level) => {
|
|
296
|
+
if (!baseCategoryKey)
|
|
297
|
+
return;
|
|
298
|
+
applyComposition(Object.assign(Object.assign({}, _composition), { [baseCategoryKey]: level }));
|
|
299
|
+
if (flexSelected)
|
|
300
|
+
flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.onSelect(false);
|
|
301
|
+
};
|
|
277
302
|
const showVariations = pickerOptions.length > 1;
|
|
278
303
|
// Клиентское описание показываем только для вариаций — у стандартного
|
|
279
304
|
// (default) тарифа описание не выводим.
|
|
280
305
|
const variationDescription = selectedVariation && !selectedVariation.isDefault
|
|
281
|
-
? ((
|
|
306
|
+
? ((_a = selectedVariation.description) !== null && _a !== void 0 ? _a : null)
|
|
282
307
|
: null;
|
|
283
308
|
const handleVariationClick = (optionId, isFree) => {
|
|
284
309
|
if (!isFree)
|
|
@@ -289,12 +314,64 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
289
314
|
return;
|
|
290
315
|
onVariationChange === null || onVariationChange === void 0 ? void 0 : onVariationChange(change);
|
|
291
316
|
};
|
|
317
|
+
/**
|
|
318
|
+
* Название категории. Пустой заголовок — у категорий, собранных из старого
|
|
319
|
+
* формата: там подписи жили в переводах, а не в данных, и локаль гостя
|
|
320
|
+
* должна выигрывать у языка настроек венью.
|
|
321
|
+
*/
|
|
322
|
+
const categoryTitle = (category) => {
|
|
323
|
+
if (category.title)
|
|
324
|
+
return category.title;
|
|
325
|
+
if (category.mode === tariff_category_mode_enum_1.TariffCategoryModeEnum.BASE)
|
|
326
|
+
return t('prices:adults');
|
|
327
|
+
if (!category.countsInPrice)
|
|
328
|
+
return t('prices:kids');
|
|
329
|
+
return t('prices:reducedPlayers', { defaultValue: 'Льготные места' });
|
|
330
|
+
};
|
|
331
|
+
/** Цена гостя категории на текущий состав. */
|
|
332
|
+
const categoryPriceHint = (category) => {
|
|
333
|
+
if (levelUnavailable || (!pricingHeads && category.countsInPrice)) {
|
|
334
|
+
return t('prices:priceDependsOnTeam', {
|
|
335
|
+
defaultValue: 'Цена зависит от состава',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const seatPrice = (0, composition_price_1.getCategorySeatPrice)(category, category.countsInPrice
|
|
339
|
+
? perPerson
|
|
340
|
+
: (0, composition_price_1.getPerPersonAmount)(tariff, pricingHeads || effectiveMin));
|
|
341
|
+
if (!category.countsInPrice) {
|
|
342
|
+
return seatPrice > 0
|
|
343
|
+
? t('prices:categoryOnTop', {
|
|
344
|
+
amount: (0, format_amount_1.formatAmount)(seatPrice, currency),
|
|
345
|
+
defaultValue: '+ {{amount}} за гостя, сверху к цене группы',
|
|
346
|
+
})
|
|
347
|
+
: t('prices:categoryFree', { defaultValue: 'Бесплатно' });
|
|
348
|
+
}
|
|
349
|
+
return (0, format_amount_1.formatAmount)(seatPrice, currency);
|
|
350
|
+
};
|
|
351
|
+
const priceSummary = (react_1.default.createElement(react_1.default.Fragment, null,
|
|
352
|
+
react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
|
|
353
|
+
react_1.default.createElement(InfoMarkS_1.default, { style: { fill: 'var(--color-text-secondary)' } }),
|
|
354
|
+
react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, hasExtraRows
|
|
355
|
+
? t('prices:totalPriceTeam', {
|
|
356
|
+
teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
|
|
357
|
+
defaultValue: 'Стоимость за команду {{teamPrice}}',
|
|
358
|
+
})
|
|
359
|
+
: t('prices:totalPriceDescription', {
|
|
360
|
+
teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
|
|
361
|
+
personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
|
|
362
|
+
}))),
|
|
363
|
+
minAmount > 0 && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, floorReached
|
|
364
|
+
? t('prices:floorReached', {
|
|
365
|
+
minAmount: minAmountLabel,
|
|
366
|
+
defaultValue: 'Уже минимум {{minAmount}} — лишние льготные места цену не снижают',
|
|
367
|
+
})
|
|
368
|
+
: t('prices:floorHint', {
|
|
369
|
+
minAmount: minAmountLabel,
|
|
370
|
+
defaultValue: 'Игра не стоит меньше {{minAmount}}',
|
|
371
|
+
})))));
|
|
292
372
|
// Show the loader whenever a slot is provided but enriched details are
|
|
293
|
-
// not yet available
|
|
294
|
-
//
|
|
295
|
-
// empty picker UI between "slot prop arrived" and "loading=true".
|
|
296
|
-
// Also wait for the `prices`/`common` translation bundles so the picker
|
|
297
|
-
// never flashes raw i18n keys.
|
|
373
|
+
// not yet available. Also wait for the `prices`/`common` bundles so the
|
|
374
|
+
// picker never flashes raw i18n keys.
|
|
298
375
|
if (!translationsReady || (slot && !details)) {
|
|
299
376
|
return (react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 16 },
|
|
300
377
|
react_1.default.createElement(skeleton_1.Skeleton, { visible: true, animate: true },
|
|
@@ -318,53 +395,35 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
318
395
|
!!variationDescription && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, variationDescription)))),
|
|
319
396
|
react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 8 },
|
|
320
397
|
react_1.default.createElement(flex_1.Flex, { gap: 'sm', align: 'center', justify: 'between' },
|
|
321
|
-
react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' },
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
?
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
: undefined, value: _children, min: 0, max: maxChildren, minusLabel: t('prices:minus', { defaultValue: 'На одного меньше' }), plusLabel: t('prices:plus', { defaultValue: 'На одного больше' }), onChange: handleChangeChildren })),
|
|
351
|
-
!hidePriceHints && !flexSelected && !!totalHeads && (react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 4, gc: 0 },
|
|
352
|
-
react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
|
|
353
|
-
react_1.default.createElement(InfoMarkS_1.default, { style: { fill: 'var(--color-text-secondary)' } }),
|
|
354
|
-
react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:totalPriceDescription', {
|
|
355
|
-
teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
|
|
356
|
-
personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
|
|
357
|
-
}))),
|
|
358
|
-
(0, reduced_price_1.getMinGameAmount)(reducedConfig) > 0 && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, floorReached
|
|
359
|
-
? t('prices:floorReached', {
|
|
360
|
-
minAmount: minAmountLabel,
|
|
361
|
-
defaultValue: 'Уже минимум {{minAmount}} — лишние льготные места цену не снижают',
|
|
362
|
-
})
|
|
363
|
-
: t('prices:floorHint', {
|
|
364
|
-
minAmount: minAmountLabel,
|
|
365
|
-
defaultValue: 'Игра не стоит меньше {{minAmount}}',
|
|
366
|
-
}))))),
|
|
367
|
-
hidePriceHints && !flexSelected && !!totalHeads && (react_1.default.createElement("div", { style: {
|
|
398
|
+
react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, !hasExtraRows && addOnCategories.length && baseCategory
|
|
399
|
+
? categoryTitle(baseCategory)
|
|
400
|
+
: t('prices:choosePlayersAmount')),
|
|
401
|
+
!hasExtraRows && !flexSelected && !!tariff[pricingHeads] && (react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, (0, format_amount_1.formatAmount)(tariff[pricingHeads], currency)))),
|
|
402
|
+
!!flexPlayersOption && (react_1.default.createElement(flex_1.Flex, { gap: 'xs', wrap: true, justify: 'start' },
|
|
403
|
+
react_1.default.createElement(tag_1.Tag, { key: 'flex-players', text: flexPlayersOption.label, onClick: handleSelectFlex, view: flexSelected ? 'primary-inverted' : 'primary' }))),
|
|
404
|
+
!flexSelected && !hasExtraRows && (react_1.default.createElement(flex_1.Flex, { gap: 'xs', wrap: true, justify: 'start' }, baseLevelOptions.map((level) => (react_1.default.createElement(tag_1.Tag, { key: `level-${level}`, text: `${level}`, onClick: () => handleSelectLevel(level), view: pricingHeads === level ? 'primary-inverted' : 'primary' }))))),
|
|
405
|
+
!flexSelected &&
|
|
406
|
+
(hasExtraRows ? categories : addOnCategories).map((category) => {
|
|
407
|
+
var _a, _b;
|
|
408
|
+
const value = Math.max(0, _composition[category.key] || 0);
|
|
409
|
+
const rowMax = (_a = maxByCategory[category.key]) !== null && _a !== void 0 ? _a : value;
|
|
410
|
+
const plusLimitHint = value >= rowMax
|
|
411
|
+
? capacityReached
|
|
412
|
+
? t('prices:capacityReached', {
|
|
413
|
+
count: capacityMax,
|
|
414
|
+
defaultValue: 'Максимум {{count}} участников',
|
|
415
|
+
})
|
|
416
|
+
: t('prices:pricingMaxReached', {
|
|
417
|
+
count: pricingMax,
|
|
418
|
+
defaultValue: 'Максимум {{count}} участников по этому тарифу',
|
|
419
|
+
})
|
|
420
|
+
: undefined;
|
|
421
|
+
return (react_1.default.createElement(ticket_row_1.TicketRow, { key: category.key, title: categoryTitle(category), priceHint: categoryPriceHint(category), description: !hidePriceHints
|
|
422
|
+
? ((_b = category.description) === null || _b === void 0 ? void 0 : _b.trim()) || undefined
|
|
423
|
+
: undefined, value: value, min: 0, max: rowMax, minusLabel: t('prices:minus', { defaultValue: 'На одного меньше' }), plusLabel: t('prices:plus', { defaultValue: 'На одного больше' }), plusDisabledHint: plusLimitHint, onChange: (next) => handleChangeCategory(category.key, next) }));
|
|
424
|
+
})),
|
|
425
|
+
!hidePriceHints && !flexSelected && bookable && (react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 4, gc: 0 }, priceSummary)),
|
|
426
|
+
hidePriceHints && !flexSelected && bookable && (react_1.default.createElement("div", { style: {
|
|
368
427
|
display: 'flex',
|
|
369
428
|
flexDirection: 'column',
|
|
370
429
|
gap: 6,
|
|
@@ -372,27 +431,16 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
|
|
|
372
431
|
border: '1px dashed var(--color-border-secondary)',
|
|
373
432
|
borderRadius: 'calc(var(--widget-radius, 12px) * 0.75)',
|
|
374
433
|
boxSizing: 'border-box',
|
|
375
|
-
} },
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
})
|
|
387
|
-
: t('prices:floorHint', {
|
|
388
|
-
minAmount: minAmountLabel,
|
|
389
|
-
defaultValue: 'Игра не стоит меньше {{minAmount}}',
|
|
390
|
-
}))))),
|
|
391
|
-
!!error && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'negative' }, error || t('prices:playersError', { minPlayers, maxPlayers: effectiveMax }))),
|
|
392
|
-
!!onSave && (react_1.default.createElement(button_1.Button, { onClick: () => onSave({
|
|
393
|
-
players: _players,
|
|
394
|
-
children: _children,
|
|
395
|
-
reducedPlayers: appliedReduced,
|
|
396
|
-
}), view: 'primary', size: 'xs' }, t('common:save')))));
|
|
434
|
+
} }, priceSummary)),
|
|
435
|
+
!flexSelected && levelUnavailable && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'attention' }, t('prices:levelUnavailable', {
|
|
436
|
+
count: pricingHeads,
|
|
437
|
+
defaultValue: 'На {{count}} участников цена не задана — выберите другое количество',
|
|
438
|
+
}))),
|
|
439
|
+
!flexSelected && belowMin && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'attention' }, t('prices:belowMinPlayers', {
|
|
440
|
+
count: effectiveMin,
|
|
441
|
+
defaultValue: 'Минимум {{count}} участников',
|
|
442
|
+
}))),
|
|
443
|
+
!!error && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'negative' }, error || t('prices:playersError', { minPlayers, maxPlayers: pricingMax }))),
|
|
444
|
+
!!onSave && (react_1.default.createElement(button_1.Button, { onClick: () => onSave(Object.assign(Object.assign({}, (0, composition_price_1.legacyCountsFromComposition)(_composition, categories)), { composition: _composition })), view: 'primary', size: 'xs', disabled: !bookable }, t('common:save')))));
|
|
397
445
|
};
|
|
398
446
|
exports.PlayersAndVariationPicker = PlayersAndVariationPicker;
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -6,15 +39,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
39
|
exports.TicketRow = void 0;
|
|
7
40
|
const MinusM_1 = __importDefault(require("@alphakits/icons/dist/MinusM"));
|
|
8
41
|
const PlusM_1 = __importDefault(require("@alphakits/icons/dist/PlusM"));
|
|
42
|
+
const dist_1 = require("@alphakits/ui/dist");
|
|
9
43
|
const typography_1 = require("@alphakits/ui/dist/typography");
|
|
10
|
-
const
|
|
44
|
+
const functional_popover_panel_style_1 = require("@escapenavigator/utils/dist/functional-popover-panel-style");
|
|
45
|
+
const react_1 = __importStar(require("react"));
|
|
11
46
|
const index_module_css_1 = __importDefault(require("./index.module.css"));
|
|
12
47
|
/**
|
|
13
48
|
* Строка спеццены: слева что это за места и сколько они снимают
|
|
14
49
|
* с группового тарифа, справа сколько таких мест в брони.
|
|
15
50
|
*/
|
|
16
|
-
const TicketRow = ({ title, priceHint, description, value, min, max, minusLabel, plusLabel, onChange, }) => {
|
|
51
|
+
const TicketRow = ({ title, priceHint, description, value, min, max, minusLabel, plusLabel, plusDisabledHint, onChange, }) => {
|
|
17
52
|
const active = value > min;
|
|
53
|
+
const plusLocked = value >= max;
|
|
54
|
+
const plusWrapRef = (0, react_1.useRef)(null);
|
|
55
|
+
const [plusHintOpen, setPlusHintOpen] = (0, react_1.useState)(false);
|
|
56
|
+
(0, dist_1.useClickOutside)(plusWrapRef, () => setPlusHintOpen(false));
|
|
18
57
|
return (react_1.default.createElement("div", { className: active ? `${index_module_css_1.default.ticket} ${index_module_css_1.default.ticketActive}` : index_module_css_1.default.ticket },
|
|
19
58
|
react_1.default.createElement("div", { className: index_module_css_1.default.body },
|
|
20
59
|
react_1.default.createElement(typography_1.Typography.Text, { view: 'primary-small', weight: 'medium' }, title),
|
|
@@ -24,7 +63,19 @@ const TicketRow = ({ title, priceHint, description, value, min, max, minusLabel,
|
|
|
24
63
|
react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": minusLabel, disabled: value <= min, onClick: () => onChange(Math.max(min, value - 1)) },
|
|
25
64
|
react_1.default.createElement(MinusM_1.default, null)),
|
|
26
65
|
react_1.default.createElement("span", { className: index_module_css_1.default.stepperValue }, value),
|
|
27
|
-
react_1.default.createElement("
|
|
28
|
-
|
|
66
|
+
react_1.default.createElement("div", { ref: plusWrapRef, className: plusLocked
|
|
67
|
+
? `${index_module_css_1.default.plusWrap} ${index_module_css_1.default.plusWrapLocked}`
|
|
68
|
+
: index_module_css_1.default.plusWrap, onClick: () => {
|
|
69
|
+
if (plusLocked && plusDisabledHint)
|
|
70
|
+
setPlusHintOpen((open) => !open);
|
|
71
|
+
} },
|
|
72
|
+
react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": plusLabel, disabled: plusLocked, onClick: (event) => {
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
onChange(Math.min(max, value + 1));
|
|
75
|
+
} },
|
|
76
|
+
react_1.default.createElement(PlusM_1.default, null)),
|
|
77
|
+
!!plusDisabledHint && (react_1.default.createElement(dist_1.Popover, { open: plusHintOpen, anchorElement: plusWrapRef.current, position: 'top', offset: [0, 8], withTransition: true },
|
|
78
|
+
react_1.default.createElement("div", { style: (0, functional_popover_panel_style_1.functionalPopoverPanel)({ padding: 8, maxWidth: 260 }) },
|
|
79
|
+
react_1.default.createElement(typography_1.Typography.Text, { view: 'caps' }, plusDisabledHint))))))));
|
|
29
80
|
};
|
|
30
81
|
exports.TicketRow = TicketRow;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* be plugged into either context.
|
|
7
7
|
*/
|
|
8
8
|
import { TariffReducedRateModeEnum } from '@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum';
|
|
9
|
+
import { TariffCategory } from '@escapenavigator/types/dist/tariff/tariff-category';
|
|
9
10
|
export type PickerSlotVariation = {
|
|
10
11
|
/**
|
|
11
12
|
* Stable picker key returned by the backend (`${tariffId}-${duration}`).
|
|
@@ -23,6 +24,13 @@ export type PickerSlotVariation = {
|
|
|
23
24
|
tariff: {
|
|
24
25
|
[k: string]: number;
|
|
25
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Категории участников тарифа — по одной строке со степпером в пикере.
|
|
29
|
+
* Бэкенд отдаёт их заполненными всегда: у тарифов, не прошедших бэкфилл,
|
|
30
|
+
* их собирает `categoriesFromLegacy`, поэтому клиенту про старый формат
|
|
31
|
+
* знать не нужно.
|
|
32
|
+
*/
|
|
33
|
+
categories?: TariffCategory[];
|
|
26
34
|
childPriceDescription?: string | null;
|
|
27
35
|
/**
|
|
28
36
|
* Минимальная стоимость игры и льготная ставка тарифа. Пикер и бэк считают
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/hooks",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/services": "^2.0
|
|
18
|
-
"@escapenavigator/types": "^2.0
|
|
19
|
-
"@escapenavigator/utils": "^2.0
|
|
17
|
+
"@escapenavigator/services": "^2.1.0",
|
|
18
|
+
"@escapenavigator/types": "^2.1.0",
|
|
19
|
+
"@escapenavigator/utils": "^2.1.0",
|
|
20
20
|
"libphonenumber-js": "^1.12.24",
|
|
21
21
|
"react": "19.2.6",
|
|
22
22
|
"react-phone-input-2": "^2.15.1"
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
"ts-jest": "^29.1.1",
|
|
33
33
|
"typescript": "^5.6"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "6566478707369811402ca08e2193bf0ead7a1172"
|
|
36
36
|
}
|