import { Router } from "express"; import { storage } from "../storage"; import { requireFormAdminOrPermission, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { validateRequest } from "../middleware/validation.middleware"; import { insertFormSchema, type Form } from "@shared/schema"; import { formsCache } from "../utils/cache"; import { logAudit, getClientIp } from "../utils/audit"; import { formatUserName } from "../utils/formatUserName"; import { indexFormWithSummaryAsync } from "./task-helpers"; import { recalculateTaskTitles } from "./shared"; export function registerFormCrudRoutes(router: ReturnType): void { async function filterFormsByAccess( forms: Form[], userId: number, organizationId: number, appRole: string ): Promise { const hasManageForms = await storage.hasAppRolePermission(appRole, ['forms.manage']); if (hasManageForms) return forms; const results = await Promise.all( forms.map(async (f) => { const canAccess = await storage.canUserAccessForm(userId, f.id, organizationId, 'participate'); return canAccess ? f : null; }) ); return results.filter(Boolean) as Form[]; } router.get('/api/forms', async (req: AuthenticatedRequest, res) => { try { let filtersObj: Record = {}; try { filtersObj = req.query.filters ? JSON.parse(req.query.filters as string) : {}; } catch {} const sortColumn = (req.query.sortColumn as string) || ''; const sortDirection = (req.query.sortDirection as string) === 'desc' ? 'desc' : 'asc'; const hasFilters = Object.keys(filtersObj).some(k => filtersObj[k]) || !!sortColumn; const getFormColVal = (f: Form, col: string): string => { if (col === 'name') return f.name || ''; if (col === 'status') return f.isActive ? 'Активна' : 'Неактивна'; if (col === 'createdAt') return f.createdAt ? new Date(f.createdAt).toLocaleDateString('ru-RU') : ''; return ''; }; const userId = req.user!.id; const organizationId = req.organizationId!; const appRole = req.user!.appRole; const hasManageForms = await storage.hasAppRolePermission(appRole, ['forms.manage']); if (!hasFilters) { if (hasManageForms) { const cacheKey = `forms:${organizationId}`; const cached = formsCache.get(cacheKey); if (cached) { return res.json({ success: true, forms: cached }); } const forms = await storage.getFormsByOrganization(organizationId); formsCache.set(cacheKey, forms); return res.json({ success: true, forms }); } // Non-admins: load without cache and filter by access const forms = await storage.getFormsByOrganization(organizationId); const accessible = await filterFormsByAccess(forms, userId, organizationId, appRole); return res.json({ success: true, forms: accessible }); } let forms = await storage.getFormsByOrganization(organizationId); Object.entries(filtersObj).forEach(([col, val]) => { if (!val) return; const vl = (val as string).toLowerCase(); forms = forms.filter(f => getFormColVal(f, col).toLowerCase().includes(vl)); }); if (sortColumn) { forms = forms.sort((a, b) => { if (sortColumn === 'createdAt' || sortColumn === 'updatedAt') { const _av = (a as Record)[sortColumn]; const _bv = (b as Record)[sortColumn]; const av = _av ? new Date(String(_av)).getTime() : 0; const bv = _bv ? new Date(String(_bv)).getTime() : 0; return sortDirection === 'asc' ? av - bv : bv - av; } const av = getFormColVal(a, sortColumn).toLowerCase(); const bv = getFormColVal(b, sortColumn).toLowerCase(); const cmp = av.localeCompare(bv, 'ru'); return sortDirection === 'asc' ? cmp : -cmp; }); } if (!hasManageForms) { forms = await filterFormsByAccess(forms, userId, organizationId, appRole); } res.json({ success: true, forms }); } catch (error) { console.error('Get forms error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении форм' }); } }); router.get('/api/forms/column-values', async (req: AuthenticatedRequest, res) => { try { const column = (req.query.column as string) || ''; const search = ((req.query.search as string) || '').toLowerCase(); let context: Record = {}; try { context = req.query.context ? JSON.parse(req.query.context as string) : {}; } catch {} const organizationId = req.organizationId!; let forms = await storage.getFormsByOrganization(organizationId); const getFormColVal2 = (f: Form, col: string): string => { if (col === 'name') return f.name || ''; if (col === 'status') return f.isActive ? 'Активна' : 'Неактивна'; if (col === 'createdAt') return f.createdAt ? new Date(f.createdAt).toLocaleDateString('ru-RU') : ''; return ''; }; Object.entries(context).forEach(([col, val]) => { if (!val) return; const vl = (val as string).toLowerCase(); forms = forms.filter(f => getFormColVal2(f, col).toLowerCase().includes(vl)); }); forms = await filterFormsByAccess(forms, req.user!.id, organizationId, req.user!.appRole); const seen = new Set(); const values: string[] = []; for (const f of forms) { const val = getFormColVal2(f, column); 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) { res.status(500).json({ error: 'Ошибка получения значений' }); } } ); /** * @swagger * /api/forms/{id}: * get: * tags: [Forms] * summary: Получить форму */ router.get('/api/forms/:id(\\d+)', async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const form = await storage.getForm(formId, req.organizationId!); if (!form) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } const hasManageForms = await storage.hasAppRolePermission(req.user!.appRole, ['forms.manage']); if (!hasManageForms) { const canAccess = await storage.canUserAccessForm(req.user!.id, formId, req.organizationId!, 'participate'); if (!canAccess) { return res.status(403).json({ success: false, error: 'Нет доступа к этой форме' }); } } res.json({ success: true, form }); } catch (error) { console.error('Get form error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении формы' }); } }); router.get('/api/forms/:id/editor', async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const organizationId = req.organizationId!; const [form, fields, statuses, transitions, tabs, rawUsers, orgRoles, canManageForm] = await Promise.all([ storage.getForm(formId, organizationId), storage.getFormFields(formId, organizationId), storage.getFormStatuses(formId, organizationId), storage.getStatusTransitions(formId, organizationId), storage.getFormTabs(formId, organizationId), storage.getUsersByOrganization(organizationId), storage.getRoles(organizationId), storage.canUserAccessForm(req.user!.id, formId, organizationId, 'admin') ]); if (!form) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } const users = rawUsers.map(user => ({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, fullName: formatUserName(user), position: user.position, appRole: user.appRole, isActive: user.isActive, emailVerified: user.emailVerified, lastLogin: user.lastLogin, createdAt: user.createdAt })); res.json({ success: true, form, fields, statuses, transitions, tabs, users, roles: orgRoles.map(r => ({ id: r.id, name: r.name })), canManageForm }); } catch (error) { console.error('Get form editor data error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении данных формы' }); } }); router.get('/api/forms/:id/export', requireFormAdminOrPermission('forms.manage'), async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const organizationId = req.organizationId!; const [form, fields, statuses, transitions, tabs] = await Promise.all([ storage.getForm(formId, organizationId), storage.getFormFields(formId, organizationId), storage.getFormStatuses(formId, organizationId), storage.getStatusTransitions(formId, organizationId), storage.getFormTabs(formId, organizationId), ]); if (!form) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } logAudit({ action: 'export.form_config', userId: req.user?.id ?? null, organizationId, details: { formId, formName: form.name }, ip: getClientIp(req), userAgent: req.headers['user-agent'] ?? null, }); res.json({ success: true, form: { id: form.id, name: form.name, description: form.description, isActive: form.isActive, chatLayout: form.chatLayout, chatEnabled: form.chatEnabled, titleTemplate: form.titleTemplate, }, tabs: tabs.map(tab => ({ id: tab.id, name: tab.name, code: tab.code, type: tab.type, position: tab.position, tableColumns: tab.tableColumns, })), fields: fields.map(field => ({ id: field.id, tabId: field.tabId, name: field.name, code: field.code, type: field.type, isRequired: field.isRequired, placeholder: field.placeholder, defaultValue: field.defaultValue, options: field.options, validationRules: field.validationRules, position: field.position, linkedFormId: field.linkedFormId, taskRelationType: field.taskRelationType, buttonActionType: field.buttonActionType, buttonUrl: field.buttonUrl, buttonFormId: field.buttonFormId, companyAutofill: field.companyAutofill, })), statuses: statuses.map(status => ({ id: status.id, name: status.name, color: status.color, position: status.position, isInitial: status.isInitial, isFinal: status.isFinal, })), transitions: transitions.map(tr => ({ id: tr.id, fromStatusId: tr.fromStatusId, toStatusId: tr.toStatusId, rejectToStatusId: tr.rejectToStatusId, actionType: tr.actionType, actionName: tr.actionName, requiredRole: tr.requiredRole, approveButtonText: tr.approveButtonText, approveButtonColor: tr.approveButtonColor, rejectButtonText: tr.rejectButtonText, rejectButtonColor: tr.rejectButtonColor, })), }); } catch (error) { console.error('Export form error:', error); res.status(500).json({ success: false, error: 'Ошибка при экспорте формы' }); } } ); router.post('/api/forms', requirePermission('forms.manage'), validateRequest(insertFormSchema.omit({ organizationId: true, createdBy: true })), async (req: AuthenticatedRequest, res) => { try { const formData = { ...req.body, organizationId: req.organizationId!, createdBy: req.user!.id }; const form = await storage.createForm(formData); formsCache.invalidatePrefix(`forms:${req.organizationId}`); indexFormWithSummaryAsync(form.id, req.organizationId!).catch(() => {}); res.status(201).json({ success: true, message: 'Форма создана', form }); } catch (error) { console.error('Create form error:', error); res.status(500).json({ success: false, error: 'Ошибка при создании формы' }); } } ); router.get('/api/forms/:id/tasks-count', async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const existingForm = await storage.getForm(formId, req.organizationId!); if (!existingForm) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } const counts = await storage.getTasksCountByForm(formId); res.json({ success: true, ...counts }); } catch (error) { console.error('Get tasks count error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении количества задач' }); } }); router.post('/api/forms/:id/recalculate-titles', requireFormAdminOrPermission('forms.manage'), async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const existingForm = await storage.getForm(formId, req.organizationId!); if (!existingForm) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } if (!existingForm.titleTemplate) { return res.status(400).json({ success: false, error: 'У формы нет шаблона названия' }); } const result = await recalculateTaskTitles(formId, req.organizationId!); res.json({ success: true, ...result }); } catch (error) { console.error('Recalculate titles error:', error); res.status(500).json({ success: false, error: 'Ошибка при пересчёте названий' }); } } ); router.delete('/api/forms/:id', requireFormAdminOrPermission('forms.manage'), async (req: AuthenticatedRequest, res) => { try { const formId = parseInt(req.params.id); if (isNaN(formId)) { return res.status(400).json({ success: false, error: 'Неверный ID формы' }); } const existingForm = await storage.getForm(formId, req.organizationId!); if (!existingForm) { return res.status(404).json({ success: false, error: 'Форма не найдена' }); } await storage.deleteForm(formId, req.organizationId!); formsCache.invalidatePrefix(`forms:${req.organizationId}`); res.json({ success: true, message: 'Форма удалена' }); } catch (error) { console.error('Delete form error:', error); res.status(500).json({ success: false, error: 'Ошибка при удалении формы' }); } } ); }