@escapenavigator/hooks 2.0.46 → 2.0.47

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.
@@ -53,13 +53,15 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
53
53
  case lead_field_type_enum_1.LeadFieldTypeEnum.DATE:
54
54
  return withHint(react_1.default.createElement(ui_1.CalendarInput, { block: true, label: label, lang: language, value: String(value || ''), error: error, onChange: (_e, payload) => onChange(payload.value) }));
55
55
  case lead_field_type_enum_1.LeadFieldTypeEnum.TIME:
56
- return withHint(react_1.default.createElement(ui_1.Select, { block: true, optionsListWidth: 'field', label: label, options: TIME_OPTIONS, selected: String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }));
56
+ return withHint(react_1.default.createElement(ui_1.Select, { block: true, optionsListWidth: 'field', label: label, options: value && !TIME_OPTIONS.some((option) => option.key === String(value))
57
+ ? [{ key: String(value), content: String(value) }, ...TIME_OPTIONS]
58
+ : TIME_OPTIONS, selected: String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }));
57
59
  case lead_field_type_enum_1.LeadFieldTypeEnum.GUESTS: {
58
60
  const min = (_a = field.min) !== null && _a !== void 0 ? _a : 1;
59
61
  const max = (_b = field.max) !== null && _b !== void 0 ? _b : 30;
60
62
  const current = typeof value === 'number' ? value : min;
61
63
  return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
62
- react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium' }, label),
64
+ react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
63
65
  react_1.default.createElement("div", { className: index_module_css_1.default.stepper },
64
66
  react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": t('lead.guestsMinus'), disabled: current <= min, onClick: () => onChange(Math.max(min, current - 1)) },
65
67
  react_1.default.createElement(MinusM_1.default, null)),
@@ -77,14 +79,14 @@ const LeadField = ({ field, value, error, language, profileCountry, questrooms,
77
79
  })), selected: String(value || ''), error: error, onChange: ({ selected }) => onChange(selected === null || selected === void 0 ? void 0 : selected.key) }));
78
80
  case lead_field_type_enum_1.LeadFieldTypeEnum.RADIO:
79
81
  return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
80
- react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium' }, label),
82
+ react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
81
83
  activeOptions.map((option) => (react_1.default.createElement(ui_1.Radio, { key: option.id, label: (0, lib_1.optionLabel)(option, language, option.id), checked: value === option.id, onChange: () => onChange(option.id) }))),
82
84
  hint,
83
85
  !!error && (react_1.default.createElement(ui_1.Typography.Text, { view: 'caps', color: 'negative' }, error))));
84
86
  case lead_field_type_enum_1.LeadFieldTypeEnum.CHECKBOX: {
85
87
  const selected = Array.isArray(value) ? value : [];
86
88
  return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 8 },
87
- react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium' }, label),
89
+ react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', weight: 'medium', color: 'primary' }, label),
88
90
  activeOptions.map((option) => (react_1.default.createElement(ui_1.Checkbox, { key: option.id, label: (0, lib_1.optionLabel)(option, language, option.id), checked: selected.includes(option.id), onChange: () => onChange(selected.includes(option.id)
89
91
  ? selected.filter((item) => item !== option.id)
90
92
  : [...selected, option.id]) }))),
@@ -39,6 +39,8 @@ type Props = {
39
39
  /** Что делать после успеха: модалка закрывается, страница остаётся. */
40
40
  onDone?: () => void;
41
41
  doneLabel?: string;
42
+ /** Контейнер прячет свою шапку, чтобы экран успеха не конкурировал с названием. */
43
+ onSuccess?: () => void;
42
44
  };
43
45
  export declare const LeadForm: React.FC<Props>;
44
46
  export {};
@@ -47,14 +47,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  };
48
48
  Object.defineProperty(exports, "__esModule", { value: true });
49
49
  exports.LeadForm = void 0;
50
- const CheckmarkCircleFillM_1 = __importDefault(require("@alphakits/icons/dist/CheckmarkCircleFillM"));
51
50
  const ui_1 = require("@alphakits/ui");
52
51
  const react_1 = __importStar(require("react"));
53
52
  const index_module_css_1 = __importDefault(require("./index.module.css"));
54
53
  const lead_field_1 = require("./lead-field");
55
54
  const lib_1 = require("./lib");
56
55
  const use_lead_t_1 = require("./use-lead-t");
