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: "Ошибка при обновлении срока задачи" }); } });