@escapenavigator/hooks 2.0.47 → 2.0.49

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.
@@ -56,6 +56,7 @@ const reduced_price_1 = require("@escapenavigator/utils/dist/reduced-price");
56
56
  const react_1 = __importStar(require("react"));
57
57
  const react_i18next_1 = require("react-i18next");
58
58
  const use_players_and_variation_picker_1 = require("../use-players-and-variation-picker");
59
+ const ticket_row_1 = require("./ticket-row");
59
60
  /**
60
61
  * Single shared component that renders the "Variation + Players" picker used
61
62
  * by the admin order modals, the widget order flow and the orders package.
@@ -240,26 +241,23 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
240
241
  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
242
  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
243
  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]);
244
+ // Льготных мест не больше, чем людей в составе. Пол выручки режет сумму,
245
+ // а не выбор: три школьника на четверых можно отметить всегда.
246
+ const maxReduced = hasReducedRate ? _players : 0;
249
247
  const handleChangeReduced = (value) => {
250
248
  setReduced(value);
251
249
  onReducedChange === null || onReducedChange === void 0 ? void 0 : onReducedChange(value);
252
250
  };
253
- // Состав уменьшился (или сменилась вариация) — вчерашние 3 льготных места
254
- // могут больше не влезать в минимум. Подтягиваем вниз, иначе бэк отдал бы
255
- // `reducedPlayersLimitExceeded` уже на сохранении.
251
+ // Состав уменьшился — вчерашние 3 льготных места могут стать больше
252
+ // группы. Подтягиваем к числу гостей, иначе бэк отдаст ошибку.
256
253
  // biome-ignore lint/correctness/useExhaustiveDependencies: односторонний clamp
257
254
  (0, react_1.useEffect)(() => {
258
255
  if (_reduced > maxReduced)
259
256
  handleChangeReduced(maxReduced);
260
257
  // eslint-disable-next-line react-hooks/exhaustive-deps
261
258
  }, [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));
259
+ const appliedReduced = Math.min(_reduced, maxReduced);
260
+ const teamPriceBase = (0, get_players_price_1.getPlayersPrice)(Object.assign({ players: _players, children: _children, price: tariff, reducedPlayers: appliedReduced }, reducedConfig));
263
261
  const teamPrice = teamPriceBase -
264
262
  (0, get_slot_discount_amount_1.getSlotDiscountAmount)({
265
263
  price: teamPriceBase,
@@ -269,15 +267,18 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
269
267
  });
270
268
  const totalHeads = _players + (_children || 0);
271
269
  const personPrice = totalHeads > 0 ? Math.round(teamPrice / totalHeads / 100) * 100 : 0;
272
- // Описание цены за детей показываем только когда дети реально выбраны.
273
- const childDescription = _children
274
- ? (_e = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription) === null || _e === void 0 ? void 0 : _e.trim()
275
- : undefined;
270
+ const seatDiscount = hasReducedRate
271
+ ? (0, reduced_price_1.getReducedSeatDiscount)(Object.assign({ price: tariff, players: _players }, reducedConfig))
272
+ : 0;
273
+ const priceWithoutReduced = (0, get_players_price_1.getPlayersPrice)(Object.assign({ players: _players, children: _children, price: tariff, reducedPlayers: 0 }, reducedConfig));
274
+ const floorReached = appliedReduced > 0 && appliedReduced * seatDiscount > priceWithoutReduced - teamPriceBase;
275
+ const minAmountLabel = (0, format_amount_1.formatAmount)((0, reduced_price_1.getMinGameAmount)(reducedConfig), currency);
276
+ const maxChildren = Math.max(0, effectiveMax - _players);
276
277
  const showVariations = pickerOptions.length > 1;
277
278
  // Клиентское описание показываем только для вариаций — у стандартного
278
279
  // (default) тарифа описание не выводим.
279
280
  const variationDescription = selectedVariation && !selectedVariation.isDefault
280
- ? ((_f = selectedVariation.description) !== null && _f !== void 0 ? _f : null)
281
+ ? ((_e = selectedVariation.description) !== null && _e !== void 0 ? _e : null)
281
282
  : null;
282
283
  const handleVariationClick = (optionId, isFree) => {
283
284
  if (!isFree)
@@ -318,7 +319,10 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
318
319
  react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 8 },
319
320
  react_1.default.createElement(flex_1.Flex, { gap: 'sm', align: 'center', justify: 'between' },
320
321
  react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, tariff.child ? t('prices:adults') : t('prices:choosePlayersAmount')),
321
- !flexSelected && !!_children && !!_players && tariff[_players] != null && (react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, (0, format_amount_1.formatAmount)(tariff[_players], currency)))),
322
+ !flexSelected && !!_players && tariff[_players] != null && (react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, (0, format_amount_1.formatAmount)(tariff[_players], currency)))),
323
+ (hasReducedRate || !!childrenOptions.length) && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:groupFromTariff', {
324
+ defaultValue: 'Цена группы по тарифу на этот состав',
325
+ }))),
322
326
  react_1.default.createElement(flex_1.Flex, { gap: 'xs', wrap: true, justify: 'start' },
323
327
  !!flexPlayersOption && (react_1.default.createElement(tag_1.Tag, { key: 'flex-players', text: flexPlayersOption.label, onClick: handleSelectFlex, view: flexSelected ? 'primary-inverted' : 'primary' })),
324
328
  playersOptions
@@ -329,47 +333,38 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
329
333
  .map((p) => (react_1.default.createElement(tag_1.Tag, { key: `adult-${p.key}`, text: `${p.key}`, onClick: () => handleChangePlayers(p.key), view: !flexSelected && _players === p.key
330
334
  ? 'primary-inverted'
331
335
  : 'primary' }))))),
332
- !flexSelected && !!childrenOptions.length && (react_1.default.createElement(flex_columns_1.FlexColumns, { columns: 1, gr: 8 },
333
- react_1.default.createElement(flex_1.Flex, { gap: 'sm', align: 'center', justify: 'between' },
334
- react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, t('prices:kids')),
335
- !!_children && tariff.child != null && (react_1.default.createElement(typography_1.Typography.Text, { view: 'title', color: 'secondary' }, (0, format_amount_1.formatAmount)(_children * tariff.child, currency)))),
336
- !hidePriceHints && !!childDescription && (react_1.default.createElement("div", { style: { whiteSpace: 'pre-line' } },
337
- react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, childDescription))),
338
- react_1.default.createElement(flex_1.Flex, { gap: 'xs', wrap: true, justify: 'start' }, childrenOptions
339
- .filter((count) => _players + count <= effectiveMax)
340
- .map((count) => {
341
- const isActive = _children === count;
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' }));
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))))),
364
- !hidePriceHints && !flexSelected && !!totalHeads && (react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
365
- react_1.default.createElement(InfoMarkS_1.default, { style: { fill: 'var(--color-text-secondary)' } }),
366
- react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:totalPriceDescription', {
367
- teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
368
- personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
369
- })))),
370
- hidePriceHints &&
371
- !flexSelected &&
372
- (!!totalHeads || !!childDescription || (!!_reduced && !!reducedDescription)) && (react_1.default.createElement("div", { style: {
336
+ !flexSelected && hasReducedRate && !!_players && (react_1.default.createElement(ticket_row_1.TicketRow, { title: reducedTitle ||
337
+ t('prices:reducedPlayers', { defaultValue: 'Льготные места' }), priceHint: seatDiscount
338
+ ? t('prices:reducedFromGroup', {
339
+ amount: (0, format_amount_1.formatAmount)(seatDiscount, currency),
340
+ defaultValue: '− {{amount}} от цены группы по тарифу, с человека',
341
+ })
342
+ : t('prices:reducedAlreadyInTariff', {
343
+ defaultValue: 'Уже заложено в цену группы на этот состав',
344
+ }), description: !hidePriceHints ? reducedDescription || undefined : undefined, value: appliedReduced, min: 0, max: maxReduced, minusLabel: t('prices:minus', { defaultValue: 'На одного меньше' }), plusLabel: t('prices:plus', { defaultValue: 'На одного больше' }), onChange: handleChangeReduced })),
345
+ !flexSelected && !!childrenOptions.length && (react_1.default.createElement(ticket_row_1.TicketRow, { title: t('prices:kids'), priceHint: t('prices:childOnTop', {
346
+ amount: (0, format_amount_1.formatAmount)(tariff.child || 0, currency),
347
+ defaultValue: '+ {{amount}} за ребёнка, сверху к цене группы',
348
+ }), description: !hidePriceHints
349
+ ? ((_f = selectedVariation === null || selectedVariation === void 0 ? void 0 : selectedVariation.childPriceDescription) === null || _f === void 0 ? void 0 : _f.trim()) || undefined
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: {
373
368
  display: 'flex',
374
369
  flexDirection: 'column',
375
370
  gap: 6,
@@ -378,21 +373,26 @@ const PlayersAndVariationPicker = ({ slot, currentTariffId, currentDuration, loa
378
373
  borderRadius: 'calc(var(--widget-radius, 12px) * 0.75)',
379
374
  boxSizing: 'border-box',
380
375
  } },
381
- !!totalHeads && (react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
376
+ react_1.default.createElement(flex_1.Flex, { justify: 'start', gap: 'xs' },
382
377
  react_1.default.createElement(InfoMarkS_1.default, { style: { fill: 'var(--color-text-secondary)' } }),
383
378
  react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, t('prices:totalPriceDescription', {
384
379
  teamPrice: (0, format_amount_1.formatAmount)(teamPrice, currency),
385
380
  personPrice: (0, format_amount_1.formatAmount)(personPrice, currency),
386
- })))),
387
- !!childDescription && (react_1.default.createElement("div", { style: { whiteSpace: 'pre-line' } },
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))))),
381
+ }))),
382
+ (0, reduced_price_1.getMinGameAmount)(reducedConfig) > 0 && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, floorReached
383
+ ? t('prices:floorReached', {
384
+ minAmount: minAmountLabel,
385
+ defaultValue: 'Уже минимум {{minAmount}} — лишние льготные места цену не снижают',
386
+ })
387
+ : t('prices:floorHint', {
388
+ minAmount: minAmountLabel,
389
+ defaultValue: 'Игра не стоит меньше {{minAmount}}',
390
+ }))))),
391
391
  !!error && (react_1.default.createElement(toast_plate_1.ToastPlate, { view: 'negative' }, error || t('prices:playersError', { minPlayers, maxPlayers: effectiveMax }))),
392
392
  !!onSave && (react_1.default.createElement(button_1.Button, { onClick: () => onSave({
393
393
  players: _players,
394
394
  children: _children,
395
- reducedPlayers: Math.min(_reduced, maxReduced),
395
+ reducedPlayers: appliedReduced,
396
396
  }), view: 'primary', size: 'xs' }, t('common:save')))));
397
397
  };
398
398
  exports.PlayersAndVariationPicker = PlayersAndVariationPicker;
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ type Props = {
3
+ title: string;
4
+ priceHint: string;
5
+ description?: string;
6
+ value: number;
7
+ min: number;
8
+ max: number;
9
+ minusLabel: string;
10
+ plusLabel: string;
11
+ onChange: (value: number) => void;
12
+ };
13
+ /**
14
+ * Строка спеццены: слева что это за места и сколько они снимают
15
+ * с группового тарифа, справа сколько таких мест в брони.
16
+ */
17
+ export declare const TicketRow: React.FC<Props>;
18
+ export {};
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TicketRow = void 0;
7
+ const MinusM_1 = __importDefault(require("@alphakits/icons/dist/MinusM"));
8
+ const PlusM_1 = __importDefault(require("@alphakits/icons/dist/PlusM"));
9
+ const typography_1 = require("@alphakits/ui/dist/typography");
10
+ const react_1 = __importDefault(require("react"));
11
+ const index_module_css_1 = __importDefault(require("./index.module.css"));
12
+ /**
13
+ * Строка спеццены: слева что это за места и сколько они снимают
14
+ * с группового тарифа, справа сколько таких мест в брони.
15
+ */
16
+ const TicketRow = ({ title, priceHint, description, value, min, max, minusLabel, plusLabel, onChange, }) => {
17
+ const active = value > min;
18
+ 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
+ react_1.default.createElement("div", { className: index_module_css_1.default.body },
20
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'primary-small', weight: 'medium' }, title),
21
+ react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, priceHint),
22
+ !!description && (react_1.default.createElement(typography_1.Typography.Text, { view: 'caps', color: 'secondary' }, description))),
23
+ react_1.default.createElement("div", { className: index_module_css_1.default.stub },
24
+ 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
+ react_1.default.createElement(MinusM_1.default, null)),
26
+ react_1.default.createElement("span", { className: index_module_css_1.default.stepperValue }, value),
27
+ react_1.default.createElement("button", { type: 'button', className: index_module_css_1.default.stepperButton, "aria-label": plusLabel, disabled: value >= max, onClick: () => onChange(Math.min(max, value + 1)) },
28
+ react_1.default.createElement(PlusM_1.default, null)))));
29
+ };
30
+ exports.TicketRow = TicketRow;
@@ -25,10 +25,9 @@ export type PickerSlotVariation = {
25
25
  };
