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
new file mode 100644
index 0000000..7f23978
--- /dev/null
+++ b/web/public/images/Project.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
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/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
new file mode 100644
index 0000000..8be3611
--- /dev/null
+++ b/web/src/components/RealtimeTable/Modals/SelectExpenseItemModal.jsx
@@ -0,0 +1,154 @@
+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';
+
+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/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 78a9586..82edac6 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -24,10 +24,13 @@ 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';
+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);
@@ -40,6 +43,10 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const [editingCell, setEditingCell] = useState(null);
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;
@@ -47,6 +54,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(() => {
@@ -64,8 +77,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
error: wsError,
updateCell: contextUpdateCell,
addRow: contextAddRow,
- deleteRow: contextDeleteRow,
startEditing: contextStartEditing,
+ addProgram: contextAddProgram,
+ addProject: contextAddProject,
} = useRealtime();
const {
@@ -113,6 +127,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setColumnsConfig({ columns: [], colors: {} });
}
};
+
loadConfig();
}, [formType, sheetName]);
@@ -363,11 +378,45 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const handleAddVspRow = useCallback((vsp_id) => {
contextAddRow({ vsp_id: vsp_id });
- }, [rowSelection, table]);
+ }, []);
+
+ const handleAddExpenseItemRow = useCallback((expense_item) => {
+ 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);
- }, [rowSelection, table])
+ }, [])
+
+ 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;
@@ -383,6 +432,39 @@ 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]);
+
+ 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([]);
@@ -409,7 +491,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
onGlobalFilterChange={handleGlobalFilterChange}
sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters}
- onAddRow={isVspAdditionRow ? handleOpenModalSelectVsp : handleAddRow}
+ onAddRow={addRow}
+ onAddProgram={addProgram}
+ onAddProject={addProject}
onDeleteRow={handleDeleteRow}
isLoadingData={isLoadingData}
formId={formId}
@@ -417,6 +501,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
direction={direction}
year={year}
isProject={formType == 'PROJECT'}
+ formType={formType}
/>
@@ -479,6 +564,20 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
onSelect={handleAddVspRow}
title="Выбор ВСП"
/>
+
setIsOpenModalSelectExpenseItem(false)}
+ formId={formId}
+ onSelect={handleAddExpenseItemRow}
+ 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/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/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js
index 58182a8..4028366 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: "project_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: "program_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..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) => {
@@ -19,6 +24,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
error: wsError,
subscribeToCellUpdates,
subscribeToRowAdds,
+ subscribeToProgramAdds,
+ subscribeToProjectAdds,
subscribeToRowDeletes,
subscribeToErrors,
subscribeToCellEditStart,
@@ -182,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) => {
@@ -201,7 +208,49 @@ 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[1];
+ const program_id = data.data.program_id;
+ const [updatedCells, newRow] = updates.reduce(
+ (res, row) => {
+ if (row[0] === "ITEM") {
+ 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 = insertProjectRow(updateData, newRow, program_id);
+ return newData;
+ });
+ });
+
+ const unsubscribeProgramAdds = subscribeToProgramAdds((data) => {
+ const newRow = data.result[1][0];
+ setData((prevData) => {
+ 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;
});
});
@@ -255,6 +304,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
return () => {
unsubscribeCellUpdates();
unsubscribeRowAdds();
+ unsubscribeProgramAdds();
+ unsubscribeProjectAdds();
unsubscribeRowDeletes();
unsubscribeErrors();
unsubscribeCellEditStart();
@@ -268,6 +319,8 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
subscribeToErrors,
subscribeToCellEditStart,
subscribeToCellEditEnd,
+ subscribeToProgramAdds,
+ subscribeToProjectAdds,
formatedToSubRows,
]);
diff --git a/web/src/components/RealtimeTable/tableStyles.css b/web/src/components/RealtimeTable/tableStyles.css
deleted file mode 100644
index e69de29..0000000
diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js
index 6609f72..2208601 100644
--- a/web/src/components/RealtimeTable/utils/cellUtils.js
+++ b/web/src/components/RealtimeTable/utils/cellUtils.js
@@ -1,4 +1,4 @@
-import { getDataRowId } from "./rowUtils";
+import { getDataRowId, getParentId } from "./rowUtils";
export function setNewValueToData(data, colId, rowId, value) {
let targetObject = data;
@@ -44,7 +44,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") {
@@ -116,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 = {
@@ -128,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;
}
@@ -145,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 = [];
}
diff --git a/web/src/components/RealtimeTable/utils/rowUtils.js b/web/src/components/RealtimeTable/utils/rowUtils.js
index 1f43dc3..30fe697 100644
--- a/web/src/components/RealtimeTable/utils/rowUtils.js
+++ b/web/src/components/RealtimeTable/utils/rowUtils.js
@@ -17,3 +17,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,