Compare commits
2 Commits
653ede59c1
...
8c978b4e33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c978b4e33 | ||
|
|
d257c3f1c1 |
5
web/public/images/Program.svg
Normal file
5
web/public/images/Program.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 21V7C8 6.07003 8 5.60504 8.10222 5.22354C8.37962 4.18827 9.18827 3.37962 10.2235 3.10222C10.605 3 11.07 3 12 3C12.93 3 13.395 3 13.7765 3.10222C14.8117 3.37962 15.6204 4.18827 15.8978 5.22354C16 5.60504 16 6.07003 16 7V21M5.2 21H18.8C19.9201 21 20.4802 21 20.908 20.782C21.2843 20.5903 21.5903 20.2843 21.782 19.908C22 19.4802 22 18.9201 22 17.8V10.2C22 9.07989 22 8.51984 21.782 8.09202C21.5903 7.71569 21.2843 7.40973 20.908 7.21799C20.4802 7 19.9201 7 18.8 7H5.2C4.07989 7 3.51984 7 3.09202 7.21799C2.71569 7.40973 2.40973 7.71569 2.21799 8.09202C2 8.51984 2 9.07989 2 10.2V17.8C2 18.9201 2 19.4802 2.21799 19.908C2.40973 20.2843 2.71569 20.5903 3.09202 20.782C3.51984 21 4.0799 21 5.2 21Z"
|
||||
stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 900 B |
5
web/public/images/Project.svg
Normal file
5
web/public/images/Project.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z"
|
||||
stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 704 B |
@ -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 (
|
||||
<ModalBackdrop isOpen={isOpen} onClick={handleBackdropClick}>
|
||||
<ModalContainer height={'max-content'} >
|
||||
<HeaderContainer>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<ModalTitle>{title}</ModalTitle>
|
||||
</Stack>
|
||||
<IconButton size="small" onClick={handleCancel}>
|
||||
<Cancel />
|
||||
</IconButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<Box>
|
||||
<Divider sx={{ marginBottom: '0.75rem' }} />
|
||||
|
||||
|
||||
{/* <Input */}
|
||||
<StyledTextField label={'Название программы'} onChange={(e) => setName(e.target.value)}></StyledTextField>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={0.75}
|
||||
sx={{
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '1.5rem',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<DangerOutlinedButton
|
||||
onClick={handleCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Отмена
|
||||
</DangerOutlinedButton>
|
||||
|
||||
<PrimaryButton
|
||||
onClick={handleSave}
|
||||
disabled={!isFormValid || isSaving}
|
||||
loading={isSaving}>
|
||||
Создать
|
||||
</PrimaryButton>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
</ModalContainer>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
};
|
||||
@ -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%',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="default"
|
||||
<DangerOutlinedButton
|
||||
onClick={handleCancel}
|
||||
style={{ height: '2.5rem' }}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
</DangerOutlinedButton>
|
||||
|
||||
<PrimaryButton
|
||||
onClick={handleSave}
|
||||
style={{ height: '2.5rem' }}
|
||||
disabled={!isFormValid || isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
loading={isSaving}>
|
||||
Выбрать
|
||||
</Button>
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ModalContainer>
|
||||
|
||||
@ -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%',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="default"
|
||||
<DangerOutlinedButton
|
||||
onClick={handleCancel}
|
||||
style={{ height: '2.5rem' }}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
</DangerOutlinedButton>
|
||||
|
||||
<PrimaryButton
|
||||
onClick={handleSave}
|
||||
style={{ height: '2.5rem' }}
|
||||
disabled={!isFormValid || isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
loading={isSaving}>
|
||||
Выбрать
|
||||
</Button>
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ModalContainer>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
<div style={tableWrapperStyle}>
|
||||
@ -479,6 +564,20 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
onSelect={handleAddVspRow}
|
||||
title="Выбор ВСП"
|
||||
/>
|
||||
<SelectExpenseItemModal
|
||||
isOpen={isOpenModalSelectExpenseItem}
|
||||
onClose={() => setIsOpenModalSelectExpenseItem(false)}
|
||||
formId={formId}
|
||||
onSelect={handleAddExpenseItemRow}
|
||||
title="Добавление строки"
|
||||
sheet={sheetName}
|
||||
/>
|
||||
<CreateProgramModal
|
||||
isOpen={isOpenModalCreateProgram}
|
||||
onClose={() => setIsOpenModalCreateProgram(false)}
|
||||
title={isProgramCreate ? "Создание программы" : "Создание проекта"}
|
||||
onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = ({
|
||||
<RemoveLine />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* проверяем на 4 форму */}
|
||||
{formType == FORM_TYPE_OPTIONS[3] && <>
|
||||
|
||||
<Tooltip title="Добавить программу">
|
||||
<IconButton onClick={onAddProgram} variant="outlined">
|
||||
<Plus />
|
||||
<Program />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Добавить проект">
|
||||
<IconButton onClick={onAddProject} variant="outlined">
|
||||
<Plus />
|
||||
<Project />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>}
|
||||
</GroupByObject>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@ -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 = [];
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
20
web/src/components/common/StyledTextField.jsx
Normal file
20
web/src/components/common/StyledTextField.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
import {
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
export const StyledTextField = ({ error, helperText, disabled, ...props }) => (
|
||||
<TextField
|
||||
className="fullWidth"
|
||||
size="small"
|
||||
error={!!error}
|
||||
helperText={helperText || error}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
width: '100%',
|
||||
'& .MuiInputBase-root': {
|
||||
height: '2.69rem',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -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 (
|
||||
<TextInputDiv>
|
||||
<label>
|
||||
{label}
|
||||
<input
|
||||
type={type}
|
||||
name={name}
|
||||
onChange={(e) => {
|
||||
setCurValue(e.target.value);
|
||||
if (!isFocused) {
|
||||
handleChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
handleChange(cur_value);
|
||||
setIsFocused(false);
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
value={cur_value}
|
||||
autoComplete={autocomplete}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</label>
|
||||
{tooltipText !== '' && <Tooltip tooltipText={tooltipText} />}
|
||||
</TextInputDiv>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@ -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;
|
||||
|
||||
@ -11,6 +11,7 @@ body {
|
||||
margin: 0;
|
||||
background: #fcfcfd;
|
||||
color: #171a1c;
|
||||
/* color: green; */
|
||||
font-family:
|
||||
-apple-system,
|
||||
Segoe UI,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user