fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
225
server/routes/form.statuses.routes.ts
Normal file
225
server/routes/form.statuses.routes.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
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: 'Ошибка при удалении перехода' });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user