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, 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: { taskList: true, institution: 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, taskList: { where: { requestWorkStep: { responsibleUserId: taskAssignedUserId }, }, 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, 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 }[]; }, ) { 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, ...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 }, }, }); }); } @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 }[]; }, ) { 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, }, }, }, }); } @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({ where: { step: v.step, requestWorkId: v.requestWorkId, taskOrderId: taskOrderId, }, }); if (!record) throw notFoundError("Task List"); 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.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 }, }), ]); } @Post("complete") @Security("keycloak") async completeTaskOrder(@Request() req: RequestWithUser, @Path() taskOrderId: string) {} } @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 }, }, }, userTask: userTaskStatus ? { some: userTaskStatus !== UserTaskStatus.Pending ? { userTaskStatus, userId: req.user.sub, } : undefined, none: userTaskStatus === UserTaskStatus.Pending ? { 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 }, }, institution: true, createdBy: true, }, }), prisma.taskOrder.count({ where }), ]); return { result: result.map((lhs) => ({ ...lhs, taskOrderStatus: lhs.userTask.find((rhs) => rhs.taskOrderId === lhs.id)?.userTaskStatus ?? lhs.taskOrderStatus, 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: { create: { userId: req.user.sub, userTaskStatus: UserTaskStatus.Accept, }, }, }, }), tx.task.updateMany({ where: { taskOrderId: taskOrderId, 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 }, }), tx.userTask.create({ data: { userId: req.user.sub, taskOrderId: taskOrderId, userTaskStatus: UserTaskStatus.Accept, }, }), ]); await Promise.all(promises); }); } }