- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
140 lines
6.1 KiB
TypeScript
140 lines
6.1 KiB
TypeScript
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: 'Ошибка при получении связанных задач' });
|
||
}
|
||
});
|
||
}
|