57
- const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, }) => {
56
+ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, contextNote, withHeader = true, onDone, doneLabel, onSuccess, }) => {
58
57
  const t = (0, use_lead_t_1.useLeadT)();
59
58
  const [values, setValues] = (0, react_1.useState)(() => (0, lib_1.buildInitialValues)(form, prefill));
60
59
  const [errors, setErrors] = (0, react_1.useState)({});
@@ -95,23 +94,24 @@ const LeadForm = ({ form, source, language, profileCountry, submit, prefill, con
95
94
  return;
96
95
  }
97
96
  setSuccess(response.data);
97
+ onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess();
98
98
  });
99
99
  if (success) {
100
- return (react_1.default.createElement("div", { className: index_module_css_1.default.success },
101
- react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 16 },
102
- react_1.default.createElement("span", { className: index_module_css_1.default.successIcon },
103
- react_1.default.createElement(CheckmarkCircleFillM_1.default, null)),
104
- react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 4 },
105
- react_1.default.createElement(ui_1.Typography.Title, { tag: 'div', view: 'xsmall', weight: 'bold' }, (0, lib_1.localized)(success.successTitle, language) || t('lead.successTitle')),
106
- react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, (0, lib_1.localized)(success.successDescription, language) ||
107
- t('lead.successText'))),
108
- !!onDone && (react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, onClick: onDone }, doneLabel || t('lead.close'))))));
100
+ return (react_1.default.createElement("div", { className: index_module_css_1.default.success, role: 'status' },
101
+ react_1.default.createElement("div", { className: index_module_css_1.default.successIcon, "aria-hidden": 'true' },
102
+ react_1.default.createElement("svg", { width: '28', height: '28', viewBox: '0 0 24 24', fill: 'none', "aria-hidden": 'true' },
103
+ react_1.default.createElement("path", { d: 'M5 12.5l4.2 4.2L19 7', stroke: 'currentColor', strokeWidth: '2.4', strokeLinecap: 'round', strokeLinejoin: 'round' }))),
104
+ react_1.default.createElement("div", { className: index_module_css_1.default.successCopy },
105
+ react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, (0, lib_1.localized)(success.successTitle, language) || t('lead.successTitle')),
106
+ react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-medium', color: 'secondary' }, (0, lib_1.localized)(success.successDescription, language) || t('lead.successText'))),
107
+ !!onDone && (react_1.default.createElement("div", { className: index_module_css_1.default.successActions },
108
+ react_1.default.createElement(ui_1.Button, { view: 'primary', size: 's', block: true, onClick: onDone }, doneLabel || t('lead.close'))))));
109
109
  }
110
110
  const title = (0, lib_1.localized)(form.clientTitle, language) || t('lead.title');
111
111
  const description = (0, lib_1.localized)(form.description, language);
112
112
  return (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 16 },
113
113
  withHeader && (react_1.default.createElement(ui_1.FlexColumns, { columns: 1, gr: 4 },
114
- react_1.default.createElement(ui_1.Typography.Title, { tag: 'div', view: 'xsmall', weight: 'bold' }, title),
114
+ react_1.default.createElement(ui_1.Typography.Title, { tag: 'h1', view: 'small', weight: 'bold' }, title),
115
115
  !!description && (react_1.default.createElement(ui_1.Typography.Text, { view: 'primary-small', color: 'secondary' }, description)))),
116
116
  !!formError && react_1.default.createElement(ui_1.ToastPlate, { view: 'negative' }, formError),
117
117
  form.fields
@@ -1,5 +1,6 @@
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
+ import { TariffReducedRateModeEnum } from '@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum';
3
4
  import React from 'react';
4
5
  import { usePlayersAndVariationPicker } from '../use-players-and-variation-picker';
5
6
  import { PickerSlotDetails, PickerSlotInfo, PickerSlotVariation, PickerVariationChange } from '../use-players-and-variation-picker/types';
