- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
151 lines
8.0 KiB
TypeScript
151 lines
8.0 KiB
TypeScript
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<typeof import("express").Router>): 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<string, unknown>;
|
|
if (ac) {
|
|
const acRules = (ac.rules as Record<string, unknown>[] | undefined) || (ac.field && ac.assigneeId ? [{ fieldCode: ac.field, value: ac.value, assigneeUserId: ac.assigneeId }] : []);
|
|
acRules.forEach((r: Record<string, unknown>) => {
|
|
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<string, unknown>;
|
|
if (cond) {
|
|
(cond.routingRules as Array<Record<string,unknown>> || []).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<Record<string,unknown>> || []).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: 'Ошибка при обновлении формы' });
|
|
}
|
|
}
|
|
);
|
|
}
|