diff --git a/src/App.tsx b/src/App.tsx index 24faf7a..044ea1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,6 @@ import { AuditLogging, CatalogueGroup, CatalogueJur, - ConsolidatedOsvPage, ConsolidationPage, DocStorage, DocumentPage, @@ -20,6 +19,7 @@ import { } from './layouts/'; import { store } from './store/store'; import { Sidebar } from './widgets/Sidebar/Sidebar'; +import { ConsolidatioReportPage } from './layouts/ConsolidatioReportPage'; // Каркас: экраны по docs/context/10-user-journey.md добавляются срезами. function App() { @@ -37,7 +37,7 @@ function App() { } /> } /> - } /> + } /> } /> } /> diff --git a/src/layouts/ConsolidatioReportPage.tsx b/src/layouts/ConsolidatioReportPage.tsx new file mode 100644 index 0000000..06179ae --- /dev/null +++ b/src/layouts/ConsolidatioReportPage.tsx @@ -0,0 +1,69 @@ +import { Space, message } from 'antd'; +import { ReactNode, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { ApiError } from '../api/client'; +import PageTitle from '../components/PageTitle'; +import { TableTag } from '../components/TableTag/TableTag'; +import { TAG_PRESETS } from '../components/TableTag/presets'; +import { getVersionCard } from '../services/api/consolidations'; +import type { ConsRussianStatus } from '../types/consolidations/common_types'; +import type { VersionCard } from '../types/consolidations/version_members'; +import { quarterShortLabel } from '../utils/labels'; +import { statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts'; +import { OsvTabs } from '../widgets/ConsolidatedOsv/OsvTabs'; +import { OsvContent } from '../widgets/ConsolidatedOsvContent/ConsolidatedOsvContent'; +import { ChessContent } from '../widgets/ConsolidatedChessContent/ConsolidatedChessContent'; + +export function ConsolidatioReportPage() { + const [card, setCard] = useState(null); + const { version_id, tab } = useParams<{ tab?: string; version_id: string }>(); + + const TAB_COMPONENTS: Record = { + osv: , + + chess: , + }; + + const ActiveTab = TAB_COMPONENTS[tab as string] ?? <>; + + const russianStatus = card ? (statusMapping[card.status] ?? card.status) : ''; + const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default; + const groupName = card?.company_group.name ?? ''; + const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : ''; + + useEffect(() => { + if (version_id) { + let actual = true; + getVersionCard(version_id) + .then((c) => { + if (actual) setCard(c); + }) + .catch((e: unknown) => { + if (actual) message.error(e instanceof ApiError ? e.message : 'Не удалось получить версию'); + }); + return () => { + actual = false; + }; + } + }, [version_id]); + return ( +
+ + Статус отчёта + + + ) + } + /> + + {ActiveTab} +
+ ); +} diff --git a/src/layouts/index.ts b/src/layouts/index.ts index b006199..90015f7 100644 --- a/src/layouts/index.ts +++ b/src/layouts/index.ts @@ -9,4 +9,4 @@ export { GroupsLayout } from './Groups'; export { AuditLogging } from './AuditLogging'; export { Admin } from './Admin'; export { DocumentPage } from './DocumentPage'; -export { ConsolidatedOsvPage } from './ConsolidatedOsvPage'; +export { ConsolidatioReportPage } from './ConsolidatioReportPage'; diff --git a/src/services/consodilation/chess.ts b/src/services/consodilation/chess.ts new file mode 100644 index 0000000..7e61aa4 --- /dev/null +++ b/src/services/consodilation/chess.ts @@ -0,0 +1,4 @@ +import { api } from "../../api/client"; +import { IntercompanyMatrixResponse } from "../../types/chess/chess"; + +export const fetchChessConsolidations = (id: string) => api.get(`/consolidations/versions/${id}/intercompany-matrix/all`); diff --git a/src/types/chess/chess.ts b/src/types/chess/chess.ts new file mode 100644 index 0000000..0524ce7 --- /dev/null +++ b/src/types/chess/chess.ts @@ -0,0 +1,17 @@ +export interface MatrixCompany { + id: string; + name: string; +} + +export interface MatrixCell { + company_a_id: string; + company_b_id: string; + amount_a: string; + amount_b: string; + variance_amount: string; +} + +export interface IntercompanyMatrixResponse { + companies: MatrixCompany[]; + cells: MatrixCell[]; +} \ No newline at end of file diff --git a/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.css b/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.css new file mode 100644 index 0000000..d9c6dc3 --- /dev/null +++ b/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.css @@ -0,0 +1,6 @@ +.empty-cell { + background-color: #f2f4f7; +} +.ant-table-tbody > tr:hover > td.empty-cell { + background-color: #f2f4f7 !important; +} \ No newline at end of file diff --git a/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx b/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx new file mode 100644 index 0000000..768a4ac --- /dev/null +++ b/src/widgets/ConsolidatedChessContent/ConsolidatedChessContent.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Table, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { useParams } from 'react-router-dom'; +import { ApiError } from '../../api/client'; +import { fetchChessConsolidations } from '../../services/consodilation/chess'; +import type { IntercompanyMatrixResponse, MatrixCell, MatrixCompany } from '../../types/chess/chess'; +import { useNotificationApi } from '../../utils/useNotification'; + +import './ConsolidatedChessContent.css'; + +const { Text } = Typography; + +export const ChessContent = () => { + const { version_id } = useParams<{ version_id: string }>(); + const notification = useNotificationApi(); + + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + + const loadData = async (vId: string) => { + setLoading(true); + try { + const res = await fetchChessConsolidations(vId); + setData(res); + } catch (e) { + if (e instanceof ApiError) { + notification.error({ description: e.message }); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (!version_id) return; + loadData(version_id); + }, [version_id]); + + const matrixData = useMemo(() => { + if (!data?.cells) return {}; + const result: Record> = {}; + + data.cells.forEach((cell) => { + if (!result[cell.company_a_id]) result[cell.company_a_id] = {}; + result[cell.company_a_id][cell.company_b_id] = cell; + }); + + return result; + }, [data?.cells]); + + const columns: ColumnsType = useMemo(() => { + if (!data?.companies) return []; + + const firstColumn: ColumnsType[number] = { + title: 'ДБ / КТ', + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 200, + render: (text) => {text}, + onCell: () => ({ style: { background: '#fff', zIndex: 2 } }), + }; + + // Внутри companyColumns + const companyColumns = data.companies.map((company) => ({ + title: company.name, + dataIndex: company.id, + key: company.id, + width: 150, + align: 'right' as const, + + onCell: (row: MatrixCompany) => { + const cell = matrixData[row.id]?.[company.id]; + if (!cell) { + // Возвращаем класс через onCell, это работает надежнее + return { className: 'empty-cell' }; + } + return {}; + }, + + render: (_: unknown, row: MatrixCompany) => { + const cell = matrixData[row.id]?.[company.id]; + + if (!cell) { + return —; + } + + return ( +
+ {cell.amount_a} + {cell.amount_b} + + {cell.variance_amount} + +
+ ); + }, + })); + + return [firstColumn, ...companyColumns]; + }, [data?.companies, matrixData]); + + return ( + + dataSource={data?.companies || []} + columns={columns} + rowKey='id' + pagination={false} + loading={loading} + scroll={{ x: 'max-content', y: 'calc(100vh - 280px)' }} + size='small' + bordered + /> + ); +}; diff --git a/src/widgets/ConsolidatedOsv/OsvTabs.tsx b/src/widgets/ConsolidatedOsv/OsvTabs.tsx index 9597458..7e0e9f4 100644 --- a/src/widgets/ConsolidatedOsv/OsvTabs.tsx +++ b/src/widgets/ConsolidatedOsv/OsvTabs.tsx @@ -1,26 +1,79 @@ import { Segmented, Tooltip } from 'antd'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; -const SOON = 'Будет в следующих срезах'; -const TABS = ['Шахматка', 'Таблица корректировки', 'Гудвилл', 'КФО', 'КФО Общий', 'Подтверждение КФО клиента']; +const TAB_CONFIG = [ + { key: 'osv', label: 'ОСВ' }, + { key: 'chess', label: 'Шахматка' }, + { + key: 'correction', + label: 'Таблица корректировки', + disabled: true, + tooltip: 'Будет в следующих срезах', + }, + { + key: 'goodwill', + label: 'Гудвилл', + disabled: true, + tooltip: 'Будет в следующих срезах', + }, + { + key: 'kfo', + label: 'КФО', + disabled: true, + tooltip: 'Будет в следующих срезах', + }, + { + key: 'kfo_common', + label: 'КФО Общий', + disabled: true, + tooltip: 'Будет в следующих срезах', + }, + { + key: 'kfo_confirm', + label: 'Подтверждение КФО клиента', + disabled: true, + tooltip: 'Будет в следующих срезах', + }, +]; -/** Ряд вкладок шагов консолидации (макет [30]): активна только «ОСВ». */ export function OsvTabs() { + const navigate = useNavigate(); + const location = useLocation(); + const { tab } = useParams<{ tab?: string }>(); + + const activeKey = tab || 'osv'; + + const handleTabChange = (newKey: string | number) => { + const newPath = location.pathname.replace(new RegExp(`/${activeKey}/?$`), `/${newKey}`); + navigate(newPath); + }; + + const options = TAB_CONFIG.map((tabItem) => { + const isActive = tabItem.key === activeKey; + + const label = ( + + {tabItem.label} + + ); + + return { + value: tabItem.key, + disabled: tabItem.disabled, + label: tabItem.tooltip ? {label} : label, + }; + }); + return ( ОСВ }, - ...TABS.map((t) => ({ - value: t, - disabled: true, - label: ( - - {t} - - ), - })), - ]} + options={options} /> ); } diff --git a/src/layouts/ConsolidatedOsvPage.tsx b/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx similarity index 50% rename from src/layouts/ConsolidatedOsvPage.tsx rename to src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx index f9dcd0e..dca5fec 100644 --- a/src/layouts/ConsolidatedOsvPage.tsx +++ b/src/widgets/ConsolidatedOsvContent/ConsolidatedOsvContent.tsx @@ -1,51 +1,33 @@ import { DownloadOutlined, FilterOutlined } from '@ant-design/icons'; -import { Alert, Button, Space, message } from 'antd'; -import { useEffect, useState } from 'react'; +import { Alert, Button, message } from 'antd'; +import { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { ApiError } from '../api/client'; -import PageTitle from '../components/PageTitle'; -import { TableTag } from '../components/TableTag/TableTag'; -import { TAG_PRESETS } from '../components/TableTag/presets'; -import { getVersionCard } from '../services/api/consolidations'; -import { exportOsv } from '../services/api/osv'; -import type { ConsolidatedRowOut } from '../types/api'; -import type { ConsRussianStatus } from '../types/consolidations/common_types'; -import type { VersionCard } from '../types/consolidations/version_members'; -import { exportFilename, saveBlob } from '../utils/download'; -import { quarterShortLabel } from '../utils/labels'; -import { statusColorMapping, statusMapping } from '../widgets/ConsoleTable/consts'; -import { CounterpartyMapModal } from '../widgets/ConsolidatedOsv/CounterpartyMapModal'; -import { OsvGaps } from '../widgets/ConsolidatedOsv/OsvGaps'; -import { OsvTable } from '../widgets/ConsolidatedOsv/OsvTable'; -import { OsvTabs } from '../widgets/ConsolidatedOsv/OsvTabs'; -import { mapEntryId } from '../widgets/ConsolidatedOsv/counterpartyMapping'; -import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../widgets/ConsolidatedOsv/useConsolidatedOsv'; +import { ApiError } from '../../api/client'; +import { exportOsv } from '../../services/api/osv'; +import type { ConsolidatedRowOut } from '../../types/api'; +import type { VersionCard } from '../../types/consolidations/version_members'; +import { exportFilename, saveBlob } from '../../utils/download'; +import { CounterpartyMapModal } from '../ConsolidatedOsv/CounterpartyMapModal'; +import { OsvGaps } from '../ConsolidatedOsv/OsvGaps'; +import { OsvTable } from '../ConsolidatedOsv/OsvTable'; +import { mapEntryId } from '../ConsolidatedOsv/counterpartyMapping'; +import { OSV_PAGE_SIZE, useConsolidatedOsv } from '../ConsolidatedOsv/useConsolidatedOsv'; -function OsvContent({ versionId }: { versionId: string }) { - const [card, setCard] = useState(null); +export function OsvContent({ card }: { card: VersionCard | null }) { const [exporting, setExporting] = useState(false); + const { version_id } = useParams<{ version_id: string }>(); + // строка, для которой открыт выбор организации («Указать» или ИНН сопоставленного ЮЛ) const [mapping, setMapping] = useState(null); - const osv = useConsolidatedOsv(versionId); - - useEffect(() => { - let actual = true; - getVersionCard(versionId) - .then((c) => { - if (actual) setCard(c); - }) - .catch((e: unknown) => { - if (actual) message.error(e instanceof ApiError ? e.message : 'Не удалось получить версию'); - }); - return () => { - actual = false; - }; - }, [versionId]); + const osv = useConsolidatedOsv(version_id ?? ''); const download = async () => { + if (!version_id) { + return; + } setExporting(true); try { - const { blob, filename } = await exportOsv(versionId, osv.filters); + const { blob, filename } = await exportOsv(version_id, osv.filters); saveBlob(blob, filename ?? exportFilename(card?.company_group.name ?? 'ГК', card?.year ?? 0, card?.quarter ?? '')); } catch (e: unknown) { message.error(e instanceof ApiError ? e.message : 'Не удалось выгрузить свод'); @@ -57,27 +39,9 @@ function OsvContent({ versionId }: { versionId: string }) { const s = osv.summary; const unmapped = s?.unmapped_total ?? 0; const onlyUnmapped = osv.filters.unmapped === true; - const russianStatus = card ? (statusMapping[card.status] ?? card.status) : ''; - const preset = statusColorMapping[russianStatus as ConsRussianStatus] ?? TAG_PRESETS.default; - const groupName = card?.company_group.name ?? ''; - const period = card ? `${card.year} · ${quarterShortLabel(card.quarter)}` : ''; + return ( -
- - Статус отчёта - - - ) - } - /> - + <>
- {mapping && ( + {mapping && version_id && ( setMapping(null)} onMapped={osv.refresh} /> )} -
+ ); } - -export function ConsolidatedOsvPage() { - const { version_id = '' } = useParams(); - // key: при смене версии всё состояние (фильтры, страница, данные) создаётся заново - return ; -}