diff --git a/web/src/pages/DictVspPage/DictVspPage.jsx b/web/src/pages/DictVspPage/DictVspPage.jsx
index 50d07b8..d9b1992 100644
--- a/web/src/pages/DictVspPage/DictVspPage.jsx
+++ b/web/src/pages/DictVspPage/DictVspPage.jsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { NameTask, TaskInfoContainer } from '../../components/common/SwitchFormTask/SwitchFormTask.style';
import { Box, CircularProgress, Stack } from '@mui/material';
@@ -11,6 +11,7 @@ import { HeaderPortal } from '../../components/common/HeaderPortal';
import { ROLES_NAME_ID } from '../../constants/constants';
import { PageContainer } from '../../components/common/PageContainer';
import DictVspFilters from './components/DictVspFilters';
+import DictVspModals from './components/DictVspModals';
import TableDictVsp from './components/TableDictVsp';
const DictVspInfo = () => {
@@ -30,6 +31,7 @@ function DictVspPage() {
const [ssps, setSsps] = useState([]);
const [selectedSsp, setSelectedSsp] = useState(null);
const { user } = useAuth();
+ const modalsRef = useRef(null);
const loadData = useCallback(async () => {
if (!user) return;
@@ -68,7 +70,7 @@ function DictVspPage() {
loadData();
}, [loadData]);
- const handleAdd = async (formData) => {
+ const handleAdd = useCallback(async (formData) => {
try {
const response = await DictVspApi.create(formData);
if (response.success) {
@@ -82,9 +84,9 @@ function DictVspPage() {
toast.error(error.response?.data?.message || 'Ошибка создания ВСП');
throw error;
}
- };
+ }, []);
- const handleEdit = async (formData) => {
+ const handleEdit = useCallback(async (formData) => {
try {
const response = await DictVspApi.update(formData.id, formData);
if (response.success) {
@@ -98,9 +100,9 @@ function DictVspPage() {
toast.error(error.response?.data?.message || 'Ошибка обновления ВСП');
throw error;
}
- };
+ }, []);
- const handleDelete = async (vspId) => {
+ const handleDelete = useCallback(async (vspId) => {
try {
const response = await DictVspApi.delete(vspId);
if (response.success) {
@@ -114,7 +116,11 @@ function DictVspPage() {
toast.error(error.response?.data?.message || 'Ошибка удаления ВСП');
throw error;
}
- };
+ }, []);
+
+ const handleAddClick = useCallback(() => modalsRef.current?.openAdd(), []);
+ const handleEditClick = useCallback((item) => modalsRef.current?.openEdit(item), []);
+ const handleExportClick = useCallback(() => modalsRef.current?.openExport(), []);
if (isLoading) {
return (
@@ -139,12 +145,18 @@ function DictVspPage() {
)}
+
diff --git a/web/src/pages/DictVspPage/components/DictVspModals.jsx b/web/src/pages/DictVspPage/components/DictVspModals.jsx
new file mode 100644
index 0000000..22b7b01
--- /dev/null
+++ b/web/src/pages/DictVspPage/components/DictVspModals.jsx
@@ -0,0 +1,65 @@
+import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
+import ModalEditAddVsp from './Modals/ModalEditAddVsp';
+import ModalExport from './Modals/ModalExport';
+
+// Состояние модалок живёт здесь, а не в таблице: открытие/закрытие не перерисовывает MRT.
+const DictVspModals = forwardRef(({ onAdd, onEdit, onDelete, ssps }, ref) => {
+ const [modalOpen, setModalOpen] = useState(false);
+ const [modalExportOpen, setModalExportOpen] = useState(false);
+ const [editingItem, setEditingItem] = useState(null);
+ const [saveLoading, setSaveLoading] = useState(false);
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ openAdd: () => {
+ setEditingItem(null);
+ setModalOpen(true);
+ },
+ openEdit: (item) => {
+ setEditingItem(item);
+ setModalOpen(true);
+ },
+ openExport: () => setModalExportOpen(true),
+ }),
+ [],
+ );
+
+ const handleSave = async (formData) => {
+ setSaveLoading(true);
+ try {
+ if (editingItem) {
+ await onEdit?.({ ...formData, id: editingItem.id });
+ } else {
+ await onAdd?.(formData);
+ }
+ setModalOpen(false);
+ } catch (error) {
+ console.error('Error saving:', error);
+ } finally {
+ setSaveLoading(false);
+ }
+ };
+
+ const closeEdit = useCallback(() => setModalOpen(false), []);
+ const closeExport = useCallback(() => setModalExportOpen(false), []);
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
+DictVspModals.displayName = 'DictVspModals';
+
+export default DictVspModals;
diff --git a/web/src/pages/DictVspPage/components/TableDictVsp.jsx b/web/src/pages/DictVspPage/components/TableDictVsp.jsx
index 9796933..db92a98 100644
--- a/web/src/pages/DictVspPage/components/TableDictVsp.jsx
+++ b/web/src/pages/DictVspPage/components/TableDictVsp.jsx
@@ -1,55 +1,19 @@
import { Box, IconButton, Stack, Typography } from '@mui/material';
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
-import { useMemo, useRef, useState } from 'react';
+import { memo, useMemo, useRef } from 'react';
import { useAuth } from '../../../app/context/AuthProvider';
import { PrimaryButton, PrimaryOutlinedButton } from '../../../components/common/Buttons/Buttons';
import { EditPenSvg } from '../../../components/common/icons/icons';
import { ROLES_NAME_ID } from '../../../constants/constants';
-import ModalEditAddVsp from './Modals/ModalEditAddVsp';
-import ModalExport from './Modals/ModalExport';
+import { filterVspBySsp } from '../utils/filterVspBySsp';
-const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, selectedSsp }) => {
+const ROW_ESTIMATED_HEIGHT = 54;
+
+const TableDictVsp = ({ data, onEditClick, onAddClick, onExportClick, loading = false, selectedSsp }) => {
const { user } = useAuth();
- const [modalOpen, setModalOpen] = useState(false);
- const [modalExportOpen, setModalExportOpen] = useState(false);
- const [editingItem, setEditingItem] = useState(null);
- const [saveLoading, setSaveLoading] = useState(false);
const tableContainerRef = useRef(null);
- const filteredData = useMemo(() => {
- if (!selectedSsp) {
- return data;
- }
- return data.filter((item) => item.ssp_id === selectedSsp.id);
- }, [data, selectedSsp]);
-
- const handleEdit = (item) => {
- setEditingItem(item);
- setModalOpen(true);
- };
-
- const handleAdd = () => {
- setEditingItem(null);
- setModalOpen(true);
- };
-
- const handleExport = () => [setModalExportOpen(true)];
-
- const handleSave = async (formData) => {
- setSaveLoading(true);
- try {
- if (editingItem) {
- await onEdit?.({ ...formData, id: editingItem.id });
- } else {
- await onAdd?.(formData);
- }
- setModalOpen(false);
- } catch (error) {
- console.error('Error saving:', error);
- } finally {
- setSaveLoading(false);
- }
- };
+ const filteredData = useMemo(() => filterVspBySsp(data, selectedSsp), [data, selectedSsp]);
const columns = useMemo(
() => [
@@ -70,9 +34,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'system_code',
header: 'Код РФ',
- size: 180,
- minSize: 50,
- maxSize: 100,
+ size: 110,
+ minSize: 90,
+ maxSize: 140,
Cell: ({ cell }) => (
{cell.getValue() || '-'}
@@ -84,9 +48,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'registration_number',
header: 'Регистрационный номер ВСП',
- size: 180,
- minSize: 150,
- maxSize: 200,
+ size: 250,
+ minSize: 200,
+ maxSize: 300,
Cell: ({ cell }) => (
{cell.getValue() || '-'}
@@ -98,9 +62,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'regional_branch',
header: 'Региональный филиал/ССП',
- size: 120,
- minSize: 100,
- maxSize: 150,
+ size: 230,
+ minSize: 180,
+ maxSize: 300,
Cell: ({ cell }) => {cell.getValue() || '-'},
enableColumnActions: false,
enableSorting: false,
@@ -118,9 +82,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'address',
header: 'Адрес объекта',
- size: 250,
+ size: 300,
minSize: 200,
- maxSize: 350,
+ maxSize: 500,
Cell: ({ cell }) => {cell.getValue() || '-'},
enableColumnActions: false,
enableSorting: false,
@@ -128,9 +92,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'approved_format',
header: 'Утверждённый формат',
- size: 150,
- minSize: 120,
- maxSize: 180,
+ size: 190,
+ minSize: 160,
+ maxSize: 240,
Cell: ({ cell }) => {cell.getValue() || '-'},
enableColumnActions: false,
enableSorting: false,
@@ -138,9 +102,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'open_date',
header: 'Дата открытия ВСП',
- size: 130,
- minSize: 110,
- maxSize: 150,
+ size: 180,
+ minSize: 150,
+ maxSize: 220,
Cell: ({ cell }) => {
const date = cell.getValue();
return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'};
@@ -151,9 +115,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'close_date',
header: 'Дата закрытия ВСП',
- size: 130,
- minSize: 110,
- maxSize: 150,
+ size: 180,
+ minSize: 150,
+ maxSize: 220,
Cell: ({ cell }) => {
const date = cell.getValue();
return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'};
@@ -164,9 +128,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'placement_type',
header: 'Форма расположения ВСП',
- size: 170,
- minSize: 140,
- maxSize: 200,
+ size: 220,
+ minSize: 180,
+ maxSize: 260,
Cell: ({ cell }) => {cell.getValue() || '-'},
enableColumnActions: false,
enableSorting: false,
@@ -174,9 +138,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'staff_count',
header: 'Штатная численность ВСП',
- size: 170,
- minSize: 140,
- maxSize: 200,
+ size: 230,
+ minSize: 190,
+ maxSize: 270,
Cell: ({ cell }) => {
const value = cell.getValue();
return {value !== undefined && value !== null ? value : '-'};
@@ -197,9 +161,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'rent_contract_num',
header: 'Номер договора',
- size: 130,
- minSize: 110,
- maxSize: 160,
+ size: 150,
+ minSize: 130,
+ maxSize: 200,
Cell: ({ cell }) => {cell.getValue() || '-'},
enableColumnActions: false,
enableSorting: false,
@@ -207,9 +171,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'rent_end_date',
header: 'Дата окончания договора',
- size: 150,
- minSize: 130,
- maxSize: 170,
+ size: 230,
+ minSize: 190,
+ maxSize: 270,
Cell: ({ cell }) => {
const date = cell.getValue();
return {date ? new Date(date).toLocaleDateString('ru-RU') : '-'};
@@ -220,9 +184,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
{
accessorKey: 'notes',
header: 'Примечания',
- size: 200,
- minSize: 150,
- maxSize: 300,
+ size: 300,
+ minSize: 200,
+ maxSize: 500,
Cell: ({ cell }) => (
{cell.getValue() || '-'}
@@ -245,6 +209,9 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
enableGlobalFilter: false,
enableSorting: false,
enablePagination: false,
+ enableRowVirtualization: true,
+ // Высоту строк MRT измеряет сам (по умолчанию): «Примечания» многострочные (pre-wrap).
+ rowVirtualizerOptions: { overscan: 5, estimateSize: () => ROW_ESTIMATED_HEIGHT },
positionActionsColumn: 'last',
// Включаем изменение размера колонок для лучшего UX при горизонтальном скролле
@@ -279,8 +246,8 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
Справочник объектов
- Экспортировать
- {user.role_id == ROLES_NAME_ID.admin && + Добавить объект}
+ Экспортировать
+ {user.role_id == ROLES_NAME_ID.admin && + Добавить объект}
),
@@ -325,7 +292,7 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
}}>
handleEdit(row.original)}
+ onClick={() => onEditClick(row.original)}
sx={{
padding: '4px',
'&:hover': {
@@ -479,23 +446,10 @@ const TableDictVsp = ({ data, onEdit, onDelete, onAdd, loading = false, ssps, se
});
return (
- <>
-
-
-
-
- setModalOpen(false)}
- onSave={handleSave}
- onDelete={onDelete}
- initialData={editingItem}
- loading={saveLoading}
- ssps={ssps}
- />
- setModalExportOpen(false)} ssps={ssps} />
- >
+
+
+
);
};
-export default TableDictVsp;
+export default memo(TableDictVsp);