@cloud-ru/uikit-product-price-summary 3.0.0 → 3.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +156 -3
  3. package/dist/cjs/components/PriceSummary/PriceSummary.d.ts +5 -2
  4. package/dist/cjs/components/PriceSummary/PriceSummary.js +6 -2
  5. package/dist/cjs/components/PriceSummary/components/InvoiceBlock/InvoiceBlock.d.ts +4 -3
  6. package/dist/cjs/components/PriceSummary/components/InvoiceBlock/InvoiceBlock.js +2 -2
  7. package/dist/cjs/components/PriceSummary/components/InvoiceDetailsBlock/InvoiceDetailsBlock.d.ts +3 -1
  8. package/dist/cjs/components/PriceSummary/components/InvoiceDetailsBlock/InvoiceDetailsBlock.js +5 -4
  9. package/dist/cjs/components/PriceSummary/components/PriceChangeBlock/PriceChangeBlock.d.ts +6 -0
  10. package/dist/cjs/components/PriceSummary/components/PriceChangeBlock/PriceChangeBlock.js +20 -0
  11. package/dist/cjs/components/PriceSummary/components/PriceChangeBlock/index.d.ts +1 -0
  12. package/dist/cjs/components/PriceSummary/components/PriceChangeBlock/index.js +17 -0
  13. package/dist/cjs/components/PriceSummary/components/PriceChangeBlock/styles.module.css +18 -0
  14. package/dist/cjs/components/PriceSummary/components/TotalValueBlock/TotalValueBlock.d.ts +2 -2
  15. package/dist/cjs/components/PriceSummary/components/TotalValueBlock/TotalValueBlock.js +28 -2
  16. package/dist/cjs/components/PriceSummary/components/TotalValueBlock/styles.module.css +8 -0
  17. package/dist/cjs/helpers/__tests__/mapInvoiceWithCalculatePrice.spec.d.ts +1 -0
  18. package/dist/cjs/helpers/__tests__/mapInvoiceWithCalculatePrice.spec.js +78 -0
  19. package/dist/cjs/helpers/formatPriceChange.d.ts +3 -0
  20. package/dist/cjs/helpers/formatPriceChange.js +23 -0
  21. package/dist/cjs/helpers/formatters.d.ts +1 -0
  22. package/dist/cjs/helpers/formatters.js +6 -0
  23. package/dist/cjs/helpers/getPriceChangeAppearance.d.ts +1 -0
  24. package/dist/cjs/helpers/getPriceChangeAppearance.js +12 -0
  25. package/dist/cjs/helpers/index.d.ts +3 -0
  26. package/dist/cjs/helpers/index.js +3 -0
  27. package/dist/cjs/helpers/mapInvoiceWithCalculatePrice.d.ts +7 -0
  28. package/dist/cjs/helpers/mapInvoiceWithCalculatePrice.js +80 -0
  29. package/dist/cjs/types/index.d.ts +29 -1
  30. package/dist/esm/components/PriceSummary/PriceSummary.d.ts +5 -2
  31. package/dist/esm/components/PriceSummary/PriceSummary.js +6 -2
  32. package/dist/esm/components/PriceSummary/components/InvoiceBlock/InvoiceBlock.d.ts +4 -3
  33. package/dist/esm/components/PriceSummary/components/InvoiceBlock/InvoiceBlock.js +3 -3
  34. package/dist/esm/components/PriceSummary/components/InvoiceDetailsBlock/InvoiceDetailsBlock.d.ts +3 -1
  35. package/dist/esm/components/PriceSummary/components/InvoiceDetailsBlock/InvoiceDetailsBlock.js +5 -4
  36. package/dist/esm/components/PriceSummary/components/PriceChangeBlock/PriceChangeBlock.d.ts +6 -0
  37. package/dist/esm/components/PriceSummary/components/PriceChangeBlock/PriceChangeBlock.js +14 -0
  38. package/dist/esm/components/PriceSummary/components/PriceChangeBlock/index.d.ts +1 -0
  39. package/dist/esm/components/PriceSummary/components/PriceChangeBlock/index.js +1 -0
  40. package/dist/esm/components/PriceSummary/components/PriceChangeBlock/styles.module.css +18 -0
  41. package/dist/esm/components/PriceSummary/components/TotalValueBlock/TotalValueBlock.d.ts +2 -2
  42. package/dist/esm/components/PriceSummary/components/TotalValueBlock/TotalValueBlock.js +29 -3
  43. package/dist/esm/components/PriceSummary/components/TotalValueBlock/styles.module.css +8 -0
  44. package/dist/esm/helpers/__tests__/mapInvoiceWithCalculatePrice.spec.d.ts +1 -0
  45. package/dist/esm/helpers/__tests__/mapInvoiceWithCalculatePrice.spec.js +76 -0
  46. package/dist/esm/helpers/formatPriceChange.d.ts +3 -0
  47. package/dist/esm/helpers/formatPriceChange.js +19 -0
  48. package/dist/esm/helpers/formatters.d.ts +1 -0
  49. package/dist/esm/helpers/formatters.js +5 -0
  50. package/dist/esm/helpers/getPriceChangeAppearance.d.ts +1 -0
  51. package/dist/esm/helpers/getPriceChangeAppearance.js +9 -0
  52. package/dist/esm/helpers/index.d.ts +3 -0
  53. package/dist/esm/helpers/index.js +3 -0
  54. package/dist/esm/helpers/mapInvoiceWithCalculatePrice.d.ts +7 -0
  55. package/dist/esm/helpers/mapInvoiceWithCalculatePrice.js +76 -0
  56. package/dist/esm/types/index.d.ts +29 -1
  57. package/package.json +2 -2
  58. package/src/components/PriceSummary/PriceSummary.tsx +24 -3
  59. package/src/components/PriceSummary/components/InvoiceBlock/InvoiceBlock.tsx +14 -4
  60. package/src/components/PriceSummary/components/InvoiceDetailsBlock/InvoiceDetailsBlock.tsx +16 -4
  61. package/src/components/PriceSummary/components/PriceChangeBlock/PriceChangeBlock.tsx +48 -0
  62. package/src/components/PriceSummary/components/PriceChangeBlock/index.ts +1 -0
  63. package/src/components/PriceSummary/components/PriceChangeBlock/styles.module.scss +23 -0
  64. package/src/components/PriceSummary/components/TotalValueBlock/TotalValueBlock.tsx +50 -12
  65. package/src/components/PriceSummary/components/TotalValueBlock/styles.module.scss +9 -1
  66. package/src/helpers/__tests__/mapInvoiceWithCalculatePrice.spec.ts +92 -0
  67. package/src/helpers/formatPriceChange.ts +27 -0
  68. package/src/helpers/formatters.ts +6 -0
  69. package/src/helpers/getPriceChangeAppearance.ts +11 -0
  70. package/src/helpers/index.ts +3 -0
  71. package/src/helpers/mapInvoiceWithCalculatePrice.ts +139 -0
  72. package/src/types/index.ts +34 -1
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mapInvoiceWithCalculatePrice = mapInvoiceWithCalculatePrice;
4
+ exports.splitInvoiceByAvailability = splitInvoiceByAvailability;
5
+ function setSkuFlags(skuFlagsById, key, flags) {
6
+ if (!key) {
7
+ return;
8
+ }
9
+ skuFlagsById.set(key, flags);
10
+ }
11
+ function getSkuFlagsById(calculatePriceData) {
12
+ const skuFlagsById = new Map();
13
+ calculatePriceData.skuResults.forEach(result => {
14
+ const flags = {
15
+ available: result.skuAvailability,
16
+ covered: result.coveredByGrants,
17
+ };
18
+ setSkuFlags(skuFlagsById, result.skuId, flags);
19
+ setSkuFlags(skuFlagsById, result.resourceSpecCode, flags);
20
+ setSkuFlags(skuFlagsById, result.resourceSpecCodeId, flags);
21
+ setSkuFlags(skuFlagsById, result.skuCode, flags);
22
+ });
23
+ return skuFlagsById;
24
+ }
25
+ function isUnavailableInvoiceItem(item, skuFlagsById) {
26
+ var _a;
27
+ if (!item.primary || !item.id) {
28
+ return false;
29
+ }
30
+ return ((_a = skuFlagsById.get(item.id)) === null || _a === void 0 ? void 0 : _a.available) === false;
31
+ }
32
+ function mapInvoiceItemWithSkuFlags(item, skuFlagsById) {
33
+ if (!item.primary || !item.id) {
34
+ return item;
35
+ }
36
+ const skuFlags = skuFlagsById.get(item.id);
37
+ if (!skuFlags || skuFlags.available === false) {
38
+ return item;
39
+ }
40
+ return Object.assign(Object.assign({}, item), { coveredByGrant: skuFlags.covered });
41
+ }
42
+ function splitInvoiceDetailsByAvailability(details, skuFlagsById) {
43
+ const unavailableItems = details.items.filter(item => isUnavailableInvoiceItem(item, skuFlagsById));
44
+ const availableItems = details.items
45
+ .filter(item => !isUnavailableInvoiceItem(item, skuFlagsById))
46
+ .map(item => mapInvoiceItemWithSkuFlags(item, skuFlagsById));
47
+ if (!availableItems.length) {
48
+ return { unavailableItems };
49
+ }
50
+ if (!unavailableItems.length) {
51
+ return {
52
+ availableDetails: Object.assign(Object.assign({}, details), { items: availableItems }),
53
+ unavailableItems,
54
+ };
55
+ }
56
+ return {
57
+ availableDetails: Object.assign(Object.assign({}, details), { items: availableItems, price: undefined, quantity: undefined }),
58
+ unavailableItems,
59
+ };
60
+ }
61
+ function mapInvoiceWithCalculatePrice(invoice, calculatePriceData) {
62
+ if (!(invoice === null || invoice === void 0 ? void 0 : invoice.length) || !(calculatePriceData === null || calculatePriceData === void 0 ? void 0 : calculatePriceData.skuResults.length)) {
63
+ return invoice;
64
+ }
65
+ const skuFlagsById = getSkuFlagsById(calculatePriceData);
66
+ return invoice.map(details => (Object.assign(Object.assign({}, details), { items: details.items.map(item => mapInvoiceItemWithSkuFlags(item, skuFlagsById)) })));
67
+ }
68
+ function splitInvoiceByAvailability(invoice, calculatePriceData) {
69
+ if (!(invoice === null || invoice === void 0 ? void 0 : invoice.length) || !(calculatePriceData === null || calculatePriceData === void 0 ? void 0 : calculatePriceData.skuResults.length)) {
70
+ return { invoice, unavailableItems: undefined };
71
+ }
72
+ const skuFlagsById = getSkuFlagsById(calculatePriceData);
73
+ const detailsSplits = invoice.map(details => splitInvoiceDetailsByAvailability(details, skuFlagsById));
74
+ const availableInvoice = detailsSplits.flatMap(({ availableDetails }) => availableDetails ? [availableDetails] : []);
75
+ const unavailableItems = detailsSplits.flatMap(detailsSplit => detailsSplit.unavailableItems);
76
+ if (!unavailableItems.length) {
77
+ return { invoice: availableInvoice, unavailableItems: undefined };
78
+ }
79
+ return { invoice: availableInvoice, unavailableItems };
80
+ }
@@ -6,7 +6,11 @@ export declare enum PricePeriod {
6
6
  Hour = "hour",
7
7
  Minute = "minute"
8
8
  }
