import { users, forms, formStatuses, tasks, taskFieldValues, fieldHistory, type User, type Form, type FormStatus, type Task, type TaskFieldValue, type FieldHistory, type InsertTask, type InsertTaskFieldValue, type InsertFieldHistory } from "@shared/schema"; import { taskRelations } from "@shared/schema"; import { taskAssignees } from "@shared/schema"; import { db } from "../db"; import { eq, and, or, desc, asc, exists, sql, ilike, inArray } from "drizzle-orm"; import { formatUserName } from "../utils/formatUserName"; import { FormsStorage } from "./forms.storage"; export class TasksCoreStorage extends FormsStorage { // Tasks async getTask(id: number, organizationId: number): Promise { const results = await db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.id, id), eq(forms.organizationId, organizationId))); const [result] = results; return result?.tasks || undefined; } async getTasksByForm(formId: number, organizationId: number): Promise { return db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId))) .orderBy(desc(tasks.createdAt)) .then(results => results.map(r => r.tasks)); } async getTasksWithFieldsByForm(formId: number, organizationId: number): Promise { const allTasks = await db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId))) .orderBy(desc(tasks.createdAt)) .then(results => results.map(r => r.tasks)); if (allTasks.length === 0) return []; const taskIds = allTasks.map(t => t.id); const allFieldValues = await db .select() .from(taskFieldValues) .where(inArray(taskFieldValues.taskId, taskIds)); const fieldValuesByTask = new Map(); for (const fv of allFieldValues) { if (!fieldValuesByTask.has(fv.taskId)) { fieldValuesByTask.set(fv.taskId, {}); } fieldValuesByTask.get(fv.taskId)![fv.fieldId] = fv.value; } return allTasks.map(task => ({ ...task, fieldValues: fieldValuesByTask.get(task.id) || {} })); } async getTasksWithFieldsByFormOptimized( formId: number, organizationId: number, options?: { limit?: number; offset?: number; assignedTo?: number } ): Promise<{ tasks: Task[]; total: number }> { const limit = options?.limit; const offset = options?.offset || 0; const whereConditions = [ eq(tasks.formId, formId), eq(forms.organizationId, organizationId), ]; if (options?.assignedTo != null) { whereConditions.push(eq(tasks.assignedTo, options.assignedTo)); } let query = db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(...whereConditions)) .orderBy(desc(tasks.updatedAt)) .offset(offset); const allTasks = limit ? await query.limit(limit).then(results => results.map(r => r.tasks)) : await query.then(results => results.map(r => r.tasks)); if (allTasks.length === 0) { return { tasks: [], total: 0 }; } const countResult = await db .select({ count: sql`count(*)` }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(...whereConditions)); const total = Number(countResult[0]?.count || 0); const taskIds = allTasks.map(t => t.id); const [allFieldValues, allTaskAssignees] = await Promise.all([ db.select().from(taskFieldValues).where(inArray(taskFieldValues.taskId, taskIds)), db .select({ taskId: taskAssignees.taskId, userId: taskAssignees.userId, firstName: users.firstName, middleName: users.middleName, lastName: users.lastName, email: users.email, }) .from(taskAssignees) .innerJoin(users, eq(taskAssignees.userId, users.id)) .where(inArray(taskAssignees.taskId, taskIds)), ]); const fieldValuesByTask = new Map(); for (const fv of allFieldValues) { if (!fieldValuesByTask.has(fv.taskId)) { fieldValuesByTask.set(fv.taskId, {}); } fieldValuesByTask.get(fv.taskId)![fv.fieldId] = fv.value; } const assigneesByTask = new Map>(); for (const a of allTaskAssignees) { if (!assigneesByTask.has(a.taskId)) { assigneesByTask.set(a.taskId, []); } assigneesByTask.get(a.taskId)!.push({ userId: a.userId, user: { firstName: a.firstName, middleName: a.middleName, lastName: a.lastName, email: a.email } }); } const tasksWithFields = allTasks.map(task => ({ ...task, fieldValues: fieldValuesByTask.get(task.id) || {}, assignees: assigneesByTask.get(task.id) || [], })); return { tasks: tasksWithFields, total }; } async getTaskCountsByStatus(formId: number, organizationId: number): Promise> { const statuses = await db .select() .from(formStatuses) .where(and(eq(formStatuses.formId, formId), eq(formStatuses.isFinal, false))) .orderBy(asc(formStatuses.position)); if (statuses.length === 0) return []; const countsResult = await db .select({ currentStatusId: tasks.currentStatusId, count: sql`count(*)::int`, }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.formId, formId), eq(forms.organizationId, organizationId))) .groupBy(tasks.currentStatusId); const countMap = new Map(countsResult.map(c => [c.currentStatusId, c.count])); return statuses.map(s => ({ statusId: s.id, name: s.name, color: s.color || '#94a3b8', count: countMap.get(s.id) || 0, })); } async getTasksByOrganization( organizationId: number, options?: { limit?: number; offset?: number; sortBy?: 'updatedAt' | 'createdAt'; order?: 'asc' | 'desc'; statusId?: number; assignedTo?: number; minimal?: boolean; } ): Promise[]> { const sortBy = options?.sortBy || 'updatedAt'; const order = options?.order || 'desc'; const limit = options?.limit; const offset = options?.offset; const statusId = options?.statusId; const assignedTo = options?.assignedTo; const minimal = options?.minimal || false; const sortColumn = sortBy === 'updatedAt' ? tasks.updatedAt : tasks.createdAt; const orderFn = order === 'desc' ? desc : asc; const conditions = [eq(forms.organizationId, organizationId)]; if (statusId) { conditions.push(eq(tasks.currentStatusId, statusId)); } if (assignedTo) { conditions.push(eq(tasks.assignedTo, assignedTo)); } if (minimal) { return await db .select({ id: tasks.id, formId: tasks.formId, title: tasks.title, currentStatusId: tasks.currentStatusId, assignedTo: tasks.assignedTo, updatedAt: tasks.updatedAt, createdAt: tasks.createdAt, status: formStatuses.name }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .leftJoin(formStatuses, eq(tasks.currentStatusId, formStatuses.id)) .where(and(...conditions)) .orderBy(orderFn(sortColumn)) .limit(limit || 1000) .offset(offset || 0); } const results = await db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(...conditions)) .orderBy(orderFn(sortColumn)) .limit(limit || 10000) .offset(offset || 0); return results.map((r) => r.tasks); } async createTask(insertTask: InsertTask): Promise { let depth = 0; let position = 0; if (insertTask.parentTaskId) { const [parentTask] = await db .select() .from(tasks) .where(eq(tasks.id, insertTask.parentTaskId)); if (parentTask) { depth = (parentTask.depth || 0) + 1; const siblings = await db .select({ count: sql`count(*)` }) .from(tasks) .where(eq(tasks.parentTaskId, insertTask.parentTaskId)); position = Number(siblings[0]?.count || 0); } } const [task] = await db .insert(tasks) .values({ ...(insertTask as typeof tasks.$inferInsert), depth, position, }) .returning(); return task; } async getSubtasks(parentTaskId: number, organizationId: number): Promise { return await db .select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and( eq(tasks.parentTaskId, parentTaskId), eq(forms.organizationId, organizationId) )) .orderBy(tasks.position) .then(results => results.map(r => r.tasks)); } async getTaskTree(taskId: number, organizationId: number): Promise { const task = await this.getTask(taskId, organizationId); if (!task) return null; const subtasks = await this.getSubtasks(taskId, organizationId); const subtasksWithChildren = await Promise.all( subtasks.map(async (subtask) => { return await this.getTaskTree(subtask.id, organizationId); }) ); return { ...task, subtasks: subtasksWithChildren.filter(Boolean) }; } async updateTask(id: number, organizationId: number, updates: Partial): Promise { const [task] = await db .update(tasks) .set({ ...updates, updatedAt: new Date() }) .where(and( eq(tasks.id, id), exists(db.select().from(forms).where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId)))) )) .returning(); return task; } async updateTaskIfNotModified( id: number, organizationId: number, updates: Partial, requiredUpdatedAt: Date, ): Promise { const [task] = await db .update(tasks) .set({ ...updates, updatedAt: new Date() }) .where(and( eq(tasks.id, id), eq(tasks.updatedAt, requiredUpdatedAt), exists(db.select().from(forms).where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId)))) )) .returning(); return task ?? null; } async deleteTask(id: number, organizationId: number): Promise { // Cleanup RAG embeddings for this task and its messages const { deleteRagEmbeddingsByTask } = await import('../services/embedding.service.js'); await deleteRagEmbeddingsByTask(organizationId, id); await db .delete(tasks) .where(and( eq(tasks.id, id), exists( db.select() .from(forms) .where(and(eq(forms.id, tasks.formId), eq(forms.organizationId, organizationId))) ) )); } async searchTasksByKeyword( organizationId: number, query: string, limit: number = 10 ): Promise[]> { const pattern = `%${query}%`; const results = await db .select({ id: tasks.id, title: tasks.title, description: tasks.description, formId: tasks.formId, currentStatusId: tasks.currentStatusId, }) .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where( and( eq(forms.organizationId, organizationId), or( ilike(tasks.title, pattern), ilike(tasks.description, pattern) ) ) ) .orderBy(desc(tasks.updatedAt)) .limit(limit); return results; } // Task Field Values async getTaskFieldValues(taskId: number, organizationId: number): Promise { return await db .select() .from(taskFieldValues) .innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id)) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(taskFieldValues.taskId, taskId), eq(forms.organizationId, organizationId))) .orderBy(taskFieldValues.fieldId) .then(results => results.map(r => r.task_field_values)); } async createTaskFieldValue(insertValue: InsertTaskFieldValue): Promise { const [value] = await db .insert(taskFieldValues) .values(insertValue) .returning(); return value; } async updateTaskFieldValue(taskId: number, fieldId: number, organizationId: number, updates: Partial): Promise { const [value] = await db .update(taskFieldValues) .set({ ...updates, updatedAt: new Date() }) .where(and( eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, fieldId), exists( db.select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId))) ) )) .returning(); return value; } async bulkUpsertTaskFieldValues(taskId: number, formId: number, updates: Array<{ fieldId: number; value: unknown }>): Promise { const results: TaskFieldValue[] = []; await db.transaction(async (tx) => { const existing = await tx .select() .from(taskFieldValues) .where(eq(taskFieldValues.taskId, taskId)); const existingMap = new Map(existing.map(v => [v.fieldId, v])); for (const { fieldId, value } of updates) { const existingValue = existingMap.get(fieldId); if (existingValue) { const [updated] = await tx .update(taskFieldValues) .set({ value, updatedAt: new Date() }) .where(and(eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, fieldId))) .returning(); results.push(updated); } else { const [inserted] = await tx .insert(taskFieldValues) .values({ taskId, fieldId, formId, value }) .returning(); results.push(inserted); } } }); return results; } async deleteTaskFieldValue(taskId: number, fieldId: number, organizationId: number): Promise { await db .delete(taskFieldValues) .where(and( eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, fieldId), exists( db.select() .from(tasks) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and(eq(tasks.id, taskId), eq(forms.organizationId, organizationId))) ) )); } // Field History async getFieldHistory(taskId: number, fieldId: number, organizationId: number): Promise { return await db .select() .from(fieldHistory) .innerJoin(tasks, eq(fieldHistory.taskId, tasks.id)) .innerJoin(forms, eq(tasks.formId, forms.id)) .where(and( eq(fieldHistory.taskId, taskId), eq(fieldHistory.fieldId, fieldId), eq(forms.organizationId, organizationId) )) .orderBy(desc(fieldHistory.changedAt)) .then(results => results.map(r => r.field_history)); } async createFieldHistory(insertHistory: InsertFieldHistory): Promise { const [history] = await db .insert(fieldHistory) .values(insertHistory) .returning(); return history; } }