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 = {}; 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[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 = 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(); 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 = 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 = 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(); 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 = {}; 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: 'Ошибка записи события экспорта' }); } } ); }