File "ModelsTable.tsx"
Full Path: /var/www/html/gitep_front/src/entities/models/ui/ModelsTable/ModelsTable.tsx
File size: 30.47 KB
MIME-type: text/x-java
Charset: utf-8
import {Flex, Modal, notification, Select, Switch, TableProps, Typography} from "antd";
import styles from './ModelsTable.module.scss';
import IntegrationTable from './IntegrationTable.module.scss';
import TableComponent from "@/shared/ui/TableComponent/TableComponent";
import { useState, useMemo, useEffect, useRef } from "react";
import arrowHide from '@shared/assets/images/icons/chevron-up.svg'
import arrowShow from '@shared/assets/images/icons/chevron-down.svg'
import { getAccountsDetailInfo } from "../../api/getAccountsInfo";
import { DrawerComponent } from "@/shared/ui/DrawerComponent/DrawerComponent";
import { AccountContentDrawer } from "../AccountContentDrawer/AccountContentDrawer";
import { getContragentInfo } from "../../api/getContragentInfo";
import { ContragentContentDrawer } from "../ContragentContentDrawer/ContragentContentDrawer";
import { getOrganizationInfo } from "../../api/getOrganizationInfo";
import { OrganizationContentDrawer } from "../OrganizationContentDrawer/OrganizationContentDrawer";
import React from 'react';
import {updateIntegration} from "../../api/updateIntegration";
import {getIntegrationInfo} from "../../api/getIntegrationDetailInfo";
import {IntegrationContentDrawer} from "../IntegrationContentDrawer/IntegrationContentDrawer";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import { deleteModelOrganization} from "@/entities/models/api/deleteOrganization";
import checkIcon from "@shared/assets/images/icons/check-test.svg";
interface ModelsBlock {
count: number;
models?: any[];
}
interface ModelsTableProps {
active: ModelsBlock;
archive?: ModelsBlock;
cash?:ModelsBlock;
modelId: number
type: string;
updateTable: () => void
roleStore: any
}
const formatCurrency = (value: number): string => {
return value?.toLocaleString('ru-RU', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
};
export const ModelsTable = ({ active, archive, cash, type, modelId, updateTable, roleStore }: ModelsTableProps) => {
const [expandedGroups, setExpandedGroups] = useState<string[]>(['active', 'archive', 'cash']);
const [scoreDetail, setScoreDetail]: any = useState({})
const [contragentDetail, setContragentDetail]: any = useState({})
const [organizationDetail, setOrganizationDetail]: any = useState({})
const [integrationDetail, setIntegrationDetail]: any = useState({});
const [detailDrawerVisible, setDetailDrawerVisible] = useState(false)
const [contragentDrawerVisible, setContragentDrawerVisible] = useState(false)
const [organizationDrawerVisible, setOrganizationDrawerVisible] = useState(false)
const [integrationDrawerVisible, setIntegrationDrawerVisible] = useState(false);
const [IsEditablebyRole, setIsEditableByRole] = useState(true);
const [isModalAccountsOpen, setIsModalAccountsOpen] = useState(false);
const [selectedDesired, setSelectedDesired] = useState<string | null>(null);
const [originRowEl, setOriginRowEl] = useState();
const [originRowId, setOriginRowId] = useState();
const [modalTitle, setModaltitle] =useState('счета');
const [updatedSelectOptions, setUpdatedSelectOptions] = useState<any | undefined>([]);
const [nameForModal, setNameForModal] = useState<string>('счет');
const dataSource: any[] = [];
const { Option, OptGroup } = Select;
function formatDate(dateString: string): string | null {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(dateString)) {
return null;
}
const [year, month, day] = dateString.split('-');
const twoDigitYear = year.slice(-2);
return `${day}.${month}.${twoDigitYear}`;
}
const toggleGroup = (groupKey: string) => {
setExpandedGroups((currentExpandedGroups) => {
let newExpandedGroups = currentExpandedGroups.includes(groupKey)
? currentExpandedGroups.filter(k => k !== groupKey)
: [...currentExpandedGroups, groupKey];
return newExpandedGroups;
});
};
const createGroupItem = (groupKey: string, count: number) => ({
key: `${groupKey}-header`,
isGroupHeader: true,
groupTitle: groupKey === 'active'
? `Активные (${count})`
: groupKey === 'cash' ? `Кассовые (${count})`: `Архив (${count})`,
groupKey,
isExpanded: expandedGroups.includes(groupKey),
});
const addModelsToDataSource = (models: any[], groupKey: string) => {
models.forEach(model => {
dataSource.push({
key: `model-${model.id}`,
isModelRow: true,
groupKey,
...model
});
});
};
if (active?.count > 0) {
dataSource.push(createGroupItem('active', active.count));
if (expandedGroups.includes('active') && active.models) {
addModelsToDataSource(active.models, 'active');
}
}
if (cash && cash.count > 0) {
dataSource.push(createGroupItem('cash', cash.count));
if (expandedGroups.includes('cash') && cash.models) {
addModelsToDataSource(cash.models, 'cash');
}
}
if (archive && archive.count > 0) {
dataSource.push(createGroupItem('archive', archive.count));
if (expandedGroups.includes('archive') && archive.models) {
addModelsToDataSource(archive.models, 'archive');
}
}
interface GroupHeaderProps {
record: Record<string, any>;
toggleGroup: (groupKey: string) => void;
arrowShow: string;
arrowHide: string;
styles: any;
key: string;
gap: number;
className: string;
style: React.CSSProperties;
children: React.ReactNode;
}
const FlexWrapper: React.FC<GroupHeaderProps> = ({
record,
toggleGroup,
arrowShow,
arrowHide,
styles,
key,
gap,
className,
style,
children,
}) => {
return (
<Flex
key={key}
gap={gap}
className={className}
style={style}
onClick={() => toggleGroup(record.groupKey)}
>
{children}
</Flex>
);
};
const GroupHeader: React.FC<GroupHeaderProps> = ({ record, toggleGroup, arrowShow, arrowHide, styles, key, gap, className, style }) => {
return (
<FlexWrapper
record={record}
toggleGroup={toggleGroup}
arrowShow={arrowShow}
arrowHide={arrowHide}
styles={styles}
key={key}
gap={gap}
className={className}
style={style}
>
{record.isExpanded ? (
<img src={arrowShow} alt="Свернуть" />
) : (
<img src={arrowHide} alt="Развернуть" />
)}
{record.groupTitle}
</FlexWrapper>
);
};
interface ModelRowProps {
styles: any;
record: Record<string, any>;
type: string;
}
const ModelRow: React.FC<ModelRowProps> = ({ styles, record, type }) => {
let displayName = '';
let accountNumber = '';
if (type === 'organization') {
displayName = record.full_name;
} else if (type === 'counterparty') {
displayName = record.name;
} else if (type === 'integration') {
displayName = record.system;
} else {
displayName = record.title || record.name;
accountNumber = record.accountNumber || '';
}
return (
<Flex className={styles.left} vertical>
<div className="font-semibold">{displayName}</div>
{accountNumber && (
<div className="text-gray-500 text-xs mt-1">
{accountNumber}
</div>
)}
</Flex>
);
};
const renderFirstColumn = (_: any, record: any, { type, arrowShow, arrowHide, styles }: any) => {
if (record.isGroupHeader) {
return (
<GroupHeader
record={record}
toggleGroup={toggleGroup}
arrowShow={arrowShow}
arrowHide={arrowHide}
styles={styles}
key={record.key}
gap={16}
className={styles.user}
style={{height: '56px'}} children={undefined}
/>
);
}
if (record.isModelRow) {
return <ModelRow styles={styles} record={record} type={type}/>;
}
return null;
};
const changeSwitchIntegration = async (record: any) => {
const params = {
...record,
is_active: !record.is_active
}
await updateIntegration(record.id, params);
updateTable();
}
const columnsForScore = useMemo<TableProps<any>['columns']>(() => [
{
title: "Название",
key: "name",
width: 408,
// render: renderFirstColumn
render: (text, record) => {
return renderFirstColumn(text, record, {
type,
arrowShow,
arrowHide,
styles
});
}
},
{
title: "Номер",
key: "number",
width: 180,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.number : '-';
},
},
{
title: "Организация",
key: "organization",
width: 220,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.organization : null;
},
},
{
title: "Добавлен",
key: "addedDate",
align: 'center',
width: 100,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? <Flex style={{margin: '0 auto'}}>{formatDate(record.date_added)}</Flex> : null;
},
},
{
title: "Остаток на дату, ₽",
key: "balanceOnDate",
width: 160,
align: 'right',
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? <Flex style={{marginLeft: 'auto'}}>{formatCurrency(Number(record.balance_of_the_date))}</Flex> : null;
},
},
{
title: "Приходы, ₽",
key: "income",
width: 120,
align: 'right',
className: 'incomeCell',
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? <Flex style={{marginLeft: 'auto'}}>{formatCurrency(record.credit)}</Flex> : null;
},
},
{
title: "Расходы, ₽",
key: "expenses",
width: 120,
align: 'right',
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? <Flex style={{marginLeft: 'auto'}}>{formatCurrency(record.debit)}</Flex> : null;
},
},
{
title: "Текущий остаток, ₽",
key: "currentBalance",
width: 170,
align: 'right',
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? (
<Flex style={{marginLeft: 'auto'}} className="font-semibold">
{record.groupKey === "cash"? formatCurrency(Number(record.balance_of_the_date) +record.credit - record.debit) : formatCurrency(Number(record.current_balance))}
</Flex>
) : null;
},
},
], [type, arrowShow, arrowHide, styles]);
const columnsForCounterparty = useMemo<TableProps<any>['columns']>(() => [
{
title: "Контрагент",
key: "name",
width: 400,
// render: renderFirstColumn
render: (text, record) => {
return renderFirstColumn(text, record, {
type,
arrowShow,
arrowHide,
styles
});
}
},
{
title: "ИНН",
key: "inn",
width: 144,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.inn : null;
},
},
{
title: "КПП",
key: "kpp",
width: 112,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.kpp : null;
},
},
{
title: "Примечание",
key: "comment",
// width: 240,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.comment : null;
},
},
], [type, arrowShow, arrowHide, styles]);
const columnsForOrganization = useMemo<TableProps<any>['columns']>(() => [
{
title: "Организация",
key: "full_name",
width: 640,
// render: renderFirstColumn
render: (text, record) => {
return renderFirstColumn(text, record, {
type,
arrowShow,
arrowHide,
styles
});
}
},
// {
// title: "Короткое название",
// key: "short_name",
// width: 180,
// render: (_, record) => {
// if (record.isGroupHeader) return null;
// return record.isModelRow ? record.short_name : null;
// },
// },
{
title: "ИНН",
key: "inn",
width: 144,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.inn : null;
},
},
{
title: "КПП",
key: "kpp",
// width: 112,
render: (_, record) => {
if (record.isGroupHeader) return null;
return record.isModelRow ? record.kpp : null;
},
},
], [type, arrowShow, arrowHide, styles]);
const columnsForIntegration = useMemo<TableProps<any>['columns']>(() => [
{
title: null,
key: "is_active",
render: (text, record) => {
return (
<Flex className={styles.switch}>
{Object.hasOwn(record, "is_active") && (
<Switch onChange={(_, e) => {
e.stopPropagation();
changeSwitchIntegration(record);
}} className={styles.switch} size="small" checked={record.is_active}/>
)}
</Flex>
)
},
width: 92
},
{
title: "Система",
key: "system",
width: 320,
render: (text, record) => {
return renderFirstColumn(text, record, {
type,
arrowShow,
arrowHide,
styles: IntegrationTable
});
}
},
{
title: "API-Key",
key: "api_key",
width: 460,
render: (text, record) => {
return record.api_key
}
},
{
title: "Дата обновления",
key: "date_refresh",
render: (text, record) => {
return record.date_refresh
}
},
{
title: "Загруженный период",
key: "date_start",
render: (text, record) => {
return record.date_start
}
},
], [type, arrowShow, arrowHide, styles]);
const columns = useMemo(() => {
switch (type) {
case 'score':
return columnsForScore;
case 'counterparty':
return columnsForCounterparty;
case 'organization':
return columnsForOrganization;
case 'integration':
return columnsForIntegration;
default:
return [];
}
}, [type, columnsForScore, columnsForCounterparty, columnsForOrganization, columnsForIntegration]);
const rowClassName = (record: any) => {
if (record.isGroupHeader) {
return record.groupKey === 'active'
? styles['active-group-header']
: styles['blocked-group-header'];
}
return styles['user-row'];
};
const handleRowClick = async (record: any) => {
if (record.type === 'score') {
const { data } = await getAccountsDetailInfo(modelId, record.id)
setScoreDetail(data)
setDetailDrawerVisible(true)
setModaltitle('счета')
}
if (record.type === "counterparty") {
const { data } = await getContragentInfo(modelId, record.id)
setContragentDetail(data)
setContragentDrawerVisible(true)
setModaltitle('контрагента')
}
if (record.type === 'organization') {
const { data } = await getOrganizationInfo(modelId, record.id)
setOrganizationDetail(data)
setOrganizationDrawerVisible(true)
setModaltitle('организации')
}
if (record.type === 'integration') {
const data = await getIntegrationInfo(record.id);
setIntegrationDetail(data)
setIntegrationDrawerVisible(true);
}
};
const rowProps = (record: any) => {
return record.isModelRow
? {
onClick: () => handleRowClick(record),
className: styles.clickableRow
}
: {};
};
const handleArticlesCancel = () => {
setIsModalAccountsOpen(false);
setSelectedDesired(null)
};
const handleChange = (value: string) => {
setSelectedDesired(value);
};
useEffect(() => {
switch (type){
case 'score':
setOriginRowEl(scoreDetail.name)
setOriginRowId(scoreDetail.id)
setNameForModal("счет")
break;
case 'counterparty':
setOriginRowEl(contragentDetail.name)
setOriginRowId(contragentDetail.id)
setNameForModal("контрагента")
break;
case 'organization':
setOriginRowEl(organizationDetail.full_name)
setOriginRowId(organizationDetail.id)
setNameForModal("ороганизацию")
break;
}
}, [type, scoreDetail, contragentDetail, organizationDetail])
function transformDataSourceToSelectOptions(dataSource: any) {
function removeParentheses(text: string): string {
return text
.replace(/\s*\([^)]*\)\s*/g, ' ')
.trim()
.replace(/\s+/g, ' ');
}
const groups = dataSource.reduce((acc: any, item: any) => {
if (item.isGroupHeader) {
acc[item.groupKey] = {
label: removeParentheses(item.groupTitle),
options: []
};
} else if (item.isModelRow && item.groupKey) {
if (acc[item.groupKey]) {
acc[item.groupKey].options.push({
label: type === 'organization'? item.full_name: item.name,
value: item.id,
is_cash: item.is_cash
});
}
}
return acc;
}, {});
return Object.keys(groups).map(key => ({
label: groups[key].label,
options: groups[key].options
}));
}
const selectOptions = transformDataSourceToSelectOptions(dataSource);
useEffect(() => {
// const updatedSelect = selectOptions.map(group => ({
// ...group,
// options: group.options.filter((option: { value: undefined; }) => option.value !== originRowId)
// }));
const updatedSelect = type === "score" ? selectOptions
.map(group => ({
...group,
options: group.options.filter((option: { is_cash: boolean; }) => {
if (scoreDetail.is_cash === true) {
return option.is_cash === true;
} else {
return option.is_cash === false;
}
})
}))
.filter(group => group.options.length > 0) : selectOptions;
const updatedSelectWithoutScore = updatedSelect.map(group => ({
...group,
options: group.options.filter((option: { value: undefined; }) => option.value !== originRowId)
}));
setUpdatedSelectOptions(updatedSelectWithoutScore)
},[originRowId])
const handleChangeArticle = async () => {
try {
await deleteModelOrganization(modelId, originRowId, selectedDesired, type );
setIsModalAccountsOpen(false);
setSelectedDesired(null);
setOrganizationDrawerVisible(false);
setDetailDrawerVisible(false);
setContragentDrawerVisible(false);
updateTable();
notification.success({
message: 'Успех',
description: 'Данные удалены'
})
} catch (err) {
notification.error({
message: 'Ошибка',
description: 'Не удалось обновить данные организации'
})
}
};
const tableRef = useRef<HTMLDivElement>(null);
const [tableHeight, setTableHeight] = useState(600);
useEffect(() => {
const calculateHeight = () => {
if (tableRef.current) {
const viewportHeight = window.innerHeight;
const tableTop = tableRef.current.getBoundingClientRect().top;
const paddingBottom = 80;
const height = viewportHeight - tableTop - paddingBottom;
setTableHeight(Math.max(height, 300));
}
};
calculateHeight();
window.addEventListener('resize', calculateHeight);
return () => window.removeEventListener('resize', calculateHeight);
}, []);
console.log(updatedSelectOptions, '[[[[')
return (
<div className={styles.usersTable} style={{ height: 'calc(100vh - 168px' }}>
<TableComponent
dataSource={dataSource}
columns={columns}
className={styles.table}
rowClassName={rowClassName}
onRow={rowProps}
pagination={false}
showHeader={true}
tableWrapperStyles={{ position: 'relative', top: 5 }}
tableRef={tableRef}
virtual
scroll={{ x: 'auto', y: tableHeight }}
components={{
body: {
cell: (props: any) => (
<td {...props} style={{ ...props.style, display: 'flex', alignItems: 'center'}} />
),
},
}}
/>
<DrawerComponent
open={detailDrawerVisible}
title={scoreDetail.name ? scoreDetail.name : ""}
handleClose={() => setDetailDrawerVisible(false)}
content={
scoreDetail && (
<AccountContentDrawer
data={scoreDetail}
isOpenDrawer={detailDrawerVisible}
onClose={() => setDetailDrawerVisible(false)}
updateTable={updateTable}
modelId={modelId}
setIsModalAccountsOpen={setIsModalAccountsOpen}
/>
)
}
IsEditablebyRole={IsEditablebyRole}
/>
<DrawerComponent
open={contragentDrawerVisible}
title={contragentDetail.name ? contragentDetail.name : ""}
handleClose={() => setContragentDrawerVisible(false)}
IsEditablebyRole={IsEditablebyRole}
content={
scoreDetail && (
<ContragentContentDrawer
data={contragentDetail}
isOpenDrawer={contragentDrawerVisible}
onClose={() => setContragentDrawerVisible(false)}
updateTable={updateTable}
modelId={modelId}
roleStore={roleStore}
setIsModalAccountsOpen={setIsModalAccountsOpen}
/>
)
}
/>
<DrawerComponent
open={organizationDrawerVisible}
title={organizationDetail.full_name ? organizationDetail.full_name : ""}
handleClose={() => setOrganizationDrawerVisible(false)}
IsEditablebyRole={IsEditablebyRole}
content={
scoreDetail && (
<OrganizationContentDrawer
data={organizationDetail}
isOpenDrawer={organizationDrawerVisible}
onClose={() => setOrganizationDrawerVisible(false)}
updateTable={updateTable}
modelId={modelId}
setIsModalAccountsOpen={setIsModalAccountsOpen}
/>
)
}
/>
<DrawerComponent
open={integrationDrawerVisible}
title={integrationDetail.name ? integrationDetail.name : ""}
handleClose={() => setIntegrationDrawerVisible(false)}
IsEditablebyRole={IsEditablebyRole}
content={
scoreDetail && (
<IntegrationContentDrawer
data={integrationDetail}
onClose={() => setIntegrationDrawerVisible(false)}
updateTable={updateTable}
/>
)
}
/>
<Modal
title={
<p className={styles["modal-articles_title"]}>Удаление {modalTitle}</p>
}
className={styles["modal-articles"]}
width={464}
closable={false}
open={isModalAccountsOpen}
onOk={handleChangeArticle}
onCancel={handleArticlesCancel}
okText="Применить"
cancelText="Отменить"
okButtonProps={{ disabled: !selectedDesired }}
>
<div>
<input
name={"originRowEl"}
type="text"
value={originRowEl}
className={styles.input}
/>
</div>
<div>
<p className={styles["modal-refine_text"]}>
Выберите {nameForModal} для переноса платежей
</p>
<Select
placeholder="Выберите счёт"
value={selectedDesired}
onChange={handleChange}
className={styles.select}
optionLabelProp="label"
optionFilterProp="label"
notFoundContent="Ничего не найдено"
showSearch
allowClear
>
{updatedSelectOptions.map((group: any) => (
<OptGroup key={group.label} label={group.label}>
{group.options.map((item: any) => (
<Option
key={item.value}
value={item.value}
label={item.label}
title={item.label}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: 'calc(100% - 24px)',
}}
>
{item.label}
</span>
{selectedDesired === item.value && (
<img
src={checkIcon}
alt="Выбрано"
style={{ width: 20, height: 20 }}
/>
)}
</div>
</Option>
))}
</OptGroup>
))}
</Select>
</div>
{!selectedDesired && (
<Flex className={styles["modal-summary"]} gap={8}>
<ExclamationCircleOutlined />
<Typography.Text>
Выберите счет, к которому привязать платежи, чтобы они не остались без привязки
</Typography.Text>
</Flex>
)}
</Modal>
</div>
);
};