vsp-choose-form2-2: выбор ВСП на фронте #74

Merged
PotapovaA merged 4 commits from vsp-choose-form2-2 into test 2026-08-13 13:02:46 +03:00
8 changed files with 5217 additions and 4912 deletions

View File

@ -149,24 +149,34 @@ const CellComponent = ({
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 { rawValue, displayValue, isNumeric } = useMemo(() => {
const value = cell.getValue(); const value = cell.getValue();
const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value)); const isNum = value !== null && value !== undefined && value !== '' && !Number.isNaN(Number(value));
let display = String(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) { if (isNum) {
const asInteger = INTEGER_COLUMN_IDS.has(column.id); const asInteger = INTEGER_COLUMN_IDS.has(column.id);
display = formatNumber(value, asInteger); display = formatNumber(value, asInteger);
} }
return { rawValue: value, displayValue: display, isNumeric: isNum }; return { rawValue: value, displayValue: display, isNumeric: isNum };
}, [cell, column.id]); }, [cell, column.id, isVspDropdown, vspOptions]);
const highlightedContent = useMemo(() => { const highlightedContent = useMemo(() => {
const searchQueries = [globalFilter, columnFilter]; const searchQueries = [globalFilter, columnFilter];
@ -223,7 +233,8 @@ const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
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
); );
}); });

View File

@ -0,0 +1,113 @@
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
import { FormControl, Select, MenuItem } from '@mui/material';
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;

View File

@ -32,7 +32,9 @@ import { getRowId } from './utils/rowUtils';
import { FORM_TYPE_OPTIONS } from '../../constants/constants'; import { FORM_TYPE_OPTIONS } from '../../constants/constants';
import { debounce } from '@mui/material'; import { debounce } from '@mui/material';
import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; import { CreateProgramModal } from './Modals/CreateProgramProjectModal';
import { FormsNoCanAddRow } from './constants/formConfig'; import { FormsNoCanAddRow } from './constants/formConfig';
import { DictVspApi } from '../../api/dict-vsp';
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); const { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
@ -95,6 +97,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
startEditing: contextStartEditing, startEditing: contextStartEditing,
addProgram: contextAddProgram, addProgram: contextAddProgram,
addProject: contextAddProject, addProject: contextAddProject,
setVspOptions,
} = useRealtime(); } = useRealtime();
const { const {
@ -144,6 +147,27 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
loadConfig(); loadConfig();
}, [formType, sheetName]); }, [formType, sheetName]);
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) => { const handleUpdateCell = useCallback(async (row, column, value) => {
return await contextUpdateCell(row, column, value); return await contextUpdateCell(row, column, value);
}, [contextUpdateCell]); }, [contextUpdateCell]);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,8 @@ export const RealtimeProvider = ({
//для ячеек которые редактируются другими пользователями //для ячеек которые редактируются другими пользователями
const [lockedCells, setLockedCells] = useState([]); const [lockedCells, setLockedCells] = useState([]);
const [vspOptions, setVspOptions] = useState([]);
const handleMessage = useCallback((data) => { const handleMessage = useCallback((data) => {
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
@ -445,6 +447,8 @@ export const RealtimeProvider = ({
retryLogin, retryLogin,
lockedCells, lockedCells,
setLockedCells, setLockedCells,
vspOptions,
setVspOptions,
releaseAllLocks: () => releaseAllLocks(userId), releaseAllLocks: () => releaseAllLocks(userId),
}} }}
> >

View File

@ -2,11 +2,14 @@ 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 { useRealtime } from './contexts/RealtimeContext'; import { useRealtime } from './contexts/RealtimeContext';
import { sectionCodeColor } from './constants/columnConfig'; import { sectionCodeColor } from './constants/columnConfig';
import { blueColumn, greenColumn } from './constants/columnColors'; import { blueColumn, greenColumn } from './constants/columnColors';
import { useAuth } from '../../app/context/AuthProvider'; import { useAuth } from '../../app/context/AuthProvider';
import { ROLES_NAME_ID } from '../../constants/constants'; import { ROLES_NAME_ID } from '../../constants/constants';
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
const EditCellPortal = React.memo(({ const EditCellPortal = React.memo(({
cell, cell,
table, table,
@ -64,13 +67,23 @@ const EditCellPortal = React.memo(({
cellId: `${cell.row.id}_${cell.column.id}`, 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(() => { const portalContent = useMemo(() => {
if (!refTbody || isSaving) return null; if (!refTbody || isSaving) return null;
if (editType === 'vsp_dropdown') {
return createPortal(
<VspDropdownEditCell {...editCellProps} />,
refTbody
);
}
return createPortal( return createPortal(
<EditCell {...editCellProps} />, <EditCell {...editCellProps} />,
refTbody refTbody
); );
}, [refTbody, isSaving, editCellProps, EditCell]); }, [refTbody, isSaving, editCellProps, EditCell, editType]);
return ( return (
<> <>
@ -178,14 +191,16 @@ export const getTableColumns = ({
const processColumns = (columns) => { const processColumns = (columns) => {
for (const col of columns) { for (const col of columns) {
col.Cell = ({ cell, table, column, row }) => { col.Cell = ({ cell, table, column, row }) => {
const { vspOptions } = useRealtime();
const props = getCachedCellProps(row, column, table); const props = getCachedCellProps(row, column, table);
const cellProps = useMemo(() => ({ const cellProps = useMemo(() => ({
row, row,
column, column,
cell, cell,
vspOptions,
...props, ...props,
}), [row, column, cell, props]); }), [row, column, cell, props, vspOptions,]);
return <Cell {...cellProps} />; return <Cell {...cellProps} />;
}; };
@ -206,11 +221,16 @@ export const getTableColumns = ({
return editComponent; 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);