896 lines
34 KiB
TypeScript
896 lines
34 KiB
TypeScript
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 } = 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);
|
||
|
||
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 search = String(req.query.search || '').toLowerCase();
|
||
|
||
const displayColumnIndex = table.treeDisplayColumnIndex ?? 0;
|
||
|
||
const getRowLabel = (row: typeof rows[number]): string => {
|
||
const values = Array.isArray(row.values) ? row.values : [];
|
||
return String(values[displayColumnIndex] ?? values[0] ?? '') || '(без названия)';
|
||
};
|
||
|
||
type TreeNode =
|
||
| { type: 'group'; id: string; label: string; children: TreeNode[] }
|
||
| { type: 'row'; id: number; label: string; row: typeof rows[number] };
|
||
|
||
const buildParentTree = (parentRows: typeof rows, parentId: number | null): 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);
|
||
const label = getRowLabel(row);
|
||
if (children.length > 0) {
|
||
return { type: 'group' as const, id: `row-group:${row.id}`, label, children: [...children, { type: 'row' as const, id: row.id, label, row }] };
|
||
}
|
||
return { type: 'row' as const, id: row.id, label, row };
|
||
});
|
||
};
|
||
|
||
const buildColumnTree = (allRows: typeof rows, columnIndex: number): TreeNode[] => {
|
||
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) {
|
||
const sorted = [...groupRows].sort((a, b) => a.position - b.position);
|
||
nodes.push({
|
||
type: 'group',
|
||
id: `group:${label}`,
|
||
label,
|
||
children: sorted.map((row) => ({ type: 'row', id: row.id, label: getRowLabel(row), row })),
|
||
});
|
||
}
|
||
if (rootRows.length > 0) {
|
||
rootRows.sort((a, b) => a.position - b.position);
|
||
nodes.push(...rootRows.map((row) => ({ type: 'row' as const, id: row.id, label: getRowLabel(row), row })));
|
||
}
|
||
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 = treeColumnIndex != null
|
||
? buildColumnTree(rows, treeColumnIndex)
|
||
: buildParentTree(rows, null);
|
||
const nodes = search ? filterNodes(rawNodes, search) : rawNodes;
|
||
|
||
res.json({ columnIndex: treeColumnIndex, 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: 'Ошибка записи события экспорта' });
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|