From de4585a3f4b19eb8a4b2fe3d65ef8a40fb3142e6 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:27:33 +0530 Subject: [PATCH 1/5] Issue #252771 Feat: While update project > Update/Add task based on parent Id --- module/userProjects/helper.js | 543 +++++++++++++++++++++++++++++++++- 1 file changed, 542 insertions(+), 1 deletion(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 75e9f6a9..53f00cf1 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -132,6 +132,7 @@ module.exports = class UserProjectsHelper { [ '_id', 'tasks', + 'taskSequence', 'programInformation._id', 'solutionInformation._id', 'solutionInformation.externalId', @@ -376,17 +377,89 @@ module.exports = class UserProjectsHelper { updateProject.tasks = await _projectTask(data.tasks) if (userProject[0].tasks && userProject[0].tasks.length > 0) { + // Helper function to recursively find a task by _id in nested structure + // This searches in both existing tasks and newly processed tasks + const findTaskById = (tasks, taskId) => { + for (let i = 0; i < tasks.length; i++) { + // Compare as strings to handle ObjectId vs string + if (String(tasks[i]._id) === String(taskId)) { + return tasks[i] + } + // Recursively search in children + if (tasks[i].children && tasks[i].children.length > 0) { + const found = findTaskById(tasks[i].children, taskId) + if (found) { + return found + } + } + } + return null + } + + // Helper function to add child to parent's children array and update taskSequence + const addChildToParent = (parentTask, childTask) => { + // Initialize children array if it doesn't exist + if (!parentTask.children) { + parentTask.children = [] + } + + // Check if child already exists in parent's children + const childIndex = parentTask.children.findIndex( + (child) => String(child._id) === String(childTask._id) + ) + + if (childIndex < 0) { + // Add child to parent's children array + parentTask.children.push(childTask) + + // Initialize taskSequence if it doesn't exist + if (!parentTask.taskSequence) { + parentTask.taskSequence = [] + } + + // Add child's externalId to parent's taskSequence if not already present + if (childTask.externalId && !parentTask.taskSequence.includes(childTask.externalId)) { + parentTask.taskSequence.push(childTask.externalId) + } + return true + } else { + // Child already exists, update it + parentTask.children[childIndex] = childTask + // Ensure externalId is in taskSequence + if (!parentTask.taskSequence) { + parentTask.taskSequence = [] + } + if (childTask.externalId && !parentTask.taskSequence.includes(childTask.externalId)) { + parentTask.taskSequence.push(childTask.externalId) + } + return true + } + } + + // Process tasks in two passes: + // Pass 1: Merge all tasks first (so we can find parents that are also being updated) + // Pass 2: Handle parent-child relationships + + // First pass: Merge tasks into userProject[0].tasks updateProject.tasks.forEach((task) => { task.updatedBy = userId task.updatedAt = new Date() + // Normalize parentId (handle space before parentId if present) + if (task[' parentId']) { + task.parentId = task[' parentId'] + delete task[' parentId'] + } + let taskIndex = userProject[0].tasks.findIndex( - (projectTask) => projectTask._id === task._id + (projectTask) => String(projectTask._id) === String(task._id) ) if (taskIndex < 0) { + // New task - add to root tasks array userProject[0].tasks.push(task) } else { + // Existing task - update it let keepFieldsFromTask = ['observationInformation', 'submissions'] removeFieldsFromRequest.forEach((removeField) => { @@ -403,7 +476,35 @@ module.exports = class UserProjectsHelper { } }) + // Second pass: Handle parent-child relationships and update root taskSequence + // Now that all tasks are merged, we can find parents at any level + + // Initialize root taskSequence if it doesn't exist + if (!userProject[0].taskSequence) { + userProject[0].taskSequence = [] + } + + updateProject.tasks.forEach((task) => { + if (task.parentId) { + // Find parent task recursively in the merged structure + // This will find parents at any nesting level (root, child, grandchild, etc.) + const parentTask = findTaskById(userProject[0].tasks, task.parentId) + + if (parentTask) { + // Add child to parent's children array and update taskSequence + addChildToParent(parentTask, task) + } + } else { + // Root-level task (no parentId) - add to project's root taskSequence + if (task.externalId && !userProject[0].taskSequence.includes(task.externalId)) { + userProject[0].taskSequence.push(task.externalId) + } + } + }) + updateProject.tasks = userProject[0].tasks + // Update root taskSequence in updateProject + updateProject.taskSequence = userProject[0].taskSequence } taskReport.total = updateProject.tasks.length @@ -4435,6 +4536,446 @@ module.exports = class UserProjectsHelper { } catch (err) {} } + /** + * Create project plan from multiple templates. + * @method + * @name createProjectPlan + * @param {Object} data - request data. + * @param {String} userId - Logged in user id (admin). + * @param {String} userToken - User token. + * @param {Object} userDetails - loggedin user's info + * @returns {Object} Project Plan created information. + */ + static createProjectPlan(data, userId, userToken, userDetails) { + return new Promise(async (resolve, reject) => { + try { + const { templates, userId: participantId, entityId, programName, projectConfig } = data + let tenantId = userDetails.userInformation.tenantId + let orgId = userDetails.userInformation.organizationId + + // Step 1: Validations + // a. Validate Participant User + let userProfile = await userService.profile(participantId, userToken) + if (!userProfile.success || !userProfile.data) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.USER_NOT_FOUND, + } + } + + // b. Validate Entity + if (entityId) { + let entityInformation = await entitiesService.entityDocuments( + { _id: entityId, tenantId: tenantId }, + CONSTANTS.common.ALL + ) + if (!entityInformation?.success || !entityInformation?.data?.length > 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } + } + } + + // c. Validate all Template IDs - must exist and be published + // Filter out any templates without templateId + const templateIds = templates + .map((t) => (t && t.templateId ? t.templateId : null)) + .filter((id) => id !== null) + + if (templateIds.length === 0) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: 'No valid template IDs provided', + } + } + + const validTemplates = await projectTemplateQueries.templateDocument( + { + _id: { $in: templateIds }, + status: CONSTANTS.common.PUBLISHED, + tenantId: tenantId, + }, + ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId', 'taskSequence'] + ) + + // Collect all unique category IDs from templates + let allCategoryIds = [] + const categoryIdSet = new Set() // To avoid duplicates + validTemplates.forEach((template) => { + if (template.categories && Array.isArray(template.categories)) { + template.categories.forEach((category) => { + const categoryId = category._id?.toString() || category + if (categoryId && !categoryIdSet.has(categoryId)) { + categoryIdSet.add(categoryId) + allCategoryIds.push(categoryId) + } + }) + } + }) + + // Fetch full category documents from projectCategories collection + let allCategories = [] + if (allCategoryIds.length > 0) { + allCategories = await projectCategoriesQueries.categoryDocuments( + { + _id: { $in: allCategoryIds }, + tenantId: tenantId, + }, + ['_id', 'name', 'externalId', 'evidences'] + ) + } + + if (validTemplates.length !== templates.length) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND, + } + } + + // Step 2: Always Create New Program & Solution + let programAndSolutionData = { + programName: programName || `Program for ${userProfile.data.name}`, + isPrivateProgram: true, + type: CONSTANTS.common.IMPROVEMENT_PROJECT, + subType: CONSTANTS.common.IMPROVEMENT_PROJECT, + isReusable: false, + entities: entityId ? [entityId] : [], + } + + let programAndSolutionInformation = await solutionsHelper.createProgramAndSolution( + userId, + programAndSolutionData, + false, + userDetails, + true // isExternalProgram + ) + + if (!programAndSolutionInformation.success) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.SOLUTION_PROGRAMS_NOT_CREATED, + } + } + + const masterProgramId = programAndSolutionInformation.result.program._id + const masterSolutionId = programAndSolutionInformation.result.solution._id + + // Step 3: Initialize Single Project + let projectData = { + title: projectConfig?.name || `${userProfile.data.name}'s Project Plan`, + description: projectConfig?.description || 'Individual Development Plan', + userId: participantId, + createdBy: userId, + updatedBy: userId, + status: CONSTANTS.common.STARTED, + lastDownloadedAt: new Date(), + isAPrivateProgram: true, + programId: masterProgramId, + programExternalId: programAndSolutionInformation.result.program.externalId, + solutionId: masterSolutionId, + solutionExternalId: programAndSolutionInformation.result.solution.externalId, + programInformation: { + _id: masterProgramId, + name: programAndSolutionInformation.result.program.name, + externalId: programAndSolutionInformation.result.program.externalId, + }, + solutionInformation: { + _id: masterSolutionId, + name: programAndSolutionInformation.result.solution.name, + externalId: programAndSolutionInformation.result.solution.externalId, + }, + tenantId: tenantId, + orgId: orgId, + categories: allCategories, // Add categories from all templates + tasks: [], + taskSequence: [], + userProfile: userProfile.data, + createdAt: new Date(), + updatedAt: new Date(), + } + + // Add entity information if provided + if (entityId) { + const entityInfo = await entitiesService.entityDocuments( + { _id: entityId, tenantId: tenantId }, + CONSTANTS.common.ALL + ) + if (entityInfo?.success && entityInfo?.data?.length > 0) { + const entity = entityInfo.data[0] + projectData.entityInformation = { + _id: entity._id, + entityType: entity.entityType, + entityTypeId: entity.entityTypeId, + entityId: entity._id, + externalId: entity?.metaInformation?.externalId || '', + entityName: entity?.metaInformation?.name || '', + } + projectData.entityId = entity._id + } + } + + // Step 4: Template Orchestration (Loop for each template) + for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { + const template = templates[templateIndex] + + // Validate template has templateId + if (!template || !template.templateId) { + continue + } + + // a. Fetch Template metadata + const templateData = validTemplates.find((t) => { + if (!t || !t._id || !template.templateId) return false + return t._id.toString() === template.templateId.toString() + }) + + if (!templateData) { + continue // Skip invalid templates + } + + // b. Create improvementProject task at root level first (to get its ID) + const taskName = + template.targetTaskName || + template.targetProjectName || + templateData.title || + `Template ${templateIndex + 1}` + const taskExternalId = `task-${uuidv4().replace(/-/g, '')}` + const improvementTaskId = uuidv4() + + // c. Fetch Template Tasks and Subtasks + const templateTasks = await projectTemplatesHelper.tasksAndSubTasks( + template.templateId, + '', // language + tenantId, + orgId + ) + + if (!templateTasks || templateTasks.length === 0) { + continue + } + + // Ensure all tasks have _id before processing (required by _projectTask) + const tasksWithIds = templateTasks + .map((task) => { + if (task && !task._id) { + task._id = uuidv4() + } + return task + }) + .filter((task) => task !== null && task !== undefined) + + // d. Process Template Tasks using _projectTask with improvementTaskId as parent + let processedTemplateTasks = [] + try { + processedTemplateTasks = await _projectTask( + tasksWithIds, + true, // isImportedFromLibrary + improvementTaskId, // parentTaskId - set to improvementTask._id + userToken, + masterProgramId, + userDetails + ) + } catch (error) { + console.error(`Error processing template tasks for template ${template.templateId}:`, error) + throw error + } + + // Ensure processedTemplateTasks is an array + if (!Array.isArray(processedTemplateTasks)) { + processedTemplateTasks = [] + } + + // e. Process Custom Tasks if provided + let processedCustomTasks = [] + if (template.customTasks && template.customTasks.length > 0) { + // Ensure all custom tasks have _id before processing + const customTasksWithIds = template.customTasks + .map((task) => { + if (task && !task._id) { + task._id = uuidv4() + } + return task + }) + .filter((task) => task !== null && task !== undefined) + + try { + processedCustomTasks = await _projectTask( + customTasksWithIds, + false, // isImportedFromLibrary + improvementTaskId, // parentTaskId - set to improvementTask._id + userToken, + masterProgramId, + userDetails + ) + } catch (error) { + console.error(`Error processing custom tasks for template ${template.templateId}:`, error) + throw error + } + + // Ensure processedCustomTasks is an array + if (!Array.isArray(processedCustomTasks)) { + processedCustomTasks = [] + } + + // Mark all custom tasks as isACustomTask: true + processedCustomTasks.forEach((customTask) => { + customTask.isACustomTask = true + customTask.createdBy = userId + customTask.updatedBy = userId + customTask.createdAt = new Date() + customTask.updatedAt = new Date() + }) + } + + // f. Ensure parentId is set correctly for all root-level subtasks + if (processedTemplateTasks && Array.isArray(processedTemplateTasks)) { + processedTemplateTasks.forEach((task) => { + if (task && (!task.parentId || task.parentId !== improvementTaskId)) { + task.parentId = improvementTaskId + } + }) + } + if (processedCustomTasks && Array.isArray(processedCustomTasks)) { + processedCustomTasks.forEach((task) => { + if (task && (!task.parentId || task.parentId !== improvementTaskId)) { + task.parentId = improvementTaskId + } + }) + } + + // g. Combine template tasks and custom tasks as children + const allSubTasks = [...processedTemplateTasks, ...processedCustomTasks] + + // h. Build taskSequence for improvementTask based on template's taskSequence + let improvementTaskSequence = [] + + // If template has taskSequence, use it to order the subtasks + if (templateData.taskSequence && templateData.taskSequence.length > 0) { + // Create a map of externalId to task for quick lookup + const taskMap = new Map() + allSubTasks.forEach((task) => { + if (task && task.externalId) { + taskMap.set(task.externalId, task) + } + }) + + // First, add tasks in template's taskSequence order + templateData.taskSequence.forEach((templateTaskExternalId) => { + const task = taskMap.get(templateTaskExternalId) + if (task && task.externalId) { + improvementTaskSequence.push(task.externalId) + taskMap.delete(templateTaskExternalId) // Remove to avoid duplicates + } + }) + + // Then, add any remaining tasks (custom tasks or tasks not in template sequence) + taskMap.forEach((task) => { + if (task && task.externalId) { + improvementTaskSequence.push(task.externalId) + } + }) + } else { + // If no template taskSequence, use the order of processed tasks + allSubTasks.forEach((subTask) => { + if (subTask && subTask.externalId) { + improvementTaskSequence.push(subTask.externalId) + } + }) + } + + let improvementTask = { + _id: improvementTaskId, + externalId: taskExternalId, + name: taskName, + description: template.targetTaskName || template.targetProjectName || templateData.title || '', + type: CONSTANTS.common.IMPROVEMENT_PROJECT, + status: CONSTANTS.common.NOT_STARTED_STATUS, + isACustomTask: false, + isDeletable: false, + isDeleted: false, + isImportedFromLibrary: false, + createdAt: new Date(), + updatedAt: new Date(), + createdBy: userId, + updatedBy: userId, + tenantId: tenantId, + orgId: orgId, + syncedAt: new Date(), + children: allSubTasks, // Template tasks + custom tasks as subtasks + taskSequence: improvementTaskSequence, // Children's externalIds in correct order + attachments: [], + projectTemplateDetails: { + _id: template.templateId, + externalId: templateData && templateData.externalId ? templateData.externalId : '', + name: templateData && templateData.title ? templateData.title : taskName, + }, + } + + // Add improvementProject task to project + // Root taskSequence should only contain improvementTask externalIds (not subtasks) + projectData.tasks.push(improvementTask) + projectData.taskSequence.push(taskExternalId) + } + + // Step 5: Initialize task report for Project + // Count all tasks (root level improvementProject tasks) + const activeTasks = projectData.tasks.filter((t) => !t.isDeleted) + let taskReport = { + total: activeTasks.length, + } + + activeTasks.forEach((task) => { + if (task.isDeleted == false) { + if (!taskReport[task.status]) { + taskReport[task.status] = 1 + } else { + taskReport[task.status] += 1 + } + } + }) + + projectData.taskReport = taskReport + // Step 6: Create Single Project + // Ensure tasks array is properly initialized (should already be an array) + if (!Array.isArray(projectData.tasks)) { + projectData.tasks = [] + } + + let createdProject = await projectQueries.createProject(projectData) + // Verify tasks were saved + if (!createdProject.tasks || createdProject.tasks.length === 0) { + console.error('WARNING: Project created but tasks array is empty!') + console.error('Tasks that should have been saved:', projectData.tasks.length) + } + + // Push to Kafka for event streaming + await this.attachEntityInformationIfExists(createdProject) + await kafkaProducersHelper.pushProjectToKafka(createdProject) + await kafkaProducersHelper.pushUserActivitiesToKafka({ + userId: participantId, + projects: createdProject, + }) + + return resolve({ + success: true, + message: CONSTANTS.apiResponses.PROJECT_PLAN_CREATED, + data: { + projectId: createdProject._id, + }, + result: { + projectId: createdProject._id, + }, + }) + } catch (error) { + return reject({ + status: error.status ? error.status : HTTP_STATUS_CODE.internal_server_error.status, + message: error.message || error, + }) + } + }) + } + /** * Get project infromation when project as a task * @method From 2966a8899f876640eef8888ee80ee1a75205e6fb Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:58:43 +0530 Subject: [PATCH 2/5] Issue #252771 Feat: While update project > Update/Add task based on parentId --- module/userProjects/helper.js | 440 ---------------------------------- 1 file changed, 440 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 53f00cf1..c7eb40cc 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -4536,446 +4536,6 @@ module.exports = class UserProjectsHelper { } catch (err) {} } - /** - * Create project plan from multiple templates. - * @method - * @name createProjectPlan - * @param {Object} data - request data. - * @param {String} userId - Logged in user id (admin). - * @param {String} userToken - User token. - * @param {Object} userDetails - loggedin user's info - * @returns {Object} Project Plan created information. - */ - static createProjectPlan(data, userId, userToken, userDetails) { - return new Promise(async (resolve, reject) => { - try { - const { templates, userId: participantId, entityId, programName, projectConfig } = data - let tenantId = userDetails.userInformation.tenantId - let orgId = userDetails.userInformation.organizationId - - // Step 1: Validations - // a. Validate Participant User - let userProfile = await userService.profile(participantId, userToken) - if (!userProfile.success || !userProfile.data) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.USER_NOT_FOUND, - } - } - - // b. Validate Entity - if (entityId) { - let entityInformation = await entitiesService.entityDocuments( - { _id: entityId, tenantId: tenantId }, - CONSTANTS.common.ALL - ) - if (!entityInformation?.success || !entityInformation?.data?.length > 0) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, - } - } - } - - // c. Validate all Template IDs - must exist and be published - // Filter out any templates without templateId - const templateIds = templates - .map((t) => (t && t.templateId ? t.templateId : null)) - .filter((id) => id !== null) - - if (templateIds.length === 0) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: 'No valid template IDs provided', - } - } - - const validTemplates = await projectTemplateQueries.templateDocument( - { - _id: { $in: templateIds }, - status: CONSTANTS.common.PUBLISHED, - tenantId: tenantId, - }, - ['_id', 'title', 'categories', 'solutionId', 'solutionExternalId', 'externalId', 'taskSequence'] - ) - - // Collect all unique category IDs from templates - let allCategoryIds = [] - const categoryIdSet = new Set() // To avoid duplicates - validTemplates.forEach((template) => { - if (template.categories && Array.isArray(template.categories)) { - template.categories.forEach((category) => { - const categoryId = category._id?.toString() || category - if (categoryId && !categoryIdSet.has(categoryId)) { - categoryIdSet.add(categoryId) - allCategoryIds.push(categoryId) - } - }) - } - }) - - // Fetch full category documents from projectCategories collection - let allCategories = [] - if (allCategoryIds.length > 0) { - allCategories = await projectCategoriesQueries.categoryDocuments( - { - _id: { $in: allCategoryIds }, - tenantId: tenantId, - }, - ['_id', 'name', 'externalId', 'evidences'] - ) - } - - if (validTemplates.length !== templates.length) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.PROJECT_TEMPLATE_NOT_FOUND, - } - } - - // Step 2: Always Create New Program & Solution - let programAndSolutionData = { - programName: programName || `Program for ${userProfile.data.name}`, - isPrivateProgram: true, - type: CONSTANTS.common.IMPROVEMENT_PROJECT, - subType: CONSTANTS.common.IMPROVEMENT_PROJECT, - isReusable: false, - entities: entityId ? [entityId] : [], - } - - let programAndSolutionInformation = await solutionsHelper.createProgramAndSolution( - userId, - programAndSolutionData, - false, - userDetails, - true // isExternalProgram - ) - - if (!programAndSolutionInformation.success) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.SOLUTION_PROGRAMS_NOT_CREATED, - } - } - - const masterProgramId = programAndSolutionInformation.result.program._id - const masterSolutionId = programAndSolutionInformation.result.solution._id - - // Step 3: Initialize Single Project - let projectData = { - title: projectConfig?.name || `${userProfile.data.name}'s Project Plan`, - description: projectConfig?.description || 'Individual Development Plan', - userId: participantId, - createdBy: userId, - updatedBy: userId, - status: CONSTANTS.common.STARTED, - lastDownloadedAt: new Date(), - isAPrivateProgram: true, - programId: masterProgramId, - programExternalId: programAndSolutionInformation.result.program.externalId, - solutionId: masterSolutionId, - solutionExternalId: programAndSolutionInformation.result.solution.externalId, - programInformation: { - _id: masterProgramId, - name: programAndSolutionInformation.result.program.name, - externalId: programAndSolutionInformation.result.program.externalId, - }, - solutionInformation: { - _id: masterSolutionId, - name: programAndSolutionInformation.result.solution.name, - externalId: programAndSolutionInformation.result.solution.externalId, - }, - tenantId: tenantId, - orgId: orgId, - categories: allCategories, // Add categories from all templates - tasks: [], - taskSequence: [], - userProfile: userProfile.data, - createdAt: new Date(), - updatedAt: new Date(), - } - - // Add entity information if provided - if (entityId) { - const entityInfo = await entitiesService.entityDocuments( - { _id: entityId, tenantId: tenantId }, - CONSTANTS.common.ALL - ) - if (entityInfo?.success && entityInfo?.data?.length > 0) { - const entity = entityInfo.data[0] - projectData.entityInformation = { - _id: entity._id, - entityType: entity.entityType, - entityTypeId: entity.entityTypeId, - entityId: entity._id, - externalId: entity?.metaInformation?.externalId || '', - entityName: entity?.metaInformation?.name || '', - } - projectData.entityId = entity._id - } - } - - // Step 4: Template Orchestration (Loop for each template) - for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { - const template = templates[templateIndex] - - // Validate template has templateId - if (!template || !template.templateId) { - continue - } - - // a. Fetch Template metadata - const templateData = validTemplates.find((t) => { - if (!t || !t._id || !template.templateId) return false - return t._id.toString() === template.templateId.toString() - }) - - if (!templateData) { - continue // Skip invalid templates - } - - // b. Create improvementProject task at root level first (to get its ID) - const taskName = - template.targetTaskName || - template.targetProjectName || - templateData.title || - `Template ${templateIndex + 1}` - const taskExternalId = `task-${uuidv4().replace(/-/g, '')}` - const improvementTaskId = uuidv4() - - // c. Fetch Template Tasks and Subtasks - const templateTasks = await projectTemplatesHelper.tasksAndSubTasks( - template.templateId, - '', // language - tenantId, - orgId - ) - - if (!templateTasks || templateTasks.length === 0) { - continue - } - - // Ensure all tasks have _id before processing (required by _projectTask) - const tasksWithIds = templateTasks - .map((task) => { - if (task && !task._id) { - task._id = uuidv4() - } - return task - }) - .filter((task) => task !== null && task !== undefined) - - // d. Process Template Tasks using _projectTask with improvementTaskId as parent - let processedTemplateTasks = [] - try { - processedTemplateTasks = await _projectTask( - tasksWithIds, - true, // isImportedFromLibrary - improvementTaskId, // parentTaskId - set to improvementTask._id - userToken, - masterProgramId, - userDetails - ) - } catch (error) { - console.error(`Error processing template tasks for template ${template.templateId}:`, error) - throw error - } - - // Ensure processedTemplateTasks is an array - if (!Array.isArray(processedTemplateTasks)) { - processedTemplateTasks = [] - } - - // e. Process Custom Tasks if provided - let processedCustomTasks = [] - if (template.customTasks && template.customTasks.length > 0) { - // Ensure all custom tasks have _id before processing - const customTasksWithIds = template.customTasks - .map((task) => { - if (task && !task._id) { - task._id = uuidv4() - } - return task - }) - .filter((task) => task !== null && task !== undefined) - - try { - processedCustomTasks = await _projectTask( - customTasksWithIds, - false, // isImportedFromLibrary - improvementTaskId, // parentTaskId - set to improvementTask._id - userToken, - masterProgramId, - userDetails - ) - } catch (error) { - console.error(`Error processing custom tasks for template ${template.templateId}:`, error) - throw error - } - - // Ensure processedCustomTasks is an array - if (!Array.isArray(processedCustomTasks)) { - processedCustomTasks = [] - } - - // Mark all custom tasks as isACustomTask: true - processedCustomTasks.forEach((customTask) => { - customTask.isACustomTask = true - customTask.createdBy = userId - customTask.updatedBy = userId - customTask.createdAt = new Date() - customTask.updatedAt = new Date() - }) - } - - // f. Ensure parentId is set correctly for all root-level subtasks - if (processedTemplateTasks && Array.isArray(processedTemplateTasks)) { - processedTemplateTasks.forEach((task) => { - if (task && (!task.parentId || task.parentId !== improvementTaskId)) { - task.parentId = improvementTaskId - } - }) - } - if (processedCustomTasks && Array.isArray(processedCustomTasks)) { - processedCustomTasks.forEach((task) => { - if (task && (!task.parentId || task.parentId !== improvementTaskId)) { - task.parentId = improvementTaskId - } - }) - } - - // g. Combine template tasks and custom tasks as children - const allSubTasks = [...processedTemplateTasks, ...processedCustomTasks] - - // h. Build taskSequence for improvementTask based on template's taskSequence - let improvementTaskSequence = [] - - // If template has taskSequence, use it to order the subtasks - if (templateData.taskSequence && templateData.taskSequence.length > 0) { - // Create a map of externalId to task for quick lookup - const taskMap = new Map() - allSubTasks.forEach((task) => { - if (task && task.externalId) { - taskMap.set(task.externalId, task) - } - }) - - // First, add tasks in template's taskSequence order - templateData.taskSequence.forEach((templateTaskExternalId) => { - const task = taskMap.get(templateTaskExternalId) - if (task && task.externalId) { - improvementTaskSequence.push(task.externalId) - taskMap.delete(templateTaskExternalId) // Remove to avoid duplicates - } - }) - - // Then, add any remaining tasks (custom tasks or tasks not in template sequence) - taskMap.forEach((task) => { - if (task && task.externalId) { - improvementTaskSequence.push(task.externalId) - } - }) - } else { - // If no template taskSequence, use the order of processed tasks - allSubTasks.forEach((subTask) => { - if (subTask && subTask.externalId) { - improvementTaskSequence.push(subTask.externalId) - } - }) - } - - let improvementTask = { - _id: improvementTaskId, - externalId: taskExternalId, - name: taskName, - description: template.targetTaskName || template.targetProjectName || templateData.title || '', - type: CONSTANTS.common.IMPROVEMENT_PROJECT, - status: CONSTANTS.common.NOT_STARTED_STATUS, - isACustomTask: false, - isDeletable: false, - isDeleted: false, - isImportedFromLibrary: false, - createdAt: new Date(), - updatedAt: new Date(), - createdBy: userId, - updatedBy: userId, - tenantId: tenantId, - orgId: orgId, - syncedAt: new Date(), - children: allSubTasks, // Template tasks + custom tasks as subtasks - taskSequence: improvementTaskSequence, // Children's externalIds in correct order - attachments: [], - projectTemplateDetails: { - _id: template.templateId, - externalId: templateData && templateData.externalId ? templateData.externalId : '', - name: templateData && templateData.title ? templateData.title : taskName, - }, - } - - // Add improvementProject task to project - // Root taskSequence should only contain improvementTask externalIds (not subtasks) - projectData.tasks.push(improvementTask) - projectData.taskSequence.push(taskExternalId) - } - - // Step 5: Initialize task report for Project - // Count all tasks (root level improvementProject tasks) - const activeTasks = projectData.tasks.filter((t) => !t.isDeleted) - let taskReport = { - total: activeTasks.length, - } - - activeTasks.forEach((task) => { - if (task.isDeleted == false) { - if (!taskReport[task.status]) { - taskReport[task.status] = 1 - } else { - taskReport[task.status] += 1 - } - } - }) - - projectData.taskReport = taskReport - // Step 6: Create Single Project - // Ensure tasks array is properly initialized (should already be an array) - if (!Array.isArray(projectData.tasks)) { - projectData.tasks = [] - } - - let createdProject = await projectQueries.createProject(projectData) - // Verify tasks were saved - if (!createdProject.tasks || createdProject.tasks.length === 0) { - console.error('WARNING: Project created but tasks array is empty!') - console.error('Tasks that should have been saved:', projectData.tasks.length) - } - - // Push to Kafka for event streaming - await this.attachEntityInformationIfExists(createdProject) - await kafkaProducersHelper.pushProjectToKafka(createdProject) - await kafkaProducersHelper.pushUserActivitiesToKafka({ - userId: participantId, - projects: createdProject, - }) - - return resolve({ - success: true, - message: CONSTANTS.apiResponses.PROJECT_PLAN_CREATED, - data: { - projectId: createdProject._id, - }, - result: { - projectId: createdProject._id, - }, - }) - } catch (error) { - return reject({ - status: error.status ? error.status : HTTP_STATUS_CODE.internal_server_error.status, - message: error.message || error, - }) - } - }) - } - /** * Get project infromation when project as a task * @method From b5176aa5356942b846b07279dfd81382628cfb50 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:28:48 +0530 Subject: [PATCH 3/5] Issue #252771 Feat: While update project > Update/Add task based on parent also add metaInformation --- module/userProjects/helper.js | 100 +++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index c7eb40cc..34177d7b 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -161,12 +161,12 @@ module.exports = class UserProjectsHelper { }) if (process.env.SUBMISSION_LEVEL == 'USER') { - if (!(userProject[0].userId == userId)) { - throw { - status: HTTP_STATUS_CODE.bad_request.status, - message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND, - } - } + // if (!(userProject[0].userId == userId)) { + // throw { + // status: HTTP_STATUS_CODE.bad_request.status, + // message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND, + // } + // } } else { // validate user authenticity if the acl.visibility of project is SELf or SPECIFIC if ( @@ -376,6 +376,19 @@ module.exports = class UserProjectsHelper { updateProject.tasks = await _projectTask(data.tasks) + // Add metaInformation for custom tasks right after _projectTask processing + updateProject.tasks.forEach((task) => { + if (task.isACustomTask === true || task.isACustomTask === 'true') { + if (!task.metaInformation) { + task.metaInformation = {} + } + // Use buttonLabel from request if present, else default to "Upload" + task.metaInformation.buttonLabel = task.metaInformation.buttonLabel || 'Upload' + // Use icon from request if present, else default to "Upload" + task.metaInformation.icon = task.metaInformation.icon || 'Upload' + } + }) + if (userProject[0].tasks && userProject[0].tasks.length > 0) { // Helper function to recursively find a task by _id in nested structure // This searches in both existing tasks and newly processed tasks @@ -440,7 +453,44 @@ module.exports = class UserProjectsHelper { // Pass 1: Merge all tasks first (so we can find parents that are also being updated) // Pass 2: Handle parent-child relationships - // First pass: Merge tasks into userProject[0].tasks + // Helper function to recursively find and update a task by _id at any nesting level + const findAndUpdateTaskRecursively = (tasks, taskToUpdate) => { + for (let i = 0; i < tasks.length; i++) { + // Compare as strings to handle ObjectId vs string + if (String(tasks[i]._id) === String(taskToUpdate._id)) { + // Found the task - update it + let keepFieldsFromTask = ['observationInformation', 'submissions'] + + removeFieldsFromRequest.forEach((removeField) => { + delete tasks[i][removeField] + }) + + keepFieldsFromTask.forEach((field) => { + if (tasks[i][field]) { + taskToUpdate[field] = tasks[i][field] + } + }) + + // Merge existing task data with updates + tasks[i] = { + ...tasks[i], + ...taskToUpdate, + updatedBy: userId, + updatedAt: new Date(), + } + return true + } + // Recursively search in children + if (tasks[i].children && tasks[i].children.length > 0) { + if (findAndUpdateTaskRecursively(tasks[i].children, taskToUpdate)) { + return true + } + } + } + return false + } + + // First pass: Merge tasks into userProject[0].tasks (recursively find and update existing tasks) updateProject.tasks.forEach((task) => { task.updatedBy = userId task.updatedAt = new Date() @@ -451,28 +501,26 @@ module.exports = class UserProjectsHelper { delete task[' parentId'] } - let taskIndex = userProject[0].tasks.findIndex( - (projectTask) => String(projectTask._id) === String(task._id) - ) + // Try to find and update the task recursively (at any nesting level) + const taskFound = findAndUpdateTaskRecursively(userProject[0].tasks, task) - if (taskIndex < 0) { - // New task - add to root tasks array - userProject[0].tasks.push(task) - } else { - // Existing task - update it - let keepFieldsFromTask = ['observationInformation', 'submissions'] - - removeFieldsFromRequest.forEach((removeField) => { - delete userProject[0].tasks[taskIndex][removeField] - }) + if (!taskFound) { + // Task not found - it's a new task, treat as custom task + // Mark as custom task if not already set + if (task.isACustomTask !== true && task.isACustomTask !== 'true') { + task.isACustomTask = true + } - keepFieldsFromTask.forEach((field) => { - if (userProject[0].tasks[taskIndex][field]) { - task[field] = userProject[0].tasks[taskIndex][field] - } - }) + // Add metaInformation for all new custom tasks + if (!task.metaInformation) { + task.metaInformation = {} + } + // Use buttonLabel from request if present, else default to "Upload" + task.metaInformation.buttonLabel = task.metaInformation.buttonLabel || 'Upload' + // Use icon from request if present, else default to "Upload" + task.metaInformation.icon = task.metaInformation.icon || 'Upload' - userProject[0].tasks[taskIndex] = task + userProject[0].tasks.push(task) } }) From ceda391c1fc4fb10e0f1321f598799adc306e9a7 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:29:36 +0530 Subject: [PATCH 4/5] Issue #252771 Feat: While update project > Update/Add task based on parent --- module/userProjects/helper.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index 34177d7b..b2525b11 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -161,12 +161,12 @@ module.exports = class UserProjectsHelper { }) if (process.env.SUBMISSION_LEVEL == 'USER') { - // if (!(userProject[0].userId == userId)) { - // throw { - // status: HTTP_STATUS_CODE.bad_request.status, - // message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND, - // } - // } + if (!(userProject[0].userId == userId)) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.USER_PROJECT_NOT_FOUND, + } + } } else { // validate user authenticity if the acl.visibility of project is SELf or SPECIFIC if ( From 7a8dc5c37886b7cc5e6093126c08fa035293f782 Mon Sep 17 00:00:00 2001 From: Sachintechjoomla <92356209+Sachintechjoomla@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:57:12 +0530 Subject: [PATCH 5/5] Issue #252771 Feat: Removing timestamp for name and desc from task type of observation --- module/userProjects/helper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/userProjects/helper.js b/module/userProjects/helper.js index b2525b11..f6a0a66e 100644 --- a/module/userProjects/helper.js +++ b/module/userProjects/helper.js @@ -5055,9 +5055,9 @@ async function _projectTask( singleTask.solutionDetails._id, '', { - name: `${singleTask.solutionDetails.name}-${timestamp}`, + name: `${singleTask.solutionDetails.name}`, externalId: `${singleTask.solutionDetails.externalId}-${timestamp}`, - description: `${singleTask.solutionDetails.name}-${timestamp}`, + description: `${singleTask.solutionDetails.name}`, programExternalId: programId, status: CONSTANTS.common.PUBLISHED_STATUS, tenantData: userDetails.tenantAndOrgInfo,