import { Router } from "express"; import { storage } from "../storage"; import { requireFormAdminOrPermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { validateRequest } from "../middleware/validation.middleware"; import { updateFormSchema } from "@shared/schema"; import { formsCache } from "../utils/cache"; import { formatUserName } from "../utils/formatUserName"; import { indexFormAsync } from "./task-helpers"; import { recalculateTaskTitles } from "./shared"; export function registerFormUpdateRoutes(router: ReturnType): void { /** * @swagger * /api/forms/{id}: * put: * tags: [Forms] * summary: Обновить форму (admin) */ router.put('/api/forms/:id', requireFormAdminOrPermission('forms.manage'), validateRequest(updateFormSchema), 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 updateData = { ...req.body }; const incomingSummary: string | undefined = updateData.aiSummary; const existingSummary: string = existingForm.aiSummary ?? ''; if (incomingSummary === '' && existingSummary) { delete updateData.aiSummary; } else if (!incomingSummary && !existingSummary) { const [fields, statuses, transitions, orgUsers, orgRolesDesc] = await Promise.all([ storage.getFormFields(formId, req.organizationId!), storage.getFormStatuses(formId, req.organizationId!), storage.getStatusTransitions(formId, req.organizationId!), storage.getUsersByOrganization(req.organizationId!), storage.getRoles(req.organizationId!), ]); const formName = updateData.name ?? existingForm.name ?? ''; const tmpl = (updateData.titleTemplate ?? existingForm.titleTemplate ?? '').trim(); const fieldNames = fields.filter(f => f.name).map(f => f.name).join(', '); const statusMap = new Map(statuses.map(s => [s.id, s.name])); const statusNames = statuses.map(s => s.name).join(', '); const fieldMap = new Map(fields.map(f => [f.code, f.name])); const userMap = new Map(orgUsers.map(u => [u.id, formatUserName(u) || u.email])); const roleMap = new Map(orgRolesDesc.map(r => [r.id, r.name])); const describeTransition = (t: typeof transitions[number]): string => { const from = statusMap.get(t.fromStatusId); const to = statusMap.get(t.toStatusId); if (!from || !to) return ''; const details: string[] = []; if (t.actionName) details.push(`кнопка «${t.actionName}»`); if (t.requiredRole === 'admin') details.push('только администраторы'); if (t.assigneeUserId) { const uname = userMap.get(t.assigneeUserId) || `пользователь #${t.assigneeUserId}`; details.push(`согласует ${uname}`); } const ac = t.assigneeConditions as Record; if (ac) { const acRules = (ac.rules as Record[] | undefined) || (ac.field && ac.assigneeId ? [{ fieldCode: ac.field, value: ac.value, assigneeUserId: ac.assigneeId }] : []); acRules.forEach((r: Record) => { const fname = fieldMap.get(String(r.fieldCode)) || r.fieldCode; if (r.assigneeSource === 'role' && r.roleId) { const rname = roleMap.get(Number(r.roleId)) || `роль #${r.roleId}`; if (fname) details.push(`если поле «${fname}»="${r.value || '...'}" → согласует роль «${rname}»`); } else { const uname = r.assigneeUserId ? (userMap.get(Number(r.assigneeUserId)) || `пользователь #${r.assigneeUserId}`) : ''; if (fname && uname) details.push(`если поле «${fname}»="${r.value || '...'}" → согласует ${uname}`); } }); if (ac.fallbackAssigneeUserId) { const u = userMap.get(Number(ac.fallbackAssigneeUserId)) || `пользователь #${ac.fallbackAssigneeUserId}`; details.push(`по умолчанию согласует ${u}`); } } const cond = t.conditions as Record; if (cond) { (cond.routingRules as Array> || []).forEach((r) => { const fname = fieldMap.get(r.fieldCode as unknown as string) || r.fieldCode; const sname = statusMap.get(Number(r.toStatusId)) || `статус #${r.toStatusId}`; if (fname) details.push(`если «${fname}»="${r.value}" → ${sname}`); }); if (cond.defaultToStatusId) { const sname = statusMap.get(Number(cond.defaultToStatusId)); if (sname) details.push(`по умолчанию → ${sname}`); } (cond.rejectRoutingRules as Array> || []).forEach((r) => { const fname = fieldMap.get(r.fieldCode as unknown as string) || r.fieldCode; const sname = statusMap.get(Number(r.toStatusId)) || `статус #${r.toStatusId}`; if (fname) details.push(`при отклонении если «${fname}»="${r.value}" → ${sname}`); }); } if (t.rejectToStatusId) { const sname = statusMap.get(t.rejectToStatusId); if (sname) details.push(`при отклонении → ${sname}`); } const base = `${from} → ${to}`; return details.length ? `${base} (${details.join(', ')})` : base; }; const transitionLines = transitions.map(describeTransition).filter(Boolean).join('; '); const parts: string[] = []; if (formName) parts.push(`Форма: ${formName}`); if (tmpl) parts.push(`Шаблон названия: ${tmpl}`); if (fieldNames) parts.push(`Поля: ${fieldNames}`); if (statusNames) parts.push(`Статусы: ${statusNames}`); if (transitionLines) parts.push(`Переходы: ${transitionLines}`); if (parts.length) updateData.aiSummary = parts.join('. ') + '.'; } const updatedForm = await storage.updateForm(formId, req.organizationId!, updateData); formsCache.invalidatePrefix(`forms:${req.organizationId}`); indexFormAsync(formId, req.organizationId!).catch(() => {}); // If titleTemplate changed, recalculate all task titles for this form const oldTemplate = existingForm.titleTemplate ?? ''; const newTemplate = updateData.titleTemplate ?? ''; let recalculation = null; if (oldTemplate !== newTemplate && newTemplate) { recalculation = await recalculateTaskTitles(formId, req.organizationId!); } let message = 'Форма обновлена'; if (recalculation) { if (recalculation.errors.length > 0) { message += ` (названия обновлены: ${recalculation.updated} из ${recalculation.total}, ошибок: ${recalculation.errors.length})`; } else if (recalculation.updated > 0) { message += ` (названия обновлены: ${recalculation.updated} из ${recalculation.total})`; } } res.json({ success: true, message, form: updatedForm, recalculation }); } catch (error) { console.error('Update form error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении формы' }); } } ); }