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, Status, 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, deleteFolder, fileLocation, getFile, getPresigned, listFile, setFile, } from "../utils/minio"; import { queryOrNot, whereDateQuery } 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, @Query() startDate?: Date, @Query() endDate?: Date, ) { return this.getTaskOrderListByCriteria( req, query, page, pageSize, assignedByUserId, taskOrderStatus, startDate, endDate, ); } @Post("list") @Security("keycloak") async getTaskOrderListByCriteria( @Request() req: RequestWithUser, @Query() query: string = "", @Query() page = 1, @Query() pageSize = 30, @Query() assignedUserId?: string, @Query() taskOrderStatus?: TaskOrderStatus, @Query() startDate?: Date, @Query() endDate?: Date, @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, mode: "insensitive" } }, { contactName: { contains: query, mode: "insensitive" } }, { contactTel: { contains: query, mode: "insensitive" } }, ]), ...whereDateQuery(startDate, endDate), } satisfies Prisma.TaskOrderWhereInput; const [result, total] = await prisma.$transaction([ prisma.taskOrder.findMany({ where, include: { userTask: true, taskList: true, institution: true, registeredBranch: true, createdBy: true, }, orderBy: [{ createdAt: "desc" }], }), 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 }, orderBy: [{ urgent: "desc" }, { createdAt: "desc" }], include: { userTask: true, taskProduct: true, taskList: { where: { requestWorkStep: { responsibleUserId: taskAssignedUserId }, }, orderBy: { id: "asc" }, include: { requestWorkStep: { include: { responsibleUser: true, requestWork: { include: { stepStatus: true, request: { include: { employee: true, quotation: { include: { customerBranch: { include: { customer: 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; remark?: 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", ); } await tx.institution.updateMany({ where: { id: body.institutionId, status: Status.CREATED }, data: { status: Status.ACTIVE }, }); const work = await tx.requestWorkStepStatus.findMany({ include: { requestWork: { include: { request: { include: { quotation: true, }, }, }, }, }, where: { OR: taskList }, }); return await tx.taskOrder.create({ include: { taskList: { include: { requestWorkStep: { include: { requestWork: { include: { request: { include: { employee: true, quotation: { include: { customerBranch: { include: { customer: 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, urgent: work.some((v) => v.requestWork.request.quotation.urgent), 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; remark?: 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); if (record.taskList.some((v) => v.taskStatus !== "Pending")) { throw new HttpError( HttpStatus.BAD_REQUEST, "One or more task is not pending", "taskListNotPending", ); } return await prisma .$transaction(async (tx) => { await Promise.all( record.taskList .filter( (lhs) => !body.taskList.find( (rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step, ), ) .map((v) => tx.task.update({ where: { id: v.id }, data: { requestWorkStep: { update: { workStatus: "Ready" } }, }, }), ), ); await tx.requestWorkStepStatus.updateMany({ where: { OR: body.taskList, workStatus: RequestWorkStatus.Ready, }, data: { workStatus: RequestWorkStatus.InProgress }, }); const work = await tx.requestWorkStepStatus.findMany({ include: { requestWork: { include: { request: { include: { quotation: true }, }, }, }, }, where: { OR: body.taskList }, }); return await tx.taskOrder.update({ where: { id: taskOrderId }, include: { taskList: { include: { requestWorkStep: { include: { requestWork: true, }, }, }, }, institution: true, registeredBranch: true, createdBy: true, }, data: { ...body, urgent: work.some((v) => v.requestWork.request.quotation.urgent), taskList: { deleteMany: record?.taskList .filter( (lhs) => !body.taskList.find( (rhs) => lhs.requestWorkId === rhs.requestWorkId && lhs.step === rhs.step, ), ) .map((v) => ({ id: v.id })), 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 }, }, }); }) .then(async (ret) => { if (body.taskOrderStatus && record.taskOrderStatus !== body.taskOrderStatus) { await prisma.notification.create({ data: { title: "มีการส่งงาน / Task Submitted", detail: "รหัสใบสั่งงาน / Order : " + record.code, receiverId: record.createdByUserId, }, }); } return ret; }); } @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), }, taskList: true, }, }); if (!record) throw notFoundError("Task Order"); await permissionCheck(req.user, record.registeredBranch); if (record.taskList.some((v) => v.taskStatus !== "Pending")) { throw new HttpError( HttpStatus.BAD_REQUEST, "One or more task is not pending", "taskListNotPending", ); } await Promise.all([deleteFolder(fileLocation.task.attachment(taskOrderId))]); await Promise.all( record.taskList.map((v) => tx.task.update({ where: { id: v.id }, data: { requestWorkStep: { update: { workStatus: "Ready" } } }, }), ), ); await tx.taskOrder.delete({ where: { id: taskOrderId } }); }); } } @Route("/api/v1/task-order/{taskOrderId}") @Tags("Task Order") export class TaskActionController extends Controller { async #getLineToken() { if (!process.env.LINE_MESSAGING_API_TOKEN) { console.warn("Line Webhook Activated but LINE_MESSAGING_API_TOKEN not set."); } return process.env.LINE_MESSAGING_API_TOKEN; } @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() }, }), prisma.notification.create({ data: { title: "มีการส่งงาน / Task Submitted", detail: "รหัสใบสั่งงาน / Order : " + record.code, receiverId: record.createdByUserId, }, }), ]); } @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: { urgent: false, 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 .updateManyAndReturn({ where: { quotationStatus: { notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], }, AND: [ { requestData: { every: { requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], }, }, }, }, { requestData: { some: {}, }, }, ], }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, include: { customerBranch: { include: { customer: { include: { branch: { where: { userId: { not: null } }, }, }, }, }, }, }, }) .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", detail: "รหัส / code : " + v.code + " Completed", receiverId: v.createdByUserId, })), }); const token = await this.#getLineToken(); if (!token) return; const textHead = "JWS ALERT:"; const textAlert = "ขอแจ้งให้ทราบว่าใบเสนอราคา"; const textAlert2 = "ได้ดำเนินการเสร็จสิ้นทุกกระบวนการเรียบร้อยแล้ว"; const textAlert3 = "หากต้องการข้อมูลเพิ่มเติม กรุณาแจ้งให้ฝ่ายที่เกี่ยวข้องทราบ 🙏"; let finalTextWork = ""; let textData = ""; let dataCustomerId: string[] = []; let textWorkList: string[] = []; let dataUserId: string[] = []; if (res) { res.forEach((data, index) => { data.customerBranch.customer.branch.forEach((item) => { if (!dataCustomerId?.includes(item.id) && item.userId) { dataCustomerId.push(item.id); dataUserId.push(item.userId); } }); textWorkList.push(`${index + 1}. เลขที่ใบเสนอราคา ${data.code} ${data.workName}`); }); finalTextWork = textWorkList.join("\n"); } textData = `${textHead}\n\n${textAlert}\n${finalTextWork}\n${textAlert2}\n\n${textAlert3}`; const data = { to: dataUserId, messages: [ { type: "text", text: textData, }, ], }; await fetch("https://api.line.me/v2/bot/message/multicast", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(data), }); }); }); } } @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, @Query() startDate?: Date, @Query() endDate?: Date, ) { 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, mode: "insensitive" } }, { contactName: { contains: query, mode: "insensitive" } }, { contactTel: { contains: query, mode: "insensitive" } }, ]), ...whereDateQuery(startDate, endDate), } 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(), }, }, }, }) .then(async (v) => { await tx.notification.createMany({ data: [ { title: "สถานะใบส่งงานมีการเปลี่ยนแปลง / Order Status Changed", detail: "รหัสใบสั่งงาน / Order : " + v.code + " InProgress", receiverId: v.createdByUserId, }, { title: "มีการรับงาน / Task Accepted", detail: "รหัสใบสั่งงาน / Order : " + v.code, receiverId: v.createdByUserId, }, ], }); }), 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); }); } }