fix(number-fields): избегаем потери точности длинных чисел

- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
2026-07-07 21:03:40 +03:00
commit 1f5ecb6da4
1089 changed files with 237546 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
import { Router } from "express";
import { storage } from "../storage";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
export function registerTaskCrudExtraRoutes(router: ReturnType<typeof import("express").Router>): void {
router.put('/api/tasks/:id/notifications/read', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
const userId = req.user!.id;
const organizationId = req.organizationId!;
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
console.log(`[tasks/:id/notifications/read] Marking read for taskId=${taskId}, userId=${userId}, orgId=${organizationId}`);
await storage.markTaskNotificationsAsRead(taskId, userId, organizationId);
res.json({ success: true });
} catch (err) {
console.error('[tasks/:id/notifications/read]', err);
res.status(500).json({ success: false, error: 'Ошибка сервера' });
}
});
router.get('/api/tasks/:id/audit-log', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
const task = await storage.getTask(taskId, req.organizationId!);
if (!task) {
return res.status(404).json({ success: false, error: 'Задача не найдена' });
}
const log = await storage.getTaskAuditLog(taskId, req.organizationId!);
res.json({ success: true, log });
} catch (error) {
console.error('Get task audit log error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении истории задачи' });
}
});
router.get('/api/tasks/:id/subtasks', async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
const subtasks = await storage.getSubtasks(taskId, req.organizationId!);
res.json({ success: true, subtasks });
} catch (error) {
console.error('Get subtasks error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении подзадач' });
}
});
router.get('/api/tasks/:id/tree', async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
const taskTree = await storage.getTaskTree(taskId, req.organizationId!);
if (!taskTree) {
return res.status(404).json({ success: false, error: 'Задача не найдена' });
}
res.json({ success: true, task: taskTree });
} catch (error) {
console.error('Get task tree error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении дерева задач' });
}
});
/**
* @swagger
* /api/tasks/{id}/related:
* get:
* tags: [Tasks]
* summary: Получить дерево связанных задач
* description: >
* Возвращает дерево задач, связанных с указанной через таблицу task_relations.
* Связи создаются либо через поля типа «задача» в форме, либо вручную через чат
* (кнопка «Привязать»). Каждый узел рекурсивно содержит своих родителей/детей
* в поле `nodes` (глубина до 10 уровней). Поле `fieldName` указывает имя поля
* связи или «Связь из чата» если привязка выполнена вручную.
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: ID задачи
* responses:
* 200:
* description: Дерево связанных задач
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* parents:
* type: array
* description: Задачи-родители (данная задача является дочерней)
* items:
* $ref: '#/components/schemas/RelatedTreeNode'
* children:
* type: array
* description: Задачи-дети (данная задача является родительской)
* items:
* $ref: '#/components/schemas/RelatedTreeNode'
* 400:
* description: Неверный ID задачи
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get('/api/tasks/:id/related', async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: 'Неверный ID задачи' });
}
const relatedTasks = await storage.getRelatedTasks(taskId, req.organizationId!);
res.json({ success: true, ...relatedTasks });
} catch (error) {
console.error('Get related tasks error:', error);
res.status(500).json({ success: false, error: 'Ошибка при получении связанных задач' });
}
});
}