diff --git a/web/src/components/RealtimeTable/Modals/SheetModals.jsx b/web/src/components/RealtimeTable/Modals/SheetModals.jsx
new file mode 100644
index 0000000..24536c0
--- /dev/null
+++ b/web/src/components/RealtimeTable/Modals/SheetModals.jsx
@@ -0,0 +1,42 @@
+import { forwardRef, memo, useCallback, useImperativeHandle, useReducer } from 'react';
+
+import { SHEET_MODALS_INITIAL_STATE, getCreateModalConfig, sheetModalsReducer } from '../utils/sheetModalsState';
+import { CreateProgramModal } from './CreateProgramProjectModal';
+import { SelectExpenseItemModal } from './SelectExpenseItemModal';
+import { SelectVspModal } from './SelectVspModal';
+
+const SheetModalsComponent = forwardRef(function SheetModals(
+ { formId, sheetName, onAddVsp, onAddExpenseItem, onAddProgram, onAddProject },
+ ref,
+) {
+ const [state, dispatch] = useReducer(sheetModalsReducer, SHEET_MODALS_INITIAL_STATE);
+
+ useImperativeHandle(ref, () => ({ open: (kind) => dispatch({ type: 'open', kind }) }), []);
+
+ const close = useCallback(() => dispatch({ type: 'close' }), []);
+
+ const createConfig = getCreateModalConfig(state.kind);
+ const createHandlers = { onAddProgram, onAddProject };
+
+ return (
+ <>
+
+
+
+ >
+ );
+});
+
+export const SheetModals = memo(SheetModalsComponent);
diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx
index 539e4b3..7e3a0dd 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -12,9 +12,7 @@ import { getTableColumns } from './tableColumns';
import { CircularProgress } from '@mui/material';
import { debounce } from '@mui/material';
import { toast } from 'react-toastify';
-import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
-import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
-import { SelectVspModal } from './Modals/SelectVspModal';
+import { SheetModals } from './Modals/SheetModals';
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
@@ -103,9 +101,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const [expanded, setExpanded] = useState(true);
const [expenseItemsVisibilityAction, setExpenseItemsVisibilityAction] = useState('hide');
- const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
- const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false);
- const [createModalType, setCreateModalType] = useState(null);
+ const modalsRef = useRef(null);
const isFormNoCanAddRow = useMemo(() => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, [formType, sheetName]);
const canModifyRows = userRoleId === ROLES_NAME_ID.admin || columnsCurStage.length > 0;
@@ -650,7 +646,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
);
const handleOpenModalSelectVsp = useCallback(() => {
- setIsOpenModalSelectVsp(true);
+ modalsRef.current?.open('vsp');
}, []);
const handleOpenModalSelectExpenseItem = useCallback(() => {
@@ -663,7 +659,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
toast.error('Выделите строку проекта');
return;
}
- setIsOpenModalSelectExpenseItem(true);
+ modalsRef.current?.open('expense');
}, [getSelectedRow]);
const handleDeleteRow = useCallback(() => {
@@ -699,7 +695,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
return;
}
- setCreateModalType('program');
+ modalsRef.current?.open('program');
}, [canModifyRows]);
const addProject = useCallback(() => {
@@ -716,13 +712,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
toast.error('Выделите строку программы');
return;
}
- setCreateModalType('project');
+ modalsRef.current?.open('project');
}, [canModifyRows, getSelectedRow]);
- const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []);
- const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []);
- const closeCreateModal = useCallback(() => setCreateModalType(null), []);
-
useEffect(() => {
return () => {
rowVirtualizerRef.current = null;
@@ -848,26 +840,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
-
-
-
>
);
diff --git a/web/src/components/RealtimeTable/utils/sheetModalsState.js b/web/src/components/RealtimeTable/utils/sheetModalsState.js
new file mode 100644
index 0000000..97623b1
--- /dev/null
+++ b/web/src/components/RealtimeTable/utils/sheetModalsState.js
@@ -0,0 +1,21 @@
+export const SHEET_MODAL_KINDS = ['vsp', 'expense', 'program', 'project'];
+
+export const SHEET_MODALS_INITIAL_STATE = { kind: null };
+
+export const sheetModalsReducer = (state, action) => {
+ switch (action?.type) {
+ case 'open':
+ return SHEET_MODAL_KINDS.includes(action.kind) ? { kind: action.kind } : state;
+ case 'close':
+ return state.kind === null ? state : SHEET_MODALS_INITIAL_STATE;
+ default:
+ return state;
+ }
+};
+
+const CREATE_MODAL_CONFIG = {
+ program: { title: 'Создание программы', handlerKey: 'onAddProgram' },
+ project: { title: 'Создание проекта', handlerKey: 'onAddProject' },
+};
+
+export const getCreateModalConfig = (kind) => CREATE_MODAL_CONFIG[kind] ?? null;
diff --git a/web/src/components/RealtimeTable/utils/sheetModalsState.test.js b/web/src/components/RealtimeTable/utils/sheetModalsState.test.js
new file mode 100644
index 0000000..a4c7281
--- /dev/null
+++ b/web/src/components/RealtimeTable/utils/sheetModalsState.test.js
@@ -0,0 +1,29 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { SHEET_MODALS_INITIAL_STATE, getCreateModalConfig, sheetModalsReducer } from './sheetModalsState.js';
+
+test('open sets the kind and close resets it', () => {
+ for (const kind of ['vsp', 'expense', 'program', 'project']) {
+ const opened = sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'open', kind });
+ assert.deepEqual(opened, { kind });
+ assert.deepEqual(sheetModalsReducer(opened, { type: 'close' }), SHEET_MODALS_INITIAL_STATE);
+ }
+});
+
+test('opening another kind replaces the current one', () => {
+ const state = sheetModalsReducer({ kind: 'vsp' }, { type: 'open', kind: 'expense' });
+ assert.deepEqual(state, { kind: 'expense' });
+});
+
+test('unknown kind or action keeps the same state object; closing a closed state too', () => {
+ assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'open', kind: 'nope' }), SHEET_MODALS_INITIAL_STATE);
+ assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'close' }), SHEET_MODALS_INITIAL_STATE);
+ assert.equal(sheetModalsReducer(SHEET_MODALS_INITIAL_STATE, { type: 'x' }), SHEET_MODALS_INITIAL_STATE);
+});
+
+test('create modal config differs for program and project, null for others', () => {
+ assert.deepEqual(getCreateModalConfig('program'), { title: 'Создание программы', handlerKey: 'onAddProgram' });
+ assert.deepEqual(getCreateModalConfig('project'), { title: 'Создание проекта', handlerKey: 'onAddProject' });
+ assert.equal(getCreateModalConfig('vsp'), null);
+ assert.equal(getCreateModalConfig(null), null);
+});