diff --git a/web/src/api/dict-vsp.js b/web/src/api/dict-vsp.js
index 9e7e9de..6bf40c6 100644
--- a/web/src/api/dict-vsp.js
+++ b/web/src/api/dict-vsp.js
@@ -21,4 +21,7 @@ export const DictVspApi = {
responseType: "blob",
});
},
+ getDropdownVsp(params){
+ return api.get(`/dict/info/dropdown`, params).then((r) => r.data);
+ }
};
diff --git a/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx
new file mode 100644
index 0000000..29a8ee4
--- /dev/null
+++ b/web/src/components/RealtimeTable/Modals/SelectVspModal.jsx
@@ -0,0 +1,139 @@
+import { useEffect, useState } from 'react';
+import {
+ Box,
+ Button,
+ Divider,
+ FormControl,
+ FormLabel,
+ IconButton,
+ Stack,
+ Select,
+ MenuItem,
+} from '@mui/material';
+import {
+ ModalBackdrop,
+ ModalContainer,
+ HeaderContainer,
+ ModalTitle,
+} from '../../common/Modal/ModalStyled';
+import { Cancel } from '../../common/icons/icons';
+import { DictVspApi } from '../../../api/dict-vsp';
+import { toast } from 'react-toastify';
+
+export const SelectVspModal = ({
+ isOpen,
+ onClose,
+ onSelect,
+ formId,
+ title = 'Выбор ВСП',
+ isSaving = false,
+}) => {
+ const [selectedValue, setSelectedValue] = useState('');
+ const [vspOptions, setVspOption] = useState([])
+
+ useEffect(() => {
+ if (formId === null) return;
+ DictVspApi.getDropdownVsp({ form_id: formId }).then(data => {
+ if (data.success) {
+ setVspOption(data.result)
+ return;
+ }
+ toast.error('Ошибка получения ВСП')
+ })
+ }, [formId])
+
+ useEffect(() => {
+ if (!isOpen) {
+ setSelectedValue('');
+ }
+ }, [isOpen])
+
+ const handleCancel = () => {
+ setSelectedValue('');
+ onClose();
+ };
+
+ const handleBackdropClick = (e) => {
+ if (e.target === e.currentTarget) {
+ handleCancel();
+ }
+ };
+
+ const handleSave = () => {
+ onSelect?.(selectedValue);
+ onClose();
+ };
+
+ const isFormValid = selectedValue;
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+ {/* Выбор ВСП */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx
index 6b37f2a..1328e53 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -24,6 +24,9 @@ import {
import { useRealtime } from './contexts/RealtimeContext';
import { toast } from 'react-toastify';
import { CircularProgress } from '@mui/material';
+import { additionVspRowTable } from './constants/addingRowConfig';
+import { SelectVspModal } from './Modals/selectVspModal';
+import { getRowId } from './utils/rowUtils';
const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
@@ -35,6 +38,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const [isPending, startTransition] = useTransition();
const [editingCell, setEditingCell] = useState(null);
+ const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false);
+
+ const isVspAdditionRow = useMemo(() => {
+ if (!formType || !sheetName) return false;
+ if (!additionVspRowTable[formType]) return false;
+ return additionVspRowTable[formType].includes(sheetName);
+ }, [formType, sheetName]);
+
const handleGlobalFilterChange = (value) => {
startTransition(() => {
setGlobalFilter(value);
@@ -284,6 +295,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
contextAddRow({ expense_item_id: expense_item_id })
}, [rowSelection, table])
+ const handleAddVspRow = useCallback((vsp_id) => {
+ contextAddRow({ vsp_id: vsp_id });
+ }, [rowSelection, table]);
+
+ const handleOpenModalSelectVsp = useCallback(() => {
+ setIsOpenModalSelectVsp(true);
+ }, [rowSelection, table])
+
const handleDeleteRow = useCallback(() => {
const allRows = table.getRowModel().rows;
@@ -293,8 +312,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
return;
}
const row = selectedRows[0];
- const expense_item_id = row.original.data.line_id;
- contextDeleteRow(expense_item_id)
+ const rowId = getRowId(row);
+ contextDeleteRow(rowId);
setRowSelection({});
}, [rowSelection, table])
@@ -312,7 +331,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
onGlobalFilterChange={handleGlobalFilterChange}
sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters}
- onAddRow={handleAddRow}
+ onAddRow={isVspAdditionRow ? handleOpenModalSelectVsp : handleAddRow}
onDeleteRow={handleDeleteRow}
isLoadingData={isLoadingData}
formId={formId}
@@ -364,6 +383,15 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
+ setIsOpenModalSelectVsp(false)}
+ formId={formId}
+ onSelect={handleAddVspRow}
+ // vspOptions={}
+ // selectedVSP={}
+ title="Выбор ВСП"
+ />
>
);
};
diff --git a/web/src/components/RealtimeTable/constants/addingRowConfig.js b/web/src/components/RealtimeTable/constants/addingRowConfig.js
new file mode 100644
index 0000000..04e069b
--- /dev/null
+++ b/web/src/components/RealtimeTable/constants/addingRowConfig.js
@@ -0,0 +1,3 @@
+export const additionVspRowTable = {
+ FORM_2: ["AHR_SECURITY", "AHR_RENT", "AHR_UTILITY"],
+};
diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js
index d1196e6..ff40381 100644
--- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js
+++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js
@@ -8,6 +8,7 @@ import React, {
} from "react";
import { useWebSocket } from "../hooks/useWebSocket";
import { toast } from "react-toastify";
+import { getRowId } from "../utils/rowUtils";
const RealtimeContext = createContext(null);
@@ -33,7 +34,8 @@ export const RealtimeProvider = ({
const handleMessage = useCallback((data) => {
// Обработка ошибок
if (data.error) {
- toast.error("Server error:", data.error);
+ console.log(data.error.message)
+ toast.error("Server error:" + data.error.message || '');
onErrorRef.current?.(data.error);
return;
}
@@ -48,7 +50,7 @@ export const RealtimeProvider = ({
case "cell_edit_end":
console.log("Cell edit ended:", data);
- if ( onCellEditEndRef.current) {
+ if (onCellEditEndRef.current) {
onCellEditEndRef.current(data);
}
break;
@@ -148,7 +150,7 @@ export const RealtimeProvider = ({
async (row, column) => {
const columnId = column.id;
const rowId = row.id;
-
+
if (!isConnected) {
onErrorRef.current?.("WebSocket не подключен");
return false;
@@ -156,7 +158,7 @@ export const RealtimeProvider = ({
const lineId = row.id || null;
const colId = columnId.slice(5); // убираем префикс "data."
-
+
const message = {
event: "cell_edit_start",
data: {
@@ -180,7 +182,7 @@ export const RealtimeProvider = ({
async (row, column) => {
const columnId = column.id;
const rowId = row.id;
-
+
if (!isConnected) {
onErrorRef.current?.("WebSocket не подключен");
return false;
@@ -188,7 +190,7 @@ export const RealtimeProvider = ({
const lineId = row.id || null;
const colId = columnId.slice(5);
-
+
const message = {
event: "cell_edit_end",
data: {
@@ -216,7 +218,7 @@ export const RealtimeProvider = ({
return false;
}
- const lineId = row.original?.data?.line_id || null;
+ const lineId = getRowId(row);
const colId = columnId.slice(5);
const message = {
event: "cell_updated",
@@ -358,4 +360,4 @@ export const RealtimeProvider = ({
{children}
);
-};
\ No newline at end of file
+};
diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js
index 439d632..964f1ab 100644
--- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js
+++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js
@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { FormsSheetApi } from "../../../api/form_sheet";
import { useRealtime } from "../contexts/RealtimeContext";
import { deleteRow, insertCell, updateCells } from "../utils/cellUtils";
+import { toast } from "react-toastify";
+import { isVspNewRow, isVspRow } from "../utils/rowUtils";
const useRealtimeData = (formId, sheetName, direction) => {
const [data, setData] = useState([]);
@@ -176,11 +178,11 @@ const useRealtimeData = (formId, sheetName, direction) => {
const unsubscribeRowAdds = subscribeToRowAdds((data) => {
const updates = data.result.data;
- const expense_item_id = data.data.expense_item_id;
+ const expense_item_id = data.data.expense_item_id || null;
const newLineId = data.result.new_line_id;
const [updatedCells, newRow] = updates.reduce(
(res, row) => {
- if (row[3]?.line_id === newLineId) {
+ if (row[3]?.line_id === newLineId || isVspNewRow(row)) {
res[1] = row;
} else {
res[0].push(row);
@@ -189,6 +191,10 @@ const useRealtimeData = (formId, sheetName, direction) => {
},
[[], null],
);
+ if (newRow === null) {
+ toast.error("Ошибка получения данных");
+ return;
+ }
setData((prevData) => {
const updateData = updateCells(prevData, updatedCells);
const newData = insertCell(updateData, newRow, expense_item_id);
diff --git a/web/src/components/RealtimeTable/hooks/useWebSocket.js b/web/src/components/RealtimeTable/hooks/useWebSocket.js
index a395e53..57e3958 100644
--- a/web/src/components/RealtimeTable/hooks/useWebSocket.js
+++ b/web/src/components/RealtimeTable/hooks/useWebSocket.js
@@ -1,22 +1,27 @@
-import { useEffect, useRef, useCallback, useState } from 'react';
+import { useEffect, useRef, useCallback, useState } from "react";
export const useWebSocket = (url, onMessage) => {
const [isConnected, setIsConnected] = useState(false);
+ const isConnectedRef = useRef(false);
const [error, setError] = useState(null);
const wsRef = useRef(null);
const reconnectTimeoutRef = useRef(null);
const reconnectAttempts = useRef(0);
+ useEffect(() => {
+ isConnectedRef.current = isConnected;
+ }, [isConnected]);
+
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) return;
try {
- console.log('Connecting to WebSocket:', url);
+ console.log("Connecting to WebSocket:", url);
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
- console.log('WebSocket connected successfully');
+ console.log("WebSocket connected successfully");
setIsConnected(true);
setError(null);
reconnectAttempts.current = 0;
@@ -25,43 +30,54 @@ export const useWebSocket = (url, onMessage) => {
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
- console.log('WebSocket message received:', data);
+ console.log("WebSocket message received:", data);
onMessage(data);
} catch (err) {
- console.error('Failed to parse WebSocket message:', err);
+ console.error("Failed to parse WebSocket message:", err);
}
};
ws.onerror = (event) => {
- console.error('WebSocket error:', event);
- console.error('WebSocket readyState:', ws.readyState);
- setError('Connection error occurred');
+ console.error("WebSocket error:", event);
+ console.error("WebSocket readyState:", ws.readyState);
+ setError("Connection error occurred");
};
ws.onclose = (event) => {
- console.log('WebSocket closed:', event.code, event.reason, 'Clean:', event.wasClean);
+ console.log(
+ "WebSocket closed:",
+ event.code,
+ event.reason,
+ "Clean:",
+ event.wasClean,
+ );
setIsConnected(false);
-
+
// Не переподключаемся если закрыто intentionally или с ошибкой авторизации
if (event.code === 1008 || event.code === 1000) {
- console.log('Connection closed intentionally or auth error, not reconnecting');
+ console.log(
+ "Connection closed intentionally or auth error, not reconnecting",
+ );
return;
}
-
+
// Reconnect with exponential backoff
if (reconnectAttempts.current < 5) {
- const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000);
+ const delay = Math.min(
+ 1000 * Math.pow(2, reconnectAttempts.current),
+ 30000,
+ );
console.log(`Reconnecting in ${delay}ms...`);
reconnectTimeoutRef.current = setTimeout(() => {
reconnectAttempts.current++;
connect();
}, delay);
} else {
- setError('Max reconnection attempts reached');
+ setError("Max reconnection attempts reached");
}
};
} catch (err) {
- console.error('Failed to create WebSocket:', err);
+ console.error("Failed to create WebSocket:", err);
setError(err.message);
}
}, [url, onMessage]);
@@ -71,7 +87,7 @@ export const useWebSocket = (url, onMessage) => {
clearTimeout(reconnectTimeoutRef.current);
}
if (wsRef.current) {
- wsRef.current.close(1000, 'Intentional disconnect');
+ wsRef.current.close(1000, "Intentional disconnect");
wsRef.current = null;
}
setIsConnected(false);
@@ -80,24 +96,30 @@ export const useWebSocket = (url, onMessage) => {
const sendMessage = useCallback((message) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
- console.log('Message sent:', message);
+ console.log("Message sent:", message);
return true;
}
- console.warn('WebSocket is not connected, readyState:', wsRef.current?.readyState);
- setError('Cannot send message: WebSocket not connected');
+ console.warn(
+ "WebSocket is not connected, readyState:",
+ wsRef.current?.readyState,
+ );
+ setError("Cannot send message: WebSocket not connected");
return false;
}, []);
- const login = useCallback((token) => {
- if (!token) {
- console.warn('No token provided for login');
- return false;
- }
- return sendMessage({
- event: 'user_login',
- data: { token }
- });
- }, [sendMessage]);
+ const login = useCallback(
+ (token) => {
+ if (!token) {
+ console.warn("No token provided for login");
+ return false;
+ }
+ return sendMessage({
+ event: "user_login",
+ data: { token },
+ });
+ },
+ [sendMessage],
+ );
useEffect(() => {
connect();
@@ -105,10 +127,10 @@ export const useWebSocket = (url, onMessage) => {
}, [connect, disconnect]);
return {
- isConnected,
+ isConnected: isConnectedRef,
error,
sendMessage,
login,
- disconnect
+ disconnect,
};
-};
\ No newline at end of file
+};
diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js
index 509286a..3a89885 100644
--- a/web/src/components/RealtimeTable/utils/cellUtils.js
+++ b/web/src/components/RealtimeTable/utils/cellUtils.js
@@ -90,7 +90,6 @@ export function updateCells(data, newRows) {
return updatedRows;
}
-
export function insertCell(data, newRow, expense_item_id) {
const updatedRows = JSON.parse(JSON.stringify(data));
const [row_type, depth, sort_order, dataRow] = newRow;
@@ -102,6 +101,12 @@ export function insertCell(data, newRow, expense_item_id) {
data: dataRow,
};
+ // Если expense_item_id === null, добавляем в конец
+ if (expense_item_id === null) {
+ updatedRows.push(newRowObject);
+ return updatedRows;
+ }
+
function findAndUpdate(rows) {
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
diff --git a/web/src/components/RealtimeTable/utils/rowUtils.js b/web/src/components/RealtimeTable/utils/rowUtils.js
new file mode 100644
index 0000000..5ba9a51
--- /dev/null
+++ b/web/src/components/RealtimeTable/utils/rowUtils.js
@@ -0,0 +1,15 @@
+export function getRowId(row) {
+ return row.original.data.id || row.original.data.line_id;
+}
+
+export function isVspRow(row) {
+ if (row?.original?.data?.id) {
+ return true;
+ }
+ return false;
+}
+export function isVspNewRow(rowData) {
+ if (!rowData) return false;
+ const newRow = rowData[3];
+ return !!newRow.id;
+}
diff --git a/web/src/components/common/Modal/ModalStyled.jsx b/web/src/components/common/Modal/ModalStyled.jsx
index 181e752..78cbb2c 100644
--- a/web/src/components/common/Modal/ModalStyled.jsx
+++ b/web/src/components/common/Modal/ModalStyled.jsx
@@ -6,7 +6,7 @@ const ModalContainer = styled.div`
/* top: 3rem;
left: 0; */
width: 50rem;
- height: 40rem;
+ height: ${({ height }) => height || '40rem'};
display: flex;
flex-direction: column;
justify-content: flex-start;
@@ -288,6 +288,28 @@ const EditContainer = styled.div`
}
`;
+const ModalBackdrop = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.05);
+ display: ${(props) => (props.isOpen ? 'flex' : 'none')};
+ align-items: center;
+ justify-content: center;
+ z-index: 3;
+`;
+
+const ModalTitle = styled.h2`
+ color: rgba(31, 31, 31, 1);
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin: 0;
+`;
+
export {
ModalContainer,
HeaderContainer,
@@ -304,4 +326,6 @@ export {
StageName,
StageListContainer,
EditContainer,
+ ModalBackdrop,
+ ModalTitle
};