From e0c8c57fc2cdf2f865d7daffe06c50138b3c3dd3 Mon Sep 17 00:00:00 2001 From: tsygankoviva Date: Thu, 6 Aug 2026 17:14:21 +0300 Subject: [PATCH] =?UTF-8?q?vsp-choose-form2-2:=20=D0=B2=D1=8B=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=20=D0=92=D0=A1=D0=9F=20=D0=BD=D0=B0=20=D1=84=D1=80=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RealtimeTable/Cell/Cell/Cell.jsx | 15 ++- .../Cell/EditCell/VspDropdownEditCell.jsx | 113 ++++++++++++++++++ .../RealtimeTable/RealtimeTable.jsx | 22 ++++ .../RealtimeTable/constants/FORM_2/AHR.js | 13 ++ .../RealtimeTable/constants/FORM_2/CAP.js | 37 +++++- .../RealtimeTable/constants/FORM_2/OPER.js | 37 +++++- .../RealtimeTable/contexts/RealtimeContext.js | 3 + .../components/RealtimeTable/tableColumns.jsx | 22 +++- 8 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx index 472c89c..7f93b6c 100644 --- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx +++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx @@ -134,24 +134,34 @@ 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 !== ''; 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]; @@ -207,7 +217,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 ); }); diff --git a/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx b/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx new file mode 100644 index 0000000..9c9e914 --- /dev/null +++ b/web/src/components/RealtimeTable/Cell/EditCell/VspDropdownEditCell.jsx @@ -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 ( + + + {position && +
+ + + +
+ } + + + ); +}); + +VspDropdownEditCell.displayName = 'VspDropdownEditCell'; + +export default VspDropdownEditCell; diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 2711f3f..27213eb 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -32,6 +32,7 @@ import { getRowId } from './utils/rowUtils'; import { FORM_TYPE_OPTIONS } from '../../constants/constants'; import { debounce } from '@mui/material'; import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; +import { DictVspApi } from '../../api/dict-vsp'; const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); @@ -87,6 +88,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { startEditing: contextStartEditing, addProgram: contextAddProgram, addProject: contextAddProject, + setVspOptions, } = useRealtime(); const { @@ -136,6 +138,26 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { loadConfig(); }, [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) => { return await contextUpdateCell(row, column, value); }, [contextUpdateCell]); diff --git a/web/src/components/RealtimeTable/constants/FORM_2/AHR.js b/web/src/components/RealtimeTable/constants/FORM_2/AHR.js index a6cf4b5..57e5be7 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/AHR.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/AHR.js @@ -29,6 +29,10 @@ export const config = { color_type: greenColumn, accessorKey: "data.header.vsp_id", }, + "data.header.vsp_address": { + color_type: greenColumn, + accessorKey: "data.header.vsp_address", + }, "data.plan.q1": { color_type: greenColumn, accessorKey: "data.plan.q1", @@ -725,6 +729,15 @@ export const config = { columnLetter: "E", size: 150, filterFn: "contains", + editType: "vsp_dropdown", + }, + { + header: "Адрес ВСП", + accessorKey: "data.header.vsp_address", + columnLetter: "E1", + size: 220, + filterFn: "contains", + editType: "vsp_dropdown", }, ], muiTableHeadCellProps: { diff --git a/web/src/components/RealtimeTable/constants/FORM_2/CAP.js b/web/src/components/RealtimeTable/constants/FORM_2/CAP.js index 0eaffdc..cadf2cd 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/CAP.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/CAP.js @@ -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", }, diff --git a/web/src/components/RealtimeTable/constants/FORM_2/OPER.js b/web/src/components/RealtimeTable/constants/FORM_2/OPER.js index 894bb85..bc7a9d4 100644 --- a/web/src/components/RealtimeTable/constants/FORM_2/OPER.js +++ b/web/src/components/RealtimeTable/constants/FORM_2/OPER.js @@ -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", }, diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js index 4028366..ea0312c 100644 --- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js +++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js @@ -43,6 +43,7 @@ export const RealtimeProvider = ({ //для ячеек которые редактируются другими пользователями const [lockedCells, setLockedCells] = useState([]); + const [vspOptions, setVspOptions] = useState([]); const handleMessage = useCallback((data) => { // Обработка ошибок if (data.error) { @@ -445,6 +446,8 @@ export const RealtimeProvider = ({ retryLogin, lockedCells, setLockedCells, + vspOptions, + setVspOptions, releaseAllLocks: () => releaseAllLocks(userId), }} > diff --git a/web/src/components/RealtimeTable/tableColumns.jsx b/web/src/components/RealtimeTable/tableColumns.jsx index 43fb911..68d854f 100644 --- a/web/src/components/RealtimeTable/tableColumns.jsx +++ b/web/src/components/RealtimeTable/tableColumns.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react' import { createPortal } from 'react-dom'; import { useParams } from 'react-router'; import { useRealtime } from './contexts/RealtimeContext'; +import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell'; const EditCellPortal = React.memo(({ cell, @@ -60,13 +61,23 @@ const EditCellPortal = React.memo(({ cellId: `${cell.row.id}_${cell.column.id}`, }), [cell, isEditable, handleChange, tableKey, table]); + const editType = cell.column.columnDef?.editType; + const portalContent = useMemo(() => { if (!refTbody || isSaving) return null; + + if (editType === 'vsp_dropdown') { + return createPortal( + , + refTbody + ); + } + return createPortal( , refTbody ); - }, [refTbody, isSaving, editCellProps, EditCell]); + }, [refTbody, isSaving, editCellProps, EditCell, editType]); return ( <> @@ -152,18 +163,19 @@ export const getTableColumns = ({ return props; }; - // Оптимизированная функция processColumns const processColumns = (columns) => { columns.forEach((col) => { col.Cell = ({ cell, table, column, row }) => { + const { vspOptions } = useRealtime(); const props = getCachedCellProps(row, column, table); 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 ; }; @@ -184,6 +196,10 @@ export const getTableColumns = ({ return editComponent; }; + if (col.editType === 'vsp_dropdown') { + col.enableEditing = (row) => row?.original?.row_type === 'INPUT'; + } + if (col.columns?.length) { processColumns(col.columns); }