import { db } from "../db"; import { tasks, taskFieldValues, users, organizations, forms, formFields, formTabs, regularTableRows, taskRelations } from "@shared/schema"; import { eq, and, asc, sql } from "drizzle-orm"; import type { TemplateVariable } from "@shared/schema"; import { TransformService, type TransformConfig, type TransformType } from "./transform.service"; import * as storage from "../storage"; import * as documentStorage from "../storage/documents.storage"; export interface ResolutionContext { task: typeof tasks.$inferSelect; fieldValues: Array<{ code: string; value: unknown; type: string; name: string; }>; assignee: typeof users.$inferSelect | null; creator: typeof users.$inferSelect | null; organization: typeof organizations.$inferSelect | null; currentUser: typeof users.$inferSelect | null; } export interface ResolutionResult { resolved: Record; unresolved: string[]; } export class DataResolutionService { private transform = new TransformService(); async resolveAll( variables: TemplateVariable[], taskId: number, organizationId: number, currentUserId: number, ): Promise { const resolved: Record = {}; const unresolved: string[] = []; const task = await this.getTask(taskId); if (!task) throw new Error(`Задача ${taskId} не найдена`); const fieldValues = await this.getFieldValues(taskId); const assignee = task.assignedTo ? await this.getUser(task.assignedTo) : null; const creator = task.createdBy ? await this.getUser(task.createdBy) : null; const organization = await this.getOrganization(organizationId); const currentUser = await this.getUser(currentUserId); const context: ResolutionContext = { task, fieldValues, assignee, creator, organization, currentUser, }; for (const variable of variables) { try { const value = await this.resolveVariable(variable, context, organizationId); resolved[variable.code] = value ?? ''; if (value === null || value === undefined || value === '') { unresolved.push(variable.code); } } catch { unresolved.push(variable.code); resolved[variable.code] = ''; } } return { resolved, unresolved }; } private async getTask(taskId: number) { const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId)).limit(1); return task; } private async getFieldValues(taskId: number) { return db .select({ code: formFields.code, value: taskFieldValues.value, type: formFields.type, name: formFields.name, }) .from(taskFieldValues) .innerJoin(formFields, eq(formFields.id, taskFieldValues.fieldId)) .where(eq(taskFieldValues.taskId, taskId)); } private async getUser(userId: number) { const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1); return user || null; } private async getOrganization(organizationId: number) { const [org] = await db.select().from(organizations).where(eq(organizations.id, organizationId)).limit(1); return org || null; } private async resolveVariable(variable: TemplateVariable, context: ResolutionContext, organizationId: number): Promise { const mapping = (variable.mapping || {}) as { fieldCode?: string; formId?: number; systemKey?: string; staticValue?: string; userId?: number; relatedTaskId?: number; tabId?: number; transform?: TransformConfig | TransformType | string; locale?: string; relationType?: 'task_field' | 'parent' | 'child' | 'contract_legal_entity'; sourceFieldCode?: string; targetFormId?: number; targetFieldCode?: string; }; let rawValue: string | null = null; switch (variable.source) { case 'task_field': { const fv = context.fieldValues.find((f) => f.code === mapping.fieldCode); rawValue = fv ? this.extractFieldDisplayValue(fv.value, fv.type) : null; break; } case 'task_system': { rawValue = mapping.systemKey ? this.getTaskField(context.task, mapping.systemKey) : null; break; } case 'assignee': { rawValue = context.assignee ? this.getUserField(context.assignee, mapping.systemKey || 'fullName') : null; break; } case 'creator': { rawValue = context.creator ? this.getUserField(context.creator, mapping.systemKey || 'fullName') : null; break; } case 'organization': { rawValue = context.organization ? this.getOrgField(context.organization, mapping.systemKey || 'name') : null; break; } case 'current_user': { rawValue = context.currentUser ? this.getUserField(context.currentUser, mapping.systemKey || 'fullName') : null; break; } case 'static': { rawValue = mapping.staticValue ?? variable.defaultValue ?? null; break; } case 'current_date': { rawValue = new Date().toISOString(); break; } case 'related_task_field': { rawValue = await this.resolveRelatedTaskField( variable, mapping, context, organizationId, ); break; } case 'task_table_tab': { rawValue = await this.resolveTableTab(context.task.id, mapping.tabId); break; } case 'asset': { rawValue = await this.resolveAsset( organizationId, mapping, context.task.id, context.fieldValues, ); break; } default: rawValue = null; } if (rawValue === null || rawValue === undefined) { rawValue = variable.defaultValue || ''; } return this.transform.applyTransform(rawValue, mapping.transform); } private extractFieldDisplayValue(value: unknown, fieldType: string): string | null { if (value === null || value === undefined) return null; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); if (typeof value === 'object') { const obj = value as Record; // Company / Contact / Task / Geo — пробуем взять label/name/title if (typeof obj.displayName === 'string') return obj.displayName; if (typeof obj.name === 'string') return obj.name; if (typeof obj.title === 'string') return obj.title; if (typeof obj.label === 'string') return obj.label; if (typeof obj.value === 'string') return obj.value; if (typeof obj.value === 'number') return String(obj.value); // Date / DateTime могут быть объектом { date: '...' } if (typeof obj.date === 'string') return obj.date; // Select / Multi-select if (Array.isArray(obj.selectedOptions)) { return obj.selectedOptions.map((o: any) => o.label || o.value || o).join(', '); } // File if (Array.isArray(obj.files)) { return obj.files.map((f: any) => f.name || f.url || '').filter(Boolean).join(', '); } return JSON.stringify(value); } return String(value); } private getTaskField(task: typeof tasks.$inferSelect, key: string): string | null { switch (key) { case 'id': return String(task.id); case 'title': return task.title; case 'description': return task.description ?? ''; case 'createdAt': return task.createdAt ? new Date(task.createdAt).toISOString() : ''; case 'dueDate': return task.dueDate ? new Date(task.dueDate).toISOString() : ''; case 'completedAt': return task.completedAt ? new Date(task.completedAt).toISOString() : ''; case 'status': return task.currentStatusId ? String(task.currentStatusId) : ''; default: return ''; } } private getUserField(user: typeof users.$inferSelect, key: string): string | null { switch (key) { case 'fullName': return `${user.lastName || ''} ${user.firstName || ''} ${user.middleName || ''}`.trim() || user.email || ''; case 'lastName': return user.lastName || ''; case 'firstName': return user.firstName || ''; case 'middleName': return user.middleName || ''; case 'position': return user.position || ''; case 'email': return user.email || ''; case 'initials': { const parts = [`${user.lastName || ''} ${user.firstName || ''} ${user.middleName || ''}`.trim()]; if (parts[0].split(/\s+/).length >= 2) { return this.transform.applyTransform(parts[0], 'initials'); } return parts[0]; } default: return ''; } } private getOrgField(org: typeof organizations.$inferSelect, key: string): string | null { switch (key) { case 'name': return org.name || ''; case 'displayName': return org.displayName || ''; default: return ''; } } private async resolveTableTab(taskId: number, tabId?: number): Promise { if (!tabId) return null; const tab = await db.query.formTabs.findFirst({ where: eq(formTabs.id, tabId), with: { form: true }, }); if (!tab) return null; const rows = await db .select() .from(regularTableRows) .where(and(eq(regularTableRows.taskId, taskId), eq(regularTableRows.tabId, tabId))) .orderBy(asc(regularTableRows.position)); const columns = (tab.tableColumns as Array<{ id: string; name: string; type: string }> | undefined) || []; return JSON.stringify({ tabName: tab.name, columns: columns.map((c) => ({ id: c.id, name: c.name, type: c.type })), rows: rows.map((r) => (r.data as Record)), }); } private async resolveAsset( organizationId: number, mapping: Record, taskId: number, fieldValues: Array<{ code: string; value: unknown; type: string; name: string }>, ): Promise { const assetType = mapping.assetType as string | undefined; const assetId = mapping.assetId ? Number(mapping.assetId) : undefined; const formId = mapping.formId ? Number(mapping.formId) : undefined; const fieldCode = mapping.fieldCode as string | undefined; const taskFieldCode = mapping.taskFieldCode as string | undefined; let asset: typeof import("@shared/schema").organizationAssets.$inferSelect | undefined; if (assetId) { asset = await documentStorage.getOrganizationAsset(assetId, organizationId); } else if (assetType && formId && fieldCode && taskFieldCode) { const taskValue = fieldValues.find((f) => f.code === taskFieldCode)?.value; if (taskValue === undefined || taskValue === null || taskValue === '') return null; asset = await documentStorage.findOrganizationAssetByFieldValue( organizationId, assetType, formId, fieldCode, String(taskValue), ); } if (!asset) return null; return `__ASSET__:${asset.fileKey}`; } private async resolveRelatedTaskField( variable: TemplateVariable, mapping: Record, context: ResolutionContext, organizationId: number, ): Promise { const relationType = mapping.relationType || 'task_field'; const targetFormId = mapping.targetFormId ? Number(mapping.targetFormId) : undefined; const targetFieldCode = mapping.targetFieldCode as string | undefined; if (!targetFieldCode) return null; const relatedTaskId = await this.getRelatedTaskId( relationType, mapping.sourceFieldCode as string | undefined, context, organizationId, targetFormId, ); if (!relatedTaskId) return null; const relatedFieldValues = await this.getRelatedTaskFieldValues(relatedTaskId); const fv = relatedFieldValues.find((f) => f.code === targetFieldCode); if (!fv) return null; return this.extractFieldDisplayValue(fv.value, fv.type); } private async getRelatedTaskId( relationType: string, sourceFieldCode: string | undefined, context: ResolutionContext, organizationId: number, targetFormId?: number, ): Promise { switch (relationType) { case 'task_field': { if (!sourceFieldCode) return null; const fv = context.fieldValues.find((f) => f.code === sourceFieldCode); if (!fv) return null; const value = fv.value; if (value === null || value === undefined || value === '') return null; // task-поле хранит ID связанной задачи числом if (typeof value === 'number') return value; if (typeof value === 'string') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } case 'contract_legal_entity': { // Ищем поле типа contract-number и берём legalEntityTaskId const contractFv = context.fieldValues.find((f) => f.type === 'contract-number'); if (!contractFv || !contractFv.value) return null; const obj = typeof contractFv.value === 'string' ? JSON.parse(contractFv.value) : contractFv.value; const legalEntityTaskId = obj?.legalEntityTaskId; if (legalEntityTaskId && typeof legalEntityTaskId === 'number') return legalEntityTaskId; return null; } case 'parent': case 'child': { const relations = await db .select() .from(taskRelations) .where(and( eq(taskRelations.organizationId, organizationId), relationType === 'parent' ? eq(taskRelations.childTaskId, context.task.id) : eq(taskRelations.parentTaskId, context.task.id), )); if (relations.length === 0) return null; const candidateIds = relations.map((r) => relationType === 'parent' ? r.parentTaskId : r.childTaskId, ); if (!targetFormId) return candidateIds[0] || null; for (const id of candidateIds) { const [relatedTask] = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1); if (relatedTask && relatedTask.formId === targetFormId) return id; } return null; } default: return null; } } private async getRelatedTaskFieldValues(taskId: number): Promise> { return db .select({ code: formFields.code, value: taskFieldValues.value, type: formFields.type, name: formFields.name, }) .from(taskFieldValues) .innerJoin(formFields, eq(formFields.id, taskFieldValues.fieldId)) .where(eq(taskFieldValues.taskId, taskId)); } }