import { Body, Controller, Delete, Get, Head, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import prisma from "../db"; import { notFoundError } from "../utils/error"; import { Prisma, QuotationStatus, RequestDataStatus, RequestWorkStatus, TaskOrderStatus, TaskStatus, UserTaskStatus, } from "@prisma/client"; import { RequestWithUser } from "../interfaces/user"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; import { queryOrNot } from "../utils/relation"; const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"]; function globalAllow(user: RequestWithUser["user"]) { const allowList = ["system", "head_of_admin"]; return allowList.some((v) => user.roles?.includes(v)); } const permissionCondCompany = createPermCondition((_) => true); const permissionCheck = createPermCheck(globalAllow); const permissionCheckCompany = createPermCheck((_) => true); @Route("/api/v1/task-order") @Tags("Task Order") export class TaskController extends Controller { @Get("stats") async getTaskOrderStats() { const task = await prisma.taskOrder.groupBy({ by: ["taskOrderStatus"], _count: true, }); return task.reduce>( (a, c) => Object.assign(a, { [c.taskOrderStatus]: c._count }), { [TaskOrderStatus.Pending]: 0, [TaskOrderStatus.InProgress]: 0, [TaskOrderStatus.Validate]: 0, [TaskOrderStatus.Complete]: 0, [TaskOrderStatus.Canceled]: 0, }, ); } @Get() @Security("keycloak") async getTaskOrderList( @Request() req: RequestWithUser, @Query() query: string = "", @Query() page = 1, @Query() pageSize = 30, @Query() assignedByUserId?: string, @Query() taskOrderStatus?: TaskOrderStatus, ) { return this.getTaskOrderListByCriteria( req, query, page, pageSize, assignedByUserId, taskOrderStatus, ); } @Post("list") @Security("keycloak") async getTaskOrderListByCriteria( @Request() req: RequestWithUser, @Query() query: string = "", @Query() page = 1, @Query() pageSize = 30, @Query() assignedUserId?: string, @Query() taskOrderStatus?: TaskOrderStatus, @Body() body?: { code?: string[] }, ) { const where = { taskOrderStatus, registeredBranch: { OR: permissionCondCompany(req.user) }, taskList: assignedUserId ? { some: { requestWorkStep: { responsibleUserId: assignedUserId }, }, } : undefined, code: body?.code ? { in: body.code } : undefined, OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { taskName: { contains: query } }, { contactName: { contains: query } }, { contactTel: { contains: query } }, ]), } satisfies Prisma.TaskOrderWhereInput; const [result, total] = await prisma.$transaction([ prisma.taskOrder.findMany({ where, include: { userTask: true, taskList: true, institution: true, registeredBranch: true, createdBy: true, }, }), prisma.taskOrder.count({ where }), ]); return { result, total, page, pageSize }; } @Get("{taskOrderId}") @Security("keycloak") async getTaskOrder( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Query() taskAssignedUserId?: string, ) { const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId }, include: { userTask: true, taskProduct: true, taskList: { where: { requestWorkStep: { responsibleUserId: taskAssignedUserId }, }, orderBy: { id: "asc" }, include: { requestWorkStep: { include: { responsibleUser: true, requestWork: { include: { request: { include: { employee: true, quotation: true, }, }, productService: { include: { service: { include: { workflow: { include: { step: { include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: true, }, }, }, }, }, }, work: true, product: true, }, }, }, }, }, }, }, }, institution: true, registeredBranch: true, createdBy: true, }, }); if (!record) throw notFoundError("Task Order"); return record; } @Post() @Security("keycloak", MANAGE_ROLES) async createTaskOrderList( @Request() req: RequestWithUser, @Body() body: { taskName: string; contactName: string; contactTel: string; institutionId: string; registeredBranchId?: string; taskList: { requestWorkId: string; step: number }[]; taskProduct: { productId: string; discount?: number }[]; }, ) { return await prisma.$transaction(async (tx) => { const last = await tx.runningNo.upsert({ where: { key: "TASK", }, create: { key: "TASK", value: 1, }, update: { value: { increment: 1 }, }, }); const current = new Date(); const year = `${current.getFullYear()}`.slice(-2).padStart(2, "0"); const month = `${current.getMonth() + 1}`.padStart(2, "0"); const code = `PO${year}${month}${last.value.toString().padStart(6, "0")}`; const { taskList, taskProduct, ...rest } = body; const userAffiliatedBranch = await tx.branch.findFirst({ include: branchRelationPermInclude(req.user), where: body.registeredBranchId ? { id: body.registeredBranchId } : { user: { some: { userId: req.user.sub } }, }, }); if (!userAffiliatedBranch) { throw new HttpError( HttpStatus.BAD_REQUEST, "You must be affilated with at least one branch or specify branch to be registered (System permission required).", "reqMinAffilatedBranch", ); } await permissionCheckCompany(req.user, userAffiliatedBranch); const updated = await tx.requestWorkStepStatus.updateMany({ where: { OR: taskList, workStatus: RequestWorkStatus.Ready, }, data: { workStatus: RequestWorkStatus.InProgress }, }); if (updated.count !== taskList.length) { throw new HttpError( HttpStatus.PRECONDITION_FAILED, "All request work to issue task order must be in ready state.", "requestWorkMustReady", ); } return await tx.taskOrder.create({ include: { taskList: { include: { requestWorkStep: { include: { requestWork: { include: { request: { include: { employee: true, quotation: true, }, }, productService: { include: { service: { include: { workflow: { include: { step: { include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: true, }, }, }, }, }, }, work: true, product: true, }, }, }, }, }, }, }, }, institution: true, createdBy: true, }, data: { ...rest, code, registeredBranchId: userAffiliatedBranch.id, createdByUserId: req.user.sub, taskList: { create: taskList }, taskProduct: { create: taskProduct }, }, }); }); } @Put("{taskOrderId}") @Security("keycloak") async editTaskById( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Body() body: { taskName: string; taskOrderStatus?: TaskOrderStatus; contactName: string; contactTel: string; institutionId: string; taskList: { requestWorkId: string; step: number }[]; taskProduct: { productId: string; discount?: number }[]; }, ) { const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, taskList: { include: { requestWorkStep: { include: { requestWork: true }, }, }, }, institution: true, createdBy: true, }, }); if (!record) throw notFoundError("Task Order"); await permissionCheckCompany(req.user, record.registeredBranch); await prisma.taskOrder.update({ where: { id: taskOrderId }, include: { taskList: { include: { requestWorkStep: { include: { requestWork: true, }, }, }, }, institution: true, registeredBranch: true, createdBy: true, }, data: { ...body, taskList: { deleteMany: record?.taskList.filter( (lhs) => !body.taskList.find( (rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step, ), ), createMany: { data: body.taskList.filter( (lhs) => !record?.taskList.find( (rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step, ), ), skipDuplicates: true, }, }, taskProduct: { deleteMany: {}, create: body.taskProduct }, }, }); } @Delete("{taskOrderId}") @Security("keycloak", MANAGE_ROLES) async deleteTask(@Request() req: RequestWithUser, @Path() taskOrderId: string) { await prisma.$transaction(async (tx) => { let record = await tx.taskOrder.findFirst({ where: { id: taskOrderId }, include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }); if (!record) throw notFoundError("Task Order"); await permissionCheck(req.user, record.registeredBranch); }); } } @Route("/api/v1/task-order/{taskOrderId}") @Tags("Task Order") export class TaskActionController extends Controller { @Post("set-task-status") @Security("keycloak") async changeTaskOrderTaskListStatus( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Body() body: { step: number; requestWorkId: string; taskStatus: TaskStatus; failedType?: string; failedComment?: string; }[], ) { return await prisma.$transaction(async (tx) => { const promises = body.map(async (v) => { const record = await tx.task.findFirst({ include: { requestWorkStep: true }, where: { step: v.step, requestWorkId: v.requestWorkId, taskOrderId: taskOrderId, }, }); if (!record) throw notFoundError("Task List"); if (v.taskStatus === TaskStatus.Restart && record.requestWorkStep.responsibleUserId) { await tx.userTask.updateMany({ where: { taskOrderId: record.taskOrderId, userId: record.requestWorkStep.responsibleUserId, }, data: { userTaskStatus: UserTaskStatus.Restart }, }); } return await tx.task.update({ where: { id: record.id }, data: { taskStatus: v.taskStatus, failedType: v.failedType, failedComment: v.failedComment, }, }); }); return await Promise.all(promises); }); } @Post("submit") @Security("keycloak") async submitTaskOrder( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Query() submitUserId?: string, // for explicit ) { submitUserId = submitUserId ?? req.user.sub; const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } }); if (!record) throw notFoundError("Task Order"); await prisma.$transaction([ prisma.requestWorkStepStatus.updateMany({ where: { task: { some: { taskOrderId: taskOrderId, taskStatus: TaskStatus.Success, requestWorkStep: { responsibleUserId: submitUserId }, }, }, }, data: { workStatus: RequestWorkStatus.Validate }, }), prisma.task.updateMany({ where: { taskOrderId: taskOrderId, taskStatus: TaskStatus.Success, requestWorkStep: { responsibleUserId: submitUserId }, }, data: { taskStatus: TaskStatus.Validate, }, }), prisma.userTask.updateMany({ where: { taskOrderId: taskOrderId, userId: submitUserId, }, data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() }, }), ]); } @Post("complete") @Security("keycloak") async completeTaskOrder(@Request() req: RequestWithUser, @Path() taskOrderId: string) { const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } }); if (!record) throw notFoundError("Task Order"); await prisma.$transaction(async (tx) => { await Promise.all([ tx.taskOrder.update({ where: { id: taskOrderId }, data: { taskOrderStatus: TaskOrderStatus.Complete, userTask: { updateMany: { where: { taskOrderId }, data: { userTaskStatus: UserTaskStatus.Submit, }, }, }, }, }), tx.requestWorkStepStatus.updateMany({ where: { task: { some: { taskOrderId, taskStatus: { notIn: [ TaskStatus.Canceled, TaskStatus.Success, TaskStatus.Validate, TaskStatus.Complete, ], }, }, }, }, data: { workStatus: RequestWorkStatus.Ready }, }), tx.task.updateMany({ where: { taskOrderId: taskOrderId, taskStatus: { notIn: [ TaskStatus.Canceled, TaskStatus.Success, TaskStatus.Validate, TaskStatus.Complete, ], }, }, data: { taskStatus: TaskStatus.Redo }, }), tx.task.updateMany({ where: { taskOrderId: taskOrderId, taskStatus: TaskStatus.Validate, }, data: { taskStatus: TaskStatus.Complete }, }), ]); await tx.requestWorkStepStatus.updateMany({ where: { task: { some: { taskOrderId, taskStatus: TaskStatus.Complete }, }, }, data: { workStatus: RequestWorkStatus.Completed }, }); const requestList = await tx.requestData.findMany({ include: { requestWork: { include: { productService: { include: { product: true, service: true, work: { include: { productOnWork: true }, }, }, }, stepStatus: true, }, }, }, where: { requestWork: { some: { stepStatus: { some: { task: { some: { taskOrderId } }, }, }, }, }, }, }); const completed: string[] = []; requestList.forEach((item) => { const completeCheck = item.requestWork.every((work) => { const stepCount = work.productService.work?.productOnWork.find( (v) => v.productId === work.productService.productId, )?.stepCount || 0; const completeCount = work.stepStatus.filter( (v) => v.workStatus === RequestWorkStatus.Completed || v.workStatus === RequestWorkStatus.Ended || v.workStatus === RequestWorkStatus.Canceled, ).length; // NOTE: step found then check if complete count equals step count if (stepCount === completeCount && completeCount > 0) return true; // NOTE: likely no step found and completed at least one if (stepCount === 0 && completeCount > 0) return true; }); if (completeCheck) completed.push(item.id); }); await tx.requestData.updateMany({ where: { id: { in: completed } }, data: { requestDataStatus: RequestDataStatus.Completed }, }); await tx.quotation.updateMany({ where: { quotationStatus: { notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], }, requestData: { every: { requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] }, }, }, }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, }); }); } } @Route("api/v1/task-order/{taskOrderId}") @Tags("Task Order") export class TaskOrderAttachmentController extends Controller { private async checkPermission(user: RequestWithUser["user"], id: string) { const data = await prisma.taskOrder.findUnique({ include: { registeredBranch: { include: branchRelationPermInclude(user) } }, where: { id }, }); if (!data) throw notFoundError("Task Order"); await permissionCheck(user, data.registeredBranch); } @Get("attachment") @Security("keycloak") async listAttachment(@Request() req: RequestWithUser, @Path() taskOrderId: string) { await this.checkPermission(req.user, taskOrderId); return await listFile(fileLocation.task.attachment(taskOrderId)); } @Get("attachment/{name}") @Security("keycloak") async getAttachment(@Path() taskOrderId: string, @Path() name: string) { return await getFile(fileLocation.task.attachment(taskOrderId, name)); } @Head("attachment/{name}") async headAttachment(@Path() taskOrderId: string, @Path() name: string) { return await getPresigned("head", fileLocation.task.attachment(taskOrderId, name)); } @Put("attachment/{name}") @Security("keycloak") async putAttachment( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Path() name: string, ) { await this.checkPermission(req.user, taskOrderId); return await setFile(fileLocation.task.attachment(taskOrderId, name)); } @Delete("attachment/{name}") @Security("keycloak") async delAttachment( @Request() req: RequestWithUser, @Path() taskOrderId: string, @Path() name: string, ) { await this.checkPermission(req.user, taskOrderId); return await deleteFile(fileLocation.task.attachment(taskOrderId, name)); } } @Route("api/v1/user-task-order") @Tags("User Task Order") export class UserTaskController extends Controller { @Get() @Security("keycloak") async getUserTask( @Request() req: RequestWithUser, @Query() query: string = "", @Query() page = 1, @Query() pageSize = 30, @Query() userTaskStatus?: UserTaskStatus, ) { const where = { taskList: { some: { requestWorkStep: { responsibleUserId: req.user.sub }, }, }, AND: userTaskStatus ? [ { OR: userTaskStatus === UserTaskStatus.Pending ? [ { userTask: { some: { userTaskStatus: { in: [UserTaskStatus.Pending, UserTaskStatus.Restart], }, userId: req.user.sub, }, }, }, { userTask: { none: { userId: req.user.sub } }, }, ] : undefined, userTask: userTaskStatus !== UserTaskStatus.Pending ? { some: { userTaskStatus, userId: req.user.sub, }, } : undefined, }, ] : undefined, OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { taskName: { contains: query } }, { contactName: { contains: query } }, { contactTel: { contains: query } }, ]), } satisfies Prisma.TaskOrderWhereInput; const [result, total] = await prisma.$transaction([ prisma.taskOrder.findMany({ where, include: { userTask: { where: { userId: req.user.sub }, }, registeredBranch: true, institution: true, createdBy: true, }, }), prisma.taskOrder.count({ where }), ]); return { result: result.map((lhs) => ({ ...lhs, taskOrderStatus: lhs.userTask.find((rhs) => rhs.taskOrderId === lhs.id)?.userTaskStatus ?? UserTaskStatus.Pending, userTask: undefined, })), page, pageSize, total, }; } @Post("accept") @Security("keycloak") async acceptTaskOrder( @Request() req: RequestWithUser, @Body() body: { taskOrderId: string[]; }, ) { const record = await prisma.taskOrder.findMany({ include: { taskList: { orderBy: { step: "asc" }, }, }, where: { id: { in: body.taskOrderId } }, }); if (!record) throw notFoundError("Task Order"); await prisma.$transaction(async (tx) => { const promises = body.taskOrderId.flatMap((taskOrderId) => [ tx.taskOrder.update({ where: { id: taskOrderId }, data: { taskOrderStatus: TaskOrderStatus.InProgress, userTask: { deleteMany: { userId: req.user.sub }, create: { userId: req.user.sub, userTaskStatus: UserTaskStatus.Accept, acceptedAt: new Date(), }, }, }, }), tx.task.updateMany({ where: { taskOrderId: taskOrderId, taskStatus: { in: [TaskStatus.Pending, TaskStatus.Restart] }, requestWorkStep: { responsibleUserId: req.user.sub }, }, data: { taskStatus: TaskStatus.InProgress, }, }), tx.requestData.updateMany({ where: { requestWork: { some: { stepStatus: { some: { task: { some: { taskOrderId: taskOrderId } } }, }, }, }, }, data: { requestDataStatus: RequestDataStatus.InProgress }, }), ]); await Promise.all(promises); }); } }