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,