26
26
  childPriceDescription?: string | null;
27
27
  /**
28
- * Минимальная стоимость игры и льготная ставка тарифа. Лимит льготных мест
29
- * пикер считает сам (`getMaxReducedPlayers`) — той же формулой, которой бэк
30
- * посчитает цену, поэтому «сколько мест влезает» не может разойтись между
31
- * UI и броней.
28
+ * Минимальная стоимость игры и льготная ставка тарифа. Пикер и бэк считают
29
+ * цену одной формулой: льготное место снимает ставку с группового тарифа,
30
+ * итог не ниже минимума. Число мест гость выбирает сам, до размера группы.
32
31
  */
33
32
  minGameAmount?: number;
34
33
  reducedRateMode?: TariffReducedRateModeEnum | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/hooks",
3
- "version": "2.0.47",
3
+ "version": "2.0.49",
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.47",
18
- "@escapenavigator/types": "^2.0.45",
19
- "@escapenavigator/utils": "^2.0.47",
17
+ "@escapenavigator/services": "^2.0.49",
18
+ "@escapenavigator/types": "^2.0.47",
19
+ "@escapenavigator/utils": "^2.0.49",
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": "cd6ca16767c8a03b4deded5c6abc8943b02ce1ee"
35
+ "gitHead": "9eaab88a1509930c7715945ce519eaeba879f550"
36
36
  }