Files
iistwin/server/routes/form.statuses.routes.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

226 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Router } from "express";
import { storage } from "../storage";
import { requireFormAdminOrPermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { formsCache } from "../utils/cache";
import { indexFormWithSummaryAsync } from "./task-helpers";
import { type Task } from "@shared/schema";
export function registerFormStatusRoutes(router: ReturnType<typeof import("express").Router>): void {
// === FORM STATUSES ENDPOINTS ===
/**
* @swagger
* /api/forms/{id}/statuses:
* get:
* tags: [Forms]
* summary: Статусы формы
* post:
* tags: [Forms]
* summary: Создать статус
*/
router.get('/api/forms/:id/statuses', 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 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: 'Нет доступа к этой форме' });
}
}
const statuses = await storage.getFormStatuses(formId, req.organizationId!);
res.json({ success: true, statuses });
} catch (error) {
console.error('Get form statuses error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении статусов формы' });
}
});
router.post('/api/forms/:id/statuses',
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: 'Форма не найдена' });
}
const { statuses } = req.body;
if (!Array.isArray(statuses)) {
return res.status(400).json({ success: false, error: 'Статусы должны быть массивом' });
}
const existingStatuses = await storage.getFormStatuses(formId, req.organizationId!);
const incomingIds = statuses.filter((s: Record<string, unknown>) => s.id).map((s: Record<string, unknown>) => s.id as number);
const statusesToDelete = existingStatuses.filter(existing => !incomingIds.includes(existing.id));
if (statusesToDelete.length > 0) {
const tasks = await storage.getTasksByForm(formId, req.organizationId!);
const usedStatusIds = new Set(tasks.map((t: Task) => t.currentStatusId));
for (const status of statusesToDelete) {
if (usedStatusIds.has(status.id)) {
return res.status(400).json({
success: false,
error: `Невозможно удалить статус "${status.name}" - он используется в существующих задачах. Сначала измените статус всех задач, использующих этот статус.`
});
}
}
}
const statusIdsToDelete = statusesToDelete.map(s => s.id);
const resultStatuses = await storage.bulkSaveFormStatuses(formId, req.organizationId!, statusIdsToDelete, statuses);
formsCache.invalidatePrefix(`forms:${req.organizationId}`);
indexFormWithSummaryAsync(formId, req.organizationId!).catch(() => {});
res.status(201).json({ success: true, message: 'Статусы сохранены', statuses: resultStatuses });
} catch (error) {
console.error('Save form statuses error:', error);
res.status(500).json({ success: false, error: 'Ошибка при сохранении статусов' });
}
}
);
// === STATUS TRANSITIONS ENDPOINTS ===
/**
* @swagger
* /api/forms/{id}/transitions:
* get:
* tags: [Forms]
* summary: Переходы между статусами
* post:
* tags: [Forms]
* summary: Создать переход
*/
router.get('/api/forms/:id/transitions',
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 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: 'Нет доступа к этой форме' });
}
}
const transitions = await storage.getStatusTransitions(formId, req.organizationId!);
res.json({ success: true, transitions });
} catch (error) {
console.error('Get transitions error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении переходов' });
}
}
);
router.post('/api/forms/:id/transitions',
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: 'Форма не найдена' });
}
const {
fromStatusId, toStatusId, rejectToStatusId, actionType, actionName,
conditions, requiredRole, assigneeUserId, assigneeConditions,
approveButtonText, approveButtonColor, rejectButtonText, rejectButtonColor
} = req.body;
if (!fromStatusId || !toStatusId || !actionType) {
return res.status(400).json({ success: false, error: 'Необходимо указать fromStatusId, toStatusId и actionType' });
}
const transitionData = {
formId, fromStatusId, toStatusId,
rejectToStatusId: rejectToStatusId || null,
actionType, actionName, conditions, requiredRole, assigneeUserId, assigneeConditions,
approveButtonText: approveButtonText || 'Одобрить',
approveButtonColor: approveButtonColor || '#22c55e',
rejectButtonText: rejectButtonText || 'Отклонить',
rejectButtonColor: rejectButtonColor || '#ef4444'
};
const transition = await storage.createStatusTransition(transitionData);
res.status(201).json({ success: true, message: 'Переход создан', transition });
} catch (error) {
console.error('Create transition error:', error);
res.status(500).json({ success: false, error: 'Ошибка при создании перехода' });
}
}
);
router.put('/api/forms/:id/transitions/:transitionId',
requireFormAdminOrPermission('forms.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const formId = parseInt(req.params.id);
const transitionId = parseInt(req.params.transitionId);
if (isNaN(formId) || isNaN(transitionId)) {
return res.status(400).json({ success: false, error: 'Неверные параметры' });
}
const existingForm = await storage.getForm(formId, req.organizationId!);
if (!existingForm) {
return res.status(404).json({ success: false, error: 'Форма не найдена' });
}
const transition = await storage.updateStatusTransition(transitionId, formId, req.organizationId!, req.body);
if (!transition) {
return res.status(404).json({ success: false, error: 'Переход не найден' });
}
res.json({ success: true, message: 'Переход обновлен', transition });
} catch (error) {
console.error('Update transition error:', error);
res.status(500).json({ success: false, error: 'Ошибка при обновлении перехода' });
}
}
);
router.delete('/api/forms/:id/transitions/:transitionId',
requireFormAdminOrPermission('forms.manage'),
async (req: AuthenticatedRequest, res) => {
try {
const formId = parseInt(req.params.id);
const transitionId = parseInt(req.params.transitionId);
if (isNaN(formId) || isNaN(transitionId)) {
return res.status(400).json({ success: false, error: 'Неверные параметры' });
}
const existingForm = await storage.getForm(formId, req.organizationId!);
if (!existingForm) {
return res.status(404).json({ success: false, error: 'Форма не найдена' });
}
await storage.deleteStatusTransition(transitionId, formId, req.organizationId!);
res.json({ success: true, message: 'Переход удален' });
} catch (error) {
console.error('Delete transition error:', error);
res.status(500).json({ success: false, error: 'Ошибка при удалении перехода' });
}
}
);
}