feat:(TEAMMSBMOB-18327) - виджет популярных шаблонов

This commit is contained in:
2025-11-17 21:00:58 +03:00
parent ed827a8d34
commit f026094a67
16 changed files with 223 additions and 32 deletions
+2 -1
View File
@@ -39,8 +39,9 @@ const PATHS_DEPOSITS = {
const PATHS_PAYMENTS = {
HOME: PATHS.PAYMENTS,
PAYMENT_ORDER: `${PATHS.PAYMENTS}/payment-order`,
EDIT_OR_DETAILS: `${PATHS.PAYMENTS}/:id`,
TEMPLATES: `${PATHS.PAYMENTS}/templates`,
CREATE_TEMPLATE: `${PATHS.PAYMENTS}/templates/new`,
EDIT_OR_DETAILS: `${PATHS.PAYMENTS}/:id`,
} as const;
const STATEMENTS_AND_INQUIRIES_PATHS = {
@@ -1,4 +1,5 @@
import { Route, Switch } from 'react-router-dom';
import { CreateTemplatePage } from '@/pages/CreateTemplatePage';
import { OrderDetailsPage } from '@/pages/OrderDetailsPage';
import { PaymentOrderPage } from '@/pages/PaymentOrderPage';
import { PaymentsMainPage } from '@/pages/PaymentsMainPage';
@@ -9,7 +10,8 @@ const AppRouter = () => (
<Switch>
<Route exact component={PaymentsMainPage} path={PATHS.HOME.PATH} />
<Route component={PaymentOrderPage} path={PATHS.PAYMENT_ORDER.PATH} />
<Route component={TemplatesPage.Page} path={PATHS.TEMPLATES.PATH} />
<Route exact component={TemplatesPage.Page} path={PATHS.TEMPLATES.PATH} />
<Route component={CreateTemplatePage.Page} path={PATHS.CREATE_TEMPLATE.PATH} />
<Route exact component={OrderDetailsPage.Page} path={PATHS.EDIT_OR_DETAILS.PATH} />
</Switch>
);
@@ -0,0 +1 @@
export * from './localization';
@@ -0,0 +1,5 @@
const LOCALIZATION = {
TITLE: 'Создание шаблона',
};
export { LOCALIZATION };
@@ -0,0 +1 @@
export * from './ui';
@@ -0,0 +1,16 @@
import type { ReactElement } from 'react';
import { PageLayoutWithSections } from '@msb/shared';
import { LOCALIZATION } from '../constants';
import { PageHeader, PageLayout } from '@/shared/ui';
namespace CreateTemplatePage {
export const Page = (): ReactElement => (
<PageLayoutWithSections>
<PageLayout>
<PageHeader.Element title={LOCALIZATION.TITLE} />
</PageLayout>
</PageLayoutWithSections>
);
}
export { CreateTemplatePage };
@@ -0,0 +1 @@
export * from './CreateTemplatePage';
@@ -30,6 +30,7 @@ import { useRublePaymentClient } from '@/entities/payments';
import { CreatePaymentButton } from '@/features/CreatePaymentButton';
import { PAYMENTS_YM_EVENTS, PATHS } from '@/shared/constants';
import { AsideInformerList } from '@/widgets/AsideInformerList';
import { FrequentlyUsedTemplates } from '@/widgets/FrequentlyUsedTemplates';
import { PaymentsListContent } from '@/widgets/PaymentsListContent';
const PaymentsMainPage = (): ReactElement => {
@@ -110,6 +111,10 @@ const PaymentsMainPage = (): ReactElement => {
}
};
const organizationId = FrequentlyUsedTemplates.useAllowWidgetShow()
? FrequentlyUsedTemplates.getOrganizationId(userAuthorities)
: undefined;
if (isLoading) {
return <PaymentsListSkeleton />;
}
@@ -156,7 +161,12 @@ const PaymentsMainPage = (): ReactElement => {
return (
<PageLayoutWithSections
asideInMobile
aside={<AsideInformerList informers={INFORMERS_LIST_ITEMS} />}
aside={
<>
<AsideInformerList informers={INFORMERS_LIST_ITEMS} />
{organizationId && <FrequentlyUsedTemplates.Element organizationId={organizationId} />}
</>
}
header={
<S.HeaderWrapper>
<Title.H1>{LOCALIZATION.PAYMENTS_TITLE}</Title.H1>
@@ -1,7 +1,15 @@
import { type ReactElement, useMemo } from 'react';
import { AUTHORITIES, checkOrganizationsHavePermission, PageLayoutWithSections, SystemResponseStatus, useAppContext } from '@msb/shared';
import {
AUTHORITIES,
checkOrganizationsHavePermission,
PageLayoutWithSections,
SystemResponseStatus,
useAppContext,
useRedirect,
} from '@msb/shared';
import { TemplatesList } from '../../../features';
import { LOCALIZATION } from '../constants';
import { PATHS } from '@/shared/constants';
import { PageHeader, PageLayout } from '@/shared/ui';
namespace TemplatesPage {
@@ -14,11 +22,12 @@ namespace TemplatesPage {
return organizations.length > 0 ? organizations[0] : '';
}, [userAuthorities]);
const createTemplateNavigation = useRedirect(PATHS.CREATE_TEMPLATE.PATH);
return (
<PageLayoutWithSections>
<PageLayout>
<PageHeader.Element buttonTitle={LOCALIZATION.CREATE} title={LOCALIZATION.TITLE} />
<PageHeader.Element buttonAction={createTemplateNavigation} buttonTitle={LOCALIZATION.CREATE} title={LOCALIZATION.TITLE} />
{organizationId === '' ? (
<SystemResponseStatus
description={LOCALIZATION.ERROR.DESCRIPTION}
@@ -46,30 +55,3 @@ namespace TemplatesPage {
}
export { TemplatesPage };
/*
{!isLoading && model ? (
<Flex column mb="7">
<PageHeader.Element
badge={{ title: model.presentation.badge, type: model.presentation.badgeType }}
subtitle={model.presentation.subtitle}
title={LOCALIZATION.TITLE}
/>
<DescriptionList dataName="payment-card-view">
{model.presentation.records.map(record =>
record.value ? (
<DescriptionRow key={record.name} label={record.title} labelWidth="30%" name={record.name}>
{record.value}
</DescriptionRow>
) : (
<Flex row mb="3" mt="6">
<Title.H5 key={record.name}>{record.title}</Title.H5>
</Flex>
)
)}
</DescriptionList>
</Flex>
) : (
<Flex column mb="7" />
)}
* */
@@ -5,6 +5,7 @@ const PATHS = {
PAYMENT_ORDER: { PATH: PATHS_PAYMENTS.PAYMENT_ORDER, TITLE: 'Платёж юридическому или физическому лицу' },
EDIT_OR_DETAILS: { PATH: PATHS_PAYMENTS.EDIT_OR_DETAILS, TITLE: '' },
TEMPLATES: { PATH: PATHS_PAYMENTS.TEMPLATES, TITLE: '' },
CREATE_TEMPLATE: { PATH: PATHS_PAYMENTS.CREATE_TEMPLATE, TITLE: '' },
} as const;
export { PATHS };
@@ -0,0 +1 @@
export * from './localization';
@@ -0,0 +1,14 @@
const LOCALIZATION = {
TITLE: 'Шаблоны',
BUTTON: 'Показать все',
ERROR: {
TEXT: 'Данные не загрузились',
BUTTON: 'Обновить',
},
EMPTY: {
TEXT: 'Создавайте шаблоны для быстрого заполнения платежей',
BUTTON: 'Создать',
},
};
export { LOCALIZATION };
@@ -0,0 +1 @@
export * from './ui';
@@ -0,0 +1,43 @@
import styled from '@emotion/styled';
const webkitBox = '-webkit-box';
const fontSizeTitle = 18; // '18px'
const lineHeightTitle = 1.33; // '24px'
const lineClamp = 2;
const Container = styled.div`
display: grid;
grid-template-columns: 2fr 20px;
grid-template-rows: 1fr;
gap: 4px 16px;
position: relative;
grid-template-areas: 'title icon';
`;
const IconWrapper = styled.div(() => ({
gridArea: 'icon',
display: 'flex',
alignItems: 'center',
justifyContent: 'right',
justifyItems: 'right',
width: '20px',
height: '20px',
verticalAlign: 'top',
}));
const TitleWrapper = styled.div(() => ({
gridArea: 'title',
'& div': {
display: webkitBox,
maxHeight: `${fontSizeTitle * lineHeightTitle * lineClamp}px`,
margin: '0 auto',
fontSize: fontSizeTitle,
lineHeight: lineHeightTitle,
'-webkit-line-clamp': String(lineClamp),
'-webkit-box-orient': 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}));
export { Container, IconWrapper, TitleWrapper };
@@ -0,0 +1,111 @@
import { type ReactElement, useMemo } from 'react';
import { Button, ButtonLink, Skeleton } from '@fractal-ui/core';
import { Icon } from '@fractal-ui/library';
import { Title, Text } from '@fractal-ui/styling';
import { type AuthoritiesResponseDto, FEATURE_TOGGLE_NAMES, usePaymentTemplate } from '@msb/http';
import { AUTHORITIES, checkOrganizationsHavePermission, Flex, range, useAppContext, useFeatureToggles, useRedirect } from '@msb/shared';
import { LOCALIZATION } from '../constants';
import * as S from './FrequentlyUsedTemplates.styles';
import { PATHS } from '@/shared/constants';
namespace FrequentlyUsedTemplates {
export interface Props {
organizationId: string;
}
export const getOrganizationId = (userAuthorities?: AuthoritiesResponseDto): string | undefined => {
const organizations = checkOrganizationsHavePermission(userAuthorities?.data.clientAuthorities || {}, [
AUTHORITIES.PAYMENT.TEMPLATE_VIEW,
]);
return organizations.length > 0 ? organizations[0] : undefined;
};
export const useAllowWidgetShow = (): boolean => {
const { userAuthorities } = useAppContext();
const { isEnabled: isTemplatesEnabled } = useFeatureToggles(FEATURE_TOGGLE_NAMES.PAYMENT_TEMPLATES);
const organizationId = useMemo(() => getOrganizationId(userAuthorities), [userAuthorities]);
return isTemplatesEnabled && organizationId !== undefined && organizationId.length > 0;
};
const loadingSkeletonHeight = 8;
const maxRecordsCount = 3;
export const Element = ({ organizationId }: Props): ReactElement => {
const { data, isLoading, isError, refetch } = usePaymentTemplate(organizationId, 0, 3, false);
const records = useMemo(
() =>
data && Array.isArray(data.pages)
? data?.pages
.reduce((accumulator, item) => accumulator.concat(item), [])
.sort((a, b) => new Date(b.usedAt ?? 0).getTime() - new Date(a.usedAt ?? 0).getTime())
.filter((record, index) => index < maxRecordsCount)
: [],
[data]
);
const createTemplate = useRedirect(PATHS.CREATE_TEMPLATE.PATH);
const listTemplate = useRedirect(PATHS.TEMPLATES.PATH);
return (
<Flex
column
backgroundColor="bg.primary"
borderRadius="16px"
boxShadow="0 0 16px 0 rgba(78, 88, 134, 0.04)"
gap={4}
padding="4"
width="100%"
>
<Flex column gap={4} mb="4">
<Title.H4>{LOCALIZATION.TITLE}</Title.H4>
{isLoading &&
[...range(0, 2)].map(index => (
<Skeleton key={index} dataName="freq-template-record" height={loadingSkeletonHeight} variant="card" />
))}
{!isLoading && isError && <Text.P2 color="text.secondary">{LOCALIZATION.ERROR.TEXT}</Text.P2>}
{!isError && !isLoading && records.length === 0 && <Text.P2 color="text.secondary">{LOCALIZATION.EMPTY.TEXT}</Text.P2>}
{!isError &&
!isLoading &&
records.length > 0 &&
records.map(record => (
<ButtonLink key={record.id} align="left" dataAction="navigate-to-template">
<S.Container>
<S.TitleWrapper>
<Text.P1>{record.templateName}</Text.P1>
</S.TitleWrapper>
{!record.accountExists && (
<S.IconWrapper>
<Icon color="text.warning" name="AttentionFilled" />
</S.IconWrapper>
)}
</S.Container>
</ButtonLink>
))}
</Flex>
{(isError || !isLoading) && (
<Button
dataAction="refetch-templates"
shape="default"
size="M"
variant="secondary"
onClick={() => {
if (isError) {
refetch();
} else if (records.length === 0) {
createTemplate();
} else {
listTemplate();
}
}}
>
{/* eslint-disable-next-line no-nested-ternary */}
{isError ? LOCALIZATION.ERROR.BUTTON : records.length === 0 ? LOCALIZATION.EMPTY.BUTTON : LOCALIZATION.BUTTON}
</Button>
)}
</Flex>
);
};
}
export { FrequentlyUsedTemplates };
@@ -0,0 +1 @@
export * from './FrequentlyUsedTemplates';