WIP: vsp-choose-form2: выбор ВСП на фронте #72
@ -1,4 +1,4 @@
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||
|
||||
// Константы вне компонента
|
||||
@ -68,25 +68,27 @@ const INTEGER_COLUMN_IDS = new Set([
|
||||
const getColorBrightness = (hexColor) => {
|
||||
if (!hexColor) return 255;
|
||||
const color = hexColor.replace('#', '');
|
||||
let r, g, b;
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
if (color.length === 3) {
|
||||
r = parseInt(color[0] + color[0], 16);
|
||||
g = parseInt(color[1] + color[1], 16);
|
||||
b = parseInt(color[2] + color[2], 16);
|
||||
r = Number.parseInt(color[0] + color[0], 16);
|
||||
g = Number.parseInt(color[1] + color[1], 16);
|
||||
b = Number.parseInt(color[2] + color[2], 16);
|
||||
} else if (color.length === 6) {
|
||||
r = parseInt(color.substring(0, 2), 16);
|
||||
g = parseInt(color.substring(2, 4), 16);
|
||||
b = parseInt(color.substring(4, 6), 16);
|
||||
r = Number.parseInt(color.substring(0, 2), 16);
|
||||
g = Number.parseInt(color.substring(2, 4), 16);
|
||||
b = Number.parseInt(color.substring(4, 6), 16);
|
||||
} else {
|
||||
return 255;
|
||||
}
|
||||
return (0.299 * r + 0.587 * g + 0.114 * b);
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
};
|
||||
|
||||
const formatNumber = (value, asInteger = false) => {
|
||||
if (value === null || value === undefined || value === '') return String(value || '');
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) return String(value);
|
||||
if (Number.isNaN(num)) return String(value);
|
||||
return num.toLocaleString('ru-RU', {
|
||||
minimumFractionDigits: asInteger ? 0 : 1,
|
||||
maximumFractionDigits: asInteger ? 0 : 1,
|
||||
@ -100,7 +102,7 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
||||
if (cleanQueries.length === 0) return displayValue;
|
||||
|
||||
const searchStr = String(originalValue ?? displayValue);
|
||||
const escapedQueries = cleanQueries.map(q => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
||||
|
||||
if (!regex.test(searchStr)) return displayValue;
|
||||
@ -108,7 +110,9 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
||||
const parts = searchStr.split(regex);
|
||||
return parts.map((part, index) => {
|
||||
if (!regex.test(part)) return part;
|
||||
return React.createElement('mark', {
|
||||
return React.createElement(
|
||||
'mark',
|
||||
{
|
||||
key: index,
|
||||
style: {
|
||||
backgroundColor: '#ffeb3b',
|
||||
@ -117,7 +121,9 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
|
||||
padding: '0 2px',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
}, part);
|
||||
},
|
||||
part,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -134,24 +140,33 @@ const CellComponent = ({
|
||||
isUpdating,
|
||||
onCellNumberClick,
|
||||
isInvalid,
|
||||
vspOptions,
|
||||
}) => {
|
||||
const { lockedCells } = useRealtime();
|
||||
const cellKey = `${row.id}_${column.id}`;
|
||||
const isLocked = lockedCells.includes(cellKey);
|
||||
|
||||
// Мемоизация значения
|
||||
const isVspDropdown = column?.columnDef?.editType === 'vsp_dropdown';
|
||||
|
||||
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
||||
const value = cell.getValue();
|
||||
const isNum = !isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
||||
const isNum = !Number.isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
||||
let display = String(value || '');
|
||||
|
||||
if (isVspDropdown && value != null) {
|
||||
const vsp = vspOptions?.find((v) => v.id === Number(value));
|
||||
if (vsp) {
|
||||
return { rawValue: value, displayValue: vsp.registration_number, isNumeric: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (isNum) {
|
||||
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
||||
display = formatNumber(value, asInteger);
|
||||
}
|
||||
|
||||
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
||||
}, [cell, column.id]);
|
||||
}, [cell, column.id, isVspDropdown, vspOptions]);
|
||||
|
||||
const highlightedContent = useMemo(() => {
|
||||
const searchQueries = [globalFilter, columnFilter];
|
||||
@ -164,13 +179,16 @@ const CellComponent = ({
|
||||
return brightness < 128 ? '#ffffff' : '#000000';
|
||||
}, [backgroundColor, isLocked]);
|
||||
|
||||
const cellStyles = useMemo(() => ({
|
||||
const cellStyles = useMemo(
|
||||
() => ({
|
||||
...BASE_CELL_STYLES,
|
||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||
color: textColor,
|
||||
...(isLocked ? LOCKED_STYLES : {}),
|
||||
...(isInvalid ? INVALID_STYLES : {}),
|
||||
}), [backgroundColor, isInvalid, isLocked, textColor]);
|
||||
}),
|
||||
[backgroundColor, isInvalid, isLocked, textColor],
|
||||
);
|
||||
|
||||
const lockBadge = useMemo(() => {
|
||||
if (!isLocked) return null;
|
||||
@ -184,11 +202,7 @@ const CellComponent = ({
|
||||
}, [isLocked, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cell"
|
||||
style={cellStyles}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className='cell' style={cellStyles} onClick={handleClick}>
|
||||
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
||||
{lockBadge}
|
||||
</div>
|
||||
@ -196,7 +210,6 @@ const CellComponent = ({
|
||||
};
|
||||
|
||||
const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
||||
// Возвращаем true если пропсы равны (не нужно перерендеривать)
|
||||
return (
|
||||
prevProps.cell.getValue() === nextProps.cell.getValue() &&
|
||||
prevProps.row.id === nextProps.row.id &&
|
||||
@ -207,7 +220,8 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
||||
prevProps.isInvalid === nextProps.isInvalid &&
|
||||
prevProps.isEditable === nextProps.isEditable &&
|
||||
prevProps.isUpdating === nextProps.isUpdating &&
|
||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick &&
|
||||
prevProps.vspOptions === nextProps.vspOptions
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
import { FormControl, MenuItem, Select } from '@mui/material';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||
|
||||
const EDITOR_STYLES = {
|
||||
position: 'absolute',
|
||||
zIndex: 12,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table }) => {
|
||||
const { endEditing: contextEndEditing, vspOptions } = useRealtime();
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState(() => value ?? '');
|
||||
const [position, setPosition] = useState();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const refFinished = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setOpen(true);
|
||||
}, 80);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!refCell?.current) return;
|
||||
|
||||
const td = refCell.current.offsetParent;
|
||||
const tr = td?.offsetParent;
|
||||
|
||||
if (td && tr) {
|
||||
setPosition({
|
||||
left: td.offsetLeft,
|
||||
width: td.offsetWidth,
|
||||
translateY: tr.style.transform || '',
|
||||
});
|
||||
}
|
||||
}, [refCell]);
|
||||
|
||||
const finishEditing = useCallback(() => {
|
||||
if (refFinished.current) return;
|
||||
refFinished.current = true;
|
||||
table.setEditingCell(null);
|
||||
const editingCell = table.getState().editingCell;
|
||||
if (editingCell) {
|
||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||
}
|
||||
}, [table, contextEndEditing]);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(e) => {
|
||||
const val = e.target.value;
|
||||
if (val == null || val === '') return;
|
||||
refFinished.current = true;
|
||||
setSelectedValue(val);
|
||||
onChange?.(val);
|
||||
const editingCell = table.getState().editingCell;
|
||||
if (editingCell) {
|
||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||
}
|
||||
},
|
||||
[onChange, table, contextEndEditing],
|
||||
);
|
||||
|
||||
const handleSelectClose = useCallback(
|
||||
(_event, reason) => {
|
||||
if (reason === 'selectOption') return;
|
||||
finishEditing();
|
||||
},
|
||||
[finishEditing],
|
||||
);
|
||||
|
||||
const editorStyles = useMemo(
|
||||
() => ({
|
||||
...EDITOR_STYLES,
|
||||
left: position?.left || 0,
|
||||
width: position?.width || 0,
|
||||
transform: position?.translateY || 0,
|
||||
}),
|
||||
[position],
|
||||
);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
{position && (
|
||||
<div style={editorStyles}>
|
||||
<FormControl fullWidth size='small'>
|
||||
<Select
|
||||
value={selectedValue}
|
||||
onChange={handleSelectChange}
|
||||
open={open}
|
||||
onClose={handleSelectClose}
|
||||
displayEmpty
|
||||
disabled={disabled}
|
||||
MenuProps={{
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: 'left' },
|
||||
}}>
|
||||
<MenuItem value='' disabled>
|
||||
Выберите ВСП
|
||||
</MenuItem>
|
||||
{vspOptions.map((option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.registration_number}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
VspDropdownEditCell.displayName = 'VspDropdownEditCell';
|
||||
|
||||
export default VspDropdownEditCell;
|
||||
@ -1,58 +1,52 @@
|
||||
import { MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
useMaterialReactTable,
|
||||
MaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
|
||||
import useRealtimeData from './hooks/useRealtimeData';
|
||||
import { getTableColumns } from './tableColumns';
|
||||
import { Cell, EditCell } from './index';
|
||||
import { TableHead } from './TableHead/TableHead';
|
||||
import SettingsPanel from './SettingPanel/SettingPanel';
|
||||
import { ColumnSelectionOverlay } from './ColumnSelectionOverlay/ColumnSelectionOverlay';
|
||||
import SettingsPanel from './SettingPanel/SettingPanel';
|
||||
import { TableHead } from './TableHead/TableHead';
|
||||
import useRealtimeData from './hooks/useRealtimeData';
|
||||
import { Cell, EditCell } from './index';
|
||||
import { getTableColumns } from './tableColumns';
|
||||
|
||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||
import { useTableScale } from './hooks/useTableScale';
|
||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||
import { useValidationRules } from './hooks/useValidationRules';
|
||||
import {
|
||||
BASE_TABLE_CONFIG,
|
||||
getTableBodyCellProps,
|
||||
getTablePaperStyles,
|
||||
TABLE_ROW_HEIGHT,
|
||||
} from './constants/tableConfig';
|
||||
import { useRealtime } from './contexts/RealtimeContext';
|
||||
import { toast } from 'react-toastify';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
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 { toast } from 'react-toastify';
|
||||
import { DictVspApi } from '../../api/dict-vsp';
|
||||
import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
|
||||
import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
|
||||
import { SelectVspModal } from './Modals/SelectVspModal';
|
||||
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
|
||||
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
|
||||
import { useRealtime } from './contexts/RealtimeContext';
|
||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||
import { useTableScale } from './hooks/useTableScale';
|
||||
import { useValidationRules } from './hooks/useValidationRules';
|
||||
import { getRowId } from './utils/rowUtils';
|
||||
|
||||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||
const {
|
||||
data,
|
||||
setData,
|
||||
isLoading: isTableLoading,
|
||||
editingCells: dataEditingCells,
|
||||
} = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||
const [selectedColumnId, setSelectedColumnId] = useState();
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [_isPending, startTransition] = useTransition();
|
||||
const [editingCell, setEditingCell] = useState(null);
|
||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||
const { errors: validationErrors, isCellInvalid } = useValidationRules(
|
||||
data,
|
||||
columnsConfig?.columns,
|
||||
);
|
||||
const { errors: validationErrors, isCellInvalid } = useValidationRules(data, columnsConfig?.columns);
|
||||
|
||||
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 [_isOpenModalCreateProject, _setIsOpenModalCreateProject] = useState(false);
|
||||
const [isProgramCreate, setIsProgramCreate] = useState(false);
|
||||
|
||||
const isVspAdditionRow = useMemo(() => {
|
||||
if (!formType || !sheetName) return false;
|
||||
@ -72,7 +66,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
setGlobalFilter(value);
|
||||
});
|
||||
}, 300),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
@ -87,6 +81,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
startEditing: contextStartEditing,
|
||||
addProgram: contextAddProgram,
|
||||
addProject: contextAddProject,
|
||||
setVspOptions,
|
||||
} = useRealtime();
|
||||
|
||||
const {
|
||||
@ -104,8 +99,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||||
const rowVirtualizerRef = useRef(null);
|
||||
|
||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } =
|
||||
useTableScale();
|
||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale();
|
||||
|
||||
useEffect(() => {
|
||||
rowVirtualizerRef.current?.measure?.();
|
||||
@ -127,8 +121,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||
configCache.current.set(cacheKey, config.config);
|
||||
setColumnsConfig(config.config);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||
} catch (_error) {
|
||||
setColumnsConfig({ columns: [], colors: {} });
|
||||
}
|
||||
};
|
||||
@ -136,15 +129,38 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
loadConfig();
|
||||
}, [formType, sheetName]);
|
||||
|
||||
const handleUpdateCell = useCallback(async (row, column, value) => {
|
||||
useEffect(() => {
|
||||
if (!formId || !columnsConfig?.columns) return;
|
||||
|
||||
const hasVspDropdown = (cols) => {
|
||||
for (const col of cols) {
|
||||
if (col.editType === 'vsp_dropdown') return true;
|
||||
if (col.columns && hasVspDropdown(col.columns)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!hasVspDropdown(columnsConfig.columns)) return;
|
||||
|
||||
DictVspApi.getDropdownVsp({ form_id: formId }).then((data) => {
|
||||
if (data.success) {
|
||||
setVspOptions(data.result);
|
||||
}
|
||||
});
|
||||
}, [formId, columnsConfig, setVspOptions]);
|
||||
|
||||
const handleUpdateCell = useCallback(
|
||||
async (row, column, value) => {
|
||||
return await contextUpdateCell(row, column, value);
|
||||
}, [contextUpdateCell]);
|
||||
},
|
||||
[contextUpdateCell],
|
||||
);
|
||||
|
||||
const handleClickRowCell = useCallback((rowId) => {
|
||||
setRowSelection((prev) => ({
|
||||
[rowId]: !prev[rowId],
|
||||
}));
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const handleColumnSelect = useCallback((columnId) => {
|
||||
setSelectedColumnId((prev) => {
|
||||
@ -168,8 +184,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
return () => document.removeEventListener('mousedown', handleDocumentClick);
|
||||
}, [selectedColumnId]);
|
||||
|
||||
const handleCellUpdateError = useCallback((rowId, columnId, error) => {
|
||||
console.error(`Error updating cell ${rowId}_${columnId}:`, error);
|
||||
const handleCellUpdateError = useCallback((_rowId, _columnId, _error) => {
|
||||
toast.error('Ошибка обновления ячейки');
|
||||
}, []);
|
||||
|
||||
@ -185,17 +200,10 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
onCellUpdateError: handleCellUpdateError,
|
||||
isCellInvalid,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating columns:', error);
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}, [
|
||||
columnsConfig,
|
||||
handleCellUpdateError,
|
||||
handleClickRowCell,
|
||||
handleUpdateCell,
|
||||
isCellInvalid,
|
||||
]);
|
||||
}, [columnsConfig, handleCellUpdateError, handleClickRowCell, handleUpdateCell, isCellInvalid]);
|
||||
|
||||
const selectedColumnIdRef = useRef(selectedColumnId);
|
||||
|
||||
@ -203,10 +211,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
selectedColumnIdRef.current = selectedColumnId;
|
||||
}, [selectedColumnId]);
|
||||
|
||||
const tableMeta = useMemo(() => ({
|
||||
const tableMeta = useMemo(
|
||||
() => ({
|
||||
updateCell: handleUpdateCell,
|
||||
getSelectedColumnId: () => selectedColumnIdRef.current,
|
||||
}), [handleUpdateCell]);
|
||||
}),
|
||||
[handleUpdateCell],
|
||||
);
|
||||
|
||||
const ROW_VIRTUALIZER_OPTIONS = {
|
||||
overscan: 5,
|
||||
@ -255,7 +266,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const column = table.getColumn(colId);
|
||||
return column?.getSize() ?? 150;
|
||||
}
|
||||
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()]
|
||||
const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()];
|
||||
return allCols[index]?.getSize() ?? 150;
|
||||
},
|
||||
}),
|
||||
@ -293,7 +304,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
minHeight: '1px',
|
||||
maxHeight: '1px',
|
||||
visibility: 'hidden',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
muiTablePaperProps: getTablePaperStyles(),
|
||||
@ -313,7 +324,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
});
|
||||
|
||||
// Конфигурация таблицы
|
||||
const tableConfig = useMemo(() => createTableConfig({
|
||||
const tableConfig = useMemo(
|
||||
() =>
|
||||
createTableConfig({
|
||||
columns,
|
||||
data,
|
||||
columnSizing,
|
||||
@ -328,7 +341,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
setColumnPinning,
|
||||
editingCell,
|
||||
contextStartEditing,
|
||||
}), [
|
||||
}),
|
||||
[
|
||||
columns,
|
||||
data,
|
||||
columnSizing,
|
||||
@ -342,11 +356,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
setColumnSizing,
|
||||
setColumnPinning,
|
||||
editingCell,
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable(tableConfig);
|
||||
|
||||
const handleNavigateToColumn = useCallback((columnId) => {
|
||||
const handleNavigateToColumn = useCallback(
|
||||
(columnId) => {
|
||||
if (!columnId) return;
|
||||
|
||||
setSelectedColumnId(columnId);
|
||||
@ -357,9 +373,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const pinnedIds = new Set(columnPinning?.left || []);
|
||||
if (pinnedIds.has(columnId)) return;
|
||||
|
||||
const centerColumns = table
|
||||
.getVisibleLeafColumns()
|
||||
.filter((column) => !pinnedIds.has(column.id));
|
||||
const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id));
|
||||
|
||||
let offset = 0;
|
||||
for (const column of centerColumns) {
|
||||
@ -371,7 +385,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
left: Math.max(0, offset - 40),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [columnPinning, containerRef, table]);
|
||||
},
|
||||
[columnPinning, containerRef, table],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataEditingCells?.line_id) {
|
||||
@ -384,13 +400,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
const row = table.getRow(dataEditingCells.line_id);
|
||||
const cell = row?.getVisibleCells().find(
|
||||
c => c.column.id === dataEditingCells.column
|
||||
);
|
||||
const cell = row?.getVisibleCells().find((c) => c.column.id === dataEditingCells.column);
|
||||
if (cell) setEditingCell(cell);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} catch (_error) {}
|
||||
});
|
||||
}, [dataEditingCells, editingCell, table]);
|
||||
|
||||
@ -406,46 +418,52 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const handleAddRow = useCallback(() => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const selectedRows = allRows.filter((row) => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error('Выделите строку для вставки');
|
||||
return;
|
||||
}
|
||||
const row = selectedRows[0];
|
||||
const expense_item_id = row.original.data.header.expense_item_id;
|
||||
contextAddRow({ expense_item_id: expense_item_id })
|
||||
}, [rowSelection, table])
|
||||
contextAddRow({ expense_item_id: expense_item_id });
|
||||
}, [rowSelection, table]);
|
||||
|
||||
const handleAddVspRow = useCallback((vsp_id) => {
|
||||
contextAddRow({ vsp_id: vsp_id });
|
||||
}, []);
|
||||
|
||||
const handleAddExpenseItemRow = useCallback((expense_item) => {
|
||||
const handleAddExpenseItemRow = useCallback(
|
||||
(expense_item) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
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]);
|
||||
},
|
||||
[rowSelection],
|
||||
);
|
||||
|
||||
const handleAddProgramRow = useCallback((name) => {
|
||||
contextAddProgram({ name: name });
|
||||
}, []);
|
||||
|
||||
const handleAddProjectRow = useCallback((name) => {
|
||||
const handleAddProjectRow = useCallback(
|
||||
(name) => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const selectedRows = allRows.filter((row) => rowSelection[row.id]);
|
||||
const row = selectedRows[0];
|
||||
contextAddProject({ name: name, program_id: row.original.data.header.program_id });
|
||||
}, [rowSelection]);
|
||||
},
|
||||
[rowSelection],
|
||||
);
|
||||
|
||||
const handleOpenModalSelectVsp = useCallback(() => {
|
||||
setIsOpenModalSelectVsp(true);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const handleOpenModalSelectExpenseItem = useCallback(() => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const selectedRows = allRows.filter((row) => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error('Выделите строку для вставки');
|
||||
return;
|
||||
@ -456,12 +474,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
return;
|
||||
}
|
||||
setIsOpenModalSelectExpenseItem(true);
|
||||
}, [rowSelection])
|
||||
}, [rowSelection]);
|
||||
|
||||
const handleDeleteRow = useCallback(() => {
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const selectedRows = allRows.filter((row) => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error('Выделите строку для удаления');
|
||||
return;
|
||||
@ -470,10 +488,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const rowId = getRowId(row);
|
||||
contextDeleteRow(rowId);
|
||||
setRowSelection({});
|
||||
}, [rowSelection, table])
|
||||
}, [rowSelection, table]);
|
||||
|
||||
const addRow = useCallback(() => {
|
||||
|
||||
if (isVspAdditionRow) {
|
||||
return handleOpenModalSelectVsp();
|
||||
}
|
||||
@ -486,13 +503,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
const addProgram = useCallback(() => {
|
||||
setIsProgramCreate(true);
|
||||
setIsOpenModalCreateProgram(true);
|
||||
}, [setIsOpenModalCreateProgram])
|
||||
}, [setIsOpenModalCreateProgram]);
|
||||
|
||||
const addProject = useCallback(() => {
|
||||
setIsProgramCreate(false);
|
||||
const allRows = table.getRowModel().rows;
|
||||
|
||||
const selectedRows = allRows.filter(row => rowSelection[row.id]);
|
||||
const selectedRows = allRows.filter((row) => rowSelection[row.id]);
|
||||
if (selectedRows.length === 0) {
|
||||
toast.error('Выделите строку для вставки');
|
||||
return;
|
||||
@ -503,7 +520,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
return;
|
||||
}
|
||||
setIsOpenModalCreateProgram(true);
|
||||
}, [setIsOpenModalCreateProgram, rowSelection])
|
||||
}, [setIsOpenModalCreateProgram, rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -540,7 +557,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
sheetName={sheetName}
|
||||
direction={direction}
|
||||
year={year}
|
||||
isProject={formType == 'PROJECT'}
|
||||
isProject={formType === 'PROJECT'}
|
||||
formType={formType}
|
||||
validationErrors={validationErrors}
|
||||
onNavigateToColumn={handleNavigateToColumn}
|
||||
@ -555,8 +572,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<MaterialReactTable table={table} />
|
||||
|
||||
<ColumnSelectionOverlay
|
||||
@ -578,8 +594,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)}
|
||||
@ -604,20 +619,20 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
onClose={() => setIsOpenModalSelectVsp(false)}
|
||||
formId={formId}
|
||||
onSelect={handleAddVspRow}
|
||||
title="Выбор ВСП"
|
||||
title='Выбор ВСП'
|
||||
/>
|
||||
<SelectExpenseItemModal
|
||||
isOpen={isOpenModalSelectExpenseItem}
|
||||
onClose={() => setIsOpenModalSelectExpenseItem(false)}
|
||||
formId={formId}
|
||||
onSelect={handleAddExpenseItemRow}
|
||||
title="Добавление строки"
|
||||
title='Добавление строки'
|
||||
sheet={sheetName}
|
||||
/>
|
||||
<CreateProgramModal
|
||||
isOpen={isOpenModalCreateProgram}
|
||||
onClose={() => setIsOpenModalCreateProgram(false)}
|
||||
title={isProgramCreate ? "Создание программы" : "Создание проекта"}
|
||||
title={isProgramCreate ? 'Создание программы' : 'Создание проекта'}
|
||||
onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow}
|
||||
/>
|
||||
</>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,14 @@ export const config = {
|
||||
color_type: orangeColumn,
|
||||
accessorKey: "data.header.name",
|
||||
},
|
||||
"data.header.vsp_id": {
|
||||
color_type: orangeColumn,
|
||||
accessorKey: "data.header.vsp_id",
|
||||
},
|
||||
"data.header.vsp_address": {
|
||||
color_type: orangeColumn,
|
||||
accessorKey: "data.header.vsp_address",
|
||||
},
|
||||
"data.header.internal_order": {
|
||||
color_type: orangeColumn,
|
||||
accessorKey: "data.header.internal_order",
|
||||
@ -830,6 +838,33 @@ export const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "ВСП РФ",
|
||||
accessorKey: "field_vsp_rf",
|
||||
columns: [
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.vsp_id",
|
||||
columnLetter: "G",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
editType: "vsp_dropdown",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "Адрес ВСП",
|
||||
accessorKey: "field_adres_vsp",
|
||||
columns: [
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.vsp_address",
|
||||
columnLetter: "G1",
|
||||
size: 220,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "Внутренний заказ",
|
||||
accessorKey: "field_vnutrenniy_zakaz",
|
||||
@ -837,7 +872,7 @@ export const config = {
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.internal_order",
|
||||
columnLetter: "G",
|
||||
columnLetter: "H",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
},
|
||||
|
||||
@ -25,6 +25,14 @@ export const config = {
|
||||
color_type: blueColumn,
|
||||
accessorKey: "data.header.name",
|
||||
},
|
||||
"data.header.vsp_id": {
|
||||
color_type: blueColumn,
|
||||
accessorKey: "data.header.vsp_id",
|
||||
},
|
||||
"data.header.vsp_address": {
|
||||
color_type: blueColumn,
|
||||
accessorKey: "data.header.vsp_address",
|
||||
},
|
||||
"data.header.internal_order": {
|
||||
color_type: blueColumn,
|
||||
accessorKey: "data.header.internal_order",
|
||||
@ -830,6 +838,33 @@ export const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "ВСП РФ",
|
||||
accessorKey: "field_vsp_rf",
|
||||
columns: [
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.vsp_id",
|
||||
columnLetter: "G",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
editType: "vsp_dropdown",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "Адрес ВСП",
|
||||
accessorKey: "field_adres_vsp",
|
||||
columns: [
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.vsp_address",
|
||||
columnLetter: "G1",
|
||||
size: 220,
|
||||
filterFn: "contains",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
header: "Внутренний заказ",
|
||||
accessorKey: "field_vnutrenniy_zakaz",
|
||||
@ -837,7 +872,7 @@ export const config = {
|
||||
{
|
||||
header: "",
|
||||
accessorKey: "data.header.internal_order",
|
||||
columnLetter: "G",
|
||||
columnLetter: "H",
|
||||
size: 150,
|
||||
filterFn: "contains",
|
||||
},
|
||||
|
||||
@ -1,114 +1,88 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import { toast } from "react-toastify";
|
||||
import { getRowId } from "../utils/rowUtils";
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { getRowId } from '../utils/rowUtils';
|
||||
|
||||
const RealtimeContext = createContext(null);
|
||||
|
||||
export const useRealtime = () => {
|
||||
const context = useContext(RealtimeContext);
|
||||
if (!context) {
|
||||
throw new Error("useRealtime must be used within RealtimeProvider");
|
||||
throw new Error('useRealtime must be used within RealtimeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const RealtimeProvider = ({
|
||||
children,
|
||||
formId,
|
||||
sheetName,
|
||||
direction,
|
||||
year,
|
||||
userId,
|
||||
isProject = false,
|
||||
}) => {
|
||||
export const RealtimeProvider = ({ children, formId, sheetName, direction, year, userId, isProject = false }) => {
|
||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||
const wsUrl =
|
||||
`${(
|
||||
process.env.REACT_APP_API_URL ||
|
||||
process.env.REACT_APP_API_URL ||
|
||||
process.env.REACT_APP_ROOT_PATH ||
|
||||
"ws://localhost:8000"
|
||||
).replace(/^http/, "ws")}` +
|
||||
(!isProject
|
||||
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`
|
||||
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`);
|
||||
const wsUrl = `${(
|
||||
process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || 'ws://localhost:8000'
|
||||
).replace(/^http/, 'ws')}${
|
||||
!isProject
|
||||
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`
|
||||
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
|
||||
}`;
|
||||
|
||||
//для ячеек которые редактируются другими пользователями
|
||||
const [lockedCells, setLockedCells] = useState([]);
|
||||
const [vspOptions, setVspOptions] = useState([]);
|
||||
const handleMessage = useCallback((data) => {
|
||||
// Обработка ошибок
|
||||
if (data.error) {
|
||||
console.log(data.error.message);
|
||||
toast.error("Server error:" + data.error.message || "");
|
||||
toast.error(`Server error:${data.error.message}` || '');
|
||||
onErrorRef.current?.(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.event) {
|
||||
case "cell_edit_start":
|
||||
console.log("Cell edit started:", data);
|
||||
case 'cell_edit_start':
|
||||
if (onCellEditStartRef.current) {
|
||||
onCellEditStartRef.current(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "cell_edit_end":
|
||||
console.log("Cell edit ended:", data);
|
||||
case 'cell_edit_end':
|
||||
if (onCellEditEndRef.current) {
|
||||
onCellEditEndRef.current(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "cell_updated":
|
||||
case 'cell_updated':
|
||||
if (data.result && onCellUpdateRef.current) {
|
||||
const updatedCells = Array.isArray(data.result)
|
||||
? data.result
|
||||
: [data.result];
|
||||
const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
|
||||
onCellUpdateRef.current(updatedCells);
|
||||
}
|
||||
break;
|
||||
|
||||
case "row_added":
|
||||
case 'row_added':
|
||||
if (data.result && onRowAddRef.current) {
|
||||
onRowAddRef.current(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "row_deleted":
|
||||
case 'row_deleted':
|
||||
if (data.result && onRowDeleteRef.current) {
|
||||
onRowDeleteRef.current(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "program_added":
|
||||
console.log("Program added:", data);
|
||||
case 'program_added':
|
||||
if (data.result && onProgramAddRef.current) {
|
||||
onProgramAddRef.current(data);
|
||||
}
|
||||
break;
|
||||
|
||||
case "project_added":
|
||||
console.log("Project added:", data);
|
||||
case 'project_added':
|
||||
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);
|
||||
@ -120,21 +94,19 @@ export const RealtimeProvider = ({
|
||||
const onCellEditStartRef = useRef(null);
|
||||
const onCellEditEndRef = useRef(null);
|
||||
const authAttemptedRef = useRef(false);
|
||||
const reconnectTimerRef = useRef(null);
|
||||
const _reconnectTimerRef = useRef(null);
|
||||
|
||||
// Функция для получения токена из localStorage
|
||||
const getAccessToken = useCallback(() => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
if (!token) {
|
||||
console.warn("No access token found in localStorage");
|
||||
return null;
|
||||
}
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error("Error reading token from localStorage:", error);
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
@ -142,13 +114,12 @@ export const RealtimeProvider = ({
|
||||
const sendLogin = useCallback(() => {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
console.error("Cannot login: no token available");
|
||||
onErrorRef.current?.("Токен авторизации не найден");
|
||||
onErrorRef.current?.('Токен авторизации не найден');
|
||||
return false;
|
||||
}
|
||||
|
||||
const loginMessage = {
|
||||
event: "user_login",
|
||||
event: 'user_login',
|
||||
data: { token },
|
||||
};
|
||||
|
||||
@ -158,7 +129,6 @@ export const RealtimeProvider = ({
|
||||
useEffect(() => {
|
||||
if (isConnected && !authAttemptedRef.current) {
|
||||
authAttemptedRef.current = true;
|
||||
console.log("WebSocket connected, sending login...");
|
||||
setTimeout(() => {
|
||||
sendLogin();
|
||||
}, 100);
|
||||
@ -188,10 +158,10 @@ export const RealtimeProvider = ({
|
||||
const startEditing = useCallback(
|
||||
async (row, column) => {
|
||||
const columnId = column.id;
|
||||
const rowId = row.id;
|
||||
const _rowId = row.id;
|
||||
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("WebSocket не подключен");
|
||||
onErrorRef.current?.('WebSocket не подключен');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -199,7 +169,7 @@ export const RealtimeProvider = ({
|
||||
const colId = columnId.slice(5); // убираем префикс "data."
|
||||
|
||||
const message = {
|
||||
event: "cell_edit_start",
|
||||
event: 'cell_edit_start',
|
||||
data: {
|
||||
line_id: lineId,
|
||||
column: colId,
|
||||
@ -220,10 +190,10 @@ export const RealtimeProvider = ({
|
||||
const endEditing = useCallback(
|
||||
async (row, column) => {
|
||||
const columnId = column.id;
|
||||
const rowId = row.id;
|
||||
const _rowId = row.id;
|
||||
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("WebSocket не подключен");
|
||||
onErrorRef.current?.('WebSocket не подключен');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -231,7 +201,7 @@ export const RealtimeProvider = ({
|
||||
const colId = columnId.slice(5);
|
||||
|
||||
const message = {
|
||||
event: "cell_edit_end",
|
||||
event: 'cell_edit_end',
|
||||
data: {
|
||||
line_id: lineId,
|
||||
column: colId,
|
||||
@ -251,16 +221,16 @@ export const RealtimeProvider = ({
|
||||
const updateCell = useCallback(
|
||||
async (row, column, value) => {
|
||||
const columnId = column.id;
|
||||
const rowId = row.id;
|
||||
const _rowId = row.id;
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("WebSocket не подключен");
|
||||
onErrorRef.current?.('WebSocket не подключен');
|
||||
return false;
|
||||
}
|
||||
|
||||
const lineId = getRowId(row);
|
||||
const colId = columnId.slice(5);
|
||||
const message = {
|
||||
event: "cell_updated",
|
||||
event: 'cell_updated',
|
||||
data: {
|
||||
line_id: lineId,
|
||||
line_id_code: row.id,
|
||||
@ -282,12 +252,12 @@ export const RealtimeProvider = ({
|
||||
const addRow = useCallback(
|
||||
async (rowData) => {
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
onErrorRef.current?.('Нет подключения или авторизации');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "row_added",
|
||||
event: 'row_added',
|
||||
data: rowData,
|
||||
};
|
||||
addCommonField(message);
|
||||
@ -300,12 +270,12 @@ export const RealtimeProvider = ({
|
||||
const addProject = useCallback(
|
||||
async (rowData) => {
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
onErrorRef.current?.('Нет подключения или авторизации');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "project_added",
|
||||
event: 'project_added',
|
||||
data: rowData,
|
||||
};
|
||||
addCommonField(message);
|
||||
@ -318,12 +288,12 @@ export const RealtimeProvider = ({
|
||||
const addProgram = useCallback(
|
||||
async (rowData) => {
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
onErrorRef.current?.('Нет подключения или авторизации');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "program_added",
|
||||
event: 'program_added',
|
||||
data: rowData,
|
||||
};
|
||||
addCommonField(message);
|
||||
@ -336,12 +306,12 @@ export const RealtimeProvider = ({
|
||||
const deleteRow = useCallback(
|
||||
async (rowId) => {
|
||||
if (!isConnectedRef) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
onErrorRef.current?.('Нет подключения или авторизации');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "row_deleted",
|
||||
event: 'row_deleted',
|
||||
data: { row_id: rowId },
|
||||
};
|
||||
addCommonField(message);
|
||||
@ -445,9 +415,10 @@ export const RealtimeProvider = ({
|
||||
retryLogin,
|
||||
lockedCells,
|
||||
setLockedCells,
|
||||
vspOptions,
|
||||
setVspOptions,
|
||||
releaseAllLocks: () => releaseAllLocks(userId),
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{children}
|
||||
</RealtimeContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,22 +1,15 @@
|
||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useParams } from 'react-router';
|
||||
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
||||
import { useRealtime } from './contexts/RealtimeContext';
|
||||
|
||||
const EditCellPortal = React.memo(({
|
||||
cell,
|
||||
table,
|
||||
EditCell,
|
||||
onSaveStart,
|
||||
onSaveEnd,
|
||||
onError,
|
||||
isEditable
|
||||
}) => {
|
||||
const EditCellPortal = React.memo(({ cell, table, EditCell, onSaveStart, onSaveEnd, onError, isEditable }) => {
|
||||
const { formId, formType, sheetName, direction } = useParams();
|
||||
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||||
const ref = useRef(null);
|
||||
const [refTbody, setRefTbody] = useState(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSaving, _setIsSaving] = useState(false);
|
||||
|
||||
const { endEditing: contextEndEditing } = useRealtime();
|
||||
|
||||
@ -29,27 +22,25 @@ const EditCellPortal = React.memo(({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback( (val) => {
|
||||
const handleChange = useCallback(
|
||||
(val) => {
|
||||
table.setEditingCell(null);
|
||||
try {
|
||||
if (table.options.meta?.updateCell) {
|
||||
const success = table.options.meta.updateCell(
|
||||
cell.row,
|
||||
cell.column,
|
||||
val
|
||||
);
|
||||
const success = table.options.meta.updateCell(cell.row, cell.column, val);
|
||||
if (!success) {
|
||||
console.error('Failed to update cell via WebSocket');
|
||||
onError?.('Не удалось сохранить изменение');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating cell:', error);
|
||||
onError?.(error.message);
|
||||
}
|
||||
}, [table, cell, onError]);
|
||||
},
|
||||
[table, cell, onError],
|
||||
);
|
||||
|
||||
const editCellProps = useMemo(() => ({
|
||||
const editCellProps = useMemo(
|
||||
() => ({
|
||||
refCell: ref,
|
||||
cell,
|
||||
value: cell.getValue(),
|
||||
@ -58,15 +49,21 @@ const EditCellPortal = React.memo(({
|
||||
tableId: tableKey,
|
||||
table,
|
||||
cellId: `${cell.row.id}_${cell.column.id}`,
|
||||
}), [cell, isEditable, handleChange, tableKey, table]);
|
||||
}),
|
||||
[cell, isEditable, handleChange, tableKey, table],
|
||||
);
|
||||
|
||||
const editType = cell.column.columnDef?.editType;
|
||||
|
||||
const portalContent = useMemo(() => {
|
||||
if (!refTbody || isSaving) return null;
|
||||
return createPortal(
|
||||
<EditCell {...editCellProps} />,
|
||||
refTbody
|
||||
);
|
||||
}, [refTbody, isSaving, editCellProps, EditCell]);
|
||||
|
||||
if (editType === 'vsp_dropdown') {
|
||||
return createPortal(<VspDropdownEditCell {...editCellProps} />, refTbody);
|
||||
}
|
||||
|
||||
return createPortal(<EditCell {...editCellProps} />, refTbody);
|
||||
}, [refTbody, isSaving, editCellProps, EditCell, editType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -76,15 +73,7 @@ const EditCellPortal = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
export const getTableColumns = ({
|
||||
Cell,
|
||||
EditCell,
|
||||
columnsConfig,
|
||||
onCellUpdateError,
|
||||
onCellNumberClick,
|
||||
isCellInvalid,
|
||||
}) => {
|
||||
export const getTableColumns = ({ Cell, EditCell, columnsConfig, onCellUpdateError, onCellNumberClick, isCellInvalid }) => {
|
||||
const columnColors = { ...columnsConfig.colors };
|
||||
const columns = structuredClone(columnsConfig.columns);
|
||||
|
||||
@ -104,18 +93,20 @@ export const getTableColumns = ({
|
||||
if (rowCache[key]) {
|
||||
const cached = rowCache[key];
|
||||
const currentGlobalFilter = table.getState().globalFilter;
|
||||
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
||||
const currentColumnFilter = table.getState().columnFilters?.find((f) => f.id === column.id)?.value;
|
||||
|
||||
if (cached.globalFilter === currentGlobalFilter &&
|
||||
if (
|
||||
cached.globalFilter === currentGlobalFilter &&
|
||||
cached.columnFilter === currentColumnFilter &&
|
||||
cached.value === value &&
|
||||
cached.isInvalid === isInvalid) {
|
||||
cached.isInvalid === isInvalid
|
||||
) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
const globalFilter = table.getState().globalFilter;
|
||||
const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
||||
const columnFilter = table.getState().columnFilters?.find((f) => f.id === column.id)?.value;
|
||||
|
||||
const props = {
|
||||
globalFilter,
|
||||
@ -132,7 +123,7 @@ export const getTableColumns = ({
|
||||
return props;
|
||||
};
|
||||
|
||||
const getCachedEditProps = (row, column, table) => {
|
||||
const getCachedEditProps = (row, column, _table) => {
|
||||
const key = `${row.id}_${column.id}`;
|
||||
|
||||
if (!editPropsCache.has(row)) {
|
||||
@ -156,14 +147,19 @@ export const getTableColumns = ({
|
||||
const processColumns = (columns) => {
|
||||
columns.forEach((col) => {
|
||||
col.Cell = ({ cell, table, column, row }) => {
|
||||
const { vspOptions } = useRealtime();
|
||||
const props = getCachedCellProps(row, column, table);
|
||||
|
||||
const cellProps = useMemo(() => ({
|
||||
const cellProps = useMemo(
|
||||
() => ({
|
||||
row,
|
||||
column,
|
||||
cell,
|
||||
vspOptions,
|
||||
...props,
|
||||
}), [row.id, column.id, cell.getValue(), props._hash]);
|
||||
}),
|
||||
[row.id, column.id, cell.getValue(), props._hash, vspOptions],
|
||||
);
|
||||
|
||||
return <Cell {...cellProps} />;
|
||||
};
|
||||
@ -171,19 +167,18 @@ export const getTableColumns = ({
|
||||
col.Edit = ({ cell, table, row, column }) => {
|
||||
const props = getCachedEditProps(row, column, table);
|
||||
|
||||
const editComponent = useMemo(() => (
|
||||
<EditCellPortal
|
||||
cell={cell}
|
||||
table={table}
|
||||
EditCell={EditCell}
|
||||
onError={onCellUpdateError}
|
||||
isEditable={props.isEditable}
|
||||
/>
|
||||
), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
|
||||
const editComponent = useMemo(
|
||||
() => <EditCellPortal cell={cell} table={table} EditCell={EditCell} onError={onCellUpdateError} isEditable={props.isEditable} />,
|
||||
[cell, table, EditCell, onCellUpdateError, props.isEditable],
|
||||
);
|
||||
|
||||
return editComponent;
|
||||
};
|
||||
|
||||
if (col.editType === 'vsp_dropdown') {
|
||||
col.enableEditing = (row) => row?.original?.row_type === 'INPUT';
|
||||
}
|
||||
|
||||
if (col.columns?.length) {
|
||||
processColumns(col.columns);
|
||||
}
|
||||
@ -202,7 +197,7 @@ export const getTableColumns = ({
|
||||
onCellNumberClick(row.id);
|
||||
}, [row.id, onCellNumberClick]);
|
||||
|
||||
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
||||
return <button onClick={handleClick}>{`${cell.getValue()} `}</button>;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user