// task.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; import mongoose from 'mongoose'; const Task = mongoose.model('Task'); const TaskSession = mongoose.model('TaskSession'); import dayjs from 'dayjs'; import { SiteService, SiteError } from '../../lib/site-lib.js'; export default class TaskService extends SiteService { static get name ( ) { return 'TaskService'; } static get slug () { return 'task'; } constructor (dtp) { super(dtp, TaskService); } async start ( ) { const { client: clientService, user: userService } = this.dtp.services; // const { jobQueue: jobQueueService } = this.dtp.services; // this.linksQueue = jobQueueService.getJobQueue('links', this.dtp.config.jobQueues.links); this.populateTask = [ { path: 'user', select: userService.USER_SELECT, }, { path: 'client', populate: clientService.populateClient, }, { path: 'project', populate: clientService.populateClientProject, }, ]; this.populateTaskSession = [ { path: 'user', select: userService.USER_SELECT, }, { path: 'client', populate: clientService.populateClient, }, { path: 'project', populate: clientService.populateClientProject, }, { path: 'task', populate: this.populateTask, }, { path: 'screenshots.image', } ]; } async createTask (user, taskDefinition) { const { text: textService } = this.dtp.services; const NOW = new Date(); const task = new Task(); task.created = NOW; task.user = user._id; task.client = mongoose.Types.ObjectId.createFromHexString(taskDefinition.clientId); task.project = mongoose.Types.ObjectId.createFromHexString(taskDefinition.projectId); task.status = 'pending'; task.note = textService.filter(taskDefinition.note); await task.save(); return task.toObject(); } async startTask (task) { if (task.status !== 'pending') { throw new SiteError(400, 'The task is not in the pending state'); } await Task.updateOne( { _id: task._id }, { $set: { status: 'active' } }, ); } async closeTask (task) { await TaskSession .find({ task: task._id, status: 'active' }) .cursor() .eachAsync(async (session) => { await this.closeTaskSession(session); }); await Task.updateOne( { _id: task._id }, { $set: { status: 'finished' } }, ); } async getTasksForUser (user, options, pagination) { const search = { user: user._id }; options = options || { }; if (options.pending) { search.status = 'pending'; } else if (options.active) { search.status = 'active'; } else if (options.finished) { search.status = 'finished'; } const tasks = await Task .find(search) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateTask) .lean(); return tasks.reverse(); } async getTasksForProject (project, options, pagination) { const search = { project: project._id }; options = options || { }; if (options.pending) { search.status = 'pending'; } else if (options.active) { search.status = 'active'; } else if (options.finished) { search.status = 'finished'; } const tasks = await Task .find(search) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateTask) .lean(); return tasks.reverse(); } async getTaskGridForUser (user) { const pagination = { skip: 0, cpp: 10 }; const pendingTasks = await this.getTasksForUser(user, { pending: true }, pagination); const activeTasks = await this.getTasksForUser(user, { active: true }, pagination); const finishedTasks = await this.getTasksForUser(user, { finished: true }, pagination); return { pendingTasks, activeTasks, finishedTasks }; } async getTaskGridForProject (project) { const pagination = { skip: 0, cpp: 10 }; const pendingTasks = await this.getTasksForProject(project, { pending: true }, pagination); const activeTasks = await this.getTasksForProject(project, { active: true }, pagination); const finishedTasks = await this.getTasksForProject(project, { finished: true }, pagination); return { pendingTasks, activeTasks, finishedTasks }; } async getTaskById (taskId) { const task = await Task .findOne({ _id: taskId }) .populate(this.populateTask) .lean(); return task; } async createTaskSession (task) { const NOW = new Date(); if (await TaskSession.findOne({ user: task.user._id, status: 'active' })) { throw new SiteError(401, "Can't start new session with a currently active session."); } const session = new TaskSession(); session.created = NOW; session.lastUpdated = NOW; session.hourlyRate = task.project.hourlyRate; session.user = task.user._id; session.client = task.client._id; session.project = task.project._id; session.task = task._id; session.status = 'active'; await session.save(); this.log.info('task session created', { user: { _id: task.user._id, username: task.user.username, }, }); return session.toObject(); } async addTaskSessionScreenshot (session, file) { const NOW = new Date(); const { image: imageService } = this.dtp.services; const image = await imageService.create(session.user._id, { }, file); await TaskSession.updateOne( { _id: session._id }, { $push: { screenshots: { created: NOW, image: image._id, }, }, }, ); } async closeTaskSession (session) { const NOW = new Date(); if (session.status !== 'active') { throw new SiteError(400, 'The session is not currently active'); } const duration = dayjs(NOW).diff(session.created, 'second'); await TaskSession.updateOne( { _id: session._id }, { $set: { finished: NOW, status: 'finished', duration, }, }, ); await Task.updateOne( { _id: session.task._id }, { $inc: { duration }, }, ); this.log.info('task session closed', { user: { _id: session.user._id, username: session.user.username, }, duration, }); } async getSessionsForTask (task, pagination) { const sessions = TaskSession .find({ task: task._id }) .sort({ created: 1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateTaskSession) .lean(); return sessions; } async getTaskSessionById (sessionId) { const session = await TaskSession .findOne({ _id: sessionId }) .populate(this.populateTaskSession) .lean(); return session; } async closeTaskSessionForUser (user) { await TaskSession .find({ user: user._id, status: 'active' }) .populate(this.populateTaskSession) .cursor() .eachAsync(async (session) => { await this.closeTaskSession(session); }); } }