import { db } from "../db"; import { tasks, taskFieldValues, taskRelations, formFields, forms } from "@shared/schema"; import { eq, and, sql, inArray, isNull } from "drizzle-orm"; import { storage } from "../storage"; export type GanttFieldRole = "start" | "end" | "milestone"; export interface GanttFieldInfo { id: number; formId: number; role: GanttFieldRole; } export interface GanttTaskDates { taskId: number; formId: number; start: Date | null; end: Date | null; milestones: Array<{ fieldId: number; date: Date }>; } function parseDateValue(value: unknown): Date | null { if (value === null || value === undefined || value === "" || value === "__auto_prolongation__") return null; if (value instanceof Date) return isNaN(value.getTime()) ? null : value; if (typeof value === "string") { const d = new Date(value); return isNaN(d.getTime()) ? null : d; } return null; } export function calculateDateShift(oldValue: Date | null | undefined, newValue: Date | null | undefined): number { const oldDate = oldValue ? new Date(oldValue).getTime() : 0; const newDate = newValue ? new Date(newValue).getTime() : 0; return newDate - oldDate; } export async function getGanttFieldsByForm(formIds: number[], organizationId: number): Promise { if (formIds.length === 0) return []; const rows = await db .select({ id: formFields.id, formId: formFields.formId, ganttRole: formFields.ganttRole }) .from(formFields) .innerJoin(forms, eq(formFields.formId, forms.id)) .where(and( inArray(formFields.formId, formIds), eq(forms.organizationId, organizationId), sql`${formFields.ganttRole} IS NOT NULL` )); return rows .filter((r): r is typeof r & { ganttRole: GanttFieldRole } => r.ganttRole === "start" || r.ganttRole === "end" || r.ganttRole === "milestone" ) .map((r) => ({ id: r.id, formId: r.formId, role: r.ganttRole })); } export async function getGanttTaskDates( taskIds: number[], ganttFields: GanttFieldInfo[], organizationId: number ): Promise { if (taskIds.length === 0 || ganttFields.length === 0) return []; const fieldIds = ganttFields.map((f) => f.id); const values = await db .select({ taskId: taskFieldValues.taskId, fieldId: taskFieldValues.fieldId, value: taskFieldValues.value }) .from(taskFieldValues) .innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id)) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and( inArray(taskFieldValues.taskId, taskIds), inArray(taskFieldValues.fieldId, fieldIds), eq(forms.organizationId, organizationId) )); const valueMap = new Map(); for (const v of values) { valueMap.set(`${v.taskId}:${v.fieldId}`, v.value); } const result: GanttTaskDates[] = []; for (const taskId of taskIds) { const task = await storage.getTask(taskId, organizationId); if (!task) continue; const startField = ganttFields.find((f) => f.formId === task.formId && f.role === "start"); const endField = ganttFields.find((f) => f.formId === task.formId && f.role === "end"); const milestoneFields = ganttFields.filter((f) => f.formId === task.formId && f.role === "milestone"); const start = startField ? parseDateValue(valueMap.get(`${taskId}:${startField.id}`)) : null; const end = endField ? parseDateValue(valueMap.get(`${taskId}:${endField.id}`)) : null; const milestones = milestoneFields .map((f) => ({ fieldId: f.id, date: parseDateValue(valueMap.get(`${taskId}:${f.id}`)) })) .filter((m): m is { fieldId: number; date: Date } => m.date !== null); result.push({ taskId, formId: task.formId, start, end, milestones }); } return result; } export async function getRelatedTaskIds(taskId: number, organizationId: number): Promise { const related = new Set(); // taskRelations (both directions) const relations = await db .select({ parentTaskId: taskRelations.parentTaskId, childTaskId: taskRelations.childTaskId }) .from(taskRelations) .where(and( eq(taskRelations.organizationId, organizationId), sql`(${taskRelations.parentTaskId} = ${taskId} OR ${taskRelations.childTaskId} = ${taskId})` )); for (const r of relations) { if (r.parentTaskId !== taskId) related.add(r.parentTaskId); if (r.childTaskId !== taskId) related.add(r.childTaskId); } // parent/child hierarchy const task = await storage.getTask(taskId, organizationId); if (task) { if (task.parentTaskId) related.add(task.parentTaskId); const children = await db .select({ id: tasks.id }) .from(tasks) .where(eq(tasks.parentTaskId, taskId)); for (const c of children) related.add(c.id); } return Array.from(related); } export async function shiftRelatedTasks( sourceTaskId: number, deltaMs: number, organizationId: number, options?: { triggeredByUserId?: number | null; skipAudit?: boolean } ): Promise<{ shifted: number; errors: string[] }> { if (deltaMs === 0) return { shifted: 0, errors: [] }; const ganttFields = await getGanttFieldsByForm([], organizationId); // Actually we need fields for all forms that have gantt fields; fetch all org gantt fields. const allGanttFields = await db .select({ id: formFields.id, formId: formFields.formId, ganttRole: formFields.ganttRole }) .from(formFields) .innerJoin(forms, eq(formFields.formId, forms.id)) .where(and( eq(forms.organizationId, organizationId), sql`${formFields.ganttRole} IS NOT NULL` )); const validFields: GanttFieldInfo[] = allGanttFields .filter((r): r is typeof r & { ganttRole: GanttFieldRole } => r.ganttRole === "start" || r.ganttRole === "end" || r.ganttRole === "milestone" ) .map((r) => ({ id: r.id, formId: r.formId, role: r.ganttRole })); const visited = new Set(); const queue: number[] = [sourceTaskId]; const shiftedTaskIds: number[] = []; const errors: string[] = []; while (queue.length > 0) { const currentId = queue.shift()!; if (visited.has(currentId)) continue; visited.add(currentId); if (currentId !== sourceTaskId) { const currentDates = await getGanttTaskDates([currentId], validFields, organizationId); if (currentDates.length === 0) continue; const dates = currentDates[0]; const startField = validFields.find((f) => f.formId === dates.formId && f.role === "start"); const endField = validFields.find((f) => f.formId === dates.formId && f.role === "end"); try { if (startField && dates.start) { const newStart = new Date(dates.start.getTime() + deltaMs); await storage.updateTaskFieldValue(currentId, startField.id, organizationId, { value: newStart.toISOString(), }); } if (endField && dates.end) { const newEnd = new Date(dates.end.getTime() + deltaMs); await storage.updateTaskFieldValue(currentId, endField.id, organizationId, { value: newEnd.toISOString(), }); } shiftedTaskIds.push(currentId); } catch (err: any) { errors.push(`Не удалось сдвинуть задачу #${currentId}: ${err.message || err}`); continue; } } const relatedIds = await getRelatedTaskIds(currentId, organizationId); for (const id of relatedIds) { if (!visited.has(id)) queue.push(id); } } // Invalidate caches for shifted tasks if (shiftedTaskIds.length > 0) { const { tasksMinimalCache } = await import("../utils/cache"); tasksMinimalCache.invalidatePrefix(`tasks:${organizationId}:minimal:`); } return { shifted: shiftedTaskIds.length, errors }; }