From 73083153da17476995a5955811b571a9cb596b42 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Tue, 21 Jul 2026 17:24:13 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=20(=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/api/expense-item.js | 7 + .../Modals/SelectExpenseItemModal.jsx | 158 ++++++++++++++++++ .../RealtimeTable/RealtimeTable.jsx | 45 ++++- .../constants/addingRowConfig.js | 4 + .../components/RealtimeTable/tableStyles.css | 0 5 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 web/src/api/expense-item.js create mode 100644 web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx delete mode 100644 web/src/components/RealtimeTable/tableStyles.css diff --git a/web/src/api/expense-item.js b/web/src/api/expense-item.js new file mode 100644 index 0000000..4365d46 --- /dev/null +++ b/web/src/api/expense-item.js @@ -0,0 +1,7 @@ +import api from "./client"; + +export const ExpenseItemApi = { + getAll(params) { + return api.get(`/expense-item/`, { params }).then((r) => r.data); + }, +}; diff --git a/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx new file mode 100644 index 0000000..0277f32 --- /dev/null +++ b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx @@ -0,0 +1,158 @@ +import { useEffect, useState } from 'react'; +import { + Box, + Button, + Divider, + FormControl, + FormLabel, + IconButton, + Stack, + Select, + MenuItem, + Autocomplete, + TextField, +} from '@mui/material'; +import { + ModalBackdrop, + ModalContainer, + HeaderContainer, + ModalTitle, +} from '../../common/Modal/ModalStyled'; +import { Cancel } from '../../common/icons/icons'; +import { DictVspApi } from '../../../api/dict-vsp'; +import { toast } from 'react-toastify'; +import { ExpenseItemApi } from '../../../api/expense-item'; + +export const SelectExpenseItemModal = ({ + isOpen, + onClose, + onSelect, + formId, + title = '', + isSaving = false, + sheet, +}) => { + const [selectedValue, setSelectedValue] = useState(''); + const [expenseItemOptions, setExpenseItemOption] = useState([]) + + useEffect(() => { + if (formId === null) return; + ExpenseItemApi.getAll({ r_start: true, sheet: sheet }).then(data => { + if (data.success) { + setExpenseItemOption(data.result) + return; + } + toast.error('Ошибка получения статья расходов') + }) + }, [formId]) + + useEffect(() => { + if (!isOpen) { + setSelectedValue(''); + } + }, [isOpen]) + + const handleCancel = () => { + setSelectedValue(''); + onClose(); + }; + + const handleBackdropClick = (e) => { + if (e.target === e.currentTarget) { + handleCancel(); + } + }; + + const handleSave = () => { + onSelect?.(selectedValue); + onClose(); + }; + + const isFormValid = selectedValue; + + if (!isOpen) return null; + + return ( + + + + + {title} + + + + + + + + + + {/* Выбор ВСП */} + + + option.name || ''} + getOptionKey={(option) => option.id} + value={selectedValue} + onChange={(event, newValue) => { + setSelectedValue(newValue); + }} + slotProps={{ + popper: { + style: { zIndex: 60 }, + }, + }} + renderInput={(params) => ( + + )} + /> + + + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index c3bdbac..396753e 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -1,4 +1,3 @@ -// components/RealtimeTable/index.js import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react'; import { createPortal } from 'react-dom'; import { @@ -25,9 +24,11 @@ import { import { useRealtime } from './contexts/RealtimeContext'; import { toast } from 'react-toastify'; import { CircularProgress } from '@mui/material'; -import { additionVspRowTable } from './constants/addingRowConfig'; +import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { SelectVspModal } from './Modals/SelectVspModal'; +import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; import { getRowId } from './utils/rowUtils'; +import { FORM_TYPE_OPTIONS } from '../../constants/constants'; const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); @@ -40,6 +41,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const [editingCell, setEditingCell] = useState(null); const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); + const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; @@ -47,6 +49,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { return additionVspRowTable[formType].includes(sheetName); }, [formType, sheetName]); + const isAdditionExpenseItem = useMemo(() => { + if (!formType || !sheetName) return false; + if (!additionExpenseRowTable[formType]) return false; + return additionExpenseRowTable[formType].includes(sheetName); + }, [formType, sheetName]); + const handleGlobalFilterChange = (value) => { startTransition(() => { setGlobalFilter(value); @@ -359,11 +367,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const handleAddVspRow = useCallback((vsp_id) => { contextAddRow({ vsp_id: vsp_id }); - }, [rowSelection, table]); + }, []); + + const handleAddExpenseItemRow = useCallback((expense_item) => { + contextAddRow({ expense_item_id: expense_item.id }); + }, []); const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); - }, [rowSelection, table]) + }, []) + + const handleOpenModalSelectExpenseItem = useCallback(() => { + setIsOpenModalSelectExpenseItem(true); + }, []) const handleDeleteRow = useCallback(() => { const allRows = table.getRowModel().rows; @@ -379,6 +395,17 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { setRowSelection({}); }, [rowSelection, table]) + const addRow = useCallback(() => { + + if (isVspAdditionRow) { + return handleOpenModalSelectVsp(); + } + if (isAdditionExpenseItem) { + return handleOpenModalSelectExpenseItem(); + } + return handleAddRow(); + }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]); + useEffect(() => { return () => { setData([]); @@ -405,7 +432,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onGlobalFilterChange={handleGlobalFilterChange} sizeMult={sizeMult} onChangeShowColumnFilters={setShowColumnFilters} - onAddRow={isVspAdditionRow ? handleOpenModalSelectVsp : handleAddRow} + onAddRow={addRow} onDeleteRow={handleDeleteRow} isLoadingData={isLoadingData} formId={formId} @@ -475,6 +502,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onSelect={handleAddVspRow} title="Выбор ВСП" /> + setIsOpenModalSelectExpenseItem(false)} + formId={formId} + onSelect={handleAddExpenseItemRow} + title="Добавление строки" + sheet={sheetName} + /> ); }; diff --git a/web/src/components/RealtimeTable/constants/addingRowConfig.js b/web/src/components/RealtimeTable/constants/addingRowConfig.js index 04e069b..7a50017 100644 --- a/web/src/components/RealtimeTable/constants/addingRowConfig.js +++ b/web/src/components/RealtimeTable/constants/addingRowConfig.js @@ -1,3 +1,7 @@ export const additionVspRowTable = { FORM_2: ["AHR_SECURITY", "AHR_RENT", "AHR_UTILITY"], }; + +export const additionExpenseRowTable = { + FORM_4: ["AHR", "CAP", "OPER"], +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/tableStyles.css b/web/src/components/RealtimeTable/tableStyles.css deleted file mode 100644 index e69de29..0000000 From 562707f4187398a9c318102665419c318f5eaca0 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 22 Jul 2026 14:48:34 +0300 Subject: [PATCH 2/4] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B0=20=D0=BA=D0=BE=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/RealtimeTable/utils/cellUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js index 304a8ab..8ce301c 100644 --- a/web/src/components/RealtimeTable/utils/cellUtils.js +++ b/web/src/components/RealtimeTable/utils/cellUtils.js @@ -42,7 +42,7 @@ export function setNewValueToData(data, colId, rowId, value) { } export function updateCells(data, newRows) { - const updatedRows = JSON.parse(JSON.stringify(data)); + const updatedRows = structuredClone(data); function deepMerge(target, source) { if (!source || typeof source !== "object") { From d257c3f1c1b7fabfaf37ef45005f67ad43a357be Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Mon, 27 Jul 2026 17:12:37 +0300 Subject: [PATCH 3/4] add-row --- web/public/images/Project.svg | 0 .../RealtimeTable/RealtimeTable.jsx | 47 +++++++++++- .../RealtimeTable/contexts/RealtimeContext.js | 76 ++++++++++++++++++- .../RealtimeTable/hooks/useRealtimeData.js | 58 ++++++++++++++ 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 web/public/images/Project.svg diff --git a/web/public/images/Project.svg b/web/public/images/Project.svg new file mode 100644 index 0000000..e69de29 diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 78a9586..442a2cd 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -24,9 +24,11 @@ import { import { useRealtime } from './contexts/RealtimeContext'; import { toast } from 'react-toastify'; import { CircularProgress } from '@mui/material'; -import { additionVspRowTable } from './constants/addingRowConfig'; +import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { SelectVspModal } from './Modals/SelectVspModal'; +import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; import { getRowId } from './utils/rowUtils'; +import { FORM_TYPE_OPTIONS } from '../../constants/constants'; import { debounce } from '@mui/material'; const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { @@ -40,6 +42,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const [editingCell, setEditingCell] = useState(null); const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); + const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; @@ -47,6 +50,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { return additionVspRowTable[formType].includes(sheetName); }, [formType, sheetName]); + const isAdditionExpenseItem = useMemo(() => { + if (!formType || !sheetName) return false; + if (!additionExpenseRowTable[formType]) return false; + return additionExpenseRowTable[formType].includes(sheetName); + }, [formType, sheetName]); + const handleGlobalFilterChange = useCallback( debounce((value) => { startTransition(() => { @@ -66,6 +75,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { addRow: contextAddRow, deleteRow: contextDeleteRow, startEditing: contextStartEditing, + addProgram: contextAddProgram, + addProject: contextAddProject, } = useRealtime(); const { @@ -113,6 +124,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { setColumnsConfig({ columns: [], colors: {} }); } }; + loadConfig(); }, [formType, sheetName]); @@ -363,11 +375,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const handleAddVspRow = useCallback((vsp_id) => { contextAddRow({ vsp_id: vsp_id }); - }, [rowSelection, table]); + }, []); + + const handleAddExpenseItemRow = useCallback((expense_item) => { + contextAddRow({ expense_item_id: expense_item.id }); + }, []); const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); - }, [rowSelection, table]) + }, []) + + const handleOpenModalSelectExpenseItem = useCallback(() => { + setIsOpenModalSelectExpenseItem(true); + }, []) const handleDeleteRow = useCallback(() => { const allRows = table.getRowModel().rows; @@ -383,6 +403,17 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { setRowSelection({}); }, [rowSelection, table]) + const addRow = useCallback(() => { + + if (isVspAdditionRow) { + return handleOpenModalSelectVsp(); + } + if (isAdditionExpenseItem) { + return handleOpenModalSelectExpenseItem(); + } + return handleAddRow(); + }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]); + useEffect(() => { return () => { setData([]); @@ -409,7 +440,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onGlobalFilterChange={handleGlobalFilterChange} sizeMult={sizeMult} onChangeShowColumnFilters={setShowColumnFilters} - onAddRow={isVspAdditionRow ? handleOpenModalSelectVsp : handleAddRow} + onAddRow={addRow} onDeleteRow={handleDeleteRow} isLoadingData={isLoadingData} formId={formId} @@ -479,6 +510,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onSelect={handleAddVspRow} title="Выбор ВСП" /> + setIsOpenModalSelectExpenseItem(false)} + formId={formId} + onSelect={handleAddExpenseItemRow} + title="Добавление строки" + sheet={sheetName} + /> ); }; diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js index 58182a8..dac68d9 100644 --- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -88,19 +88,33 @@ export const RealtimeProvider = ({ } break; + case "program_added": + console.log("Program added:", data); + if (data.result && onProgramAddRef.current) { + onProgramAddRef.current(data); + } + break; + + case "project_added": + console.log("Project added:", data); + if (data.result && onProjectAddRef.current) { + onProjectAddRef.current(data); + } + break; + default: console.log("Unknown event:", data.event); } }, []); - const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket( - wsUrl, - handleMessage, - ); + const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = + useWebSocket(wsUrl, handleMessage); const onCellUpdateRef = useRef(null); const onRowAddRef = useRef(null); const onRowDeleteRef = useRef(null); + const onProgramAddRef = useRef(null); + const onProjectAddRef = useRef(null); const onErrorRef = useRef(null); const onAuthSuccessRef = useRef(null); const onCellEditStartRef = useRef(null); @@ -283,6 +297,42 @@ export const RealtimeProvider = ({ [isConnected, sendMessage, sheetName, formId, direction], ); + const addProject = useCallback( + async (rowData) => { + if (!isConnectedRef) { + onErrorRef.current?.("Нет подключения или авторизации"); + return false; + } + + const message = { + event: "row_added", + data: rowData, + }; + addCommonField(message); + + return sendMessage(message); + }, + [isConnected, sendMessage, sheetName, formId, direction], + ); + + const addProgram = useCallback( + async (rowData) => { + if (!isConnectedRef) { + onErrorRef.current?.("Нет подключения или авторизации"); + return false; + } + + const message = { + event: "row_added", + data: rowData, + }; + addCommonField(message); + + return sendMessage(message); + }, + [isConnected, sendMessage, sheetName, formId, direction], + ); + const deleteRow = useCallback( async (rowId) => { if (!isConnectedRef) { @@ -322,6 +372,20 @@ export const RealtimeProvider = ({ }; }, []); + const subscribeToProgramAdds = useCallback((callback) => { + onProgramAddRef.current = callback; + return () => { + onProgramAddRef.current = null; + }; + }, []); + + const subscribeToProjectAdds = useCallback((callback) => { + onProjectAddRef.current = callback; + return () => { + onProjectAddRef.current = null; + }; + }, []); + const subscribeToErrors = useCallback((callback) => { onErrorRef.current = callback; return () => { @@ -366,9 +430,13 @@ export const RealtimeProvider = ({ endEditing, updateCell, addRow, + addProgram, + addProject, deleteRow, subscribeToRowAdds, subscribeToRowDeletes, + subscribeToProgramAdds, + subscribeToProjectAdds, subscribeToErrors, subscribeToAuthSuccess, subscribeToCellEditStart, diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js index 77ad80a..583b4fd 100644 --- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -19,6 +19,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { error: wsError, subscribeToCellUpdates, subscribeToRowAdds, + subscribeToProgramAdds, + subscribeToProjectAdds, subscribeToRowDeletes, subscribeToErrors, subscribeToCellEditStart, @@ -206,6 +208,58 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { }); }); + const unsubscribeProjectAdds = subscribeToProjectAdds((data) => { + const updates = data.result.data; + const expense_item_id = data.data.expense_item_id || null; + const newLineId = data.result.new_line_id; + const [updatedCells, newRow] = updates.reduce( + (res, row) => { + if (row[3]?.line_id === newLineId || isVspNewRow(row)) { + res[1] = row; + } else { + res[0].push(row); + } + return res; + }, + [[], null], + ); + if (newRow === null) { + toast.error("Ошибка получения данных"); + return; + } + setData((prevData) => { + const updateData = updateCells(prevData, updatedCells); + const newData = insertCell(updateData, newRow, expense_item_id); + return newData; + }); + }); + + const unsubscribeProgramAdds = subscribeToProgramAdds((data) => { + const updates = data.result.data; + const expense_item_id = data.data.expense_item_id || null; + const newLineId = data.result.new_line_id; + const [updatedCells, newRow] = updates.reduce( + (res, row) => { + if (row[3]?.line_id === newLineId || isVspNewRow(row)) { + res[1] = row; + } else { + res[0].push(row); + } + return res; + }, + [[], null], + ); + if (newRow === null) { + toast.error("Ошибка получения данных"); + return; + } + setData((prevData) => { + const updateData = updateCells(prevData, updatedCells); + const newData = insertCell(updateData, newRow, expense_item_id); + return newData; + }); + }); + const unsubscribeRowDeletes = subscribeToRowDeletes((data) => { const updatedCells = data.result; const lineIdForDelete = data.data.row_id; @@ -255,6 +309,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { return () => { unsubscribeCellUpdates(); unsubscribeRowAdds(); + unsubscribeProgramAdds(); + unsubscribeProjectAdds(); unsubscribeRowDeletes(); unsubscribeErrors(); unsubscribeCellEditStart(); @@ -268,6 +324,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { subscribeToErrors, subscribeToCellEditStart, subscribeToCellEditEnd, + subscribeToProgramAdds, + subscribeToProjectAdds, formatedToSubRows, ]); From 8c978b4e339bb31ecbcd216f47bcd32d38e58b23 Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Tue, 28 Jul 2026 16:03:52 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC?= =?UTF-8?q?,=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B2?= =?UTF-8?q?=204=20=D1=84=D0=BE=D1=80=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/public/images/Program.svg | 5 + web/public/images/Project.svg | 5 + .../Modals/CreateProgramProjectModal.jsx | 110 ++++++++++++++++ .../Modals/SelectExpenseItemModal.jsx | 18 ++- .../RealtimeTable/Modals/SelectVspModal.jsx | 18 ++- .../RealtimeTable/RealtimeTable.jsx | 66 +++++++++- .../SettingPanel/SettingPanel.jsx | 35 ++++-- .../RealtimeTable/contexts/RealtimeContext.js | 4 +- .../RealtimeTable/hooks/useRealtimeData.js | 53 ++++---- .../RealtimeTable/utils/cellUtils.js | 50 +++++++- .../RealtimeTable/utils/rowUtils.js | 4 + web/src/components/common/StyledTextField.jsx | 20 +++ web/src/components/common/TextInput.jsx | 117 ------------------ web/src/components/common/icons/icons.jsx | 17 +++ web/src/index.css | 1 + 15 files changed, 335 insertions(+), 188 deletions(-) create mode 100644 web/public/images/Program.svg create mode 100644 web/src/components/RealtimeTable/Modals/CreateProgramProjectModal.jsx create mode 100644 web/src/components/common/StyledTextField.jsx delete mode 100644 web/src/components/common/TextInput.jsx diff --git a/web/public/images/Program.svg b/web/public/images/Program.svg new file mode 100644 index 0000000..bcb350f --- /dev/null +++ b/web/public/images/Program.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/web/public/images/Project.svg b/web/public/images/Project.svg index e69de29..7f23978 100644 --- a/web/public/images/Project.svg +++ b/web/public/images/Project.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/web/src/components/RealtimeTable/Modals/CreateProgramProjectModal.jsx b/web/src/components/RealtimeTable/Modals/CreateProgramProjectModal.jsx new file mode 100644 index 0000000..d694001 --- /dev/null +++ b/web/src/components/RealtimeTable/Modals/CreateProgramProjectModal.jsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from 'react'; +import { + Box, + Button, + Divider, + FormControl, + FormLabel, + IconButton, + Stack, + Select, + MenuItem, + Autocomplete, + TextField, +} from '@mui/material'; +import { + ModalBackdrop, + ModalContainer, + HeaderContainer, + ModalTitle, +} from '../../common/Modal/ModalStyled'; +import { Cancel } from '../../common/icons/icons'; +import { DictVspApi } from '../../../api/dict-vsp'; +import { toast } from 'react-toastify'; +import { ExpenseItemApi } from '../../../api/expense-item'; +import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons'; +import { StyledTextField } from '../../common/StyledTextField'; + +export const CreateProgramModal = ({ + isOpen, + onClose, + onCreate, + title = '', + isSaving = false, + sheet, +}) => { + const [name, setName] = useState(''); + + useEffect(() => { + if (!isOpen) { + setName(''); + } + }, [isOpen]) + + const handleCancel = () => { + setName(''); + onClose(); + }; + + const handleBackdropClick = (e) => { + if (e.target === e.currentTarget) { + handleCancel(); + } + }; + + const handleSave = () => { + onCreate?.(name); + onClose(); + }; + + const isFormValid = name !== ''; + + if (!isOpen) return null; + + return ( + + + + + {title} + + + + + + + + + + + {/* setName(e.target.value)}> + + + Отмена + + + + Создать + + + + + + + ); +}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx index 0277f32..8be3611 100644 --- a/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx +++ b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx @@ -22,6 +22,7 @@ import { Cancel } from '../../common/icons/icons'; import { DictVspApi } from '../../../api/dict-vsp'; import { toast } from 'react-toastify'; import { ExpenseItemApi } from '../../../api/expense-item'; +import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons'; export const SelectExpenseItemModal = ({ isOpen, @@ -132,24 +133,19 @@ export const SelectExpenseItemModal = ({ width: '100%', }} > - - + diff --git a/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx index 29a8ee4..352f1d3 100644 --- a/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx +++ b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx @@ -19,6 +19,7 @@ import { import { Cancel } from '../../common/icons/icons'; import { DictVspApi } from '../../../api/dict-vsp'; import { toast } from 'react-toastify'; +import { DangerOutlinedButton, PrimaryButton } from '../../common/Buttons/Buttons'; export const SelectVspModal = ({ isOpen, @@ -113,24 +114,19 @@ export const SelectVspModal = ({ width: '100%', }} > - - + diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 442a2cd..82edac6 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -30,6 +30,7 @@ import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; import { getRowId } from './utils/rowUtils'; import { FORM_TYPE_OPTIONS } from '../../constants/constants'; import { debounce } from '@mui/material'; +import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); @@ -43,6 +44,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); + const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false); + const [isOpenModalCreateProject, setIsOpenModalCreateProject] = useState(false); + const [isProgramCreate, setIsProgramCreate] = useState(false) const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; @@ -73,7 +77,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { error: wsError, updateCell: contextUpdateCell, addRow: contextAddRow, - deleteRow: contextDeleteRow, startEditing: contextStartEditing, addProgram: contextAddProgram, addProject: contextAddProject, @@ -378,16 +381,42 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { }, []); const handleAddExpenseItemRow = useCallback((expense_item) => { - contextAddRow({ expense_item_id: expense_item.id }); + const allRows = table.getRowModel().rows; + const selectedRows = allRows.filter(row => rowSelection[row.id]); + const row = selectedRows[0]; + contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id }); + }, [rowSelection]); + + const handleAddProgramRow = useCallback((name) => { + contextAddProgram({ name: name }); }, []); + const handleAddProjectRow = useCallback((name) => { + const allRows = table.getRowModel().rows; + const selectedRows = allRows.filter(row => rowSelection[row.id]); + const row = selectedRows[0]; + contextAddProject({ name: name, program_id: row.original.data.header.program_id }); + }, [rowSelection]); + const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); }, []) const handleOpenModalSelectExpenseItem = useCallback(() => { + const allRows = table.getRowModel().rows; + + const selectedRows = allRows.filter(row => rowSelection[row.id]); + if (selectedRows.length === 0) { + toast.error('Выделите строку для вставки'); + return; + } + const row = selectedRows[0]; + if (row.original.row_type !== 'ITEM') { + toast.error('Выделите строку проекта'); + return; + } setIsOpenModalSelectExpenseItem(true); - }, []) + }, [rowSelection]) const handleDeleteRow = useCallback(() => { const allRows = table.getRowModel().rows; @@ -414,6 +443,28 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { return handleAddRow(); }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow]); + const addProgram = useCallback(() => { + setIsProgramCreate(true); + setIsOpenModalCreateProgram(true); + }, [setIsOpenModalCreateProgram]) + + const addProject = useCallback(() => { + setIsProgramCreate(false); + const allRows = table.getRowModel().rows; + + const selectedRows = allRows.filter(row => rowSelection[row.id]); + if (selectedRows.length === 0) { + toast.error('Выделите строку для вставки'); + return; + } + const row = selectedRows[0]; + if (row.original.row_type !== 'GROUP') { + toast.error('Выделите строку программы'); + return; + } + setIsOpenModalCreateProgram(true); + }, [setIsOpenModalCreateProgram, rowSelection]) + useEffect(() => { return () => { setData([]); @@ -441,6 +492,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { sizeMult={sizeMult} onChangeShowColumnFilters={setShowColumnFilters} onAddRow={addRow} + onAddProgram={addProgram} + onAddProject={addProject} onDeleteRow={handleDeleteRow} isLoadingData={isLoadingData} formId={formId} @@ -448,6 +501,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { direction={direction} year={year} isProject={formType == 'PROJECT'} + formType={formType} />
@@ -518,6 +572,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { title="Добавление строки" sheet={sheetName} /> + setIsOpenModalCreateProgram(false)} + title={isProgramCreate ? "Создание программы" : "Создание проекта"} + onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow} + /> ); }; diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx index 292ee04..8ac307c 100644 --- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx +++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx @@ -11,6 +11,8 @@ import { Minus, Pin, Plus, + Program, + Project, RemoveLine, Search, } from '../../common/icons/icons'; @@ -19,6 +21,7 @@ import ZoomSlider from './ZoomSlider'; import SearchComponent from '../../common/SearchComponent'; import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions'; import { exportSheet, exportSheetProject } from '../../../utils/exportFile'; +import { FORM_TYPE_OPTIONS } from '../../../constants/constants'; const SettingPanel = ({ selectedColumnId, @@ -34,6 +37,8 @@ const SettingPanel = ({ sizeMult, onChangeShowColumnFilters, onAddRow, + onAddProgram, + onAddProject, onDeleteRow, isLoadingData, formId, @@ -41,13 +46,13 @@ const SettingPanel = ({ direction, year, isProject, + formType, }) => { const [isPinned, setIsPinned] = useState(false); const [isExporting, setIsExporting] = useState(false); const [selectedColumnIds, setSelectedColumnIds] = useState(); const [showColumnFilters, setShowColumnFilters] = useState(false); - // TODO: console.log(!!!) везде посомтреть useEffect(() => { if (!selectedColumnId) { setIsPinned(false); @@ -65,7 +70,7 @@ const SettingPanel = ({ }, [columnVisibility]); //TO DO: сделать - const { addColorForCells, addFormulaForCells, deleteFormula } = {}; + const { addColorForCells,} = {}; const handleChangeShowColumnFilters = () => { const show = table.getState().showColumnFilters; @@ -73,10 +78,6 @@ const SettingPanel = ({ onChangeShowColumnFilters(!show); }; - const handleClickAddFormula = () => { }; - - const handleClickDeleteFormula = () => { }; - const handleExport = async () => { if (isExporting) return; if (isProject) { @@ -111,11 +112,6 @@ const SettingPanel = ({ console.warn('[ColumnPin] клик Pin: selectedColumnId пустой'); return; } - console.log('[ColumnPin] клик Pin:', { - selectedColumnId, - isPinned, - action: isPinned ? 'unpin' : 'pin', - }); isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId); @@ -143,6 +139,23 @@ const SettingPanel = ({ + {/* проверяем на 4 форму */} + {formType == FORM_TYPE_OPTIONS[3] && <> + + + + + + + + + + + + + + + } } diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js index dac68d9..4028366 100644 --- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -305,7 +305,7 @@ export const RealtimeProvider = ({ } const message = { - event: "row_added", + event: "project_added", data: rowData, }; addCommonField(message); @@ -323,7 +323,7 @@ export const RealtimeProvider = ({ } const message = { - event: "row_added", + event: "program_added", data: rowData, }; addCommonField(message); diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js index 583b4fd..31c2dc9 100644 --- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -1,9 +1,14 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { FormsSheetApi } from "../../../api/form_sheet"; import { useRealtime } from "../contexts/RealtimeContext"; -import { deleteRow, insertCell, updateCells } from "../utils/cellUtils"; +import { + deleteRow, + insertCell, + insertProjectRow, + updateCells, +} from "../utils/cellUtils"; import { toast } from "react-toastify"; -import { isVspNewRow, isVspRow } from "../utils/rowUtils"; +import { getParentId, isVspNewRow, isVspRow } from "../utils/rowUtils"; import { ProjectsApi } from "../../../api/projects"; const useRealtimeData = (formId, sheetName, direction, formType, year) => { @@ -184,7 +189,7 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { const unsubscribeRowAdds = subscribeToRowAdds((data) => { const updates = data.result.data; - const expense_item_id = data.data.expense_item_id || null; + const parent_id = getParentId(data.data); const newLineId = data.result.new_line_id; const [updatedCells, newRow] = updates.reduce( (res, row) => { @@ -203,18 +208,17 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { } setData((prevData) => { const updateData = updateCells(prevData, updatedCells); - const newData = insertCell(updateData, newRow, expense_item_id); + const newData = insertCell(updateData, newRow, parent_id); return newData; }); }); const unsubscribeProjectAdds = subscribeToProjectAdds((data) => { - const updates = data.result.data; - const expense_item_id = data.data.expense_item_id || null; - const newLineId = data.result.new_line_id; + const updates = data.result[1]; + const program_id = data.data.program_id; const [updatedCells, newRow] = updates.reduce( (res, row) => { - if (row[3]?.line_id === newLineId || isVspNewRow(row)) { + if (row[0] === "ITEM") { res[1] = row; } else { res[0].push(row); @@ -229,33 +233,24 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => { } setData((prevData) => { const updateData = updateCells(prevData, updatedCells); - const newData = insertCell(updateData, newRow, expense_item_id); + const newData = insertProjectRow(updateData, newRow, program_id); return newData; }); }); const unsubscribeProgramAdds = subscribeToProgramAdds((data) => { - const updates = data.result.data; - const expense_item_id = data.data.expense_item_id || null; - const newLineId = data.result.new_line_id; - const [updatedCells, newRow] = updates.reduce( - (res, row) => { - if (row[3]?.line_id === newLineId || isVspNewRow(row)) { - res[1] = row; - } else { - res[0].push(row); - } - return res; - }, - [[], null], - ); - if (newRow === null) { - toast.error("Ошибка получения данных"); - return; - } + const newRow = data.result[1][0]; setData((prevData) => { - const updateData = updateCells(prevData, updatedCells); - const newData = insertCell(updateData, newRow, expense_item_id); + const updateData = structuredClone(prevData); + const [row_type, depth, sort_order, dataRow] = newRow; + const newRowObject = { + row_type: row_type, + depth: depth, + sort_order: sort_order, + subRows: [], + data: dataRow, + }; + const newData = [...updateData, newRowObject]; return newData; }); }); diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js index 8ce301c..938c2b0 100644 --- a/web/src/components/RealtimeTable/utils/cellUtils.js +++ b/web/src/components/RealtimeTable/utils/cellUtils.js @@ -1,3 +1,5 @@ +import { getParentId } from "./rowUtils"; + export function setNewValueToData(data, colId, rowId, value) { let targetObject = data; if (rowId && rowId.length) { @@ -114,7 +116,7 @@ export function updateCells(data, newRows) { return updatedRows; } -export function insertCell(data, newRow, expense_item_id) { +export function insertCell(data, newRow, parent_id) { const updatedRows = JSON.parse(JSON.stringify(data)); const [row_type, depth, sort_order, dataRow] = newRow; const newRowObject = { @@ -126,7 +128,7 @@ export function insertCell(data, newRow, expense_item_id) { }; // Если expense_item_id === null, добавляем в конец - if (expense_item_id === null) { + if (parent_id === null) { updatedRows.push(newRowObject); return updatedRows; } @@ -143,7 +145,47 @@ export function insertCell(data, newRow, expense_item_id) { row.sort_order = row.sort_order + 1; } - if (!adding & (row.data.header.expense_item_id === expense_item_id)) { + if (!adding & (getParentId(row.data.header) === parent_id)) { + if (!("subRows" in row)) { + row.subRows = []; + } + row.subRows.push(newRowObject); + adding = true; + } + + if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) { + const found = findAndUpdate(row.subRows); + } + } + } + findAndUpdate(updatedRows); + return updatedRows; +} + +export function insertProjectRow(data, newRow, program_id) { + const updatedRows = structuredClone(data); + const [row_type, depth, sort_order, dataRow] = newRow; + const newRowObject = { + row_type: row_type, + depth: depth, + sort_order: sort_order, + subRows: [], + data: dataRow, + }; + + let adding = false; + function findAndUpdate(rows) { + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + + if ( + (row.sort_order >= newRowObject.sort_order) & + (row.data.line_id != newRowObject.data.line_id) + ) { + row.sort_order = row.sort_order + 1; + } + + if (!adding & (row.data.header.program_id === program_id)) { if (!("subRows" in row)) { row.subRows = []; } @@ -173,7 +215,7 @@ export function deleteRow(data, lineId) { rows.splice(i, 1); i--; } - + if ((rowDeleteSortOrder > 0) & (row.sort_order >= rowDeleteSortOrder)) { row.sort_order = row.sort_order - 1; } diff --git a/web/src/components/RealtimeTable/utils/rowUtils.js b/web/src/components/RealtimeTable/utils/rowUtils.js index 5ba9a51..669ca8b 100644 --- a/web/src/components/RealtimeTable/utils/rowUtils.js +++ b/web/src/components/RealtimeTable/utils/rowUtils.js @@ -13,3 +13,7 @@ export function isVspNewRow(rowData) { const newRow = rowData[3]; return !!newRow.id; } + +export function getParentId(data){ + return data.project_id || data.expense_item_id || null; +} diff --git a/web/src/components/common/StyledTextField.jsx b/web/src/components/common/StyledTextField.jsx new file mode 100644 index 0000000..08bf15c --- /dev/null +++ b/web/src/components/common/StyledTextField.jsx @@ -0,0 +1,20 @@ + +import { + TextField, +} from '@mui/material'; +export const StyledTextField = ({ error, helperText, disabled, ...props }) => ( + +); \ No newline at end of file diff --git a/web/src/components/common/TextInput.jsx b/web/src/components/common/TextInput.jsx deleted file mode 100644 index e6ce2b6..0000000 --- a/web/src/components/common/TextInput.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useState } from 'react'; -import styled from '@emotion/styled'; - -import Tooltip from './Tooltip'; - -const TextInput = ({ - label, - name, - type = 'text', - onChange, - onChangeWithName, - tooltipText = '', - value = '', - placeholder = '', - autocomplete = 'new-password', -}) => { - const [cur_value, setCurValue] = useState(value); - const [isFocused, setIsFocused] = useState(false); - - const handleChange = (inputValue) => { - if (onChangeWithName) { - onChangeWithName(name, inputValue); - } - - if (onChange) { - onChange(inputValue); - } - }; - - return ( - - - {tooltipText !== '' && } - - ); -}; - -const TextInputDiv = styled.div` - position: relative; - width: 100%; - - label { - color: inherit; - font-size: inherit; - display: flex; - flex-direction: column; - gap: 0.2rem; /* Уменьшено еще на 10% (0.225rem * 0.9) */ - } - - input { - max-width: 100%; - width: 100%; - flex-grow: 1; - flex-shrink: 1; - border: 1px solid #ddd; - border-radius: 4px; - height: 1.6rem; /* Уменьшено еще на 10% (1.8rem * 0.9) */ - min-height: 36px; /* Уменьшено еще на 10% (40px * 0.9) */ - background-color: rgb(240, 242, 246); - padding: 0 0.4rem; /* Уменьшено еще на 10% (0.45rem * 0.9) */ - font-size: inherit; - transition: border-color 0.2s ease; - - &:focus { - outline: none; - border-color: #258141; - box-shadow: 0 0 0 2px rgba(37, 129, 65, 0.1); - } - - &:hover { - border-color: #999; - } - } - - /* Медиа-запросы для адаптивности */ - @media (max-width: 768px) { - input { - height: 2rem; /* Уменьшено еще на 10% (2.25rem * 0.9) */ - min-height: 39px; /* Уменьшено еще на 10% (43px * 0.9) */ - font-size: 13px; /* Уменьшено еще на 10% (14.4px * 0.9) */ - } - } - - @media (max-width: 480px) { - label { - font-size: 0.73rem; /* Уменьшено еще на 10% (0.81rem * 0.9) */ - } - - input { - height: 2.3rem; /* Уменьшено еще на 10% (2.52rem * 0.9) */ - min-height: 42px; /* Уменьшено еще на 10% (47px * 0.9) */ - padding: 0 0.6rem; /* Уменьшено еще на 10% (0.675rem * 0.9) */ - } - } -`; - -export default TextInput; diff --git a/web/src/components/common/icons/icons.jsx b/web/src/components/common/icons/icons.jsx index 190fc1d..307d430 100644 --- a/web/src/components/common/icons/icons.jsx +++ b/web/src/components/common/icons/icons.jsx @@ -108,6 +108,23 @@ export const AddColumn = styled.div` background-position: center; `; +export const Project = styled.div` + background-image: url('images/Project.svg'); + width: 1.25rem; + height: 1.25rem; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +`; +export const Program = styled.div` + background-image: url('images/Program.svg'); + width: 1.25rem; + height: 1.25rem; + background-repeat: no-repeat; + background-size: contain; + background-position: center; +`; + export const DeleteColumn = styled.div` background-image: url('images/RemoveColumn.svg'); width: 1.25rem; diff --git a/web/src/index.css b/web/src/index.css index 0aa8b73..bbf9f13 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -11,6 +11,7 @@ body { margin: 0; background: #fcfcfd; color: #171a1c; + /* color: green; */ font-family: -apple-system, Segoe UI,