Files
iistwin/server/routes/data-tables.routes.ts

937 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express';
import path from 'path';
import { storage } from "../storage";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { logAudit } from "../utils/audit";
import {
getDirectoryFolders,
createDirectoryFolder,
updateDirectoryFolder,
deleteDirectoryFolder,
moveDirectoryToFolder,
reorderDirectoryFolders,
} from "../storage/folder.storage";
export function registerDataTableRoutes(app: import("express").Express): void {
// =====================================================
// DATA TABLES API (Справочники / Directories)
// =====================================================
/**
* @swagger
* /api/tables:
* get:
* tags: [Tables]
* summary: Список справочников (data tables)
* responses:
* 200:
* description: Справочники организации
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Table'
* post:
* tags: [Tables]
* summary: Создать справочник (admin)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [name, code]
* properties:
* name:
* type: string
* code:
* type: string
* columns:
* type: array
* items:
* type: object
* responses:
* 201:
* description: Справочник создан
* /api/tables/{id}:
* get:
* tags: [Tables]
* summary: Получить справочник
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Данные справочника
* patch:
* tags: [Tables]
* summary: Обновить справочник
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* columns:
* type: array
* items:
* type: object
* responses:
* 200:
* description: Обновлён
* delete:
* tags: [Tables]
* summary: Удалить справочник (admin)
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Удалён
* /api/tables/{tableId}/rows:
* get:
* tags: [Tables]
* summary: Строки справочника
* parameters:
* - name: tableId
* in: path
* required: true
* schema:
* type: integer
* - name: search
* in: query
* schema:
* type: string
* - name: page
* in: query
* schema:
* type: integer
* - name: limit
* in: query
* schema:
* type: integer
* responses:
* 200:
* description: Строки
* post:
* tags: [Tables]
* summary: Добавить строку
* parameters:
* - name: tableId
* in: path
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* description: Map columnCode → value
* responses:
* 201:
* description: Строка добавлена
*/
// Get all directories/tables accessible to current user
app.get(['/api/tables', '/api/directories'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tables = await storage.getDataTablesWithAccess(req.user!.id, req.organizationId!);
res.json({ tables });
} catch (error) {
console.error('Get tables error:', error);
res.status(500).json({ error: 'Ошибка получения списка таблиц' });
}
}
);
// =====================================================
// DIRECTORY FOLDERS (must be defined BEFORE /api/directories/:id)
// =====================================================
app.get('/api/directories/folders', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const folders = await getDirectoryFolders(req.organizationId!);
res.json({ success: true, folders });
} catch (error) {
console.error('Get directory folders error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении папок' });
}
});
app.post('/api/directories/folders', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const { name, parentId, position } = req.body;
if (!name || typeof name !== 'string') {
return res.status(400).json({ success: false, error: 'Название папки обязательно' });
}
const folder = await createDirectoryFolder({
organizationId: req.organizationId!,
name,
parentId: parentId ?? null,
position: position ?? 0,
});
res.status(201).json({ success: true, folder });
} catch (error) {
console.error('Create directory folder error:', error);
res.status(500).json({ success: false, error: 'Ошибка при создании папки' });
}
});
app.put('/api/directories/folders/:id', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const id = parseInt(req.params.id);
const { name, parentId, position } = req.body;
const folder = await updateDirectoryFolder(id, req.organizationId!, { name, parentId, position });
if (!folder) {
return res.status(404).json({ success: false, error: 'Папка не найдена' });
}
res.json({ success: true, folder });
} catch (error) {
console.error('Update directory folder error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении папки' });
}
});
app.delete('/api/directories/folders/:id', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const id = parseInt(req.params.id);
await deleteDirectoryFolder(id, req.organizationId!);
res.json({ success: true });
} catch (error) {
console.error('Delete directory folder error:', error);
res.status(500).json({ success: false, error: 'Ошибка при удалении папки' });
}
});
app.patch('/api/directories/folders/reorder', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const { updates } = req.body;
if (!Array.isArray(updates)) {
return res.status(400).json({ success: false, error: 'Некорректные данные' });
}
await reorderDirectoryFolders(req.organizationId!, updates);
res.json({ success: true });
} catch (error) {
console.error('Reorder directory folders error:', error);
res.status(500).json({ success: false, error: 'Ошибка при изменении порядка' });
}
});
app.patch('/api/directories/:id/folder', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const id = parseInt(req.params.id);
const { folderId } = req.body;
const table = await moveDirectoryToFolder(id, req.organizationId!, folderId ?? null);
if (!table) {
return res.status(404).json({ success: false, error: 'Справочник не найден' });
}
res.json({ success: true, table });
} catch (error) {
console.error('Move directory to folder error:', error);
res.status(500).json({ success: false, error: 'Ошибка при перемещении справочника' });
}
});
// Get single directory/table with rows
app.get(['/api/tables/:id', '/api/directories/:id'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const table = await storage.getDataTableFull(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) {
return res.status(403).json({ error: 'Нет доступа к таблице' });
}
res.json({ table, role });
} catch (error) {
console.error('Get table error:', error);
res.status(500).json({ error: 'Ошибка получения таблицы' });
}
}
);
// Create directory/table
app.post(['/api/tables', '/api/directories'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const { name, description, columns } = req.body;
if (!name || !columns || columns.length === 0) {
return res.status(400).json({ error: 'Название и колонки обязательны' });
}
const table = await storage.createDataTable({
name,
description,
columns,
organizationId: req.organizationId!,
createdBy: req.user!.id
});
res.status(201).json({ table });
} catch (error) {
console.error('Create table error:', error);
res.status(500).json({ error: 'Ошибка создания таблицы' });
}
}
);
// Update directory/table
app.patch(['/api/tables/:id', '/api/directories/:id'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для редактирования таблицы' });
}
const { name, description, columns, treeColumnIndex, treeDisplayColumnIndex, treeColumns, treeDisplayColumns } = req.body;
const updates: Record<string, unknown> = {};
if (name !== undefined) updates.name = name;
if (description !== undefined) updates.description = description;
if (columns !== undefined) updates.columns = columns;
if (treeColumnIndex !== undefined) updates.treeColumnIndex = treeColumnIndex === null ? null : Number(treeColumnIndex);
if (treeDisplayColumnIndex !== undefined) updates.treeDisplayColumnIndex = treeDisplayColumnIndex === null ? null : Number(treeDisplayColumnIndex);
if (treeColumns !== undefined) updates.treeColumns = Array.isArray(treeColumns) ? treeColumns : null;
if (treeDisplayColumns !== undefined) updates.treeDisplayColumns = Array.isArray(treeDisplayColumns) ? treeDisplayColumns : null;
const updatedTable = await storage.updateDataTable(tableId, req.organizationId!, updates as Parameters<typeof storage.updateDataTable>[2]);
res.json({ table: updatedTable });
} catch (error) {
console.error('Update table error:', error);
res.status(500).json({ error: 'Ошибка обновления таблицы' });
}
}
);
// Delete directory/table
app.delete(['/api/tables/:id', '/api/directories/:id'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для удаления таблицы' });
}
await storage.deleteDataTable(tableId, req.organizationId!);
res.json({ success: true });
} catch (error) {
console.error('Delete table error:', error);
res.status(500).json({ error: 'Ошибка удаления таблицы' });
}
}
);
// =====================================================
// DATA TABLE ROWS API
// =====================================================
// Column values for directories (autocomplete for filters)
app.get(['/api/tables/:tableId/column-values', '/api/directories/:tableId/column-values'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const columnIdx = parseInt(req.query.column as string);
const search = ((req.query.search as string) || '').toLowerCase();
const contextStr = req.query.context as string;
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) return res.status(404).json({ error: 'Таблица не найдена' });
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) return res.status(403).json({ error: 'Нет доступа' });
let rows = await storage.getDataTableRows(tableId, req.organizationId!);
if (contextStr) {
try {
const context: Record<string, string> = JSON.parse(contextStr);
Object.entries(context).forEach(([col, val]) => {
const ci = parseInt(col);
if (!isNaN(ci) && val) {
rows = rows.filter(r => {
const v = Array.isArray(r.values) ? r.values : [];
return String(v[ci] || '').toLowerCase().includes(val.toLowerCase());
});
}
});
} catch {}
}
const seen = new Set<string>();
const values: string[] = [];
for (const row of rows) {
const rv = Array.isArray(row.values) ? row.values : [];
const val = String(rv[columnIdx] || '').trim();
if (!val || seen.has(val)) continue;
if (search && !val.toLowerCase().includes(search)) continue;
seen.add(val);
values.push(val);
if (values.length >= 20) break;
}
res.json({ values });
} catch (error) {
console.error('Column values error:', error);
res.status(500).json({ error: 'Ошибка получения значений' });
}
}
);
// Get directory/table rows (with optional filter/sort)
app.get(['/api/tables/:tableId/rows', '/api/directories/:tableId/rows'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) {
return res.status(403).json({ error: 'Нет доступа к таблице' });
}
let rows = await storage.getDataTableRows(tableId, req.organizationId!);
const filtersStr = req.query.filters as string;
if (filtersStr) {
try {
const filters: Record<string, string> = JSON.parse(filtersStr);
Object.entries(filters).forEach(([col, val]) => {
const ci = parseInt(col);
if (!isNaN(ci) && val) {
rows = rows.filter(r => {
const v = Array.isArray(r.values) ? r.values : [];
return String(v[ci] || '').toLowerCase().includes(val.toLowerCase());
});
}
});
} catch {}
}
const sortColumn = req.query.sortColumn as string;
const sortDir = req.query.sortDirection as string;
if (sortColumn !== undefined && (sortDir === 'asc' || sortDir === 'desc')) {
const ci = parseInt(sortColumn);
if (!isNaN(ci)) {
rows = [...rows].sort((a, b) => {
const av = Array.isArray(a.values) ? a.values : [];
const bv = Array.isArray(b.values) ? b.values : [];
const aStr = String(av[ci] || '').toLowerCase();
const bStr = String(bv[ci] || '').toLowerCase();
const cmp = aStr.localeCompare(bStr, 'ru');
return sortDir === 'asc' ? cmp : -cmp;
});
}
}
res.json({ rows });
} catch (error) {
console.error('Get table rows error:', error);
res.status(500).json({ error: 'Ошибка получения строк' });
}
}
);
// Get table rows as a tree (grouped by treeColumnIndex or parentId hierarchy)
app.get(['/api/tables/:tableId/rows/tree', '/api/directories/:tableId/rows/tree'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) {
return res.status(403).json({ error: 'Нет доступа' });
}
let rows = await storage.getDataTableRows(tableId, req.organizationId!);
const treeColumnIndex = table.treeColumnIndex;
const treeDisplayColumnIndex = table.treeDisplayColumnIndex;
const search = String(req.query.search || '').toLowerCase();
const filtersStr = req.query.filters as string;
if (filtersStr) {
try {
const filters: Record<string, string> = JSON.parse(filtersStr);
Object.entries(filters).forEach(([col, val]) => {
const ci = parseInt(col);
if (!isNaN(ci) && val) {
rows = rows.filter((r) => {
const v = Array.isArray(r.values) ? r.values : [];
return String(v[ci] || '').toLowerCase() === String(val).toLowerCase();
});
}
});
} catch {}
}
const treeColumns = table.treeColumns ?? (treeColumnIndex != null ? [treeColumnIndex] : []);
const treeDisplayColumns = table.treeDisplayColumns ?? (treeDisplayColumnIndex != null ? [treeDisplayColumnIndex] : [0]);
const displayColumnIndex = treeDisplayColumnIndex ?? treeDisplayColumns[0] ?? 0;
const getRowLabel = (row: typeof rows[number]): string => {
const values = Array.isArray(row.values) ? row.values : [];
const parts = treeDisplayColumns
.map((ci) => String(values[ci] ?? '').trim())
.filter(Boolean);
return parts.length > 0 ? parts.join(' ') : '(без названия)';
};
type TreeNode =
| { type: 'group'; id: string; label: string; level: number; children: TreeNode[] }
| { type: 'row'; id: number; label: string; level: number; row: typeof rows[number] };
const buildParentTree = (parentRows: typeof rows, parentId: number | null, level = 0): TreeNode[] => {
const items = parentRows
.filter((r) => (r.parentId ?? null) === parentId)
.sort((a, b) => a.position - b.position);
return items.map((row) => {
const children = buildParentTree(parentRows, row.id, level + 1);
const label = getRowLabel(row);
if (children.length > 0) {
return { type: 'group' as const, id: `row-group:${row.id}`, label, level, children: [...children, { type: 'row' as const, id: row.id, label, level: level + 1, row }] };
}
return { type: 'row' as const, id: row.id, label, level, row };
});
};
const buildMultiColumnTree = (allRows: typeof rows, columns: number[], level = 0): TreeNode[] => {
if (level >= columns.length) {
return allRows
.sort((a, b) => a.position - b.position)
.map((row) => ({ type: 'row' as const, id: row.id, label: getRowLabel(row), level, row }));
}
const columnIndex = columns[level];
const groups = new Map<string, typeof rows[number][]>();
const rootRows: typeof rows[number][] = [];
for (const row of allRows) {
const vals = Array.isArray(row.values) ? row.values : [];
const key = String(vals[columnIndex] ?? '').trim();
if (!key) {
rootRows.push(row);
continue;
}
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(row);
}
const nodes: TreeNode[] = [];
for (const [label, groupRows] of groups) {
nodes.push({
type: 'group',
id: `group:${level}:${label}`,
label,
level,
children: buildMultiColumnTree(groupRows, columns, level + 1),
});
}
if (rootRows.length > 0) {
nodes.push(...buildMultiColumnTree(rootRows, columns, level + 1));
}
return nodes.sort((a, b) => {
const al = a.type === 'group' ? a.label : getRowLabel(a.row);
const bl = b.type === 'group' ? b.label : getRowLabel(b.row);
return al.localeCompare(bl, 'ru');
});
};
const filterNodes = (nodes: TreeNode[], query: string): TreeNode[] => {
if (!query) return nodes;
const result: TreeNode[] = [];
for (const node of nodes) {
if (node.type === 'row') {
const label = node.label.toLowerCase();
if (label.includes(query)) result.push(node);
} else {
const children = filterNodes(node.children, query);
if (children.length > 0) {
result.push({ ...node, children });
} else if (node.label.toLowerCase().includes(query)) {
result.push({ ...node, children: [] });
}
}
}
return result;
};
const rawNodes = treeColumns.length > 0
? buildMultiColumnTree(rows, treeColumns)
: buildParentTree(rows, null);
const nodes = search ? filterNodes(rawNodes, search) : rawNodes;
res.json({
columnIndex: treeColumnIndex,
treeColumnIndex,
treeColumns,
treeDisplayColumns,
displayColumnIndex,
nodes,
});
} catch (error) {
console.error('Get table rows tree error:', error);
res.status(500).json({ error: 'Ошибка получения дерева строк' });
}
}
);
// Create table row
app.post(['/api/tables/:tableId/rows', '/api/directories/:tableId/rows'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role || role === 'reader') {
return res.status(403).json({ error: 'Нет прав для добавления строк' });
}
const { values, parentId } = req.body;
if (!values || !Array.isArray(values)) {
return res.status(400).json({ error: 'Значения обязательны' });
}
const row = await storage.createDataTableRow({ tableId, values, parentId: parentId ?? null });
res.status(201).json({ row });
} catch (error) {
console.error('Create table row error:', error);
res.status(500).json({ error: 'Ошибка создания строки' });
}
}
);
// Update table row (values and/or hierarchy)
app.patch(['/api/tables/:tableId/rows/:rowId', '/api/directories/:tableId/rows/:rowId'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const rowId = parseInt(req.params.rowId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role || role === 'reader') {
return res.status(403).json({ error: 'Нет прав для редактирования строк' });
}
const { values, parentId, position } = req.body;
let row;
if (parentId !== undefined || position !== undefined) {
row = await storage.moveDataTableRow(
rowId,
tableId,
req.organizationId!,
parentId ?? null,
position ?? 0
);
}
if (values !== undefined) {
if (!Array.isArray(values)) {
return res.status(400).json({ error: 'Значения должны быть массивом' });
}
row = await storage.updateDataTableRow(rowId, tableId, req.organizationId!, { values });
}
if (!row) {
return res.status(400).json({ error: 'Не переданы данные для обновления' });
}
res.json({ row });
} catch (error: any) {
console.error('Update table row error:', error);
res.status(400).json({ error: error.message || 'Ошибка обновления строки' });
}
}
);
// Delete table row
app.delete(['/api/tables/:tableId/rows/:rowId', '/api/directories/:tableId/rows/:rowId'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const rowId = parseInt(req.params.rowId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role || role === 'reader') {
return res.status(403).json({ error: 'Нет прав для удаления строк' });
}
await storage.deleteDataTableRow(rowId, tableId, req.organizationId!);
res.json({ success: true });
} catch (error) {
console.error('Delete table row error:', error);
res.status(500).json({ error: 'Ошибка удаления строки' });
}
}
);
// =====================================================
// DATA TABLE LINKS API (подсправочники)
// =====================================================
// Get subdirectories for a parent table
app.get(['/api/tables/:id/subdirectories', '/api/directories/:id/subdirectories'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) {
return res.status(403).json({ error: 'Нет доступа к таблице' });
}
const links = await storage.getDataTableLinks(tableId, req.organizationId!);
res.json({ subdirectories: links });
} catch (error) {
console.error('Get subdirectories error:', error);
res.status(500).json({ error: 'Ошибка получения подсправочников' });
}
}
);
// Create a subdirectory link
app.post(['/api/tables/:id/subdirectories', '/api/directories/:id/subdirectories'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для управления таблицей' });
}
const { childTableId, parentColumnIndex, childColumnIndex, parentValues } = req.body;
if (!childTableId || typeof childTableId !== 'number') {
return res.status(400).json({ error: 'Необходимо указать подсправочник (childTableId)' });
}
const childTable = await storage.getDataTable(childTableId, req.organizationId!);
if (!childTable) {
return res.status(404).json({ error: 'Подсправочник не найден' });
}
const link = await storage.createDataTableLink({
organizationId: req.organizationId!,
parentTableId: tableId,
childTableId,
parentColumnIndex: parentColumnIndex ?? 0,
childColumnIndex: childColumnIndex ?? 0,
parentValues: Array.isArray(parentValues) ? parentValues : [],
});
res.status(201).json({ link });
} catch (error: any) {
console.error('Create subdirectory error:', error);
res.status(400).json({ error: error.message || 'Ошибка создания подсправочника' });
}
}
);
// Update a subdirectory link
app.patch(['/api/tables/:id/subdirectories/:linkId', '/api/directories/:id/subdirectories/:linkId'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const linkId = parseInt(req.params.linkId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для управления таблицей' });
}
const existing = await storage.getDataTableLink(linkId, req.organizationId!);
if (!existing || existing.parentTableId !== tableId) {
return res.status(404).json({ error: 'Связь не найдена' });
}
const { childTableId, parentColumnIndex, childColumnIndex, parentValues } = req.body;
const updates: Partial<import("@shared/schema").DataTableLink> = {};
if (childTableId !== undefined) updates.childTableId = childTableId;
if (parentColumnIndex !== undefined) updates.parentColumnIndex = parentColumnIndex;
if (childColumnIndex !== undefined) updates.childColumnIndex = childColumnIndex;
if (parentValues !== undefined) updates.parentValues = Array.isArray(parentValues) ? parentValues : [];
const link = await storage.updateDataTableLink(linkId, req.organizationId!, updates);
res.json({ link });
} catch (error: any) {
console.error('Update subdirectory error:', error);
res.status(400).json({ error: error.message || 'Ошибка обновления подсправочника' });
}
}
);
// Delete a subdirectory link
app.delete(['/api/tables/:id/subdirectories/:linkId', '/api/directories/:id/subdirectories/:linkId'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.id);
const linkId = parseInt(req.params.linkId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (role !== 'admin') {
return res.status(403).json({ error: 'Нет прав для управления таблицей' });
}
const existing = await storage.getDataTableLink(linkId, req.organizationId!);
if (!existing || existing.parentTableId !== tableId) {
return res.status(404).json({ error: 'Связь не найдена' });
}
await storage.deleteDataTableLink(linkId, req.organizationId!);
res.json({ success: true });
} catch (error: any) {
console.error('Delete subdirectory error:', error);
res.status(400).json({ error: error.message || 'Ошибка удаления подсправочника' });
}
}
);
// Log data table Excel/CSV export
app.post(['/api/tables/:tableId/export', '/api/directories/:tableId/export'],
authenticateToken,
tenantIsolation,
async (req: AuthenticatedRequest, res) => {
try {
const tableId = parseInt(req.params.tableId);
const table = await storage.getDataTable(tableId, req.organizationId!);
if (!table) {
return res.status(404).json({ error: 'Таблица не найдена' });
}
const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!);
if (!role) {
return res.status(403).json({ error: 'Нет доступа к таблице' });
}
const exportFormat: string = req.body?.format ?? 'xlsx';
logAudit({
action: `export.table_${exportFormat}`,
userId: req.user!.id,
organizationId: req.organizationId!,
details: {
tableId,
tableName: table.name,
format: exportFormat,
},
ip: req.ip ?? null,
userAgent: req.headers['user-agent'] ?? null,
});
res.json({ success: true });
} catch (error) {
console.error('Table export log error:', error);
res.status(500).json({ error: 'Ошибка записи события экспорта' });
}
}
);
}