import { forms, formFields, formStatuses, tasks, taskFieldValues, dataTables, externalServices, regularTableRows, type ExternalService, type InsertExternalService, type RegularTableRow, type InsertRegularTableRow } from "@shared/schema"; import { taskRelations } from "@shared/schema"; import { db } from "../db"; import { eq, and, or, asc, isNotNull, inArray } from "drizzle-orm"; import type { RelatedTreeNode } from "../storage"; import { DataTablesCoreStorage } from "./data-tables-core.storage"; export class DataTablesStorage extends DataTablesCoreStorage { // Related Tasks (for task-type fields) async getRelatedTasks(taskId: number, organizationId: number): Promise<{ parents: RelatedTreeNode[]; children: RelatedTreeNode[]; }> { const getTaskInfo = async (id: number) => { const [row] = await db .select({ id: tasks.id, title: tasks.title, formId: tasks.formId, formName: forms.name, isFinal: formStatuses.isFinal, statusName: formStatuses.name, statusColor: formStatuses.color, }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .leftJoin(formStatuses, eq(tasks.currentStatusId, formStatuses.id)) .where(and(eq(tasks.id, id), eq(forms.organizationId, organizationId))); return row; }; const buildParentTree = async (nodeId: number, pathVisited: Set, depth: number): Promise => { if (depth >= 10) return []; const rels = await db .select({ parentTaskId: taskRelations.parentTaskId, fieldName: formFields.name }) .from(taskRelations) .leftJoin(formFields, eq(taskRelations.fieldId, formFields.id)) .where(and(eq(taskRelations.childTaskId, nodeId), eq(taskRelations.organizationId, organizationId))); const result: RelatedTreeNode[] = []; for (const rel of rels) { if (pathVisited.has(rel.parentTaskId)) continue; const info = await getTaskInfo(rel.parentTaskId); if (!info) continue; const branchVisited = new Set(pathVisited); branchVisited.add(rel.parentTaskId); const nodes = await buildParentTree(rel.parentTaskId, branchVisited, depth + 1); result.push({ ...info, isFinal: info.isFinal ?? false, fieldName: rel.fieldName ?? 'Связь из чата', nodes }); } return result; }; const buildChildTree = async (nodeId: number, pathVisited: Set, depth: number): Promise => { if (depth >= 10) return []; const rels = await db .select({ childTaskId: taskRelations.childTaskId, fieldName: formFields.name }) .from(taskRelations) .leftJoin(formFields, eq(taskRelations.fieldId, formFields.id)) .where(and(eq(taskRelations.parentTaskId, nodeId), eq(taskRelations.organizationId, organizationId))); const result: RelatedTreeNode[] = []; for (const rel of rels) { if (pathVisited.has(rel.childTaskId)) continue; const info = await getTaskInfo(rel.childTaskId); if (!info) continue; const branchVisited = new Set(pathVisited); branchVisited.add(rel.childTaskId); const nodes = await buildChildTree(rel.childTaskId, branchVisited, depth + 1); result.push({ ...info, isFinal: info.isFinal ?? false, fieldName: rel.fieldName ?? 'Связь из чата', nodes }); } return result; }; const rootVisited = new Set([taskId]); const [relParents, relChildren] = await Promise.all([ buildParentTree(taskId, new Set(rootVisited), 0), buildChildTree(taskId, new Set(rootVisited), 0), ]); const [taskRow] = await db .select({ parentTaskId: tasks.parentTaskId }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId))); let parents = relParents; if (taskRow?.parentTaskId) { const alreadyIncluded = relParents.some(p => p.id === taskRow.parentTaskId); if (!alreadyIncluded) { const info = await getTaskInfo(taskRow.parentTaskId); if (info) { const hierarchyNodes = await buildParentTree(taskRow.parentTaskId, new Set([taskId, taskRow.parentTaskId]), 0); parents = [ { ...info, isFinal: info.isFinal ?? false, fieldName: 'Иерархия', nodes: hierarchyNodes }, ...relParents, ]; } } } const subtaskRows = await db .select({ id: tasks.id }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.parentTaskId, taskId), eq(forms.organizationId, organizationId))); let children = relChildren; // --- GROUP relChildren by their parentTaskId (intermediate nodes) --- if (relChildren.length > 0) { const relChildIds = relChildren.map(c => c.id); const relChildParentTaskIds = relChildIds.length > 0 ? await db.select({ id: tasks.id, parentTaskId: tasks.parentTaskId }) .from(tasks) .where(and(inArray(tasks.id, relChildIds), eq(forms.organizationId, organizationId))) .innerJoin(forms, eq(tasks.formId, forms.id)) : []; const grouped = new Map(); const ungrouped: RelatedTreeNode[] = []; console.log('[DEBUG] relChildIds:', relChildIds, 'relChildParentTaskIds:', JSON.stringify(relChildParentTaskIds)); for (const child of relChildren) { const ppt = relChildParentTaskIds.find(p => p.id === child.id)?.parentTaskId; console.log('[DEBUG] child.id:', child.id, 'ppt:', ppt, 'taskId:', taskId); if (ppt && ppt !== taskId) { if (!grouped.has(ppt)) grouped.set(ppt, []); grouped.get(ppt)!.push(child); } else { ungrouped.push(child); } } const newRelChildren: RelatedTreeNode[] = [...ungrouped]; for (const [intermediateId, childNodes] of grouped) { const intInfo = await getTaskInfo(intermediateId); if (intInfo) { newRelChildren.push({ ...intInfo, isFinal: intInfo.isFinal ?? false, fieldName: 'Иерархия', nodes: childNodes, }); } } children = newRelChildren; } // --- extraChildren via parentTaskId, grouped by their task_relations parent --- const relChildIdsSet = new Set(children.map(c => c.id)); const hierarchySubtasks = subtaskRows.filter(s => !relChildIdsSet.has(s.id)); if (hierarchySubtasks.length > 0) { const hcIds = hierarchySubtasks.map(h => h.id); const hcRelationParents = hcIds.length > 0 ? await db.select({ childTaskId: taskRelations.childTaskId, parentTaskId: taskRelations.parentTaskId, fieldName: formFields.name }) .from(taskRelations) .leftJoin(formFields, eq(taskRelations.fieldId, formFields.id)) .where(and(inArray(taskRelations.childTaskId, hcIds), eq(taskRelations.organizationId, organizationId))) : []; const extraChildren: RelatedTreeNode[] = []; const grouped = new Map(); const ungrouped: RelatedTreeNode[] = []; for (const sub of hierarchySubtasks) { const info = await getTaskInfo(sub.id); if (!info) continue; const subNodes = await buildChildTree(sub.id, new Set([taskId, sub.id]), 0); const node: RelatedTreeNode = { ...info, isFinal: info.isFinal ?? false, fieldName: 'Иерархия', nodes: subNodes }; const rel = hcRelationParents.find(r => r.childTaskId === sub.id); if (rel && rel.parentTaskId !== taskId) { if (!grouped.has(rel.parentTaskId)) grouped.set(rel.parentTaskId, []); grouped.get(rel.parentTaskId)!.push(node); } else { ungrouped.push(node); } } for (const [intermediateId, childNodes] of grouped) { const intInfo = await getTaskInfo(intermediateId); if (intInfo) { extraChildren.push({ ...intInfo, isFinal: intInfo.isFinal ?? false, fieldName: 'Связь из чата', nodes: childNodes, }); } } children = [...children, ...ungrouped, ...extraChildren]; } const ownFieldLinks = await db .select({ linkedTaskId: taskFieldValues.value, taskRelationType: formFields.taskRelationType, fieldName: formFields.name, }) .from(taskFieldValues) .innerJoin(formFields, eq(taskFieldValues.fieldId, formFields.id)) .where(and( eq(taskFieldValues.taskId, taskId), eq(formFields.type, 'task'), isNotNull(formFields.taskRelationType), )); const parseLinkedId = (raw: unknown): number | null => { if (raw === null || raw === undefined || raw === '') return null; if (typeof raw === 'number') return isNaN(raw) ? null : Math.trunc(raw); const s = String(raw); try { const parsed = JSON.parse(s); const n = parseInt(String(parsed)); return isNaN(n) ? null : n; } catch { const n = parseInt(s); return isNaN(n) ? null : n; } }; const collectTreeIds = (nodes: RelatedTreeNode[]): number[] => { const ids: number[] = []; for (const node of nodes) { ids.push(node.id); ids.push(...collectTreeIds(node.nodes)); } return ids; }; const parentIds = new Set(collectTreeIds(parents)); const childIds = new Set(collectTreeIds(children)); for (const link of ownFieldLinks) { const linkedId = parseLinkedId(link.linkedTaskId); if (!linkedId) continue; if (link.taskRelationType === 'parent' && !parentIds.has(linkedId)) { const info = await getTaskInfo(linkedId); if (info) { parents = [...parents, { ...info, isFinal: info.isFinal ?? false, fieldName: link.fieldName ?? 'Связанная задача', nodes: [] }]; parentIds.add(linkedId); } } else if (link.taskRelationType === 'child' && !childIds.has(linkedId)) { const info = await getTaskInfo(linkedId); if (info) { children = [...children, { ...info, isFinal: info.isFinal ?? false, fieldName: link.fieldName ?? 'Связанная задача', nodes: [] }]; childIds.add(linkedId); } } } const taskIdStr = String(taskId); const taskIdJsonStr = JSON.stringify(taskIdStr); const inboundFieldLinksWithValue = await db .select({ sourceTaskId: taskFieldValues.taskId, linkedTaskValue: taskFieldValues.value, taskRelationType: formFields.taskRelationType, fieldName: formFields.name, }) .from(taskFieldValues) .innerJoin(formFields, eq(taskFieldValues.fieldId, formFields.id)) .where(and( eq(formFields.type, 'task'), isNotNull(formFields.taskRelationType), or( eq(taskFieldValues.value, taskIdStr), eq(taskFieldValues.value, taskIdJsonStr), ), )); for (const link of inboundFieldLinksWithValue) { if (link.taskRelationType === 'parent' && !childIds.has(link.sourceTaskId)) { const info = await getTaskInfo(link.sourceTaskId); if (info) { children = [...children, { ...info, isFinal: info.isFinal ?? false, fieldName: link.fieldName ?? 'Связанная задача', nodes: [] }]; childIds.add(link.sourceTaskId); } } else if (link.taskRelationType === 'child' && !parentIds.has(link.sourceTaskId)) { const info = await getTaskInfo(link.sourceTaskId); if (info) { parents = [...parents, { ...info, isFinal: info.isFinal ?? false, fieldName: link.fieldName ?? 'Связанная задача', nodes: [] }]; parentIds.add(link.sourceTaskId); } } } return { parents, children }; } // External Services async getExternalServices(organizationId: number): Promise { return await db .select() .from(externalServices) .where(eq(externalServices.organizationId, organizationId)) .orderBy(asc(externalServices.serviceName)); } async getExternalService(id: number, organizationId: number): Promise { const [service] = await db .select() .from(externalServices) .where(and( eq(externalServices.id, id), eq(externalServices.organizationId, organizationId) )); return service || undefined; } async getExternalServiceByType(serviceType: string, organizationId: number): Promise { const [service] = await db .select() .from(externalServices) .where(and( eq(externalServices.serviceType, serviceType), eq(externalServices.organizationId, organizationId), eq(externalServices.isActive, true) )); return service || undefined; } async createExternalService(service: InsertExternalService): Promise { const [created] = await db .insert(externalServices) .values(service) .returning(); return created; } async updateExternalService(id: number, organizationId: number, updates: Partial): Promise { const [updated] = await db .update(externalServices) .set({ ...updates, updatedAt: new Date() }) .where(and( eq(externalServices.id, id), eq(externalServices.organizationId, organizationId) )) .returning(); return updated; } async deleteExternalService(id: number, organizationId: number): Promise { await db .delete(externalServices) .where(and( eq(externalServices.id, id), eq(externalServices.organizationId, organizationId) )); } // === Regular Table Rows === async getRegularTableRows(taskId: number, tabId: number): Promise { return db .select() .from(regularTableRows) .where(and( eq(regularTableRows.taskId, taskId), eq(regularTableRows.tabId, tabId) )) .orderBy(asc(regularTableRows.position)); } async getRegularTableRow(id: number, taskId: number, tabId: number): Promise { const [row] = await db .select() .from(regularTableRows) .where(and( eq(regularTableRows.id, id), eq(regularTableRows.taskId, taskId), eq(regularTableRows.tabId, tabId) )); return row || undefined; } async createRegularTableRow(row: InsertRegularTableRow): Promise { const existingRows = await this.getRegularTableRows(row.taskId, row.tabId); const maxPosition = existingRows.length > 0 ? Math.max(...existingRows.map(r => r.position)) : -1; const [created] = await db .insert(regularTableRows) .values({ ...row, position: row.position ?? maxPosition + 1, }) .returning(); return created; } async updateRegularTableRow(id: number, taskId: number, tabId: number, updates: Partial): Promise { const [updated] = await db .update(regularTableRows) .set({ ...updates, updatedAt: new Date() }) .where(and( eq(regularTableRows.id, id), eq(regularTableRows.taskId, taskId), eq(regularTableRows.tabId, tabId) )) .returning(); if (!updated) { throw new Error("Строка таблицы не найдена"); } return updated; } async deleteRegularTableRow(id: number, taskId: number, tabId: number): Promise { await db .delete(regularTableRows) .where(and( eq(regularTableRows.id, id), eq(regularTableRows.taskId, taskId), eq(regularTableRows.tabId, tabId) )); } }