9
- export type TotalSumType = 'equal' | 'from';
9
+ export type TotalSumType = 'equal' | 'from' | 'to';
10
+ export type TotalValueRange = {
11
+ min: number;
12
+ max: number;
13
+ };
10
14
  export type DiscountItem = {
11
15
  value: number;
12
16
  percent?: number;
@@ -35,10 +39,12 @@ export type BaseInvoiceItem = (PriceInvoiceItem | DiscountInvoiceItem) & {
35
39
  };
36
40
  export type PrimaryInvoiceItem = BaseInvoiceItem & {
37
41
  primary: true;
42
+ id?: string;
38
43
  coveredByGrant?: boolean;
39
44
  };
40
45
  export type SecondaryInvoiceItem = BaseInvoiceItem & {
41
46
  primary?: false;
47
+ id?: never;
42
48
  coveredByGrant?: never;
43
49
  };
44
50
  export type InvoiceItem = PrimaryInvoiceItem | SecondaryInvoiceItem;
@@ -52,3 +58,25 @@ export type PriceDeltaDetails = {
52
58
  value: number;
53
59
  type: 'increased' | 'decreased';
54
60
  };
61
+ export type PriceChangeDetails = {
62
+ /**
63
+ * Signed difference between the current and previous price.
64
+ * A positive value increases the cost, a negative value is a discount.
65
+ */
66
+ value: number;
67
+ percentage?: number;
68
+ };
69
+ export type SkuPriceResult = {
70
+ skuId?: string;
71
+ skuCode?: string;
72
+ resourceSpecCode?: string;
73
+ resourceSpecCodeId?: string;
74
+ skuAvailability?: boolean;
75
+ coveredByGrants?: boolean;
76
+ };
77
+ export type CalculatePriceData = {
78
+ skuResults: SkuPriceResult[];
79
+ allow?: boolean;
80
+ };
81
+ /** @deprecated Use CalculatePriceData instead. */
82
+ export type CalculatePriceResponse = CalculatePriceData;
@@ -1,12 +1,15 @@
1
1
  import { WithLayoutType, WithSupportProps } from '@cloud-ru/uikit-product-utils';
2
2
  import { LinkProps } from '@snack-uikit/link';
3
- import { DiscountDetails, InvoiceDetails } from '../../types';
3
+ import { CalculatePriceData, DiscountDetails, InvoiceDetails, PriceChangeDetails } from '../../types';
4
4
  import { ContentBlockProps } from '../ContentBlock';
5
5
  import { HeaderBlockProps } from './components/HeaderBlock';
6
6
  import { TotalValueBlockProps } from './components/TotalValueBlock';
7
7
  export type PriceSummaryProps = WithLayoutType<WithSupportProps<TotalValueBlockProps & HeaderBlockProps & ContentBlockProps & {
8
+ basePrice?: number;
8
9
  discount?: DiscountDetails;
10
+ priceChange?: PriceChangeDetails;
9
11
  invoice?: InvoiceDetails[];
12
+ calculatePriceData?: CalculatePriceData;
10
13
  invoiceExpandedDefault?: boolean;
11
14
  docsLink?: {
12
15
  href?: LinkProps['href'];
@@ -14,4 +17,4 @@ export type PriceSummaryProps = WithLayoutType<WithSupportProps<TotalValueBlockP
14
17
  };
15
18
  className?: string;
16
19
  }>>;
17
- export declare function PriceSummary({ value, totalSumType, hint, period, onPeriodChanged, periodOptions, promoBadge, loading, dataError, onRetry, discount, invoice, invoiceExpandedDefault, docsLink, className, layoutType, hintAppearance, showHintTooltip, hintTooltipText, hintLink, showHintLink, valueDelta, ...rest }: PriceSummaryProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function PriceSummary({ value, totalSumType, hint, period, onPeriodChanged, periodOptions, promoBadge, loading, dataError, onRetry, discount, invoice, calculatePriceData, invoiceExpandedDefault, docsLink, className, layoutType, hintAppearance, showHintTooltip, hintTooltipText, hintLink, showHintLink, valueDelta, priceChange, basePrice, ...rest }: PriceSummaryProps): import("react/jsx-runtime").JSX.Element;
@@ -11,17 +11,21 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import cn from 'classnames';
14
+ import { useMemo } from 'react';
14
15
  import { useLocale } from '@cloud-ru/uikit-product-locale';
15
16
  import { extractSupportProps } from '@cloud-ru/uikit-product-utils';
16
17
  import { Link } from '@snack-uikit/link';
18
+ import { splitInvoiceByAvailability } from '../../helpers';
17
19
  import { ContentBlock } from '../ContentBlock';
18
20
  import { DiscountBlock } from './components/DiscountBlock';
19
21
  import { HeaderBlock } from './components/HeaderBlock';
20
22
  import { InvoiceBlock } from './components/InvoiceBlock';
23
+ import { PriceDetailsBlock } from './components/PriceChangeBlock';
21
24
  import { TotalValueBlock } from './components/TotalValueBlock';
22
25
  import styles from './styles.module.css';
23
26
  export function PriceSummary(_a) {
24
- var { value, totalSumType, hint, period, onPeriodChanged, periodOptions, promoBadge, loading, dataError, onRetry, discount, invoice, invoiceExpandedDefault = true, docsLink, className, layoutType, hintAppearance, showHintTooltip = false, hintTooltipText, hintLink, showHintLink, valueDelta } = _a, rest = __rest(_a, ["value", "totalSumType", "hint", "period", "onPeriodChanged", "periodOptions", "promoBadge", "loading", "dataError", "onRetry", "discount", "invoice", "invoiceExpandedDefault", "docsLink", "className", "layoutType", "hintAppearance", "showHintTooltip", "hintTooltipText", "hintLink", "showHintLink", "valueDelta"]);
27
+ var { value, totalSumType, hint, period, onPeriodChanged, periodOptions, promoBadge, loading, dataError, onRetry, discount, invoice, calculatePriceData, invoiceExpandedDefault = true, docsLink, className, layoutType, hintAppearance, showHintTooltip = false, hintTooltipText, hintLink, showHintLink, valueDelta, priceChange, basePrice } = _a, rest = __rest(_a, ["value", "totalSumType", "hint", "period", "onPeriodChanged", "periodOptions", "promoBadge", "loading", "dataError", "onRetry", "discount", "invoice", "calculatePriceData", "invoiceExpandedDefault", "docsLink", "className", "layoutType", "hintAppearance", "showHintTooltip", "hintTooltipText", "hintLink", "showHintLink", "valueDelta", "priceChange", "basePrice"]);
25
28
  const { t } = useLocale('PriceSummary');
26
- return (_jsxs("div", Object.assign({ className: cn(styles.priceSummary, className) }, extractSupportProps(rest), { children: [_jsx(HeaderBlock, { period: period, onPeriodChanged: onPeriodChanged, periodOptions: periodOptions, promoBadge: promoBadge, layoutType: layoutType }), _jsxs(ContentBlock, { loading: loading, dataError: dataError, onRetry: onRetry, children: [discount && _jsx(DiscountBlock, { value: discount, layoutType: layoutType }), _jsx(TotalValueBlock, { value: value, totalSumType: totalSumType, hint: hint, hintAppearance: hintAppearance, showHintTooltip: showHintTooltip, hintTooltipText: hintTooltipText, hintLink: hintLink, showHintLink: showHintLink, valueDelta: valueDelta }), (invoice === null || invoice === void 0 ? void 0 : invoice.length) && (_jsx(InvoiceBlock, { invoice: invoice, invoiceExpandedDefault: invoiceExpandedDefault, layoutType: layoutType })), (docsLink === null || docsLink === void 0 ? void 0 : docsLink.href) && (_jsx(Link, { size: 'm', textMode: 'default', text: docsLink.text || t('docsLink'), href: docsLink.href, target: '_blank' }))] })] })));
29
+ const { invoice: enrichedInvoice, unavailableItems } = useMemo(() => splitInvoiceByAvailability(invoice, calculatePriceData), [invoice, calculatePriceData]);
30
+ return (_jsxs("div", Object.assign({ className: cn(styles.priceSummary, className) }, extractSupportProps(rest), { children: [_jsx(HeaderBlock, { period: period, onPeriodChanged: onPeriodChanged, periodOptions: periodOptions, promoBadge: promoBadge, layoutType: layoutType }), _jsxs(ContentBlock, { loading: loading, dataError: dataError, onRetry: onRetry, children: [discount && _jsx(DiscountBlock, { value: discount, layoutType: layoutType }), (basePrice || priceChange) && _jsx(PriceDetailsBlock, { priceChange: priceChange, basePrice: basePrice }), _jsx(TotalValueBlock, { value: value, totalSumType: totalSumType, hint: hint, hintAppearance: hintAppearance, showHintTooltip: showHintTooltip, hintTooltipText: hintTooltipText, hintLink: hintLink, showHintLink: showHintLink, valueDelta: valueDelta }), ((enrichedInvoice === null || enrichedInvoice === void 0 ? void 0 : enrichedInvoice.length) || (unavailableItems === null || unavailableItems === void 0 ? void 0 : unavailableItems.length)) && (_jsx(InvoiceBlock, { invoice: enrichedInvoice, unavailableItems: unavailableItems, invoiceExpandedDefault: invoiceExpandedDefault, layoutType: layoutType })), (docsLink === null || docsLink === void 0 ? void 0 : docsLink.href) && (_jsx(Link, { size: 'm', textMode: 'default', text: docsLink.text || t('docsLink'), href: docsLink.href, target: '_blank' }))] })] })));
27
31
  }
@@ -1,7 +1,8 @@
1
1
  import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
2
- import { InvoiceDetails } from '../../../../types';
2
+ import { InvoiceDetails, InvoiceItem } from '../../../../types';
3
3
  export type InvoiceBlockProps = WithLayoutType<{
4
- invoice: InvoiceDetails[];
4
+ invoice?: InvoiceDetails[];
5
+ unavailableItems?: InvoiceItem[];
5
6
  invoiceExpandedDefault?: boolean;
6
7
  }>;
7
- export declare function InvoiceBlock({ invoice, invoiceExpandedDefault, layoutType }: InvoiceBlockProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function InvoiceBlock({ invoice, unavailableItems, invoiceExpandedDefault, layoutType }: InvoiceBlockProps): import("react/jsx-runtime").JSX.Element;
@@ -1,12 +1,12 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useId } from 'react';
3
3
  import { useLocale } from '@cloud-ru/uikit-product-locale';
4
4
  import { AccordionSecondary } from '@snack-uikit/accordion';
5
5
  import { Typography } from '@snack-uikit/typography';
6
6
  import { InvoiceDetailsBlock } from '../InvoiceDetailsBlock';
7
7
  import styles from './styles.module.css';
8
- export function InvoiceBlock({ invoice, invoiceExpandedDefault, layoutType }) {
8
+ export function InvoiceBlock({ invoice, unavailableItems, invoiceExpandedDefault, layoutType }) {
9
9
  const { t } = useLocale('PriceSummary');
10
10
  const invoiceBlockId = useId();
11
- return (_jsx(AccordionSecondary, { expandedDefault: invoiceExpandedDefault ? invoiceBlockId : undefined, className: styles.accordion, children: _jsx(AccordionSecondary.CollapseBlock, { id: invoiceBlockId, header: _jsx(Typography.SansBodyM, { className: styles.accordionHeaderTitle, children: t('orderDetails') }), children: _jsx("div", { className: styles.accordionContent, children: invoice.map((invoice, index) => (_jsx(InvoiceDetailsBlock, { invoice: invoice, layoutType: layoutType }, index))) }) }) }));
11
+ return (_jsx(AccordionSecondary, { expandedDefault: invoiceExpandedDefault ? invoiceBlockId : undefined, className: styles.accordion, children: _jsx(AccordionSecondary.CollapseBlock, { id: invoiceBlockId, header: _jsx(Typography.SansBodyM, { className: styles.accordionHeaderTitle, children: t('orderDetails') }), children: _jsxs("div", { className: styles.accordionContent, children: [(unavailableItems === null || unavailableItems === void 0 ? void 0 : unavailableItems.length) && (_jsx(InvoiceDetailsBlock, { invoice: { title: t('unavailableByContract'), items: unavailableItems }, layoutType: layoutType, showCoveredByGrantLabels: false, titleAsRedTag: true })), invoice === null || invoice === void 0 ? void 0 : invoice.map((invoice, index) => (_jsx(InvoiceDetailsBlock, { invoice: invoice, layoutType: layoutType }, index)))] }) }) }));
12
12
  }
@@ -2,5 +2,7 @@ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
2
2
  import { InvoiceDetails } from '../../../../types';
3
3
  export type InvoiceDetailsBlockProps = WithLayoutType<{
4
4
  invoice: InvoiceDetails;
5
+ showCoveredByGrantLabels?: boolean;
6
+ titleAsRedTag?: boolean;
5
7
  }>;
6
- export declare function InvoiceDetailsBlock({ invoice, layoutType }: InvoiceDetailsBlockProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function InvoiceDetailsBlock({ invoice, layoutType, showCoveredByGrantLabels, titleAsRedTag, }: InvoiceDetailsBlockProps): import("react/jsx-runtime").JSX.Element;
@@ -1,18 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useLocale } from '@cloud-ru/uikit-product-locale';
3
+ import { PromoTag } from '@snack-uikit/promo-tag';
3
4
  import { Typography } from '@snack-uikit/typography';
4
5
  import { formatCurrency, formatQuantity } from '../../../../helpers';
5
6
  import { CoveredByGrantLabel } from '../CoveredByGrantLabel';
6
7
  import { Divider } from '../Divider';
7
8
  import { InvoiceItemBlock } from '../InvoiceItemBlock';
8
9
  import styles from './styles.module.css';
9
- export function InvoiceDetailsBlock({ invoice, layoutType }) {
10
+ export function InvoiceDetailsBlock({ invoice, layoutType, showCoveredByGrantLabels = true, titleAsRedTag = false, }) {
10
11
  var _a;
11
12
  const { t } = useLocale('PriceSummary');
12
13
  const primaryItems = invoice.items.filter(item => item.primary);
13
14
  const firstValue = (_a = primaryItems[0]) === null || _a === void 0 ? void 0 : _a.coveredByGrant;
14
15
  const allSameValue = primaryItems.length > 0 && primaryItems.every(item => item.coveredByGrant === firstValue);
15
- const showBlockLabel = allSameValue && firstValue !== undefined;
16
- const showItemLabels = !showBlockLabel;
17
- return (_jsxs("div", { className: styles.main, children: [showBlockLabel && _jsx(CoveredByGrantLabel, { covered: firstValue }), invoice.title && (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.header, children: [_jsx(Typography.SansLabelM, { children: invoice.title }), invoice.quantity && _jsx(Typography.SansLabelM, { children: formatQuantity(invoice.quantity) })] }), _jsx(Divider, {})] })), invoice.items.map((item, index) => (_jsx(InvoiceItemBlock, { item: item, index: index, layoutType: layoutType, showCoveredByGrantLabel: showItemLabels }, index))), invoice.price !== undefined && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx("div", { className: styles.footer, children: _jsxs(Typography.SansLabelM, { children: [t('price'), ": ", formatCurrency(invoice.price)] }) })] }))] }));
16
+ const showBlockLabel = showCoveredByGrantLabels && allSameValue && firstValue !== undefined;
17
+ const showItemLabels = showCoveredByGrantLabels && !showBlockLabel;
18
+ return (_jsxs("div", { className: styles.main, children: [showBlockLabel && _jsx(CoveredByGrantLabel, { covered: firstValue }), invoice.title && (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.header, children: [titleAsRedTag ? (_jsx(PromoTag, { appearance: 'red', color: 'decor', size: 'xxs', text: invoice.title })) : (_jsx(Typography.SansLabelM, { children: invoice.title })), invoice.quantity && _jsx(Typography.SansLabelM, { children: formatQuantity(invoice.quantity) })] }), _jsx(Divider, {})] })), invoice.items.map((item, index) => (_jsx(InvoiceItemBlock, { item: item, index: index, layoutType: layoutType, showCoveredByGrantLabel: showItemLabels }, index))), invoice.price !== undefined && (_jsxs(_Fragment, { children: [_jsx(Divider, {}), _jsx("div", { className: styles.footer, children: _jsxs(Typography.SansLabelM, { children: [t('price'), ": ", formatCurrency(invoice.price)] }) })] }))] }));
18
19
  }
