Files
iistwin/server/routes/calendar.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

165 lines
5.4 KiB
TypeScript

import { Router } from "express";
import { db } from "../db";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { tasks, forms, formStatuses, taskAssignees } from "@shared/schema";
import { eq, and, or, gte, lte, sql, inArray, isNotNull, asc, exists } from "drizzle-orm";
export const calendarRouter = Router();
interface CalendarTask {
id: number;
title: string;
dueDate: string;
formId: number;
currentStatusId: number;
statusColor: string | null;
statusName: string | null;
isCompleted: boolean;
isAllDay: boolean;
}
function parseDateParam(value: unknown): Date | null {
if (typeof value !== "string" || !value) return null;
const d = new Date(value);
if (isNaN(d.getTime())) return null;
return d;
}
function isMidnight(date: Date): boolean {
// All-day tasks are stored in the DB with time 00:00:00 (no timezone).
// Checking UTC midnight makes the detection independent of the client's timezone.
return date.getUTCHours() === 0 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0 && date.getUTCMilliseconds() === 0;
}
function parseFormIds(value: unknown): number[] | undefined {
if (typeof value !== "string" || !value) return undefined;
const ids = value
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n));
return ids.length > 0 ? ids : undefined;
}
// GET /api/calendar/tasks?from=ISO&to=ISO&formIds=1,2,3
// Returns tasks assigned to current user within date range, excluding completed/final tasks.
calendarRouter.get("/api/calendar/tasks", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const from = parseDateParam(req.query.from);
const to = parseDateParam(req.query.to);
const formIds = parseFormIds(req.query.formIds);
if (!from || !to) {
return res.status(400).json({ success: false, error: "Требуются параметры from и to" });
}
const userId = req.user!.id;
const organizationId = req.organizationId!;
const conditions = [
eq(forms.organizationId, organizationId),
eq(tasks.isCompleted, false),
eq(formStatuses.isFinal, false),
isNotNull(tasks.dueDate),
gte(tasks.dueDate, from),
lte(tasks.dueDate, to),
or(eq(tasks.assignedTo, userId), exists(
db
.select()
.from(taskAssignees)
.where(and(
eq(taskAssignees.taskId, tasks.id),
eq(taskAssignees.userId, userId)
))
))
];
if (formIds) {
conditions.push(inArray(tasks.formId, formIds));
}
const results = await db
.select({
id: tasks.id,
title: tasks.title,
dueDate: tasks.dueDate,
formId: tasks.formId,
currentStatusId: tasks.currentStatusId,
statusColor: formStatuses.color,
statusName: formStatuses.name,
isCompleted: tasks.isCompleted,
})
.from(tasks)
.innerJoin(forms, eq(tasks.formId, forms.id))
.innerJoin(formStatuses, eq(tasks.currentStatusId, formStatuses.id))
.where(and(...conditions))
.orderBy(asc(tasks.dueDate));
const mapped: CalendarTask[] = results.map((row) => {
const due = row.dueDate ? new Date(row.dueDate) : null;
return {
id: row.id,
title: row.title,
dueDate: row.dueDate ? row.dueDate.toISOString() : "",
formId: row.formId,
currentStatusId: row.currentStatusId,
statusColor: row.statusColor,
statusName: row.statusName,
isCompleted: row.isCompleted ?? false,
isAllDay: due ? isMidnight(due) : false,
};
});
res.json({ success: true, tasks: mapped });
} catch (error) {
console.error("Calendar tasks error:", error);
res.status(500).json({ success: false, error: "Ошибка при получении задач календаря" });
}
});
// PUT /api/calendar/tasks/:id/due-date
// Updates only dueDate for drag-and-drop rescheduling.
calendarRouter.put("/api/calendar/tasks/:id/due-date", authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => {
try {
const taskId = parseInt(req.params.id, 10);
const { dueDate } = req.body;
if (isNaN(taskId)) {
return res.status(400).json({ success: false, error: "Неверный ID задачи" });
}
const newDueDate = dueDate ? new Date(dueDate) : null;
if (newDueDate && isNaN(newDueDate.getTime())) {
return res.status(400).json({ success: false, error: "Неверная дата" });
}
const organizationId = req.organizationId!;
const [updated] = await db
.update(tasks)
.set({
dueDate: newDueDate,
updatedAt: new Date(),
})
.where(and(
eq(tasks.id, taskId),
exists(
db
.select()
.from(forms)
.where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId)))
)
))
.returning();
if (!updated) {
return res.status(404).json({ success: false, error: "Задача не найдена" });
}
res.json({ success: true, task: updated });
} catch (error) {
console.error("Calendar update due date error:", error);
res.status(500).json({ success: false, error: "Ошибка при обновлении срока задачи" });
}
});