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