добавление строки

This commit is contained in:
PotapovaA 2026-05-29 11:23:25 +03:00
parent 52de655149
commit 99e7a445cf
12 changed files with 130 additions and 60 deletions

View File

@ -1,10 +1,9 @@
import api from './client'; import api from './client';
export const FormsSheetApi = { export const FormsSheetApi = {
get(formId, sheetName) { get(formId, sheetName, params) {
return api return api
.get(`/form/${formId}/sheet/${sheetName}`) .get(`/form/${formId}/sheet/${sheetName}`,params)
// .get(`/forms/${form}/sheet/${sheetName}`)
.then((r) => r.data); .then((r) => r.data);
}, },
}; };

View File

@ -50,7 +50,7 @@ export const AppRoutes = () => {
<Route path="/new-table" element={<NewTablePage />} /> <Route path="/new-table" element={<NewTablePage />} />
<Route path="/tables-new" element={<TablesTest />} /> <Route path="/tables-new" element={<TablesTest />} />
<Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} /> <Route path="/tables/:form/:sheetName" element={<NewFormTablePage />} />
<Route path="/table/form/:formId/form-type/:formType/:sheetName" element={<NewFormTablePage />} /> <Route path="/table/form/:formId/form-type/:formType/:sheetName/:direction" element={<NewFormTablePage />} />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

View File

