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>
    );
};