diff --git a/web/package-lock.json b/web/package-lock.json index 1a7ae02..2873032 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,7 +20,8 @@ "react-dom": "^18.2.0", "react-router": "^6.30.0", "react-router-dom": "^6.30.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "xlsx": "^0.18.5" }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -1562,6 +1563,15 @@ "@types/react": "*" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1687,6 +1697,19 @@ "node": ">= 6" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1732,6 +1755,15 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1787,6 +1819,18 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/css-selector-tokenizer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", @@ -2088,6 +2132,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3012,6 +3065,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", @@ -3284,6 +3349,45 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yaml": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", diff --git a/web/package.json b/web/package.json index f2eee39..507cd1e 100644 --- a/web/package.json +++ b/web/package.json @@ -21,7 +21,8 @@ "react-dom": "^18.2.0", "react-router": "^6.30.0", "react-router-dom": "^6.30.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "xlsx": "^0.18.5" }, "devDependencies": { "@biomejs/biome": "^1.9.4", diff --git a/web/src/components/RealtimeTable/constants/FORM_1/AHR.js b/web/src/components/RealtimeTable/constants/FORM_1/AHR.js index 210e4dd..b79a8d5 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/AHR.js @@ -811,7 +811,7 @@ export const config = { header: ' ', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/AHR_P_NO_LOCAL.js b/web/src/components/RealtimeTable/constants/FORM_1/AHR_P_NO_LOCAL.js index e17f504..3634808 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/AHR_P_NO_LOCAL.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/AHR_P_NO_LOCAL.js @@ -799,7 +799,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/CAP.js b/web/src/components/RealtimeTable/constants/FORM_1/CAP.js index 525c098..0626d06 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/CAP.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/CAP.js @@ -819,7 +819,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/CAP_NO_LOCAL.js b/web/src/components/RealtimeTable/constants/FORM_1/CAP_NO_LOCAL.js index 5a64a3b..57d3dbc 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/CAP_NO_LOCAL.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/CAP_NO_LOCAL.js @@ -819,7 +819,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/OPER.js b/web/src/components/RealtimeTable/constants/FORM_1/OPER.js index 8029453..25853fa 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/OPER.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/OPER.js @@ -815,7 +815,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_1/SMETA.js b/web/src/components/RealtimeTable/constants/FORM_1/SMETA.js index 6c80682..5fccbcb 100644 --- a/web/src/components/RealtimeTable/constants/FORM_1/SMETA.js +++ b/web/src/components/RealtimeTable/constants/FORM_1/SMETA.js @@ -207,7 +207,7 @@ export const config = { header: '', accessorKey: 'data.name', columnLetter: 'C', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_2/AHR.js b/web/src/components/RealtimeTable/constants/FORM_2/AHR.js index b9a33fa..343ac01 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/AHR.js @@ -720,7 +720,7 @@ export const config = { header: "Наименование", accessorKey: "data.header.name", columnLetter: "D", - size: 150, + size: 250, filterFn: "contains", }, { diff --git a/web/src/components/RealtimeTable/constants/FORM_2/AHR_LIMIT.js b/web/src/components/RealtimeTable/constants/FORM_2/AHR_LIMIT.js index 319db9c..4d4afd5 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/AHR_LIMIT.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/AHR_LIMIT.js @@ -106,7 +106,7 @@ export const config = { header: '', accessorKey: 'data.name', columnLetter: 'D', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_2/CAP.js b/web/src/components/RealtimeTable/constants/FORM_2/CAP.js index 776be69..0d67692 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/CAP.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/CAP.js @@ -833,7 +833,7 @@ export const config = { header: "", accessorKey: "data.header.name", columnLetter: "F", - size: 150, + size: 250, filterFn: "contains", }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_2/OPER.js b/web/src/components/RealtimeTable/constants/FORM_2/OPER.js index 1fd41cf..3b28800 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/OPER.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/OPER.js @@ -833,7 +833,7 @@ export const config = { header: "", accessorKey: "data.header.name", columnLetter: "F", - size: 150, + size: 250, filterFn: "contains", }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_2/SMETA.js b/web/src/components/RealtimeTable/constants/FORM_2/SMETA.js index 83e2ad5..9cb3e33 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/SMETA.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/SMETA.js @@ -125,7 +125,7 @@ export const config = { header: '', accessorKey: 'data.name', columnLetter: 'C', - size: 150, + size: 250, filterFn: 'contains', }, ], diff --git a/web/src/components/RealtimeTable/constants/FORM_4/AHR.js b/web/src/components/RealtimeTable/constants/FORM_4/AHR.js index 29ba442..b9aaa1f 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/AHR.js @@ -797,7 +797,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'D', - size: 150, + size: 250, filterFn: 'contains', muiTableHeadCellProps: { sx: { diff --git a/web/src/components/RealtimeTable/constants/FORM_4/CAP.js b/web/src/components/RealtimeTable/constants/FORM_4/CAP.js index 4ef9681..0bace3b 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/CAP.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/CAP.js @@ -797,7 +797,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'D', - size: 150, + size: 250, filterFn: 'contains', muiTableHeadCellProps: { sx: { diff --git a/web/src/components/RealtimeTable/constants/FORM_4/OPER.js b/web/src/components/RealtimeTable/constants/FORM_4/OPER.js index 548277b..c3652fa 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/OPER.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/OPER.js @@ -797,7 +797,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'D', - size: 150, + size: 250, filterFn: 'contains', muiTableHeadCellProps: { sx: { diff --git a/web/src/components/RealtimeTable/constants/FORM_4/SMETA.js b/web/src/components/RealtimeTable/constants/FORM_4/SMETA.js index 18c2e05..f480da2 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/SMETA.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/SMETA.js @@ -209,7 +209,7 @@ export const config = { header: "", accessorKey: "data.name", columnLetter: "C", - size: 150, + size: 250, filterFn: "contains", muiTableHeadCellProps: { sx: { diff --git a/web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js b/web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js index 73c3d5f..a4697bb 100644 --- a/web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js +++ b/web/src/components/RealtimeTable/constants/FORM_4/STRUCTURE.js @@ -843,7 +843,7 @@ export const config = { header: '', accessorKey: 'data.header.name', columnLetter: 'F', - size: 150, + size: 250, filterFn: 'contains', muiTableHeadCellProps: { sx: { diff --git a/web/src/pages/Svod2Page/Svod2Page.jsx b/web/src/pages/Svod2Page/Svod2Page.jsx index 81af2e4..626cf50 100644 --- a/web/src/pages/Svod2Page/Svod2Page.jsx +++ b/web/src/pages/Svod2Page/Svod2Page.jsx @@ -3,9 +3,12 @@ import { Alert, Box, Button, Paper, Tab, Tabs, Typography } from '@mui/material' import { MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { MRT_Localization_RU } from 'material-react-table/locales/ru'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; import { HeaderSwitch } from '../../components/HeaderMenu/HeaderSwitch'; import { PrimaryButton } from '../../components/common/Buttons/Buttons'; +import { ExportWithTextButton } from '../../components/common/Buttons/ButtonsActions'; +import { exportSummaryTableToXlsx } from '../../utils/exportSummaryTable'; import { useSvod } from '../SvodPage/SvodContext'; import { SvodFilters } from '../SvodPage/SvodFilters'; @@ -356,6 +359,7 @@ const matchesSearch = (row, query) => { export default function Svod2Page() { const { getSheetFilters, updateSheetFilters, getSummary, invalidateSummary } = useSvod(); const [sheet, setSheet] = useState('GO'); + const [exporting, setExporting] = useState(false); const [tableStateBySheet, setTableStateBySheet] = useState(createInitialTableState); const [columnFiltersBySheet, setColumnFiltersBySheet] = useState(() => Object.fromEntries(SHEETS.map(({ value }) => [value, []]))); const latestSummaryRequestRef = useRef(Object.fromEntries(SHEETS.map(({ value }) => [value, 0]))); @@ -529,6 +533,16 @@ export default function Svod2Page() { noRecordsToDisplay: 'Нет данных для выбранных параметров', }, }); + const exportSummary = useCallback(async () => { + setExporting(true); + try { + await exportSummaryTableToXlsx(table, `Свод_2_${sheet}_${year}`); + } catch (_error) { + toast.error('Не удалось экспортировать свод'); + } finally { + setExporting(false); + } + }, [sheet, table, year]); let content = ; if (!hasCurrentData && !loading && !error) { content = ( @@ -589,15 +603,23 @@ export default function Svod2Page() { )} {hasCurrentData ? ( - + <> + + + ) : ( { + setExporting(true); + try { + await exportSummaryTableToXlsx(table, `Свод_1_${sheet}_${year}`); + } catch (_error) { + toast.error('Не удалось экспортировать свод'); + } finally { + setExporting(false); + } + }, [sheet, table, year]); return ( )} + {sheet === 'FORM_3' ? ( - ) : ( - )} diff --git a/web/src/utils/exportSummaryTable.js b/web/src/utils/exportSummaryTable.js new file mode 100644 index 0000000..15d7bd4 --- /dev/null +++ b/web/src/utils/exportSummaryTable.js @@ -0,0 +1,49 @@ +const getColumnHeader = (column) => { + const headers = []; + let currentColumn = column; + + while (currentColumn) { + const header = currentColumn.columnDef?.header; + if (typeof header === 'string' && header) headers.unshift(header); + currentColumn = currentColumn.parent; + } + + return headers.join(' / ') || column.id; +}; + +const normalizeCellValue = (value) => { + if (value === null || value === undefined) return ''; + if (typeof value === 'object') return JSON.stringify(value); + return value; +}; + +const getColumnWidth = (header, values) => { + const contentWidth = values.reduce((width, value) => Math.max(width, String(value ?? '').length), header.length); + return { wch: Math.min(Math.max(contentWidth + 2, 10), 50) }; +}; + +export const exportSummaryTableToXlsx = async (table, filename) => { + const XLSX = await import('xlsx'); + const columns = table.getAllLeafColumns().filter((column) => column.getIsVisible() && !column.id.startsWith('mrt-')); + const tableRows = table.getFilteredRowModel().flatRows; + const headers = columns.map(getColumnHeader); + const rows = tableRows.map((row) => + columns.map((column) => { + const value = normalizeCellValue(row.getValue(column.id)); + return column.id === 'name' && row.depth > 0 ? `${' '.repeat(row.depth)}${value}` : value; + }), + ); + + const worksheet = XLSX.utils.aoa_to_sheet([headers, ...rows]); + worksheet['!cols'] = columns.map((_, index) => + getColumnWidth( + headers[index], + rows.map((row) => row[index]), + ), + ); + worksheet['!autofilter'] = { ref: worksheet['!ref'] }; + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Свод'); + XLSX.writeFile(workbook, `${filename}.xlsx`, { compression: true }); +};