feat(TEAMMSBMOB-19897): платеж из шаблона
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
import { network, getDataOrThrowError, RUBLE_PAYMENT_TEMPLATE_FILL_ENDPOINT, type RublePaymentTemplateFill } from '@msb/http';
|
||||
import type { ServerResponse } from '../../../model';
|
||||
import { getTimeOffsetHours } from '../../../network/utils';
|
||||
|
||||
const fetchPaymentTemplateFill = async (id: string, organizationId: string): Promise<RublePaymentTemplateFill.Record> => {
|
||||
const path = RUBLE_PAYMENT_TEMPLATE_FILL_ENDPOINT.replace(':id', id);
|
||||
const response = await network.client.get<ServerResponse<RublePaymentTemplateFill.Record>>(path, {
|
||||
headers: { OrganizationId: organizationId, TimeOffset: getTimeOffsetHours() },
|
||||
});
|
||||
|
||||
return getDataOrThrowError(response).data;
|
||||
};
|
||||
|
||||
export { fetchPaymentTemplateFill };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './fetchPaymentTemplateFill';
|
||||
@@ -6,12 +6,13 @@ const fetchPaymentTemplate = async (
|
||||
organizationId: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
sortingField: string,
|
||||
search?: string
|
||||
): Promise<RublePaymentTemplate.Record[]> => {
|
||||
const data = {
|
||||
params: {
|
||||
paging: { offset, limit },
|
||||
multiSort: [{ field: 'usedAt', direction: 'DESC' }],
|
||||
multiSort: [{ field: sortingField, direction: 'DESC' }],
|
||||
...(search
|
||||
? {
|
||||
filter: {
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './getPaymentCopy';
|
||||
export * from './getPaymentInfo';
|
||||
export * from './getPaymentTemplate';
|
||||
export * from './fetchPaymentCreateTemplate';
|
||||
export * from './fetchPaymentTemplateFill';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const RUBLE_PAYMENT_TEMPLATE_FILL_ENDPOINT = '/ruble-payment-client/ruble-payment/template/fill/:id' as const;
|
||||
|
||||
export { RUBLE_PAYMENT_TEMPLATE_FILL_ENDPOINT };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './const';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace RublePaymentTemplateFill {
|
||||
export interface PaymentInfo {
|
||||
paymentTypeName?: string;
|
||||
paymentType?: string;
|
||||
operationType?: string;
|
||||
paymentPriority?: string;
|
||||
paymentPurpose?: string;
|
||||
}
|
||||
export interface PaymentAmount {
|
||||
paymentAmount: string;
|
||||
paymentAmountIncludeVat: string;
|
||||
paymentCurrency: string;
|
||||
vatCalculationMethod: string;
|
||||
vatAmount: string;
|
||||
vatPercentRate: number;
|
||||
}
|
||||
export interface PayerDetails {
|
||||
name: string;
|
||||
innKio: string;
|
||||
kpp: string;
|
||||
accountNumber: string;
|
||||
accountType: string;
|
||||
clientAccountId: string;
|
||||
bankName: string;
|
||||
bankBic: string;
|
||||
bankCorrespondentAccountNumber: string;
|
||||
bankLocalityName: string;
|
||||
bankLocalityType: string;
|
||||
thirdPartyPayment: boolean;
|
||||
branchName: string;
|
||||
balanceBranchName: string;
|
||||
}
|
||||
export interface BudgetInfo {
|
||||
budgetPayment: boolean;
|
||||
docAuthorStatus: string;
|
||||
kbk: string;
|
||||
oktmo: string;
|
||||
paymentBasis: string;
|
||||
taxPeriodValue: string;
|
||||
taxPeriodValueType: string;
|
||||
paymentBasisDocNumber: string;
|
||||
paymentBasisDocDate: string;
|
||||
}
|
||||
export interface RecipientDetails {
|
||||
name: string;
|
||||
innKio: string;
|
||||
kpp: string;
|
||||
accountNumber: string;
|
||||
bankName: string;
|
||||
bankBic: string;
|
||||
bankCorrespondentAccountNumber: string;
|
||||
bankLocalityName: string;
|
||||
bankLocalityType: string;
|
||||
}
|
||||
|
||||
export interface Record {
|
||||
id: string;
|
||||
userId: string;
|
||||
userFio: string;
|
||||
templateName: string;
|
||||
bankClientId: string;
|
||||
clientRnk: string;
|
||||
eskClientId: string,
|
||||
paymentInfo: PaymentInfo;
|
||||
paymentAmount: PaymentAmount;
|
||||
payerDetails: PayerDetails;
|
||||
recipientDetails?: RecipientDetails;
|
||||
budgetInfo: BudgetInfo;
|
||||
version: number;
|
||||
accountExists?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export type { RublePaymentTemplateFill };
|
||||
@@ -30,7 +30,7 @@ namespace RublePaymentInfo {
|
||||
readonly accountNumber: string;
|
||||
readonly accountType?: string;
|
||||
readonly clientAccountId: string;
|
||||
readonly clientF1AccountId: number;
|
||||
readonly clientF1AccountId?: number;
|
||||
readonly bankName: string;
|
||||
readonly bankBic: string;
|
||||
readonly bankCorrespondentAccountNumber: string;
|
||||
@@ -54,6 +54,21 @@ namespace RublePaymentInfo {
|
||||
export interface DocumentDetails {
|
||||
readonly payerAccountReceiptDate: string;
|
||||
}
|
||||
export interface BudgetInfo {
|
||||
readonly budgetPayment: boolean;
|
||||
readonly docAuthorStatus?: string;
|
||||
readonly kbk?: string;
|
||||
readonly oktmo?: string;
|
||||
readonly paymentBasis?: string;
|
||||
readonly taxPeriodValueType?: string;
|
||||
readonly taxPeriodType?: string;
|
||||
readonly taxPeriod?: string;
|
||||
readonly taxPeriodYear?: number;
|
||||
readonly taxPeriodDate?: string;
|
||||
readonly taxPeriodCustomsCode?: string;
|
||||
readonly paymentBasisDocNumber?: string;
|
||||
readonly paymentBasisDocDate?: string;
|
||||
}
|
||||
export interface Record {
|
||||
readonly id: string;
|
||||
readonly status: string;
|
||||
@@ -73,21 +88,7 @@ namespace RublePaymentInfo {
|
||||
readonly paymentAmount?: PaymentAmount;
|
||||
readonly payerDetails?: PayerDetails;
|
||||
readonly recipientDetails?: RecipientDetails;
|
||||
readonly budgetInfo: {
|
||||
readonly budgetPayment: boolean;
|
||||
readonly docAuthorStatus?: string;
|
||||
readonly kbk?: string;
|
||||
readonly oktmo?: string;
|
||||
readonly paymentBasis?: string;
|
||||
readonly taxPeriodValueType?: string;
|
||||
readonly taxPeriodType?: string;
|
||||
readonly taxPeriod?: string;
|
||||
readonly taxPeriodYear?: number;
|
||||
readonly taxPeriodDate?: string;
|
||||
readonly taxPeriodCustomsCode?: string;
|
||||
readonly paymentBasisDocNumber?: string;
|
||||
readonly paymentBasisDocDate?: string;
|
||||
};
|
||||
readonly budgetInfo?: BudgetInfo;
|
||||
readonly createdAt: string;
|
||||
readonly version: number;
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './getPaymentCopy';
|
||||
export * from './getPaymentInfo';
|
||||
export * from './getPaymentTemplate';
|
||||
export * from './fetchPaymentCreateTemplate';
|
||||
export * from './fetchPaymentTemplateFill';
|
||||
|
||||
@@ -4,11 +4,19 @@ import type { RublePaymentTemplate } from '../../endpoints';
|
||||
|
||||
const tenSeconds = 10_000;
|
||||
|
||||
const usePaymentTemplate = (organizationId: string, offset: number = 0, limit: number = 25, isStrict: boolean = false, search?: string) =>
|
||||
const usePaymentTemplate = (
|
||||
organizationId: string,
|
||||
offset: number = 0,
|
||||
limit: number = 25,
|
||||
isStrict: boolean = false,
|
||||
sortByDate: boolean = false,
|
||||
search?: string
|
||||
) =>
|
||||
useInfiniteQuery<RublePaymentTemplate.Record[], Error | undefined>({
|
||||
cacheTime: tenSeconds,
|
||||
queryKey: [GET_RUBLE_PAYMENTS_TEMPLATE_QUERY, organizationId, offset, limit, ...(search ? [search] : [])],
|
||||
queryFn: ({ pageParam = { offset, limit } }) => fetchPaymentTemplate(organizationId, pageParam.offset, pageParam.limit, search),
|
||||
queryFn: ({ pageParam = { offset, limit } }) =>
|
||||
fetchPaymentTemplate(organizationId, pageParam.offset, pageParam.limit, sortByDate ? 'createdAt' : 'usedAt', search),
|
||||
getNextPageParam: (page, allPages) => {
|
||||
if (isStrict) {
|
||||
return;
|
||||
|
||||
@@ -38,10 +38,11 @@ const PATHS_DEPOSITS = {
|
||||
|
||||
const PATHS_PAYMENTS = {
|
||||
HOME: PATHS.PAYMENTS,
|
||||
EDIT_OR_DETAILS: `${PATHS.PAYMENTS}/:id`,
|
||||
PAYMENT_ORDER: `${PATHS.PAYMENTS}/payment-order`,
|
||||
TEMPLATES: `${PATHS.PAYMENTS}/templates`,
|
||||
CREATE_TEMPLATE: `${PATHS.PAYMENTS}/templates/new`,
|
||||
EDIT_OR_DETAILS: `${PATHS.PAYMENTS}/:id`,
|
||||
EDIT_TEMPLATE: `${PATHS.PAYMENTS}/templates/:id`,
|
||||
} as const;
|
||||
|
||||
const STATEMENTS_AND_INQUIRIES_PATHS = {
|
||||
|
||||
@@ -26,4 +26,15 @@ const useHashedRedirect = (path: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { useRedirect, useHashedRedirect };
|
||||
const useHashedReplace = (path: string) => {
|
||||
const history = useHistory();
|
||||
|
||||
return useCallback(
|
||||
(hash: string) => {
|
||||
history.replace(`${path}#${hash}`);
|
||||
},
|
||||
[path, history]
|
||||
);
|
||||
};
|
||||
|
||||
export { useRedirect, useHashedRedirect, useHashedReplace };
|
||||
|
||||
@@ -12,6 +12,7 @@ const AppRouter = () => (
|
||||
<Route component={PaymentOrderPage} path={PATHS.PAYMENT_ORDER.PATH} />
|
||||
<Route exact component={TemplatesPage.Page} path={PATHS.TEMPLATES.PATH} />
|
||||
<Route component={CreateTemplatePage.Page} path={PATHS.CREATE_TEMPLATE.PATH} />
|
||||
<Route component={CreateTemplatePage.Page} path={PATHS.EDIT_TEMPLATE.PATH} />
|
||||
<Route exact component={OrderDetailsPage.Page} path={PATHS.EDIT_OR_DETAILS.PATH} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const LOCALIZATION = {
|
||||
TEMPLATE: {
|
||||
TITLE: 'Название шаблона',
|
||||
ERROR: {
|
||||
TITLE: 'Указанный в шаблоне счёт списания недоступен',
|
||||
DESCRIPTION: 'Для создания платежа выберите действующий счёт',
|
||||
BUTTON: 'Закрыть',
|
||||
},
|
||||
},
|
||||
PAYEE_BLOCK: {
|
||||
TITLE: 'Получатель',
|
||||
FIELD_NAME_OR_INN: {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ValidationBankSerials } from '@msb/shared';
|
||||
import type { TaxPeriodType, TaxPeriodValueType } from '../constants';
|
||||
import { FIELD_IDENTITY, validationErrorMessages, BUDGET_FIELD_IDS, type BudgetFieldId } from '../constants';
|
||||
import { TAX_PERIOD_TYPE, TAX_PERIOD_VALUE_TYPE } from '../constants';
|
||||
import {
|
||||
FIELD_IDENTITY,
|
||||
validationErrorMessages,
|
||||
BUDGET_FIELD_IDS,
|
||||
TAX_PERIOD_TYPE,
|
||||
TAX_PERIOD_VALUE_TYPE,
|
||||
type BudgetFieldId,
|
||||
} from '../constants';
|
||||
import { ValidationMessage } from './ValidationMessage';
|
||||
|
||||
namespace FormValidation {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { fetchPaymentInfo, fetchPaymentCopy, RUBLE_PAYMENT_CLIENT_STATUS } from '@msb/http';
|
||||
import { fetchPaymentInfo, fetchPaymentCopy, RUBLE_PAYMENT_CLIENT_STATUS, fetchPaymentTemplateFill } from '@msb/http';
|
||||
import type {
|
||||
PaymentDocumentData,
|
||||
RublePaymentClientRublePaymentRequestData,
|
||||
RublePaymentCopy,
|
||||
RublePaymentDto,
|
||||
RublePaymentInfo,
|
||||
RublePaymentTemplateFill,
|
||||
} from '@msb/http';
|
||||
import type { UserDeviceInfo, UserBlockingStatus } from '@msb/shared';
|
||||
import { getUserDeviceInfo, isOrganizationBlocked } from '@msb/shared';
|
||||
import { type UserDeviceInfo, type UserBlockingStatus, getLocalDateTimeISO, getUserDeviceInfo, isOrganizationBlocked } from '@msb/shared';
|
||||
import { FIELD_IDENTITY } from '../constants';
|
||||
import type { PayeeAccount } from '../ui/PayeeFormFields';
|
||||
import type { PayerFieldForm } from '../ui/PayerFormFields';
|
||||
@@ -19,6 +19,7 @@ import { rublePaymentClientRublePayment } from '@/entities/sign';
|
||||
|
||||
namespace PaymentOrderFormFields {
|
||||
export type ForceUpdate = () => void;
|
||||
export type ShowStatusModal = () => void;
|
||||
export type ShowSnackbarMessageFn = (type: TOAST_TYPE) => void;
|
||||
export type ShowSignResultModalFn = (isSuccess: boolean) => void;
|
||||
|
||||
@@ -37,6 +38,7 @@ namespace PaymentOrderFormFields {
|
||||
export enum ACTION_TYPE {
|
||||
REPEAT = 'R',
|
||||
EDIT = 'E',
|
||||
TEMPLATE = 'T',
|
||||
}
|
||||
|
||||
export enum VALIDATION_LEVEL {
|
||||
@@ -76,17 +78,47 @@ namespace PaymentOrderFormFields {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface Template {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Form extends PayeeAccount.Input, PayerFieldForm.Input {
|
||||
check(): boolean;
|
||||
save(): boolean;
|
||||
signAndSend(): boolean;
|
||||
restore(identity: string): void;
|
||||
restore(identity: string, showStatusModal?: ShowStatusModal): void;
|
||||
readonly isSaving: boolean;
|
||||
readonly isSigningAndSending: boolean;
|
||||
readonly isFromTemplate: boolean;
|
||||
readonly templateName: string;
|
||||
getDocumentStatus(): string | null;
|
||||
updateAddressFromPayeeAccount(payeeAccount: string): void;
|
||||
}
|
||||
|
||||
const mapTemplate2Info = (record: RublePaymentTemplateFill.Record): RublePaymentInfo.Record => {
|
||||
const result = {
|
||||
...record,
|
||||
status: 'NEW',
|
||||
canCreateDio: false,
|
||||
docSourceSystem: 'WEB',
|
||||
archive: false,
|
||||
docCreationType: 'TEMPLATE',
|
||||
docCreationPage: '',
|
||||
id: '',
|
||||
sampleDocId: record.id,
|
||||
createdAt: getLocalDateTimeISO(new Date()),
|
||||
paymentInfo: {
|
||||
paymentTypeName: record.paymentInfo?.paymentTypeName || '',
|
||||
paymentType: record.paymentInfo?.paymentType || '',
|
||||
operationType: record.paymentInfo?.operationType || '',
|
||||
paymentPriority: record.paymentInfo?.paymentPriority || '',
|
||||
paymentPurpose: record.paymentInfo?.paymentPurpose || '',
|
||||
},
|
||||
};
|
||||
|
||||
return result as RublePaymentInfo.Record;
|
||||
};
|
||||
|
||||
class FormImpl implements Form {
|
||||
private _payeeFieldsOutput: PayeeAccount.Output | null = null;
|
||||
private _payerFieldsOutput: PayerFieldForm.Output | null = null;
|
||||
@@ -101,6 +133,7 @@ namespace PaymentOrderFormFields {
|
||||
private readonly _blockingStatus: UserBlockingStatus | null;
|
||||
private _isSaving = false;
|
||||
private _isSigningAndSending = false;
|
||||
private _template: Template | null = null;
|
||||
constructor(
|
||||
organizationId: string,
|
||||
forceUpdate: ForceUpdate,
|
||||
@@ -125,6 +158,14 @@ namespace PaymentOrderFormFields {
|
||||
return this._isSigningAndSending;
|
||||
}
|
||||
|
||||
get isFromTemplate(): boolean {
|
||||
return this._template !== null;
|
||||
}
|
||||
|
||||
get templateName(): string {
|
||||
return this._template?.name || '';
|
||||
}
|
||||
|
||||
getDocumentStatus(): string | null {
|
||||
return this._documentData?.status || null;
|
||||
}
|
||||
@@ -137,11 +178,15 @@ namespace PaymentOrderFormFields {
|
||||
|
||||
// interface Form
|
||||
|
||||
restore(identity: string) {
|
||||
restore(identity: string, showStatusModal?: ShowStatusModal) {
|
||||
this._template = null;
|
||||
|
||||
const identityValue = identity.startsWith('#') ? identity.substring(1) : identity;
|
||||
const iValue = identityValue.substring(0, 1);
|
||||
|
||||
if (iValue.length === 0 || !(iValue === ACTION_TYPE.EDIT || iValue === ACTION_TYPE.REPEAT)) {
|
||||
const types = [ACTION_TYPE.EDIT, ACTION_TYPE.REPEAT, ACTION_TYPE.TEMPLATE];
|
||||
|
||||
if (iValue.length === 0 || !types.includes(iValue as ACTION_TYPE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -163,6 +208,18 @@ namespace PaymentOrderFormFields {
|
||||
this.restoreForm(result, this._payeeFieldsOutput, this._payerFieldsOutput);
|
||||
});
|
||||
break;
|
||||
case ACTION_TYPE.TEMPLATE:
|
||||
fetchPaymentTemplateFill(id, organizationId).then(result => {
|
||||
this._template = { name: result.templateName };
|
||||
this._documentData = { docCreationType: DOC_CREATION_TYPE.TEMPLATE };
|
||||
this._repeatDocument = { ...mapTemplate2Info(result), docCreationType: DOC_CREATION_TYPE.TEMPLATE };
|
||||
this.restoreForm(mapTemplate2Info(result), this._payeeFieldsOutput, this._payerFieldsOutput);
|
||||
|
||||
if (result.accountExists !== true && showStatusModal) {
|
||||
showStatusModal();
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
|
||||
const ContainerWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 16px;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { useEffect, useCallback, useReducer, useRef, useState } from 'react';
|
||||
import { Input } from '@fractal-ui/composites';
|
||||
import { Button, ScrollContainer } from '@fractal-ui/core';
|
||||
import { ErrorIcon, OkIcon } from '@fractal-ui/library';
|
||||
import { useSnackbar } from '@fractal-ui/overlays';
|
||||
import { StatusModal, useSnackbar } from '@fractal-ui/overlays';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
import { queryClient, QUERY_KEYS_OPERATIONS_HISTORY } from '@msb/http';
|
||||
import {
|
||||
AUTHORITIES,
|
||||
@@ -79,6 +81,10 @@ namespace PaymentOrderForm {
|
||||
const toPayments = useRedirect(GLOBAL_PATHS.PAYMENTS);
|
||||
const [showSuccessScreen, setShowSuccessScreen] = useState(false);
|
||||
const { handleReachGoal } = useYaMetrika();
|
||||
const [showStatusModal, setShowStatusModal] = useState(false);
|
||||
const handleCloseStatusModal = useCallback(() => {
|
||||
setShowStatusModal(false);
|
||||
}, [setShowStatusModal]);
|
||||
|
||||
const modalShowCallback = useCallback(
|
||||
(isSuccess: boolean) => {
|
||||
@@ -113,7 +119,9 @@ namespace PaymentOrderForm {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
formFields.restore(hash);
|
||||
formFields.restore(hash, () => {
|
||||
setShowStatusModal(true);
|
||||
});
|
||||
}, [hash, formFields]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -123,69 +131,99 @@ namespace PaymentOrderForm {
|
||||
const handleSubmit = () => {};
|
||||
|
||||
return (
|
||||
<Form
|
||||
render={({ form }) => {
|
||||
formRef.current = form;
|
||||
<>
|
||||
<Form
|
||||
render={({ form }) => {
|
||||
formRef.current = form;
|
||||
|
||||
const documentStatus = formFields.getDocumentStatus();
|
||||
const documentStatus = formFields.getDocumentStatus();
|
||||
|
||||
const canShowByAuthorities = canSend && Boolean(currentOrg);
|
||||
const canShowByAuthorities = canSend && Boolean(currentOrg);
|
||||
|
||||
const isStatusOk = documentStatus ? isSignAndSendAllowedStatus(documentStatus) : true;
|
||||
const isStatusOk = documentStatus ? isSignAndSendAllowedStatus(documentStatus) : true;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSuccessScreen && (
|
||||
<YaMetrikaReachGoal
|
||||
goalType={YM_GOALS.SCREEN_VIEW}
|
||||
params={{ [YM_GOALS.SCREEN_VIEW]: { screen_name: PAYMENTS_YM_EVENTS.PAYMENT_SUCCESS } }}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer autoHeight autoHeightMax="78vh">
|
||||
<PageHeader.Element title={title} />
|
||||
<PayeeFormFields formInput={formFields} formRef={formRef} />
|
||||
<PayerFormFields formInput={formFields} formRef={formRef} orgHandleChange={orgHandleChange} />
|
||||
</ScrollContainer>
|
||||
|
||||
<S.ButtonsWrapper>
|
||||
{canShowByAuthorities && isStatusOk && (
|
||||
return (
|
||||
<>
|
||||
{showSuccessScreen && (
|
||||
<YaMetrikaReachGoal
|
||||
goalType={YM_GOALS.SCREEN_VIEW}
|
||||
params={{ [YM_GOALS.SCREEN_VIEW]: { screen_name: PAYMENTS_YM_EVENTS.PAYMENT_SUCCESS } }}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer autoHeight autoHeightMax="78vh">
|
||||
<PageHeader.Element title={title} />
|
||||
{formFields.isFromTemplate && (
|
||||
<Input
|
||||
disabled
|
||||
label={LOCALIZATION.TEMPLATE.TITLE}
|
||||
labelPosition="top"
|
||||
name="template-name"
|
||||
value={formFields.templateName}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
<PayeeFormFields formInput={formFields} formRef={formRef} />
|
||||
<PayerFormFields formInput={formFields} formRef={formRef} orgHandleChange={orgHandleChange} />
|
||||
</ScrollContainer>
|
||||
|
||||
<S.ButtonsWrapper>
|
||||
{canShowByAuthorities && isStatusOk && (
|
||||
<Button
|
||||
dataAction="Sign and Send"
|
||||
isLoading={formFields.isSigningAndSending}
|
||||
size="M"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
handleReachGoal(YM_GOALS.BUTTON_CLICK, {
|
||||
[YM_GOALS.BUTTON_CLICK]: { payment_create: { element_name: PAYMENTS_YM_EVENTS.PAYMENT_SIGN_AND_SEND } },
|
||||
});
|
||||
formFields.signAndSend();
|
||||
}}
|
||||
>
|
||||
{LOCALIZATION.BUTTON.SIGN_AND_SEND}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
dataAction="Sign and Send"
|
||||
isLoading={formFields.isSigningAndSending}
|
||||
dataAction="Save"
|
||||
disabled={!currentOrg}
|
||||
isLoading={formFields.isSaving}
|
||||
size="M"
|
||||
variant="primary"
|
||||
variant="blue"
|
||||
onClick={() => {
|
||||
handleReachGoal(YM_GOALS.BUTTON_CLICK, {
|
||||
[YM_GOALS.BUTTON_CLICK]: { payment_create: { element_name: PAYMENTS_YM_EVENTS.PAYMENT_SIGN_AND_SEND } },
|
||||
[YM_GOALS.BUTTON_CLICK]: { payment_create: { element_name: PAYMENTS_YM_EVENTS.PAYMENT_SAVE } },
|
||||
});
|
||||
formFields.signAndSend();
|
||||
formFields.save();
|
||||
}}
|
||||
>
|
||||
{LOCALIZATION.BUTTON.SIGN_AND_SEND}
|
||||
{LOCALIZATION.BUTTON.SAVE}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
dataAction="Save"
|
||||
disabled={!currentOrg}
|
||||
isLoading={formFields.isSaving}
|
||||
size="M"
|
||||
variant="blue"
|
||||
onClick={() => {
|
||||
handleReachGoal(YM_GOALS.BUTTON_CLICK, {
|
||||
[YM_GOALS.BUTTON_CLICK]: { payment_create: { element_name: PAYMENTS_YM_EVENTS.PAYMENT_SAVE } },
|
||||
});
|
||||
formFields.save();
|
||||
}}
|
||||
>
|
||||
{LOCALIZATION.BUTTON.SAVE}
|
||||
</Button>
|
||||
</S.ButtonsWrapper>
|
||||
|
||||
</>
|
||||
);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</S.ButtonsWrapper>
|
||||
|
||||
</>
|
||||
);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
<StatusModal
|
||||
preventCloseOnOutside
|
||||
actions={[
|
||||
{
|
||||
text: LOCALIZATION.TEMPLATE.ERROR.BUTTON,
|
||||
variant: 'primary',
|
||||
dataAction: 'close-warning',
|
||||
onClick: handleCloseStatusModal,
|
||||
},
|
||||
]}
|
||||
header={LOCALIZATION.TEMPLATE.ERROR.TITLE}
|
||||
isOpen={showStatusModal}
|
||||
size="M"
|
||||
type="warning"
|
||||
onClose={handleCloseStatusModal}
|
||||
>
|
||||
<Text.P2>{LOCALIZATION.TEMPLATE.ERROR.DESCRIPTION}</Text.P2>
|
||||
</StatusModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const lineClamp = 3;
|
||||
|
||||
const Container = styled.div`
|
||||
display: grid;
|
||||
cursor: pointer;
|
||||
grid-template-columns: 40px 10fr 1fr 40px;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 4px 16px;
|
||||
|
||||
@@ -1,38 +1,59 @@
|
||||
import { type ReactElement } from 'react';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
import { ButtonIcon } from '@fractal-ui/core';
|
||||
import { EditIcon, Icon } from '@fractal-ui/library';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
import { type RublePaymentTemplate } from '@msb/http';
|
||||
import { formatAccountNumber, getFormattedBalance, useInfiniteScroll } from '@msb/shared';
|
||||
import { formatAccountNumber, getFormattedBalance, useHashedRedirect, useInfiniteScroll } from '@msb/shared';
|
||||
import { LOCALIZATION } from '../constants';
|
||||
import * as S from './TemplateCard.styles';
|
||||
import { PATHS } from '@/shared/constants';
|
||||
|
||||
namespace TemplateCard {
|
||||
type VisibilityHandler = () => Promise<any>;
|
||||
export type TemplateSelection = (id: string) => void;
|
||||
export interface Props {
|
||||
record: RublePaymentTemplate.Record;
|
||||
isEditDisabled: boolean;
|
||||
visibilityHandler: VisibilityHandler | null;
|
||||
selectionHandler?: TemplateSelection;
|
||||
}
|
||||
|
||||
export const Element = ({ record, visibilityHandler }: Props): ReactElement => {
|
||||
export const Element = ({ record, visibilityHandler, isEditDisabled, selectionHandler }: Props): ReactElement => {
|
||||
const { observerTarget, isNextLoading } = useInfiniteScroll(visibilityHandler);
|
||||
const navigation = useHashedRedirect(PATHS.PAYMENT_ORDER.PATH);
|
||||
const onClickCallback = useCallback(
|
||||
event => {
|
||||
if (event.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionHandler) {
|
||||
selectionHandler(record.id);
|
||||
} else {
|
||||
navigation(`T${record.id}`);
|
||||
}
|
||||
},
|
||||
[navigation, record.id, selectionHandler]
|
||||
);
|
||||
|
||||
return (
|
||||
<S.Container ref={visibilityHandler !== null && !isNextLoading ? observerTarget : null}>
|
||||
<S.Container ref={visibilityHandler !== null && !isNextLoading ? observerTarget : null} onClick={onClickCallback}>
|
||||
<S.IconWrapper>
|
||||
<Icon name="DocEdit" size="L" />
|
||||
</S.IconWrapper>
|
||||
<S.ButtonWrapper>
|
||||
<ButtonIcon
|
||||
dataAction="other"
|
||||
icon={EditIcon}
|
||||
variant="ghost"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
// TODO: - продолжение в следующем ПР!
|
||||
console.log('EDIT!');
|
||||
}}
|
||||
/>
|
||||
{!isEditDisabled && (
|
||||
<ButtonIcon
|
||||
dataAction="other"
|
||||
icon={EditIcon}
|
||||
variant="ghost"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
// TODO: - продолжение в следующем ПР!
|
||||
console.log('EDIT!');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</S.ButtonWrapper>
|
||||
<S.TitleWrapper>
|
||||
<Text.P1>{record.templateName}</Text.P1>
|
||||
|
||||
@@ -8,20 +8,31 @@ import { LOCALIZATION } from '../constants';
|
||||
import { TemplateCard } from './TemplateCard';
|
||||
|
||||
namespace TemplatesList {
|
||||
export type TemplateSelection = (id: string) => void;
|
||||
export interface Props {
|
||||
isInfiniteScroll: boolean;
|
||||
organizationId: string;
|
||||
disableEdit?: boolean;
|
||||
selectionHandler?: TemplateSelection;
|
||||
}
|
||||
|
||||
const debounceLength = 0;
|
||||
const debounceSeconds = 1000;
|
||||
const skeletonNumber = 3;
|
||||
const magicSkeletonHeight = 112;
|
||||
const limitTemplateCount = 25;
|
||||
|
||||
export const Element = ({ isInfiniteScroll, organizationId }: Props): ReactElement => {
|
||||
export const Element = ({ isInfiniteScroll, organizationId, disableEdit, selectionHandler }: Props): ReactElement => {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const debounceSearchField = useDebounce(searchValue.length > debounceLength ? searchValue : undefined, debounceSeconds);
|
||||
const { data, isLoading, fetchNextPage } = usePaymentTemplate(organizationId, 0, 25, !isInfiniteScroll, debounceSearchField);
|
||||
const { data, isLoading, fetchNextPage } = usePaymentTemplate(
|
||||
organizationId,
|
||||
0,
|
||||
limitTemplateCount,
|
||||
!isInfiniteScroll,
|
||||
true,
|
||||
debounceSearchField
|
||||
);
|
||||
const records = useMemo(
|
||||
() =>
|
||||
data && Array.isArray(data.pages)
|
||||
@@ -32,6 +43,7 @@ namespace TemplatesList {
|
||||
[data]
|
||||
);
|
||||
const visibilityHandler = isInfiniteScroll && !isLoading ? fetchNextPage : null;
|
||||
const isEditDisabled = disableEdit ?? false;
|
||||
|
||||
if (isLoading && (searchValue.length > 0 || records.length === 0)) {
|
||||
return (
|
||||
@@ -60,7 +72,9 @@ namespace TemplatesList {
|
||||
records.map((record, index) => (
|
||||
<TemplateCard.Element
|
||||
key={record.id}
|
||||
isEditDisabled={isEditDisabled}
|
||||
record={record}
|
||||
selectionHandler={selectionHandler}
|
||||
visibilityHandler={records.length - 1 === index ? visibilityHandler : null}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -18,6 +18,7 @@ import { PaymentOrderFormFields, PaymentOrderForm } from '@/features/PaymentOrde
|
||||
import { PATHS } from '@/shared/constants';
|
||||
import { PageHeader, PageLayout } from '@/shared/ui';
|
||||
import { LOCALIZATION as PAYMENTS_LIST_LOCALIZATION } from '@/widgets/PaymentsListContent';
|
||||
import { PaymentTemplateFill } from '@/widgets/PaymentTemplateFill';
|
||||
|
||||
const PaymentOrderPage = (): ReactElement => {
|
||||
const { userAuthorities } = useAppContext();
|
||||
@@ -38,9 +39,12 @@ const PaymentOrderPage = (): ReactElement => {
|
||||
const systemResponseDescription = isPaymentsEnabled
|
||||
? LOCALIZATION.CONTACT_OWNER_ORGANIZATION
|
||||
: PAYMENTS_LIST_LOCALIZATION.ERROR_DESCRIPTION;
|
||||
const organizationId = PaymentTemplateFill.useAllowWidgetShow()
|
||||
? PaymentTemplateFill.getOrganizationId(userAuthorities)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<PageLayoutWithSections aside={''}>
|
||||
<PageLayoutWithSections aside={organizationId && <PaymentTemplateFill.Element organizationId={organizationId} />}>
|
||||
<PageLayout>
|
||||
{shouldShowSystemResponse ? (
|
||||
<>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
BlockingType,
|
||||
PATHS as GLOBAL_PATHS,
|
||||
BlockingService,
|
||||
Flex,
|
||||
} from '@msb/shared';
|
||||
import { LOCALIZATION, INFORMERS_LIST_ITEMS, allTab, signedTab, draftTab, rejectedTab, completedTab } from '../constants';
|
||||
import { PaymentsListSkeleton } from './PaymentsListSkeleton';
|
||||
@@ -195,10 +196,10 @@ const PaymentsMainPage = (): ReactElement => {
|
||||
<PageLayoutWithSections
|
||||
aside={
|
||||
shouldShowBlockingScreen ? undefined : (
|
||||
<>
|
||||
<Flex column gap={6}>
|
||||
<AsideInformerList informers={INFORMERS_LIST_ITEMS} />
|
||||
{organizationId && <FrequentlyUsedTemplates.Element organizationId={organizationId} />}
|
||||
</>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
asideInMobile={!shouldShowBlockingScreen}
|
||||
|
||||
@@ -6,6 +6,7 @@ const PATHS = {
|
||||
EDIT_OR_DETAILS: { PATH: PATHS_PAYMENTS.EDIT_OR_DETAILS, TITLE: '' },
|
||||
TEMPLATES: { PATH: PATHS_PAYMENTS.TEMPLATES, TITLE: '' },
|
||||
CREATE_TEMPLATE: { PATH: PATHS_PAYMENTS.CREATE_TEMPLATE, TITLE: '' },
|
||||
EDIT_TEMPLATE: { PATH: PATHS_PAYMENTS.EDIT_TEMPLATE, TITLE: '' },
|
||||
} as const;
|
||||
|
||||
export { PATHS };
|
||||
|
||||
+21
-4
@@ -3,7 +3,16 @@ 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 {
|
||||
AUTHORITIES,
|
||||
checkOrganizationsHavePermission,
|
||||
Flex,
|
||||
range,
|
||||
useAppContext,
|
||||
useFeatureToggles,
|
||||
useHashedRedirect,
|
||||
useRedirect,
|
||||
} from '@msb/shared';
|
||||
import { LOCALIZATION } from '../constants';
|
||||
import * as S from './FrequentlyUsedTemplates.styles';
|
||||
import { PATHS } from '@/shared/constants';
|
||||
@@ -33,11 +42,11 @@ namespace FrequentlyUsedTemplates {
|
||||
const maxRecordsCount = 3;
|
||||
|
||||
export const Element = ({ organizationId }: Props): ReactElement => {
|
||||
const { data, isLoading, isError, refetch } = usePaymentTemplate(organizationId, 0, 3, false);
|
||||
const { data, isLoading, isError, refetch } = usePaymentTemplate(organizationId, 0, 3, true);
|
||||
const records = useMemo(
|
||||
() =>
|
||||
data && Array.isArray(data.pages)
|
||||
? 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)
|
||||
@@ -46,6 +55,7 @@ namespace FrequentlyUsedTemplates {
|
||||
);
|
||||
const createTemplate = useRedirect(PATHS.CREATE_TEMPLATE.PATH);
|
||||
const listTemplate = useRedirect(PATHS.TEMPLATES.PATH);
|
||||
const createFromTemplate = useHashedRedirect(PATHS.PAYMENT_ORDER.PATH);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -69,7 +79,14 @@ namespace FrequentlyUsedTemplates {
|
||||
!isLoading &&
|
||||
records.length > 0 &&
|
||||
records.map(record => (
|
||||
<ButtonLink key={record.id} align="left" dataAction="navigate-to-template">
|
||||
<ButtonLink
|
||||
key={record.id}
|
||||
align="left"
|
||||
dataAction="navigate-to-template"
|
||||
onClick={() => {
|
||||
createFromTemplate(`T${record.id}`);
|
||||
}}
|
||||
>
|
||||
<S.Container>
|
||||
<S.TitleWrapper>
|
||||
<Text.P1>{record.templateName}</Text.P1>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './localization';
|
||||
@@ -0,0 +1,7 @@
|
||||
const LOCALIZATION = {
|
||||
TITLE: 'Заполнить из шаблона',
|
||||
BUTTON: 'Все шаблоны',
|
||||
MODAL_TITLE: 'Заполнить из шаблона',
|
||||
};
|
||||
|
||||
export { LOCALIZATION };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ui';
|
||||
+43
@@ -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,125 @@
|
||||
import { type ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { Button, ButtonLink } from '@fractal-ui/core';
|
||||
import { Icon } from '@fractal-ui/library';
|
||||
import { Text, Title } from '@fractal-ui/styling';
|
||||
import { type AuthoritiesResponseDto, FEATURE_TOGGLE_NAMES, usePaymentTemplate } from '@msb/http';
|
||||
import { AUTHORITIES, checkOrganizationsHavePermission, Flex, useAppContext, useFeatureToggles, useHashedReplace } from '@msb/shared';
|
||||
import { LOCALIZATION } from '../constants';
|
||||
import * as S from './PaymentTemplateFill.styles';
|
||||
import { PATHS } from '@/shared/constants';
|
||||
import { Modal } from '@fractal-ui/overlays';
|
||||
import { TemplatesList } from '@/features/TemplatesList';
|
||||
|
||||
namespace PaymentTemplateFill {
|
||||
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 maxVisibleRecordsCount = 3;
|
||||
const maxRecordsCount = 4;
|
||||
|
||||
export const Element = ({ organizationId }: Props): ReactElement => {
|
||||
const { data, isLoading, isError } = usePaymentTemplate(organizationId, 0, maxRecordsCount, true);
|
||||
const [records, isButtonVisible] = useMemo(() => {
|
||||
const totalRecords =
|
||||
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())
|
||||
: [];
|
||||
const visibleRecords = totalRecords.filter((record, index) => index < maxVisibleRecordsCount);
|
||||
|
||||
return [visibleRecords, totalRecords.length > maxVisibleRecordsCount];
|
||||
}, [data]);
|
||||
const fillFromTemplate = useHashedReplace(PATHS.PAYMENT_ORDER.PATH);
|
||||
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
|
||||
const modalSelectionHandler = useCallback(
|
||||
(id: string) => {
|
||||
setIsTemplatesOpen(false);
|
||||
fillFromTemplate(`T${id}`);
|
||||
},
|
||||
[fillFromTemplate]
|
||||
);
|
||||
|
||||
if ((records.length === 0 && !isLoading) || isError || isLoading) {
|
||||
return <Flex column />;
|
||||
}
|
||||
|
||||
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%"
|
||||
>
|
||||
<Title.H4>{LOCALIZATION.TITLE}</Title.H4>
|
||||
{records.map(record => (
|
||||
<ButtonLink
|
||||
key={record.id}
|
||||
align="left"
|
||||
dataAction="navigate-to-fill-template"
|
||||
onClick={() => {
|
||||
fillFromTemplate(`T${record.id}`);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
{isButtonVisible && (
|
||||
<Button
|
||||
dataAction="select-template"
|
||||
shape="default"
|
||||
size="M"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsTemplatesOpen(true);
|
||||
}}
|
||||
>
|
||||
{LOCALIZATION.BUTTON}
|
||||
</Button>
|
||||
)}
|
||||
<Modal
|
||||
preventCloseOnOutside
|
||||
header={LOCALIZATION.MODAL_TITLE}
|
||||
isOpen={isTemplatesOpen}
|
||||
size="M"
|
||||
onClose={() => {
|
||||
setIsTemplatesOpen(false);
|
||||
}}
|
||||
>
|
||||
<TemplatesList.Element disableEdit isInfiniteScroll organizationId={organizationId} selectionHandler={modalSelectionHandler} />
|
||||
</Modal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { PaymentTemplateFill };
|
||||
@@ -0,0 +1 @@
|
||||
export * from './PaymentTemplateFill';
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './AsideInformerList';
|
||||
export * from './PaymentsListContent';
|
||||
export * from './FrequentlyUsedTemplates';
|
||||
export * from './PaymentTemplateFill';
|
||||
|
||||
Reference in New Issue
Block a user