@@ -0,0 +1,6 @@
1
+ import { PriceChangeDetails } from '../../../../types';
2
+ export type PriceDetailsBlockProps = {
3
+ basePrice?: number;
4
+ priceChange?: PriceChangeDetails;
5
+ };
6
+ export declare function PriceDetailsBlock({ basePrice, priceChange }: PriceDetailsBlockProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import cn from 'classnames';
3
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
4
+ import { Typography } from '@snack-uikit/typography';
5
+ import { formatCurrency, formatPriceChange, getPriceChangeAppearance } from '../../../../helpers';
6
+ import styles from './styles.module.css';
7
+ export function PriceDetailsBlock({ basePrice, priceChange }) {
8
+ const { t } = useLocale('PriceSummary');
9
+ const formattedValue = priceChange ? formatPriceChange(priceChange) : undefined;
10
+ if (!basePrice && !priceChange) {
11
+ return null;
12
+ }
13
+ return (_jsxs("div", { className: styles.priceDetails, children: [basePrice && (_jsxs("div", { className: styles.priceDetailsRow, children: [_jsx(Typography.SansBodyS, { tag: 'div', children: t('basePrice') }), _jsx("div", { className: styles.priceDetailsValue, children: _jsx(Typography.SansLabelM, { tag: 'div', children: formatCurrency(basePrice) }) })] })), priceChange && (_jsxs("div", { className: cn(styles.priceDetailsRow, styles.priceChangeRow), "data-appearance": getPriceChangeAppearance(priceChange.value), children: [_jsx(Typography.SansBodyS, { tag: 'div', children: t('priceChange') }), _jsx("div", { className: styles.priceDetailsValue, title: formattedValue, children: _jsx(Typography.SansLabelM, { tag: 'div', children: formattedValue }) })] }))] }));
14
+ }
@@ -0,0 +1 @@
1
+ export * from './PriceChangeBlock';
@@ -0,0 +1 @@
1
+ export * from './PriceChangeBlock';
@@ -0,0 +1,18 @@
1
+ .priceDetailsValue{
2
+ overflow-wrap:anywhere;
3
+ text-align:end;
4
+ }
5
+
6
+ .priceDetailsRow{
7
+ display:flex;
8
+ gap:var(--dimension-1m, 8px);
9
+ color:var(--sys-neutral-text-support, #6d707f);
10
+ }
11
+ .priceDetailsRow > *{
12
+ flex:1 1 0;
13
+ min-width:0;
14
+ }
15
+
16
+ .priceChangeRow[data-appearance=increased]{
17
+ color:var(--sys-red-text-main, #7a2d2d);
18
+ }
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { ValueOf } from '@snack-uikit/utils';
3
- import { PriceDeltaDetails, TotalSumType } from '../../../../types';
3
+ import { PriceDeltaDetails, TotalSumType, TotalValueRange } from '../../../../types';
4
4
  export declare const APPEARANCE_STATE: {
5
5
  readonly Default: "default";
6
6
  readonly UserError: "userError";
@@ -9,7 +9,7 @@ export declare const APPEARANCE_STATE: {
9
9
  };
10
10
  export type AppearanceState = ValueOf<typeof APPEARANCE_STATE>;
11
11
  export type TotalValueBlockProps = {
12
- value?: number;
12
+ value?: number | TotalValueRange;
13
13
  valueDelta?: PriceDeltaDetails;
14
14
  totalSumType?: TotalSumType;
15
15
  hint?: string;
@@ -5,7 +5,7 @@ import { AlarmFilledSVG, CrossFilledSVG, QuestionSVG } from '@snack-uikit/icons'
5
5
  import { Link } from '@snack-uikit/link';
6
6
  import { Tooltip } from '@snack-uikit/tooltip';
7
7
  import { Typography } from '@snack-uikit/typography';
8
- import { formatCurrency } from '../../../../helpers';
8
+ import { formatCurrency, formatCurrencyValue } from '../../../../helpers';
9
9
  import styles from './styles.module.css';
10
10
  export const APPEARANCE_STATE = {
11
11
  Default: 'default',
@@ -32,8 +32,34 @@ function getAppearanceIcon(appearance) {
32
32
  }
33
33
  return _jsx(Component, { size: 16, "data-appearance": appearance, className: styles.hintIcon });
34
34
  }
35
+ function getTotalSumPrefix(totalSumType, fromPrefix, toPrefix) {
36
+ if (totalSumType === 'from') {
37
+ return `${fromPrefix} `;
38
+ }
39
+ if (totalSumType === 'to') {
40
+ return `${toPrefix} `;
41
+ }
42
+ return '';
43
+ }
44
+ function isTotalValueRange(value) {
45
+ return typeof value === 'object' && value !== null;
46
+ }
47
+ function formatTotalValueRange(value) {
48
+ return `${formatCurrencyValue(value.min)} — ${formatCurrencyValue(value.max)} ₽`;
49
+ }
50
+ function formatTotalValue(value, totalSumType, fromPrefix, toPrefix) {
51
+ if (isTotalValueRange(value)) {
52
+ return formatTotalValueRange(value);
53
+ }
54
+ if (value === undefined) {
55
+ return 'N/A';
56
+ }
57
+ return `${getTotalSumPrefix(totalSumType, fromPrefix, toPrefix)}${formatCurrency(value)}`;
58
+ }
35
59
  export function TotalValueBlock({ value, totalSumType = 'equal', hint, hintAppearance = APPEARANCE_STATE.Default, showHintTooltip, hintTooltipText, hintLink, showHintLink, valueDelta, }) {
36
60
  const { t } = useLocale('PriceSummary');
37
- const totalSumPrefix = totalSumType === 'from' ? `${t('totalSumFromPrefix')} ` : '';
38
- return (_jsxs("div", { className: styles.content, "data-appearance": hintAppearance, children: [_jsx(Typography.LightHeadlineS, { children: value !== undefined ? `${totalSumPrefix}${formatCurrency(Number(value))}` : 'N/A' }), valueDelta && (_jsx(Typography.SansBodyS, { className: styles.valueDelta, children: `${t(`${valueDelta.type}Price`)} ${formatCurrency(valueDelta.value)}` })), _jsx(Tooltip, { open: showHintTooltip && hintTooltipText ? undefined : false, tip: hintTooltipText, placement: 'left-start', children: hint && (_jsxs("div", { className: styles.hint, "data-appearance": hintAppearance, children: [getAppearanceIcon(hintAppearance), _jsx(Typography.SansBodyS, { children: hint })] })) }), showHintLink && (_jsx(Link, { textMode: 'accent', appearance: 'neutral', size: 's', href: hintLink === null || hintLink === void 0 ? void 0 : hintLink.href, text: hintLink === null || hintLink === void 0 ? void 0 : hintLink.text, className: styles.link }))] }));
61
+ const displayedAppearance = hintAppearance !== null && hintAppearance !== void 0 ? hintAppearance : APPEARANCE_STATE.Default;
62
+ const displayedHint = hint !== null && hint !== void 0 ? hint : t('preliminaryHint');
63
+ const totalValue = formatTotalValue(value, totalSumType, t('totalSumFromPrefix'), t('totalSumToPrefix'));
64
+ return (_jsxs("div", { className: styles.content, "data-appearance": displayedAppearance, children: [_jsx(Typography.LightHeadlineS, { className: isTotalValueRange(value) ? styles.rangeValue : undefined, children: totalValue }), valueDelta && (_jsx(Typography.SansBodyS, { className: styles.valueDelta, children: `${t(`${valueDelta.type}Price`)} ${formatCurrency(valueDelta.value)}` })), _jsx(Tooltip, { open: showHintTooltip && hintTooltipText ? undefined : false, tip: hintTooltipText, placement: 'left-start', children: displayedHint && (_jsxs("div", { className: styles.hint, "data-appearance": displayedAppearance, children: [getAppearanceIcon(displayedAppearance), _jsx(Typography.SansBodyS, { children: displayedHint })] })) }), showHintLink && (_jsx(Link, { textMode: 'accent', appearance: 'neutral', size: 's', href: hintLink === null || hintLink === void 0 ? void 0 : hintLink.href, text: hintLink === null || hintLink === void 0 ? void 0 : hintLink.text, className: styles.link }))] }));
39
65
  }
@@ -1,4 +1,5 @@
1
1
  .content{
2
+ container-type:inline-size;
2
3
  display:flex;
3
4
  flex-direction:column;
4
5
  text-align:start;
@@ -13,6 +14,13 @@
13
14
  color:var(--sys-neutral-text-disabled, #aaaebd);
14
15
  }
15
16
 
17
+ .rangeValue{
18
+ max-width:100%;
19
+ font-size:clamp(18px, 8cqi, 24px);
20
+ line-height:30px;
21
+ overflow-wrap:anywhere;
22
+ }
23
+
16
24
  .hint{
17
25
  display:flex;
18
26
  align-items:center;
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mapInvoiceWithCalculatePrice } from '../mapInvoiceWithCalculatePrice';
3
+ const coveredSkuId = 'd4d5b1d9-7318-44c2-830b-5be7650f760f';
4
+ const uncoveredSkuId = 'ee5a2062-fdb3-43f9-a93e-2ae6224a35d6';
5
+ const unavailableSkuId = 'adb93523-71f8-4010-a469-3eb06079d4a5';
6
+ const resourceSpecCode = 'cpu.1c';
7
+ const invoice = [
8
+ {
9
+ quantity: 1,
10
+ title: 'Compute',
11
+ price: 100,
12
+ items: [
13
+ { id: coveredSkuId, quantity: 1, primary: true, label: 'Covered SKU', price: 40 },
14
+ { id: uncoveredSkuId, quantity: 1, primary: true, label: 'Uncovered SKU', price: 30 },
15
+ { id: unavailableSkuId, quantity: 1, primary: true, label: 'Unavailable SKU', price: 20 },
16
+ { quantity: 1, primary: false, label: 'Secondary SKU', price: 10 },
17
+ { quantity: 1, primary: true, label: 'SKU without id', price: 10 },
18
+ ],
19
+ },
20
+ ];
21
+ const calculatePriceData = {
22
+ skuResults: [
23
+ {
24
+ resourceSpecCodeId: coveredSkuId,
25
+ skuAvailability: true,
26
+ coveredByGrants: true,
27
+ },
28
+ {
29
+ resourceSpecCodeId: uncoveredSkuId,
30
+ skuAvailability: true,
31
+ coveredByGrants: false,
32
+ },
33
+ {
34
+ resourceSpecCodeId: unavailableSkuId,
35
+ skuAvailability: false,
36
+ coveredByGrants: true,
37
+ },
38
+ {
39
+ resourceSpecCode,
40
+ skuAvailability: true,
41
+ coveredByGrants: true,
42
+ },
43
+ ],
44
+ };
45
+ describe('mapInvoiceWithCalculatePrice', () => {
46
+ it('should return the original invoice when calculatePriceData is missing', () => {
47
+ expect(mapInvoiceWithCalculatePrice(invoice, undefined)).toBe(invoice);
48
+ });
49
+ it('should return the original invoice when skuResults are empty', () => {
50
+ expect(mapInvoiceWithCalculatePrice(invoice, { skuResults: [] })).toBe(invoice);
51
+ });
52
+ it('should set coveredByGrant for available SKUs matched by resourceSpecCodeId', () => {
53
+ const result = mapInvoiceWithCalculatePrice(invoice, calculatePriceData);
54
+ expect(result === null || result === void 0 ? void 0 : result[0].items[0]).toMatchObject({ id: coveredSkuId, coveredByGrant: true });
55
+ expect(result === null || result === void 0 ? void 0 : result[0].items[1]).toMatchObject({ id: uncoveredSkuId, coveredByGrant: false });
56
+ });
57
+ it('should not set coveredByGrant when skuAvailability is false', () => {
58
+ const result = mapInvoiceWithCalculatePrice(invoice, calculatePriceData);
59
+ expect(result === null || result === void 0 ? void 0 : result[0].items[2]).toEqual(invoice[0].items[2]);
60
+ expect(result === null || result === void 0 ? void 0 : result[0].items[2]).not.toHaveProperty('coveredByGrant');
61
+ });
62
+ it('should not set coveredByGrant for secondary items or items without id', () => {
63
+ const result = mapInvoiceWithCalculatePrice(invoice, calculatePriceData);
64
+ expect(result === null || result === void 0 ? void 0 : result[0].items[3]).toEqual(invoice[0].items[3]);
65
+ expect(result === null || result === void 0 ? void 0 : result[0].items[4]).toEqual(invoice[0].items[4]);
66
+ });
67
+ it('should match SKUs by resourceSpecCode', () => {
68
+ const invoiceWithResourceSpecCode = [
69
+ {
70
+ items: [{ id: resourceSpecCode, quantity: 1, primary: true, label: 'CPU', price: 100 }],
71
+ },
72
+ ];
73
+ const result = mapInvoiceWithCalculatePrice(invoiceWithResourceSpecCode, calculatePriceData);
74
+ expect(result === null || result === void 0 ? void 0 : result[0].items[0]).toMatchObject({ id: resourceSpecCode, coveredByGrant: true });
75
+ });
76
+ });
@@ -0,0 +1,3 @@
1
+ import { PriceChangeDetails } from '../types';
2
+ export declare function getSign(value: number): "+" | "-" | "";
3
+ export declare function formatPriceChange(value: PriceChangeDetails): string;
@@ -0,0 +1,19 @@
1
+ import { formatNumber } from '@cloud-ru/ft-formatters';
2
+ import { formatCurrency } from './formatters';
3
+ export function getSign(value) {
4
+ if (value > 0) {
5
+ return '+';
6
+ }
7
+ if (value < 0) {
8
+ return '-';
9
+ }
10
+ return '';
11
+ }
12
+ export function formatPriceChange(value) {
13
+ const sign = getSign(value.value);
14
+ const amount = `${sign}${formatCurrency(Math.abs(value.value))}`;
15
+ if (value.percentage === undefined) {
16
+ return amount;
17
+ }
18
+ return `${amount} (${sign}${formatNumber(Math.abs(value.percentage))}%)`;
19
+ }
@@ -1,2 +1,3 @@
1
+ export declare function formatCurrencyValue(value: number): string;
1
2
  export declare function formatCurrency(value: number): string;
2
3
  export declare function formatQuantity(value: string | number): string;
@@ -1,4 +1,9 @@
1
1
  import { formatNumber } from '@cloud-ru/ft-formatters';
2
+ export function formatCurrencyValue(value) {
3
+ return formatNumber(value.toFixed(2), {
4
+ precision: 2,
5
+ });
6
+ }
2
7
  export function formatCurrency(value) {
3
8
  return formatNumber(value.toFixed(2), {
4
9
  precision: 2,
@@ -0,0 +1 @@
1
+ export declare function getPriceChangeAppearance(value: number): "increased" | "decreased" | "unchanged";
@@ -0,0 +1,9 @@
1
+ export function getPriceChangeAppearance(value) {
2
+ if (value > 0) {
3
+ return 'increased';
4
+ }
5
+ if (value < 0) {
6
+ return 'decreased';
7
+ }
8
+ return 'unchanged';
9
+ }
@@ -1 +1,4 @@
1
1
  export * from './formatters';
2
+ export * from './mapInvoiceWithCalculatePrice';
3
+ export * from './formatPriceChange';
4
+ export * from './getPriceChangeAppearance';
@@ -1 +1,4 @@
1
1
  export * from './formatters';
2
+ export * from './mapInvoiceWithCalculatePrice';
3
+ export * from './formatPriceChange';
4
+ export * from './getPriceChangeAppearance';
@@ -0,0 +1,7 @@
1
+ import { CalculatePriceData, InvoiceDetails, InvoiceItem } from '../types';
2
+ export type InvoiceAvailabilitySplit = {
3
+ invoice: InvoiceDetails[] | undefined;
4
+ unavailableItems: InvoiceItem[] | undefined;
5
+ };
6
+ export declare function mapInvoiceWithCalculatePrice(invoice: InvoiceDetails[] | undefined, calculatePriceData: CalculatePriceData | undefined): InvoiceDetails[] | undefined;
7
+ export declare function splitInvoiceByAvailability(invoice: InvoiceDetails[] | undefined, calculatePriceData: CalculatePriceData | undefined): InvoiceAvailabilitySplit;
@@ -0,0 +1,76 @@
1
+ function setSkuFlags(skuFlagsById, key, flags) {
2
+ if (!key) {
3
+ return;
4
+ }
5
+ skuFlagsById.set(key, flags);
6
+ }
7
+ function getSkuFlagsById(calculatePriceData) {
8
+ const skuFlagsById = new Map();
9
+ calculatePriceData.skuResults.forEach(result => {
10
+ const flags = {
11
+ available: result.skuAvailability,
12
+ covered: result.coveredByGrants,
13
+ };
14
+ setSkuFlags(skuFlagsById, result.skuId, flags);
15
+ setSkuFlags(skuFlagsById, result.resourceSpecCode, flags);
16
+ setSkuFlags(skuFlagsById, result.resourceSpecCodeId, flags);
17
+ setSkuFlags(skuFlagsById, result.skuCode, flags);
18
+ });
19
+ return skuFlagsById;
20
+ }
21
+ function isUnavailableInvoiceItem(item, skuFlagsById) {
22
+ var _a;
23
+ if (!item.primary || !item.id) {
24
+ return false;
25
+ }
26
+ return ((_a = skuFlagsById.get(item.id)) === null || _a === void 0 ? void 0 : _a.available) === false;
27
+ }
28
+ function mapInvoiceItemWithSkuFlags(item, skuFlagsById) {
29
+ if (!item.primary || !item.id) {
30
+ return item;
31
+ }
32
+ const skuFlags = skuFlagsById.get(item.id);
33
+ if (!skuFlags || skuFlags.available === false) {
34
+ return item;
35
+ }
36
+ return Object.assign(Object.assign({}, item), { coveredByGrant: skuFlags.covered });
37
+ }
38
+ function splitInvoiceDetailsByAvailability(details, skuFlagsById) {
39
+ const unavailableItems = details.items.filter(item => isUnavailableInvoiceItem(item, skuFlagsById));
40
+ const availableItems = details.items
41
+ .filter(item => !isUnavailableInvoiceItem(item, skuFlagsById))
42
+ .map(item => mapInvoiceItemWithSkuFlags(item, skuFlagsById));
43
+ if (!availableItems.length) {
44
+ return { unavailableItems };
45
+ }
46
+ if (!unavailableItems.length) {
47
+ return {
48
+ availableDetails: Object.assign(Object.assign({}, details), { items: availableItems }),
49
+ unavailableItems,
50
+ };
51
+ }
52
+ return {
53
+ availableDetails: Object.assign(Object.assign({}, details), { items: availableItems, price: undefined, quantity: undefined }),
54
+ unavailableItems,
55
+ };
56
+ }
57
+ export function mapInvoiceWithCalculatePrice(invoice, calculatePriceData) {
58
+ if (!(invoice === null || invoice === void 0 ? void 0 : invoice.length) || !(calculatePriceData === null || calculatePriceData === void 0 ? void 0 : calculatePriceData.skuResults.length)) {
59
+ return invoice;
60
+ }
61
+ const skuFlagsById = getSkuFlagsById(calculatePriceData);
62
+ return invoice.map(details => (Object.assign(Object.assign({}, details), { items: details.items.map(item => mapInvoiceItemWithSkuFlags(item, skuFlagsById)) })));
63
+ }
64
+ export function splitInvoiceByAvailability(invoice, calculatePriceData) {
65
+ if (!(invoice === null || invoice === void 0 ? void 0 : invoice.length) || !(calculatePriceData === null || calculatePriceData === void 0 ? void 0 : calculatePriceData.skuResults.length)) {
66
+ return { invoice, unavailableItems: undefined };
67
+ }
68
+ const skuFlagsById = getSkuFlagsById(calculatePriceData);
69
+ const detailsSplits = invoice.map(details => splitInvoiceDetailsByAvailability(details, skuFlagsById));
70
+ const availableInvoice = detailsSplits.flatMap(({ availableDetails }) => availableDetails ? [availableDetails] : []);
71
+ const unavailableItems = detailsSplits.flatMap(detailsSplit => detailsSplit.unavailableItems);
72
+ if (!unavailableItems.length) {
73
+ return { invoice: availableInvoice, unavailableItems: undefined };
74
+ }
75
+ return { invoice: availableInvoice, unavailableItems };
76
+ }