Files
iistwin/server/services/gantt-shift.service.ts

212 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<GanttFieldInfo[]> {
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`,
isNull(formFields.deletedAt)
));
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<GanttTaskDates[]> {
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<string, unknown>();
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<number[]> {
const related = new Set<number>();
// 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`,
isNull(formFields.deletedAt)
));
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<number>();
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 };
}