@ -21,10 +21,9 @@ import {
getTablePaperStyles, getTablePaperStyles,
} from './constants/tableConfig'; } from './constants/tableConfig';
import { useRealtime } from './contexts/RealtimeContext'; import { useRealtime } from './contexts/RealtimeContext';
import { setNewValueToData, updateExistingStructure } from './utils/cellUtils';
const RealtimeTable = ({ formType, formId, sheetName }) => { const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { data, setData } = useRealtimeData(formId, sheetName); const { data, setData } = useRealtimeData(formId, sheetName, direction);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState(''); const [globalFilter, setGlobalFilter] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false); const [showColumnFilters, setShowColumnFilters] = useState(false);
@ -39,7 +38,8 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
subscribeToErrors, subscribeToErrors,
subscribeToAuthSuccess, subscribeToAuthSuccess,
error: wsError, error: wsError,
updateCell: contextUpdateCell updateCell: contextUpdateCell,
addRow: contextAddRow,
} = useRealtime(); } = useRealtime();
const { const {
@ -196,6 +196,19 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
const table = useMaterialReactTable(tableConfig); const table = useMaterialReactTable(tableConfig);
const handleAddRow = useCallback(() => {
const allRows = table.getRowModel().rows;
// Фильтруем выделенные строки
const selectedRows = allRows.filter(row => rowSelection[row.id]);
if (selectedRows.length === 0) {
toasr.error('Выделите строку для вставки');
return;
}
const row = selectedRows[0];
const expense_item_id = row.original.data.header.expense_item_id;
contextAddRow({expense_item_id:expense_item_id})
}, [rowSelection, table])
if (!data) { if (!data) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
@ -213,6 +226,7 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
onGlobalFilterChange={setGlobalFilter} onGlobalFilterChange={setGlobalFilter}
sizeMult={sizeMult} sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters} onChangeShowColumnFilters={setShowColumnFilters}
onAddRow={handleAddRow}
/> />
<div style={tableWrapperStyle}> <div style={tableWrapperStyle}>

View File

@ -29,6 +29,7 @@ const SettingPanel = ({
onGlobalFilterChange, onGlobalFilterChange,
sizeMult, sizeMult,
onChangeShowColumnFilters, onChangeShowColumnFilters,
onAddRow,
}) => { }) => {
const [isPinned, setIsPinned] = useState(false); const [isPinned, setIsPinned] = useState(false);
const [columnTree, setColumnTree] = useState(); const [columnTree, setColumnTree] = useState();
@ -54,7 +55,7 @@ const SettingPanel = ({
); );
}, [columnVisibility]); }, [columnVisibility]);
const { addRow, addColorForCells, addFormulaForCells, deleteFormula } = {}; const { addColorForCells, addFormulaForCells, deleteFormula } = {};
const handleChangeShowColumnFilters = () => { const handleChangeShowColumnFilters = () => {
const show = table.getState().showColumnFilters; const show = table.getState().showColumnFilters;
@ -92,7 +93,7 @@ const SettingPanel = ({
<GroupByObject title="Строки"> <GroupByObject title="Строки">
<Tooltip title="Добавить строку"> <Tooltip title="Добавить строку">
<IconButton onClick={addRow} variant="outlined"> <IconButton onClick={onAddRow} variant="outlined">
<Plus /> <Plus />
<AddLine /> <AddLine />
</IconButton> </IconButton>

View File

@ -33,15 +33,14 @@ export const RealtimeProvider = ({
const handleMessage = useCallback((data) => { const handleMessage = useCallback((data) => {
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
toast.error("Server error:", data.error) toast.error("Server error:", data.error);
onErrorRef.current?.(data.error); onErrorRef.current?.(data.error);
return; return;
} }
// Обработка событий
switch (data.event) { switch (data.event) {
case "cell_updated": case "cell_updated":
console.log(onCellUpdateRef.current) console.log(onCellUpdateRef.current);
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
@ -52,7 +51,7 @@ export const RealtimeProvider = ({
case "row_added": case "row_added":
if (data.result && onRowAddRef.current) { if (data.result && onRowAddRef.current) {
onRowAddRef.current(data.result); onRowAddRef.current(data);
} }
break; break;
@ -145,7 +144,6 @@ export const RealtimeProvider = ({
return false; return false;
} }
const lineId = row.original?.data?.line_id || null; const lineId = row.original?.data?.line_id || null;
const colId = columnId.slice(5); const colId = columnId.slice(5);
const message = { const message = {
@ -156,7 +154,6 @@ export const RealtimeProvider = ({
value: value, value: value,
}, },
}; };
console.log(message);
const sent = sendMessage(message); const sent = sendMessage(message);
if (!sent) { if (!sent) {
@ -165,10 +162,7 @@ export const RealtimeProvider = ({
return true; return true;
}, },
[ [isConnected, sendMessage],
isConnected,
sendMessage,
],
); );
const addRow = useCallback( const addRow = useCallback(
@ -181,8 +175,10 @@ export const RealtimeProvider = ({
const message = { const message = {
event: "row_added", event: "row_added",
data: rowData, data: rowData,
sheet: sheetName,
form_id: formId,
direction: direction,
}; };
return sendMessage(message); return sendMessage(message);
}, },
[isConnected, sendMessage], [isConnected, sendMessage],

View File

@ -1,9 +1,9 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { FormsSheetApi } from "../../../api/form_sheet"; import { FormsSheetApi } from "../../../api/form_sheet";
import { useRealtime } from "../contexts/RealtimeContext"; import { useRealtime } from "../contexts/RealtimeContext";
import { updateExistingStructure } from "../utils/cellUtils"; import { insertCell, updateCells } from "../utils/cellUtils";
const useRealtimeData = (formId, sheetName) => { const useRealtimeData = (formId, sheetName, direction) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -71,7 +71,14 @@ const useRealtimeData = (formId, sheetName) => {
const loadInitialData = async () => { const loadInitialData = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const res = await FormsSheetApi.get(formId, sheetName); let res;
if (direction !== null) {
res = await FormsSheetApi.get(formId, sheetName, {
params: { direction: direction },
});
} else {
res = await FormsSheetApi.get(formId, sheetName);
}
const data = res.result; const data = res.result;
if (!data || data.length === 0) { if (!data || data.length === 0) {
@ -119,8 +126,22 @@ const useRealtimeData = (formId, sheetName) => {
if (!updatedCells || updatedCells.length === 0) return; if (!updatedCells || updatedCells.length === 0) return;
setData((prevData) => { setData((prevData) => {
const newData = updateExistingStructure(prevData, updatedCells) const newData = updateCells(prevData, updatedCells);
return newData return newData;
});
});
const unsubscribeRowAdds = subscribeToRowAdds((data) => {
const updates = data.result;
const expense_item_id = data.data.expense_item_id;
const updatedCells = updates.slice(0, -1);
const newRow = updates.at(-1);
setData((prevData) => {
const updateData = updateCells(prevData, updatedCells);
const newData = insertCell(updateData, newRow, expense_item_id);
console.log(newData)
return newData;
}); });
}); });
@ -150,7 +171,7 @@ const useRealtimeData = (formId, sheetName) => {
return () => { return () => {
unsubscribeCellUpdates(); unsubscribeCellUpdates();
// unsubscribeRowAdds(); unsubscribeRowAdds();
unsubscribeRowDeletes(); unsubscribeRowDeletes();
unsubscribeErrors(); unsubscribeErrors();
}; };

View File

@ -42,10 +42,10 @@ export function setNewValueToData(data, colId, rowId, value) {
return true; return true;
} }
export function updateExistingStructure(existingRows, updateItems) { export function updateCells(data, newRows) {
const updatedRows = JSON.parse(JSON.stringify(existingRows)); const updatedRows = JSON.parse(JSON.stringify(data));
updateItems.forEach((update) => { newRows.forEach((update) => {
const [rowType, depth, sortOrder, newData] = update; const [rowType, depth, sortOrder, newData] = update;
// Рекурсивная функция для поиска и обновления строки // Рекурсивная функция для поиска и обновления строки
@ -53,20 +53,20 @@ export function updateExistingStructure(existingRows, updateItems) {
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
const row = rows[i]; const row = rows[i];
// Проверяем совпадение по всем ключам
if ( if (
row.row_type === rowType && row.row_type === rowType &&
row.depth === depth && row.depth === depth &&
row.sort_order === sortOrder row.sort_order === sortOrder
) { ) {
// Обновляем только те поля, которые пришли в newData
if (newData && typeof newData === "object") { if (newData && typeof newData === "object") {
Object.keys(newData).forEach((key) => { Object.keys(newData).forEach((key) => {
if (row.data && row.data[key]) { if (row.data && row.data[key]) {
// Если поле существует в data, обновляем его if (typeof row.data[key] === "object") {
row.data[key] = { ...row.data[key], ...newData[key] }; row.data[key] = { ...row.data[key], ...newData[key] };
} else {
row.data[key] = newData[key];
}
} else if (row[key]) { } else if (row[key]) {
// Если поле на уровне самой строки (не в data)
row[key] = newData[key]; row[key] = newData[key];
} }
}); });
@ -74,7 +74,6 @@ export function updateExistingStructure(existingRows, updateItems) {
return true; // Нашли и обновили return true; // Нашли и обновили
} }
// Рекурсивно ищем в subRows
if ( if (
row.subRows && row.subRows &&
Array.isArray(row.subRows) && Array.isArray(row.subRows) &&
@ -92,3 +91,37 @@ export function updateExistingStructure(existingRows, updateItems) {
return updatedRows; 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;
const newRowObject = {
row_type: row_type,
depth: depth,
sort_order: sort_order,
subRows: [],
data: dataRow,
};
function findAndUpdate(rows) {
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (row.data.header.expense_item_id === expense_item_id) {
if (!("subRows" in row)) {
row.subRows = [];
}
row.subRows.push(newRowObject);
return true;
}
if (row.subRows && Array.isArray(row.subRows) && row.subRows.length > 0) {
const found = findAndUpdate(row.subRows);
if (found) return true;
}
}
return false;
}
findAndUpdate(updatedRows);
return updatedRows;
}

View File

@ -22,9 +22,10 @@ const TableCard = ({ table, onNavigate }) => {
</div> </div>
<SectionTitle> <SectionTitle>
<h2>{table.name}</h2> <h2>{table.name}</h2>
<UsersList> <h2>{table?.direction}</h2>
{/* <UsersList>
<div className="img-user"></div> <div className="img-user"></div>
</UsersList> </UsersList> */}
</SectionTitle> </SectionTitle>
</SectionStartPart> </SectionStartPart>
<Right /> <Right />

View File

@ -22,7 +22,7 @@ const TablesList = ({
if (!formInfo){ if (!formInfo){
return; return;
} }
const path = `${basePath}/form/${formInfo.id}/form-type/${formInfo.form_type_code}/${table.sheet}`; const path = `${basePath}/form/${formInfo.id}/form-type/${formInfo.form_type_code}/${table.sheet}/${table?.direction}`;
return ( return (
<TableCard <TableCard

View File

@ -24,7 +24,7 @@ const TableInfo = () => {
}; };
export default function NewTablePage() { export default function NewTablePage() {
const { formId, formType, sheetName } = useParams() const { formId, formType, sheetName, direction } = useParams()
const { user } = useAuth(); const { user } = useAuth();
return ( return (
@ -34,8 +34,9 @@ export default function NewTablePage() {
formId={formId} formId={formId}
sheetName={sheetName} sheetName={sheetName}
userId={user.id} userId={user.id}
direction={direction === 'null' ? null : direction}
> >
<RealtimeTable formType={formType} formId={formId} sheetName={sheetName} /> <RealtimeTable formType={formType} formId={formId} sheetName={sheetName} direction={direction === 'null' ? null : direction} />
</RealtimeProvider> </RealtimeProvider>
)} )}

View File

@ -63,7 +63,13 @@ export default function TaskPage() {
setIsLoading(true); setIsLoading(true);
try { try {
const data = await TasksApi.getSheets(id); const data = await TasksApi.getSheets(id);
setTables(data.result.sheets.map(table => ({ name: SHEET_NAME[table], sheet: table }))); // Преобразуем данные с учетом direction
const mappedTables = data.result.all_sheets.map(sheet => ({
name: SHEET_NAME[sheet.sheet_name] || sheet.sheet_name,
sheet: sheet.sheet_name,
direction: sheet.direction
}));
setTables(mappedTables);
} catch (error) { } catch (error) {
toast.error( toast.error(
error?.response?.data?.detail || 'Ошибка получения данных задачи', error?.response?.data?.detail || 'Ошибка получения данных задачи',
@ -83,9 +89,9 @@ export default function TaskPage() {
setIsLoading(true); setIsLoading(true);
try { try {
const data = await TasksApi.getById(id); const data = await TasksApi.getById(id);
setTask(data.result) setTask(data.result);
} catch (error) { } catch (error) {
// обработка ошибки
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -97,13 +103,14 @@ export default function TaskPage() {
}, [id]); }, [id]);
const handleCreateProject = (projectData) => { const handleCreateProject = (projectData) => {
// логика создания проекта
}; };
// Фильтруем таблицы по query // Фильтруем таблицы по query (учитывая name и direction)
const filteredTables = tables.filter((item) => const filteredTables = tables.filter((item) => {
item.name.toLowerCase().includes(query.toLowerCase()), const searchString = `${item.name} ${item.direction || ''}`.toLowerCase();
); return searchString.includes(query.toLowerCase());
});
if (isLoading) { if (isLoading) {
return ( return (
@ -124,7 +131,6 @@ export default function TaskPage() {
return ( return (
<> <>
{/* <TaskInfo task={task} /> */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -145,14 +151,12 @@ export default function TaskPage() {
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', gap: 2, justifyContent: 'space-between' }}>
<SearchComponent <SearchComponent
placeholder="Поиск по названию таблицы" placeholder="Поиск по названию или направлению"
value={query} value={query}
onChange={setQuery} onChange={setQuery}
/> />
<Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}> <Stack sx={{ flexDirection: 'row', spacing: '.5rem', justifyContent: 'end' }}>
<ExportWithTextButton onClick={handleExport} /> <ExportWithTextButton onClick={handleExport} />
<PrimaryOutlinedButton <PrimaryOutlinedButton
variant="outlined" variant="outlined"
onClick={() => setModalStageOpen(true)} onClick={() => setModalStageOpen(true)}