WIP: vsp-choose-form2: выбор ВСП на фронте #72
@ -1,214 +1,228 @@
|
|||||||
import React, { memo, useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback } from 'react';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||||
|
|
||||||
// Константы вне компонента
|
// Константы вне компонента
|
||||||
const BASE_CELL_STYLES = {
|
const BASE_CELL_STYLES = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
border: '2px solid transparent',
|
border: '2px solid transparent',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCKED_STYLES = {
|
const LOCKED_STYLES = {
|
||||||
border: '2px solid #e0e0e0',
|
border: '2px solid #e0e0e0',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
opacity: 0.85,
|
opacity: 0.85,
|
||||||
};
|
};
|
||||||
|
|
||||||
const INVALID_STYLES = {
|
const INVALID_STYLES = {
|
||||||
border: '2px solid #D32F2F',
|
border: '2px solid #D32F2F',
|
||||||
backgroundColor: '#FDEDED',
|
backgroundColor: '#FDEDED',
|
||||||
color: '#C60C0C',
|
color: '#C60C0C',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOCK_BADGE_STYLES = {
|
const LOCK_BADGE_STYLES = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '2px',
|
top: '2px',
|
||||||
right: '2px',
|
right: '2px',
|
||||||
background: 'rgba(0, 0, 0, 0.7)',
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: '9px',
|
fontSize: '9px',
|
||||||
padding: '1px 4px',
|
padding: '1px 4px',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||||
letterSpacing: '0.3px',
|
letterSpacing: '0.3px',
|
||||||
backdropFilter: 'blur(4px)',
|
backdropFilter: 'blur(4px)',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTAINER_STYLES = {
|
const CONTAINER_STYLES = {
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
WebkitLineClamp: 3,
|
WebkitLineClamp: 3,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTEGER_COLUMN_IDS = new Set([
|
const INTEGER_COLUMN_IDS = new Set([
|
||||||
'data.header.num_group',
|
'data.header.num_group',
|
||||||
'data.nomenclature_group_id',
|
'data.nomenclature_group_id',
|
||||||
'data.header.item_id',
|
'data.header.item_id',
|
||||||
'data.article_id',
|
'data.article_id',
|
||||||
'data.code_razdela',
|
'data.code_razdela',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getColorBrightness = (hexColor) => {
|
const getColorBrightness = (hexColor) => {
|
||||||
if (!hexColor) return 255;
|
if (!hexColor) return 255;
|
||||||
const color = hexColor.replace('#', '');
|
const color = hexColor.replace('#', '');
|
||||||
let r, g, b;
|
let r;
|
||||||
if (color.length === 3) {
|
let g;
|
||||||
r = parseInt(color[0] + color[0], 16);
|
let b;
|
||||||
g = parseInt(color[1] + color[1], 16);
|
if (color.length === 3) {
|
||||||
b = parseInt(color[2] + color[2], 16);
|
r = Number.parseInt(color[0] + color[0], 16);
|
||||||
} else if (color.length === 6) {
|
g = Number.parseInt(color[1] + color[1], 16);
|
||||||
r = parseInt(color.substring(0, 2), 16);
|
b = Number.parseInt(color[2] + color[2], 16);
|
||||||
g = parseInt(color.substring(2, 4), 16);
|
} else if (color.length === 6) {
|
||||||
b = parseInt(color.substring(4, 6), 16);
|
r = Number.parseInt(color.substring(0, 2), 16);
|
||||||
} else {
|
g = Number.parseInt(color.substring(2, 4), 16);
|
||||||
return 255;
|
b = Number.parseInt(color.substring(4, 6), 16);
|
||||||
}
|
} else {
|
||||||
return (0.299 * r + 0.587 * g + 0.114 * b);
|
return 255;
|
||||||
|
}
|
||||||
|
return 0.299 * r + 0.587 * g + 0.114 * b;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatNumber = (value, asInteger = false) => {
|
const formatNumber = (value, asInteger = false) => {
|
||||||
if (value === null || value === undefined || value === '') return String(value || '');
|
if (value === null || value === undefined || value === '') return String(value || '');
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (isNaN(num)) return String(value);
|
if (Number.isNaN(num)) return String(value);
|
||||||
return num.toLocaleString('ru-RU', {
|
return num.toLocaleString('ru-RU', {
|
||||||
minimumFractionDigits: asInteger ? 0 : 1,
|
minimumFractionDigits: asInteger ? 0 : 1,
|
||||||
maximumFractionDigits: asInteger ? 0 : 1,
|
maximumFractionDigits: asInteger ? 0 : 1,
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Оптимизированная функция подсветки
|
// Оптимизированная функция подсветки
|
||||||
const createHighlightedContent = (originalValue, displayValue, searchQueries) => {
|
const createHighlightedContent = (originalValue, displayValue, searchQueries) => {
|
||||||
const cleanQueries = searchQueries.filter(Boolean);
|
const cleanQueries = searchQueries.filter(Boolean);
|
||||||
if (cleanQueries.length === 0) return displayValue;
|
if (cleanQueries.length === 0) return displayValue;
|
||||||
|
|
||||||
const searchStr = String(originalValue ?? 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');
|
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
||||||
|
|
||||||
if (!regex.test(searchStr)) return displayValue;
|
if (!regex.test(searchStr)) return displayValue;
|
||||||
|
|
||||||
const parts = searchStr.split(regex);
|
const parts = searchStr.split(regex);
|
||||||
return parts.map((part, index) => {
|
return parts.map((part, index) => {
|
||||||
if (!regex.test(part)) return part;
|
if (!regex.test(part)) return part;
|
||||||
return React.createElement('mark', {
|
return React.createElement(
|
||||||
key: index,
|
'mark',
|
||||||
style: {
|
{
|
||||||
backgroundColor: '#ffeb3b',
|
key: index,
|
||||||
color: '#000',
|
style: {
|
||||||
fontWeight: 'bold',
|
backgroundColor: '#ffeb3b',
|
||||||
padding: '0 2px',
|
color: '#000',
|
||||||
borderRadius: '2px',
|
fontWeight: 'bold',
|
||||||
},
|
padding: '0 2px',
|
||||||
}, part);
|
borderRadius: '2px',
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
part,
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const CellComponent = ({
|
const CellComponent = ({
|
||||||
cell,
|
cell,
|
||||||
row,
|
row,
|
||||||
column,
|
column,
|
||||||
onClick,
|
onClick,
|
||||||
globalFilter,
|
globalFilter,
|
||||||
columnFilter,
|
columnFilter,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
color,
|
color,
|
||||||
isEditable,
|
isEditable,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
isInvalid,
|
isInvalid,
|
||||||
|
vspOptions,
|
||||||
}) => {
|
}) => {
|
||||||
const { lockedCells } = useRealtime();
|
const { lockedCells } = useRealtime();
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
const isLocked = lockedCells.includes(cellKey);
|
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 !== '';
|
|
||||||
let display = String(value || '');
|
|
||||||
|
|
||||||
if (isNum) {
|
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
||||||
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
const value = cell.getValue();
|
||||||
display = formatNumber(value, asInteger);
|
const isNum = !Number.isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
||||||
}
|
let display = String(value || '');
|
||||||
|
|
||||||
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
if (isVspDropdown && value != null) {
|
||||||
}, [cell, column.id]);
|
const vsp = vspOptions?.find((v) => v.id === Number(value));
|
||||||
|
if (vsp) {
|
||||||
|
return { rawValue: value, displayValue: vsp.registration_number, isNumeric: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
if (isNum) {
|
||||||
const searchQueries = [globalFilter, columnFilter];
|
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
||||||
return createHighlightedContent(rawValue, displayValue, searchQueries);
|
display = formatNumber(value, asInteger);
|
||||||
}, [rawValue, displayValue, globalFilter, columnFilter]);
|
}
|
||||||
|
|
||||||
const textColor = useMemo(() => {
|
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
||||||
if (isLocked) return '#999999';
|
}, [cell, column.id, isVspDropdown, vspOptions]);
|
||||||
const brightness = getColorBrightness(backgroundColor);
|
|
||||||
return brightness < 128 ? '#ffffff' : '#000000';
|
|
||||||
}, [backgroundColor, isLocked]);
|
|
||||||
|
|
||||||
const cellStyles = useMemo(() => ({
|
const highlightedContent = useMemo(() => {
|
||||||
...BASE_CELL_STYLES,
|
const searchQueries = [globalFilter, columnFilter];
|
||||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
return createHighlightedContent(rawValue, displayValue, searchQueries);
|
||||||
color: textColor,
|
}, [rawValue, displayValue, globalFilter, columnFilter]);
|
||||||
...(isLocked ? LOCKED_STYLES : {}),
|
|
||||||
...(isInvalid ? INVALID_STYLES : {}),
|
|
||||||
}), [backgroundColor, isInvalid, isLocked, textColor]);
|
|
||||||
|
|
||||||
const lockBadge = useMemo(() => {
|
const textColor = useMemo(() => {
|
||||||
if (!isLocked) return null;
|
if (isLocked) return '#999999';
|
||||||
return <div style={LOCK_BADGE_STYLES}>🔒</div>;
|
const brightness = getColorBrightness(backgroundColor);
|
||||||
}, [isLocked]);
|
return brightness < 128 ? '#ffffff' : '#000000';
|
||||||
|
}, [backgroundColor, isLocked]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const cellStyles = useMemo(
|
||||||
if (!isLocked && onClick) {
|
() => ({
|
||||||
onClick();
|
...BASE_CELL_STYLES,
|
||||||
}
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
}, [isLocked, onClick]);
|
color: textColor,
|
||||||
|
...(isLocked ? LOCKED_STYLES : {}),
|
||||||
|
...(isInvalid ? INVALID_STYLES : {}),
|
||||||
|
}),
|
||||||
|
[backgroundColor, isInvalid, isLocked, textColor],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const lockBadge = useMemo(() => {
|
||||||
<div
|
if (!isLocked) return null;
|
||||||
className="cell"
|
return <div style={LOCK_BADGE_STYLES}>🔒</div>;
|
||||||
style={cellStyles}
|
}, [isLocked]);
|
||||||
onClick={handleClick}
|
|
||||||
>
|
const handleClick = useCallback(() => {
|
||||||
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
if (!isLocked && onClick) {
|
||||||
{lockBadge}
|
onClick();
|
||||||
</div>
|
}
|
||||||
);
|
}, [isLocked, onClick]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cell' style={cellStyles} onClick={handleClick}>
|
||||||
|
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
||||||
|
{lockBadge}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
||||||
// Возвращаем true если пропсы равны (не нужно перерендеривать)
|
return (
|
||||||
return (
|
prevProps.cell.getValue() === nextProps.cell.getValue() &&
|
||||||
prevProps.cell.getValue() === nextProps.cell.getValue() &&
|
prevProps.row.id === nextProps.row.id &&
|
||||||
prevProps.row.id === nextProps.row.id &&
|
prevProps.column.id === nextProps.column.id &&
|
||||||
prevProps.column.id === nextProps.column.id &&
|
prevProps.globalFilter === nextProps.globalFilter &&
|
||||||
prevProps.globalFilter === nextProps.globalFilter &&
|
prevProps.columnFilter === nextProps.columnFilter &&
|
||||||
prevProps.columnFilter === nextProps.columnFilter &&
|
prevProps.backgroundColor === nextProps.backgroundColor &&
|
||||||
prevProps.backgroundColor === nextProps.backgroundColor &&
|
prevProps.isInvalid === nextProps.isInvalid &&
|
||||||
prevProps.isInvalid === nextProps.isInvalid &&
|
prevProps.isEditable === nextProps.isEditable &&
|
||||||
prevProps.isEditable === nextProps.isEditable &&
|
prevProps.isUpdating === nextProps.isUpdating &&
|
||||||
prevProps.isUpdating === nextProps.isUpdating &&
|
prevProps.onCellNumberClick === nextProps.onCellNumberClick &&
|
||||||
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
prevProps.vspOptions === nextProps.vspOptions
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
@ -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;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,14 @@ export const config = {
|
|||||||
color_type: orangeColumn,
|
color_type: orangeColumn,
|
||||||
accessorKey: "data.header.name",
|
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": {
|
"data.header.internal_order": {
|
||||||
color_type: orangeColumn,
|
color_type: orangeColumn,
|
||||||
accessorKey: "data.header.internal_order",
|
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: "Внутренний заказ",
|
header: "Внутренний заказ",
|
||||||
accessorKey: "field_vnutrenniy_zakaz",
|
accessorKey: "field_vnutrenniy_zakaz",
|
||||||
@ -837,7 +872,7 @@ export const config = {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
accessorKey: "data.header.internal_order",
|
accessorKey: "data.header.internal_order",
|
||||||
columnLetter: "G",
|
columnLetter: "H",
|
||||||
size: 150,
|
size: 150,
|
||||||
filterFn: "contains",
|
filterFn: "contains",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -25,6 +25,14 @@ export const config = {
|
|||||||
color_type: blueColumn,
|
color_type: blueColumn,
|
||||||
accessorKey: "data.header.name",
|
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": {
|
"data.header.internal_order": {
|
||||||
color_type: blueColumn,
|
color_type: blueColumn,
|
||||||
accessorKey: "data.header.internal_order",
|
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: "Внутренний заказ",
|
header: "Внутренний заказ",
|
||||||
accessorKey: "field_vnutrenniy_zakaz",
|
accessorKey: "field_vnutrenniy_zakaz",
|
||||||
@ -837,7 +872,7 @@ export const config = {
|
|||||||
{
|
{
|
||||||
header: "",
|
header: "",
|
||||||
accessorKey: "data.header.internal_order",
|
accessorKey: "data.header.internal_order",
|
||||||
columnLetter: "G",
|
columnLetter: "H",
|
||||||
size: 150,
|
size: 150,
|
||||||
filterFn: "contains",
|
filterFn: "contains",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,454 +1,425 @@
|
|||||||
import React, {
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
createContext,
|
import { toast } from 'react-toastify';
|
||||||
useContext,
|
import { useWebSocket } from '../hooks/useWebSocket';
|
||||||
useCallback,
|
import { getRowId } from '../utils/rowUtils';
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
} from "react";
|
|
||||||
import { useWebSocket } from "../hooks/useWebSocket";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { getRowId } from "../utils/rowUtils";
|
|
||||||
|
|
||||||
const RealtimeContext = createContext(null);
|
const RealtimeContext = createContext(null);
|
||||||
|
|
||||||
export const useRealtime = () => {
|
export const useRealtime = () => {
|
||||||
const context = useContext(RealtimeContext);
|
const context = useContext(RealtimeContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useRealtime must be used within RealtimeProvider");
|
throw new Error('useRealtime must be used within RealtimeProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RealtimeProvider = ({
|
export const RealtimeProvider = ({ children, formId, sheetName, direction, year, userId, isProject = false }) => {
|
||||||
children,
|
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||||
formId,
|
const wsUrl = `${(
|
||||||
sheetName,
|
process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || 'ws://localhost:8000'
|
||||||
direction,
|
).replace(/^http/, 'ws')}${
|
||||||
year,
|
!isProject
|
||||||
userId,
|
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`
|
||||||
isProject = false,
|
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
|
||||||
}) => {
|
}`;
|
||||||
// Формируем 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 [lockedCells, setLockedCells] = useState([]);
|
const [lockedCells, setLockedCells] = useState([]);
|
||||||
const handleMessage = useCallback((data) => {
|
const [vspOptions, setVspOptions] = useState([]);
|
||||||
// Обработка ошибок
|
const handleMessage = useCallback((data) => {
|
||||||
if (data.error) {
|
// Обработка ошибок
|
||||||
console.log(data.error.message);
|
if (data.error) {
|
||||||
toast.error("Server error:" + data.error.message || "");
|
toast.error(`Server error:${data.error.message}` || '');
|
||||||
onErrorRef.current?.(data.error);
|
onErrorRef.current?.(data.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (data.event) {
|
switch (data.event) {
|
||||||
case "cell_edit_start":
|
case 'cell_edit_start':
|
||||||
console.log("Cell edit started:", data);
|
if (onCellEditStartRef.current) {
|
||||||
if (onCellEditStartRef.current) {
|
onCellEditStartRef.current(data);
|
||||||
onCellEditStartRef.current(data);
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case "cell_edit_end":
|
case 'cell_edit_end':
|
||||||
console.log("Cell edit ended:", data);
|
if (onCellEditEndRef.current) {
|
||||||
if (onCellEditEndRef.current) {
|
onCellEditEndRef.current(data);
|
||||||
onCellEditEndRef.current(data);
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case "cell_updated":
|
case 'cell_updated':
|
||||||
if (data.result && onCellUpdateRef.current) {
|
if (data.result && onCellUpdateRef.current) {
|
||||||
const updatedCells = Array.isArray(data.result)
|
const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
|
||||||
? data.result
|
onCellUpdateRef.current(updatedCells);
|
||||||
: [data.result];
|
}
|
||||||
onCellUpdateRef.current(updatedCells);
|
break;
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "row_added":
|
case 'row_added':
|
||||||
if (data.result && onRowAddRef.current) {
|
if (data.result && onRowAddRef.current) {
|
||||||
onRowAddRef.current(data);
|
onRowAddRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "row_deleted":
|
case 'row_deleted':
|
||||||
if (data.result && onRowDeleteRef.current) {
|
if (data.result && onRowDeleteRef.current) {
|
||||||
onRowDeleteRef.current(data);
|
onRowDeleteRef.current(data);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "program_added":
|
case 'program_added':
|
||||||
console.log("Program added:", data);
|
if (data.result && onProgramAddRef.current) {
|
||||||
if (data.result && onProgramAddRef.current) {
|
onProgramAddRef.current(data);
|
||||||
onProgramAddRef.current(data);
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
case "project_added":
|
case 'project_added':
|
||||||
console.log("Project added:", data);
|
if (data.result && onProjectAddRef.current) {
|
||||||
if (data.result && onProjectAddRef.current) {
|
onProjectAddRef.current(data);
|
||||||
onProjectAddRef.current(data);
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log("Unknown event:", data.event);
|
}
|
||||||
}
|
}, []);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } =
|
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(wsUrl, handleMessage);
|
||||||
useWebSocket(wsUrl, handleMessage);
|
|
||||||
|
|
||||||
const onCellUpdateRef = useRef(null);
|
const onCellUpdateRef = useRef(null);
|
||||||
const onRowAddRef = useRef(null);
|
const onRowAddRef = useRef(null);
|
||||||
const onRowDeleteRef = useRef(null);
|
const onRowDeleteRef = useRef(null);
|
||||||
const onProgramAddRef = useRef(null);
|
const onProgramAddRef = useRef(null);
|
||||||
const onProjectAddRef = useRef(null);
|
const onProjectAddRef = useRef(null);
|
||||||
const onErrorRef = useRef(null);
|
const onErrorRef = useRef(null);
|
||||||
const onAuthSuccessRef = useRef(null);
|
const onAuthSuccessRef = useRef(null);
|
||||||
const onCellEditStartRef = useRef(null);
|
const onCellEditStartRef = useRef(null);
|
||||||
const onCellEditEndRef = useRef(null);
|
const onCellEditEndRef = useRef(null);
|
||||||
const authAttemptedRef = useRef(false);
|
const authAttemptedRef = useRef(false);
|
||||||
const reconnectTimerRef = useRef(null);
|
const _reconnectTimerRef = useRef(null);
|
||||||
|
|
||||||
// Функция для получения токена из localStorage
|
// Функция для получения токена из localStorage
|
||||||
const getAccessToken = useCallback(() => {
|
const getAccessToken = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem('access_token');
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn("No access token found in localStorage");
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.error("Error reading token from localStorage:", error);
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
}, []);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sendLogin = useCallback(() => {
|
const sendLogin = useCallback(() => {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error("Cannot login: no token available");
|
onErrorRef.current?.('Токен авторизации не найден');
|
||||||
onErrorRef.current?.("Токен авторизации не найден");
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const loginMessage = {
|
const loginMessage = {
|
||||||
event: "user_login",
|
event: 'user_login',
|
||||||
data: { token },
|
data: { token },
|
||||||
};
|
};
|
||||||
|
|
||||||
return sendMessage(loginMessage);
|
return sendMessage(loginMessage);
|
||||||
}, [getAccessToken, sendMessage]);
|
}, [getAccessToken, sendMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isConnected && !authAttemptedRef.current) {
|
if (isConnected && !authAttemptedRef.current) {
|
||||||
authAttemptedRef.current = true;
|
authAttemptedRef.current = true;
|
||||||
console.log("WebSocket connected, sending login...");
|
setTimeout(() => {
|
||||||
setTimeout(() => {
|
sendLogin();
|
||||||
sendLogin();
|
}, 100);
|
||||||
}, 100);
|
}
|
||||||
}
|
}, [isConnected, sendLogin]);
|
||||||
}, [isConnected, sendLogin]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
authAttemptedRef.current = false;
|
authAttemptedRef.current = false;
|
||||||
}
|
}
|
||||||
}, [isConnected]);
|
}, [isConnected]);
|
||||||
|
|
||||||
const addCommonField = (message) => {
|
const addCommonField = (message) => {
|
||||||
if (isProject) {
|
if (isProject) {
|
||||||
message.report_type = sheetName;
|
message.report_type = sheetName;
|
||||||
message.project_id = formId;
|
message.project_id = formId;
|
||||||
message.year = year;
|
message.year = year;
|
||||||
} else {
|
} else {
|
||||||
message.sheet = sheetName;
|
message.sheet = sheetName;
|
||||||
message.form_id = formId;
|
message.form_id = formId;
|
||||||
message.direction = direction;
|
message.direction = direction;
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для начала редактирования ячейки
|
// Функция для начала редактирования ячейки
|
||||||
const startEditing = useCallback(
|
const startEditing = useCallback(
|
||||||
async (row, column) => {
|
async (row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const _rowId = row.id;
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineId = row.id || null;
|
const lineId = row.id || null;
|
||||||
const colId = columnId.slice(5); // убираем префикс "data."
|
const colId = columnId.slice(5); // убираем префикс "data."
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_edit_start",
|
event: 'cell_edit_start',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
column: colId,
|
column: colId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sent = sendMessage(message);
|
const sent = sendMessage(message);
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnected, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Функция для завершения редактирования ячейки
|
// Функция для завершения редактирования ячейки
|
||||||
const endEditing = useCallback(
|
const endEditing = useCallback(
|
||||||
async (row, column) => {
|
async (row, column) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const _rowId = row.id;
|
||||||
|
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineId = row.id || null;
|
const lineId = row.id || null;
|
||||||
const colId = columnId.slice(5);
|
const colId = columnId.slice(5);
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_edit_end",
|
event: 'cell_edit_end',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
column: colId,
|
column: colId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sent = sendMessage(message);
|
const sent = sendMessage(message);
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnected, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateCell = useCallback(
|
const updateCell = useCallback(
|
||||||
async (row, column, value) => {
|
async (row, column, value) => {
|
||||||
const columnId = column.id;
|
const columnId = column.id;
|
||||||
const rowId = row.id;
|
const _rowId = row.id;
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("WebSocket не подключен");
|
onErrorRef.current?.('WebSocket не подключен');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineId = getRowId(row);
|
const lineId = getRowId(row);
|
||||||
const colId = columnId.slice(5);
|
const colId = columnId.slice(5);
|
||||||
const message = {
|
const message = {
|
||||||
event: "cell_updated",
|
event: 'cell_updated',
|
||||||
data: {
|
data: {
|
||||||
line_id: lineId,
|
line_id: lineId,
|
||||||
line_id_code: row.id,
|
line_id_code: row.id,
|
||||||
column: colId,
|
column: colId,
|
||||||
value: value,
|
value: value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const sent = sendMessage(message);
|
const sent = sendMessage(message);
|
||||||
|
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage],
|
[isConnected, sendMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "row_added",
|
event: 'row_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProject = useCallback(
|
const addProject = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "project_added",
|
event: 'project_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addProgram = useCallback(
|
const addProgram = useCallback(
|
||||||
async (rowData) => {
|
async (rowData) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "program_added",
|
event: 'program_added',
|
||||||
data: rowData,
|
data: rowData,
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteRow = useCallback(
|
const deleteRow = useCallback(
|
||||||
async (rowId) => {
|
async (rowId) => {
|
||||||
if (!isConnectedRef) {
|
if (!isConnectedRef) {
|
||||||
onErrorRef.current?.("Нет подключения или авторизации");
|
onErrorRef.current?.('Нет подключения или авторизации');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
event: "row_deleted",
|
event: 'row_deleted',
|
||||||
data: { row_id: rowId },
|
data: { row_id: rowId },
|
||||||
};
|
};
|
||||||
addCommonField(message);
|
addCommonField(message);
|
||||||
|
|
||||||
return sendMessage(message);
|
return sendMessage(message);
|
||||||
},
|
},
|
||||||
[isConnected, sendMessage, sheetName, formId, direction],
|
[isConnected, sendMessage, sheetName, formId, direction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscribeToCellUpdates = useCallback((callback) => {
|
const subscribeToCellUpdates = useCallback((callback) => {
|
||||||
onCellUpdateRef.current = callback;
|
onCellUpdateRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onCellUpdateRef.current = null;
|
onCellUpdateRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToRowAdds = useCallback((callback) => {
|
const subscribeToRowAdds = useCallback((callback) => {
|
||||||
onRowAddRef.current = callback;
|
onRowAddRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onRowAddRef.current = null;
|
onRowAddRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToRowDeletes = useCallback((callback) => {
|
const subscribeToRowDeletes = useCallback((callback) => {
|
||||||
onRowDeleteRef.current = callback;
|
onRowDeleteRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onRowDeleteRef.current = null;
|
onRowDeleteRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToProgramAdds = useCallback((callback) => {
|
const subscribeToProgramAdds = useCallback((callback) => {
|
||||||
onProgramAddRef.current = callback;
|
onProgramAddRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onProgramAddRef.current = null;
|
onProgramAddRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToProjectAdds = useCallback((callback) => {
|
const subscribeToProjectAdds = useCallback((callback) => {
|
||||||
onProjectAddRef.current = callback;
|
onProjectAddRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onProjectAddRef.current = null;
|
onProjectAddRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToErrors = useCallback((callback) => {
|
const subscribeToErrors = useCallback((callback) => {
|
||||||
onErrorRef.current = callback;
|
onErrorRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onErrorRef.current = null;
|
onErrorRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToAuthSuccess = useCallback((callback) => {
|
const subscribeToAuthSuccess = useCallback((callback) => {
|
||||||
onAuthSuccessRef.current = callback;
|
onAuthSuccessRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onAuthSuccessRef.current = null;
|
onAuthSuccessRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToCellEditStart = useCallback((callback) => {
|
const subscribeToCellEditStart = useCallback((callback) => {
|
||||||
onCellEditStartRef.current = callback;
|
onCellEditStartRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onCellEditStartRef.current = null;
|
onCellEditStartRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const subscribeToCellEditEnd = useCallback((callback) => {
|
const subscribeToCellEditEnd = useCallback((callback) => {
|
||||||
onCellEditEndRef.current = callback;
|
onCellEditEndRef.current = callback;
|
||||||
return () => {
|
return () => {
|
||||||
onCellEditEndRef.current = null;
|
onCellEditEndRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const retryLogin = useCallback(() => {
|
const retryLogin = useCallback(() => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
authAttemptedRef.current = false;
|
authAttemptedRef.current = false;
|
||||||
sendLogin();
|
sendLogin();
|
||||||
}
|
}
|
||||||
}, [isConnected, sendLogin]);
|
}, [isConnected, sendLogin]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RealtimeContext.Provider
|
<RealtimeContext.Provider
|
||||||
value={{
|
value={{
|
||||||
isConnected,
|
isConnected,
|
||||||
error: lastError,
|
error: lastError,
|
||||||
startEditing,
|
startEditing,
|
||||||
endEditing,
|
endEditing,
|
||||||
updateCell,
|
updateCell,
|
||||||
addRow,
|
addRow,
|
||||||
addProgram,
|
addProgram,
|
||||||
addProject,
|
addProject,
|
||||||
deleteRow,
|
deleteRow,
|
||||||
subscribeToRowAdds,
|
subscribeToRowAdds,
|
||||||
subscribeToRowDeletes,
|
subscribeToRowDeletes,
|
||||||
subscribeToProgramAdds,
|
subscribeToProgramAdds,
|
||||||
subscribeToProjectAdds,
|
subscribeToProjectAdds,
|
||||||
subscribeToErrors,
|
subscribeToErrors,
|
||||||
subscribeToAuthSuccess,
|
subscribeToAuthSuccess,
|
||||||
subscribeToCellEditStart,
|
subscribeToCellEditStart,
|
||||||
subscribeToCellEditEnd,
|
subscribeToCellEditEnd,
|
||||||
subscribeToCellUpdates,
|
subscribeToCellUpdates,
|
||||||
retryLogin,
|
retryLogin,
|
||||||
lockedCells,
|
lockedCells,
|
||||||
setLockedCells,
|
setLockedCells,
|
||||||
releaseAllLocks: () => releaseAllLocks(userId),
|
vspOptions,
|
||||||
}}
|
setVspOptions,
|
||||||
>
|
releaseAllLocks: () => releaseAllLocks(userId),
|
||||||
{children}
|
}}>
|
||||||
</RealtimeContext.Provider>
|
{children}
|
||||||
);
|
</RealtimeContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,210 +1,205 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
||||||
import { useRealtime } from './contexts/RealtimeContext';
|
import { useRealtime } from './contexts/RealtimeContext';
|
||||||
|
|
||||||
const EditCellPortal = React.memo(({
|
const EditCellPortal = React.memo(({ cell, table, EditCell, onSaveStart, onSaveEnd, onError, isEditable }) => {
|
||||||
cell,
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
table,
|
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||||||
EditCell,
|
const ref = useRef(null);
|
||||||
onSaveStart,
|
const [refTbody, setRefTbody] = useState(null);
|
||||||
onSaveEnd,
|
const [isSaving, _setIsSaving] = useState(false);
|
||||||
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 { endEditing: contextEndEditing } = useRealtime();
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||||||
if (tbodyRef && tbodyRef !== refTbody) {
|
if (tbodyRef && tbodyRef !== refTbody) {
|
||||||
setRefTbody(tbodyRef);
|
setRefTbody(tbodyRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = useCallback( (val) => {
|
const handleChange = useCallback(
|
||||||
table.setEditingCell(null);
|
(val) => {
|
||||||
try {
|
table.setEditingCell(null);
|
||||||
if (table.options.meta?.updateCell) {
|
try {
|
||||||
const success = table.options.meta.updateCell(
|
if (table.options.meta?.updateCell) {
|
||||||
cell.row,
|
const success = table.options.meta.updateCell(cell.row, cell.column, val);
|
||||||
cell.column,
|
if (!success) {
|
||||||
val
|
onError?.('Не удалось сохранить изменение');
|
||||||
);
|
}
|
||||||
if (!success) {
|
}
|
||||||
console.error('Failed to update cell via WebSocket');
|
} catch (error) {
|
||||||
onError?.('Не удалось сохранить изменение');
|
onError?.(error.message);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
} catch (error) {
|
[table, cell, onError],
|
||||||
console.error('Error updating cell:', error);
|
);
|
||||||
onError?.(error.message);
|
|
||||||
}
|
|
||||||
}, [table, cell, onError]);
|
|
||||||
|
|
||||||
const editCellProps = useMemo(() => ({
|
const editCellProps = useMemo(
|
||||||
refCell: ref,
|
() => ({
|
||||||
cell,
|
refCell: ref,
|
||||||
value: cell.getValue(),
|
cell,
|
||||||
disabled: !isEditable,
|
value: cell.getValue(),
|
||||||
onChange: handleChange,
|
disabled: !isEditable,
|
||||||
tableId: tableKey,
|
onChange: handleChange,
|
||||||
table,
|
tableId: tableKey,
|
||||||
cellId: `${cell.row.id}_${cell.column.id}`,
|
table,
|
||||||
}), [cell, isEditable, handleChange, tableKey, table]);
|
cellId: `${cell.row.id}_${cell.column.id}`,
|
||||||
|
}),
|
||||||
|
[cell, isEditable, handleChange, tableKey, table],
|
||||||
|
);
|
||||||
|
|
||||||
const portalContent = useMemo(() => {
|
const editType = cell.column.columnDef?.editType;
|
||||||
if (!refTbody || isSaving) return null;
|
|
||||||
return createPortal(
|
|
||||||
<EditCell {...editCellProps} />,
|
|
||||||
refTbody
|
|
||||||
);
|
|
||||||
}, [refTbody, isSaving, editCellProps, EditCell]);
|
|
||||||
|
|
||||||
return (
|
const portalContent = useMemo(() => {
|
||||||
<>
|
if (!refTbody || isSaving) return null;
|
||||||
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
|
||||||
{portalContent}
|
if (editType === 'vsp_dropdown') {
|
||||||
</>
|
return createPortal(<VspDropdownEditCell {...editCellProps} />, refTbody);
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return createPortal(<EditCell {...editCellProps} />, refTbody);
|
||||||
|
}, [refTbody, isSaving, editCellProps, EditCell, editType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
||||||
|
{portalContent}
|
||||||
|
</>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getTableColumns = ({ Cell, EditCell, columnsConfig, onCellUpdateError, onCellNumberClick, isCellInvalid }) => {
|
||||||
|
const columnColors = { ...columnsConfig.colors };
|
||||||
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
|
|
||||||
export const getTableColumns = ({
|
const cellPropsCache = new WeakMap();
|
||||||
Cell,
|
const editPropsCache = new WeakMap();
|
||||||
EditCell,
|
|
||||||
columnsConfig,
|
|
||||||
onCellUpdateError,
|
|
||||||
onCellNumberClick,
|
|
||||||
isCellInvalid,
|
|
||||||
}) => {
|
|
||||||
const columnColors = { ...columnsConfig.colors };
|
|
||||||
const columns = structuredClone(columnsConfig.columns);
|
|
||||||
|
|
||||||
const cellPropsCache = new WeakMap();
|
const getCachedCellProps = (row, column, table) => {
|
||||||
const editPropsCache = new WeakMap();
|
const key = `${row.id}_${column.id}`;
|
||||||
|
const value = row.getValue(column.id);
|
||||||
|
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
||||||
|
|
||||||
const getCachedCellProps = (row, column, table) => {
|
if (!cellPropsCache.has(row)) {
|
||||||
const key = `${row.id}_${column.id}`;
|
cellPropsCache.set(row, {});
|
||||||
const value = row.getValue(column.id);
|
}
|
||||||
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
|
||||||
|
|
||||||
if (!cellPropsCache.has(row)) {
|
const rowCache = cellPropsCache.get(row);
|
||||||
cellPropsCache.set(row, {});
|
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 rowCache = cellPropsCache.get(row);
|
if (
|
||||||
if (rowCache[key]) {
|
cached.globalFilter === currentGlobalFilter &&
|
||||||
const cached = rowCache[key];
|
cached.columnFilter === currentColumnFilter &&
|
||||||
const currentGlobalFilter = table.getState().globalFilter;
|
cached.value === value &&
|
||||||
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
cached.isInvalid === isInvalid
|
||||||
|
) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cached.globalFilter === currentGlobalFilter &&
|
const globalFilter = table.getState().globalFilter;
|
||||||
cached.columnFilter === currentColumnFilter &&
|
const columnFilter = table.getState().columnFilters?.find((f) => f.id === column.id)?.value;
|
||||||
cached.value === value &&
|
|
||||||
cached.isInvalid === isInvalid) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalFilter = table.getState().globalFilter;
|
const props = {
|
||||||
const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
globalFilter,
|
||||||
|
columnFilter,
|
||||||
|
isEditable: row.original?.row_type === 'INPUT' || false,
|
||||||
|
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
||||||
|
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
||||||
|
isInvalid,
|
||||||
|
value,
|
||||||
|
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}_${value}_${isInvalid}`,
|
||||||
|
};
|
||||||
|
|
||||||
const props = {
|
rowCache[key] = props;
|
||||||
globalFilter,
|
return props;
|
||||||
columnFilter,
|
};
|
||||||
isEditable: row.original?.row_type === 'INPUT' || false,
|
|
||||||
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
|
||||||
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
|
||||||
isInvalid,
|
|
||||||
value,
|
|
||||||
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}_${value}_${isInvalid}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
rowCache[key] = props;
|
const getCachedEditProps = (row, column, _table) => {
|
||||||
return props;
|
const key = `${row.id}_${column.id}`;
|
||||||
};
|
|
||||||
|
|
||||||
const getCachedEditProps = (row, column, table) => {
|
if (!editPropsCache.has(row)) {
|
||||||
const key = `${row.id}_${column.id}`;
|
editPropsCache.set(row, {});
|
||||||
|
}
|
||||||
|
|
||||||
if (!editPropsCache.has(row)) {
|
const rowCache = editPropsCache.get(row);
|
||||||
editPropsCache.set(row, {});
|
if (rowCache[key]) {
|
||||||
}
|
return rowCache[key];
|
||||||
|
}
|
||||||
|
|
||||||
const rowCache = editPropsCache.get(row);
|
const props = {
|
||||||
if (rowCache[key]) {
|
isEditable: row.original?.row_type === 'INPUT' || false,
|
||||||
return rowCache[key];
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const props = {
|
rowCache[key] = props;
|
||||||
isEditable: row.original?.row_type === 'INPUT' || false,
|
return props;
|
||||||
};
|
};
|
||||||
|
|
||||||
rowCache[key] = props;
|
// Оптимизированная функция processColumns
|
||||||
return props;
|
const processColumns = (columns) => {
|
||||||
};
|
columns.forEach((col) => {
|
||||||
|
col.Cell = ({ cell, table, column, row }) => {
|
||||||
|
const { vspOptions } = useRealtime();
|
||||||
|
const props = getCachedCellProps(row, column, table);
|
||||||
|
|
||||||
// Оптимизированная функция processColumns
|
const cellProps = useMemo(
|
||||||
const processColumns = (columns) => {
|
() => ({
|
||||||
columns.forEach((col) => {
|
row,
|
||||||
col.Cell = ({ cell, table, column, row }) => {
|
column,
|
||||||
const props = getCachedCellProps(row, column, table);
|
cell,
|
||||||
|
vspOptions,
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
[row.id, column.id, cell.getValue(), props._hash, vspOptions],
|
||||||
|
);
|
||||||
|
|
||||||
const cellProps = useMemo(() => ({
|
return <Cell {...cellProps} />;
|
||||||
row,
|
};
|
||||||
column,
|
|
||||||
cell,
|
|
||||||
...props,
|
|
||||||
}), [row.id, column.id, cell.getValue(), props._hash]);
|
|
||||||
|
|
||||||
return <Cell {...cellProps} />;
|
col.Edit = ({ cell, table, row, column }) => {
|
||||||
};
|
const props = getCachedEditProps(row, column, table);
|
||||||
|
|
||||||
col.Edit = ({ cell, table, row, column }) => {
|
const editComponent = useMemo(
|
||||||
const props = getCachedEditProps(row, column, table);
|
() => <EditCellPortal cell={cell} table={table} EditCell={EditCell} onError={onCellUpdateError} isEditable={props.isEditable} />,
|
||||||
|
[cell, table, EditCell, onCellUpdateError, props.isEditable],
|
||||||
|
);
|
||||||
|
|
||||||
const editComponent = useMemo(() => (
|
return editComponent;
|
||||||
<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) {
|
if (col.columns?.length) {
|
||||||
processColumns(col.columns);
|
processColumns(col.columns);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
processColumns(columns);
|
processColumns(columns);
|
||||||
|
|
||||||
const rowNumber = {
|
const rowNumber = {
|
||||||
accessorKey: 'sort_order',
|
accessorKey: 'sort_order',
|
||||||
header: '#',
|
header: '#',
|
||||||
size: 50,
|
size: 50,
|
||||||
enableEditing: false,
|
enableEditing: false,
|
||||||
Cell: ({ cell, row }) => {
|
Cell: ({ cell, row }) => {
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onCellNumberClick(row.id);
|
onCellNumberClick(row.id);
|
||||||
}, [row.id, onCellNumberClick]);
|
}, [row.id, onCellNumberClick]);
|
||||||
|
|
||||||
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
return <button onClick={handleClick}>{`${cell.getValue()} `}</button>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return [rowNumber, ...columns];
|
return [rowNumber, ...columns];
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user