@@ -39,6 +40,23 @@ export type PlayersAndVariationPickerProps = {
39
40
  players: number;
40
41
  /** Children form field value (defaults to 0). */
41
42
  child?: number;
43
+ /** Сколько мест выбрано по льготной ставке (школьники и т.п.). */
44
+ reducedPlayers?: number;
45
+ /**
46
+ * Льготная ставка и минимальная стоимость игры. По умолчанию берутся из
47
+ * выбранной вариации (`/slots/details`). Явно передают там, где условия
48
+ * нужно взять со снимка на заказе, а не с текущего тарифа — карточка уже
49
+ * оформленной брони.
50
+ */
51
+ reduced?: {
52
+ minGameAmount?: number | null;
53
+ reducedRateMode?: TariffReducedRateModeEnum | null;
54
+ reducedRate?: number | null;
55
+ reducedTitle?: string | null;
56
+ reducedPriceDescription?: string | null;
57
+ };
58
+ /** Fires when the guest changes the number of reduced seats. */
59
+ onReducedChange?: (value: number) => void;
42
60
  /**
43
61
  * Override for the price map used to render player buttons. Defaults to
44
62
  * the selected variation's tariff; pass an explicit value when the host
@@ -78,6 +96,7 @@ export type PlayersAndVariationPickerProps = {
78
96
  onSave?: (data: {
79
97
  players: number;
80
98
  children: number;
99
+ reducedPlayers: number;
81
100
  }) => void;
82
101
  /**
83
102
  * When editing an existing order, pass the order id here so the backend
@@ -52,6 +52,7 @@ const format_amount_1 = require("@escapenavigator/utils/dist/format-amount");
52
52
  const get_players_price_1 = require("@escapenavigator/utils/dist/get-players-price");
53
53
  const get_prices_1 = require("@escapenavigator/utils/dist/get-prices");
54
54
  const get_slot_discount_amount_1 = require("@escapenavigator/utils/dist/get-slot-discount-amount");
55
+ const reduced_price_1 = require("@escapenavigator/utils/dist/reduced-price");
55
56
  const react_1 = __importStar(require("react"));
56
57
  const react_i18next_1 = require("react-i18next");
57
58
  const use_players_and_variation_picker_1 = require("../use-players-and-variation-picker");
@@ -64,8 +65,8 @@ const use_players_and_variation_picker_1 = require("../use-players-and-variation
64
65
  * buttons, kids row, price hint, save button) was reimplemented in three
65
66
  * places with subtle drift. This component is the single source of truth.
66
67
  */
67
- const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loadSlotDetails, onVariationChange, onVariationResolved, onDetailsLoaded, players = 0, child = 0, tariff: tariffOverride, currency, minPlayers, maxPlayersLimit, slotDiscount, slotDiscountType, error, t, onChange, onSave, orderId, onLoadingChange, hidePriceHints = false, flexPlayersOption, }) => {
68
- var _a, _b;
68
+ 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, }) => {
69
+ var _a, _b, _c, _d, _e, _f;
69
70
  /**
70
71
  * Страховка от «голых ключей» перевода. Пикер рендерит `prices:*` /
71
72
  * `common:*` через переданный хостом `t`, но сам хост часто берёт
@@ -132,12 +133,16 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
132
133
  ]);
133
134
  const [_players, setPlayers] = (0, react_1.useState)(players);
134
135
  const [_children, setChildren] = (0, react_1.useState)(child);
136
+ const [_reduced, setReduced] = (0, react_1.useState)(reducedPlayers);
135
137
  (0, react_1.useEffect)(() => {
136
138
  setPlayers(players);
137
139
  }, [players]);
138
140
  (0, react_1.useEffect)(() => {
139
141
  setChildren(child);
140
142
  }, [child]);
143
+ (0, react_1.useEffect)(() => {
144
+ setReduced(reducedPlayers);
145
+ }, [reducedPlayers]);
141
146
  const flexSelected = !!(flexPlayersOption === null || flexPlayersOption === void 0 ? void 0 : flexPlayersOption.selected);
142
147
  const handleChangePlayers = (value) => {
143
148
  setPlayers(value);
@@ -215,11 +220,46 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
215
220
  const maxChildren = maxFromTariff + 1;
216
221
  return Array.from({ length: maxChildren }, (_, i) => i);
217
222
  }, [tariff, maxFromTariff]);
218
- const teamPriceBase = (0, get_players_price_1.getPlayersPrice)({
219
- players: _players,
220
- children: _children,
221
- price: tariff,
222
- });
223
+ // Условия льготной ставки: снимок с брони (если хост его передал) важнее
224
+ // текущего тарифа — иначе повышение цен переоценило бы открытую карточку.
225
+ const reducedConfig = (0, react_1.useMemo)(() => {
226
+ var _a, _b, _c, _d, _e, _f;
227
+ return ({
228
+ 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,
229
+ 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,
230
+ 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,
231
+ });
232
+ }, [
233
+ reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.minGameAmount,
234
+ reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRateMode,
235
+ reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedRate,
236
+ selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.minGameAmount,
237
+ selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRateMode,
238
+ selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedRate,
239
+ ]);
240
+ const reducedTitle = (_b = (_a = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedTitle) !== null && _a !== void 0 ? _a : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedTitle) !== null && _b !== void 0 ? _b : '';
241
+ const reducedDescription = ((_d = (_c = reducedOverride === null || reducedOverride === void 0 ? void 0 : reducedOverride.reducedPriceDescription) !== null && _c !== void 0 ? _c : selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.reducedPriceDescription) !== null && _d !== void 0 ? _d : '').trim();
242
+ const hasReducedRate = !!reducedConfig.reducedRateMode && !!reducedConfig.reducedRate;
243
+ // Лимит льготных мест не приходит с бэка числом: он выводится из
244
+ // минимальной стоимости игры той же формулой, которой бэк посчитает цену.
245
+ const maxReduced = (0, react_1.useMemo)(() => hasReducedRate
246
+ ? (0, reduced_price_1.getMaxReducedPlayers)(Object.assign({ price: tariff, players: _players }, reducedConfig))
247
+ : 0, [hasReducedRate, tariff, _players, reducedConfig]);
248
+ const reducedCounts = (0, react_1.useMemo)(() => Array.from({ length: maxReduced + 1 }, (_, index) => index), [maxReduced]);
249
+ const handleChangeReduced = (value) => {
250
+ setReduced(value);
251
+ onReducedChange === null || onReducedChange === void 0 ? void 0 : onReducedChange(value);
252
+ };
253
+ // Состав уменьшился (или сменилась вариация) — вчерашние 3 льготных места
254
+ // могут больше не влезать в минимум. Подтягиваем вниз, иначе бэк отдал бы
255
+ // `reducedPlayersLimitExceeded` уже на сохранении.
256
+ // biome-ignore lint/correctness/useExhaustiveDependencies: односторонний clamp
257
+ (0, react_1.useEffect)(() => {
258
+ if (_reduced > maxReduced)
259
+ handleChangeReduced(maxReduced);
260
+ // eslint-disable-next-line react-hooks/exhaustive-deps
261
+ }, [maxReduced, _reduced]);
262
+ const teamPriceBase = (0, get_players_price_1.getPlayersPrice)(Object.assign({ players: _players, children: _children, price: tariff, reducedPlayers: Math.min(_reduced, maxReduced) }, reducedConfig));
223
263
  const teamPrice = teamPriceBase -
224
264
  (0, get_slot_discount_amount_1.getSlotDiscountAmount)({
225
265
  price: teamPriceBase,
@@ -231,13 +271,13 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
231
271
  const personPrice = totalHeads > 0 ? Math.round(teamPrice / totalHeads / 100) * 100 : 0;
232
272
  // Описание цены за детей показываем только когда дети реально выбраны.
233
273
  const childDescription = _children
234
- ? (_a = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription) === null || _a === void 0 ? void 0 : _a.trim()
274
+ ? (_e = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription) === null || _e === void 0 ? void 0 : _e.trim()
235
275
  : undefined;
236
276
  const showVariations = pickerOptions.length > 1;
237
277
  // Клиентское описание показываем только для вариаций — у стандартного
238
278
  // (default) тарифа описание не выводим.
239
279
  const variationDescription = selectedVariation && !selectedVariation.isDefault
240
- ? ((_b = selectedVariation.description) !== null && _b !== void 0 ? _b : null)
280
+ ? ((_f = selectedVariation.description) !== null && _f !== void 0 ? _f : null)
241
281
  : null;
242
282
  const handleVariationClick = (optionId, isFree) => {
243
283
  if (!isFree)
@@ -301,13 +341,35 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
301
341
  const isActive = _children === count;
302
342
  return (react_1.default.createElement(tag_1.Tag, { key: `child-${count}`, text: count === 0 ? t('prices:noKids') : `${count}`, onClick: () => handleChangeChildren(isActive ? 0 : count), view: isActive ? 'primary-inverted' : 'primary' }));
303
343
  })))),
344
+ !flexSelected && hasReducedRate && !!_players && (react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 8 },
345
+ react_1.default.createElement(flex_1.Flex, { gap: 'sm', align: 'center', justify: 'between' },
346
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, reducedTitle ||
347
+ t('prices:reducedPlayers', { defaultValue: 'Льготные места' })),
348
+ !!_reduced && (react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, `− ${(0, format_amount_1.formatAmount)(Math.min(_reduced, maxReduced) *
349
+ (0, reduced_price_1.getReducedSeatDiscount)(Object.assign({ price: tariff, players: _players }, reducedConfig)), currency)}`))),
350
+ maxReduced > 0 ? (react_1.default.createElement(flex_1.Flex, { gap: 'xs', wrap: true, justify: 'start' }, reducedCounts.map((count) => {
351
+ const isActive = Math.min(_reduced, maxReduced) === count;
352
+ return (react_1.default.createElement(tag_1.Tag, { key: `reduced-${count}`, text: count === 0
353
+ ? t('prices:noReducedPlayers', {
354
+ defaultValue: 'Нет',
355
+ })
356
+ : `${count}`, onClick: () => handleChangeReduced(isActive ? 0 : count), view: isActive ? 'primary-inverted' : 'primary' }));
357
+ }))) : (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:reducedNotAvailable', {
358
+ players: _players,
359
+ minAmount: (0, format_amount_1.formatAmount)(reducedConfig.minGameAmount || 0, currency),
360
+ defaultValue: 'На такой состав льготных мест нет: игра не может стоить меньше {{minAmount}}.',
361
+ }))),
362
+ !hidePriceHints && !!reducedDescription && !!_reduced && (react_1.default.createElement("div", { style: { whiteSpace: 'pre-line' } },
363
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, reducedDescription))))),
304
364
  !hidePriceHints && !flexSelected && !!totalHeads && (react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
305
365
  react_1.default.createElement(InfoMarkS_1.default, { style: { fill: 'var(--color-text-secondary)' } }),
306
366
  react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:totalPriceDescription', {
307
367
  teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
308
368
  personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
309
369
  })))),
310
- hidePriceHints && !flexSelected && (!!totalHeads || !!childDescription) && (react_1.default.createElement("div", { style: {
370
+ hidePriceHints &&
371
+ !flexSelected &&
372
+ (!!totalHeads || !!childDescription || (!!_reduced && !!reducedDescription)) && (react_1.default.createElement("div", { style: {
311
373
  display: 'flex',
312
374
  flexDirection: 'column',
313
375
  gap: 6,
@@ -323,8 +385,14 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
323
385
  personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
324
386
  })))),
325
387
  !!childDescription && (react_1.default.createElement("div", { style: { whiteSpace: 'pre-line' } },
326
- react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, childDescription))))),
388
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, childDescription))),
389
+ !!_reduced && !!reducedDescription && (react_1.default.createElement("div", { style: { whiteSpace: 'pre-line' } },
390
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, reducedDescription))))),
327
391
  !!error && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'negative' }, error || t('prices:playersError', { minPlayers, maxPlayers: effectiveMax }))),
328
- !!onSave && (react_1.default.createElement(button_1.Button, { onClick: () => onSave({ players: _players, children: _children }), view: 'primary', size: 'xs' }, t('common:save')))));
392
+ !!onSave && (react_1.default.createElement(button_1.Button, { onClick: () => onSave({
393
+ players: _players,
394
+ children: _children,
395
+ reducedPlayers: Math.min(_reduced, maxReduced),
396
+ }), view: 'primary', size: 'xs' }, t('common:save')))));
329
397
  };
330
398
  exports.PlayersAndVariationPicker = PlayersAndVariationPicker;
@@ -5,6 +5,7 @@
5
5
  * (`OpenapiSlotRO`/`OpenapiSlotEnrichedRO`) contracts so a single picker can
6
6
  * be plugged into either context.
7
7
  */
8
+ import { TariffReducedRateModeEnum } from '@escapenavigator/types/dist/tariff/enum/tariff-reduced-rate-mode.enum';
8
9
  export type PickerSlotVariation = {
9
10
  /**
10
11
  * Stable picker key returned by the backend (`${tariffId}-${duration}`).
@@ -23,6 +24,17 @@ export type PickerSlotVariation = {
23
24
  [k: string]: number;
24
25
  };
25
26
  childPriceDescription?: string | null;
27
+ /**
28
+ * Минимальная стоимость игры и льготная ставка тарифа. Лимит льготных мест
29
+ * пикер считает сам (`getMaxReducedPlayers`) — той же формулой, которой бэк
30
+ * посчитает цену, поэтому «сколько мест влезает» не может разойтись между
31
+ * UI и броней.
32
+ */
33
+ minGameAmount?: number;
34
+ reducedRateMode?: TariffReducedRateModeEnum | null;
35
+ reducedRate?: number;
36
+ reducedTitle?: string | null;
37
+ reducedPriceDescription?: string | null;
26
38
  minPlayers: number;
27
39
  maxPlayers: number;
28
40
  numSeatsAvailable: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/hooks",
3
- "version": "2.0.46",
3
+ "version": "2.0.47",
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.46",
18
- "@escapenavigator/types": "^2.0.44",
19
- "@escapenavigator/utils": "^2.0.46",
17
+ "@escapenavigator/services": "^2.0.47",
18
+ "@escapenavigator/types": "^2.0.45",
19
+ "@escapenavigator/utils": "^2.0.47",
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": "1a84aa81dd428759ad51821e8095fd0b80b7cc62"
35
+ "gitHead": "cd6ca16767c8a03b4deded5c6abc8943b02ce1ee"
36
36
  }