import { Prisma, QuotationStatus, RequestDataStatus, RequestWorkStatus, TaskStatus, } from "@prisma/client"; import { Body, Controller, Delete, Get, Head, Path, Post, Put, Query, Request, Route, Security, Tags, } from "tsoa"; import { RequestWithUser } from "../interfaces/user"; import prisma from "../db"; import { branchRelationPermInclude, createPermCheck, createPermCondition, } from "../services/permission"; import { queryOrNot, whereDateQuery } from "../utils/relation"; import { notFoundError } from "../utils/error"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { getGroupUser } from "../services/keycloak"; // User in company can edit. const permissionCheck = createPermCheck((_) => true); // User in company can see. const permissionCond = createPermCondition((_) => true); @Route("/api/v1/request-data") @Tags("Request List") export class RequestDataController extends Controller { @Get("stats") @Security("keycloak") async getRequestDataStats(@Request() req: RequestWithUser) { const where = { quotation: { registeredBranch: { OR: permissionCond(req.user) }, }, } satisfies Prisma.RequestDataWhereInput; const list = await prisma.requestData.groupBy({ _count: true, by: "requestDataStatus", where: where, }); return list.reduce>( (a, c) => Object.assign(a, { [c.requestDataStatus]: c._count }), { [RequestDataStatus.Pending]: 0, [RequestDataStatus.Ready]: 0, [RequestDataStatus.InProgress]: 0, [RequestDataStatus.Completed]: 0, [RequestDataStatus.Canceled]: 0, }, ); } @Get() @Security("keycloak") async getRequestDataList( @Request() req: RequestWithUser, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() query: string = "", @Query() responsibleOnly?: boolean, @Query() requestDataStatus?: RequestDataStatus, @Query() quotationId?: string, @Query() code?: string, @Query() incomplete?: boolean, @Query() startDate?: Date, @Query() endDate?: Date, ) { const where = { OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { quotation: { code: { contains: query, mode: "insensitive" } } }, { quotation: { workName: { contains: query, mode: "insensitive" } } }, { quotation: { customerBranch: { OR: [ { code: { contains: query, mode: "insensitive" } }, { registerName: { contains: query, mode: "insensitive" } }, { registerNameEN: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } }, { lastNameEN: { contains: query, mode: "insensitive" } }, ], }, }, }, { employee: { OR: [ { employeePassport: { some: { number: { contains: query, mode: "insensitive" } }, }, }, { code: { contains: query, mode: "insensitive" } }, { firstName: { contains: query, mode: "insensitive" } }, { firstNameEN: { contains: query, mode: "insensitive" } }, { lastName: { contains: query, mode: "insensitive" } }, { lastNameEN: { contains: query, mode: "insensitive" } }, ], }, }, ]), code, requestDataStatus: incomplete ? { notIn: [RequestDataStatus.Completed, RequestDataStatus.Canceled], } : requestDataStatus, requestWork: responsibleOnly ? { some: { productService: { service: { workflow: { step: { some: { OR: [ { responsiblePerson: { some: { userId: req.user.sub }, }, }, { responsibleGroup: { some: { group: { in: await getGroupUser(req.user.sub).then((r) => r.map(({ name }: { name: string }) => name), ), }, }, }, }, ], }, }, }, }, }, }, } : undefined, quotation: { id: quotationId, registeredBranch: { OR: permissionCond(req.user) }, }, ...whereDateQuery(startDate, endDate), } satisfies Prisma.RequestDataWhereInput; const [result, total] = await prisma.$transaction([ prisma.requestData.findMany({ where, include: { quotation: { include: { productServiceList: { include: { service: { include: { workflow: { include: { step: { orderBy: { order: "asc" }, include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: true, responsibleGroup: true, }, }, }, }, }, }, }, }, customerBranch: { include: { customer: true }, }, }, }, employee: { include: { employeePassport: { orderBy: { expireDate: "desc" }, }, customerBranch: { include: { province: { include: { employmentOffice: true, }, }, district: { include: { employmentOffice: true, }, }, }, }, }, }, }, take: pageSize, skip: (page - 1) * pageSize, orderBy: [{ quotation: { urgent: "desc" } }, { createdAt: "desc" }], }), prisma.requestData.count({ where }), ]); const dataRequestData = result.map((item) => { const employee = item.employee; const dataOffice = employee.customerBranch.district?.employmentOffice.at(0) ?? employee.customerBranch.province?.employmentOffice.at(0); return { ...item, dataOffice, }; }); return { result: dataRequestData, page, pageSize, total, }; } @Get("{requestDataId}") @Security("keycloak") async getRequestData(@Path() requestDataId: string) { const record = await prisma.requestData.findFirst({ where: { id: requestDataId }, include: { quotation: { include: { customerBranch: { include: { customer: true } }, debitNoteQuotation: { select: { code: true }, }, invoice: { include: { installments: true, payment: true, }, }, createdBy: true, }, }, employee: { include: { employeePassport: { orderBy: { expireDate: "desc" }, }, }, }, }, }); if (!record) throw notFoundError("Request Data"); return record; } @Post("update-messenger") @Security("keycloak") async updateRequestData( @Request() req: RequestWithUser, @Body() boby: { defaultMessengerId: string; requestDataId: string[]; }, ) { const record = await prisma.requestData.updateManyAndReturn({ where: { id: { in: boby.requestDataId }, quotation: { registeredBranch: { OR: permissionCond(req.user), }, }, }, data: { defaultMessengerId: boby.defaultMessengerId, }, }); if (record.length <= 0) throw notFoundError("Request Data"); return record[0]; } } @Route("/api/v1/request-data/{requestDataId}") @Tags("Request List") export class RequestDataActionController 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("reject-request-cancel") @Security("keycloak") async rejectRequestCancel( @Request() req: RequestWithUser, @Path() requestDataId: string, @Body() body: { reason?: string; }, ) { const result = await prisma.requestData.updateManyAndReturn({ where: { id: requestDataId, quotation: { registeredBranch: { OR: permissionCond(req.user), }, }, }, data: { rejectRequestCancel: true, rejectRequestCancelReason: body.reason || "", }, }); if (result.length <= 0) throw notFoundError("Request Data"); return result[0]; } @Post("request-work/{requestWorkId}/reject-request-cancel") @Security("keycloak") async rejectWorkRequestCancel( @Request() req: RequestWithUser, @Path() requestWorkId: string, @Body() body: { reason?: string; }, ) { const result = await prisma.requestWork.updateManyAndReturn({ where: { id: requestWorkId, request: { quotation: { registeredBranch: { OR: permissionCond(req.user), }, }, }, }, data: { rejectRequestCancel: true, rejectRequestCancelReason: body.reason || "", }, }); if (result.length <= 0) throw notFoundError("Request Data"); return result[0]; } @Post("cancel") @Security("keycloak") async cancelRequestData(@Request() req: RequestWithUser, @Path() requestDataId: string) { const result = await prisma.requestData.findFirst({ where: { id: requestDataId, quotation: { registeredBranch: { OR: permissionCond(req.user), }, }, }, include: { quotation: { include: { customerBranch: { include: { customer: { include: { branch: { where: { userId: { not: null } } } } }, }, }, }, }, }, }); if (!result) throw notFoundError("Request Data"); await prisma.$transaction(async (tx) => { const workStepCondition = { requestWork: { requestDataId }, workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] }, }; await Promise.all([ tx.requestData.update({ where: { id: requestDataId }, data: { requestDataStatus: RequestDataStatus.Canceled, }, }), tx.requestWorkStepStatus.updateMany({ where: workStepCondition, data: { workStatus: RequestWorkStatus.Canceled, }, }), tx.task.updateMany({ where: { taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] }, requestWorkStep: workStepCondition, }, data: { taskStatus: TaskStatus.Canceled }, }), ]); await Promise.all([ tx.quotation .updateManyAndReturn({ where: { requestData: { every: { requestDataStatus: RequestDataStatus.Canceled }, }, }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, }) .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, })), }); }), tx.taskOrder .updateManyAndReturn({ where: { taskList: { every: { taskStatus: TaskStatus.Canceled }, }, }, data: { taskOrderStatus: TaskStatus.Canceled }, }) .then(async (res) => { await Promise.all( res.map((v) => tx.notification.create({ data: { title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, }, }), ), ); }), ]); 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 dataUserId: string[] = []; result.quotation.customerBranch.customer.branch.forEach((item) => { if (!dataCustomerId?.includes(item.id) && item.userId) { dataCustomerId.push(item.id); dataUserId.push(item.userId); } }); finalTextWork = `เลขที่ใบเสนอราคา: ${result.code} ${result.quotation.workName}`; 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), }); }); } @Put("request-work/step-status/{step}") @Security("keycloak") async updateRequestWorkDataStepStatus( @Path() requestDataId: string, @Path() step: number, @Body() payload: { workStatus?: RequestWorkStatus; requestWorkId: string; attributes?: Record; customerDuty?: boolean | null; customerDutyCost?: number | null; companyDuty?: boolean | null; companyDutyCost?: number | null; individualDuty?: boolean | null; individualDutyCost?: number | null; responsibleUserLocal?: boolean | null; responsibleUserId?: string | null; }[], ) { payload.forEach((item) => { if (!item.responsibleUserId) item.responsibleUserId = undefined; }); return await prisma.$transaction(async (tx) => { const workStepCondition = await tx.requestData.findFirst({ where: { id: requestDataId, }, select: { id: true }, }); if (!workStepCondition) { throw new Error("RequestWork not found requestDataId"); } const data = await Promise.all( payload.map(async (item) => { return await tx.requestWorkStepStatus.upsert({ include: { requestWork: { include: { request: true, }, }, }, where: { step_requestWorkId: { step: step, requestWorkId: item.requestWorkId, }, requestWork: { request: { id: requestDataId }, }, }, create: { ...item, step: step, requestWorkId: item.requestWorkId, }, update: item, }); }), ); if ( data.some((item) => { return ( item.workStatus === "Ready" && item.requestWork.request.requestDataStatus === "Pending" ); }) ) { await tx.requestData.updateMany({ where: { id: requestDataId, requestDataStatus: "Pending", }, data: { requestDataStatus: "Ready" }, }); } if ( data.some((item) => { return ( item.workStatus === "InProgress" || item.workStatus === "Waiting" || item.workStatus === "Validate" || item.workStatus === "Completed" || item.workStatus === "Ended" ); }) ) { await tx.requestData.update({ where: { id: requestDataId, }, data: { requestDataStatus: "InProgress" }, }); } if ( data.some((item) => { return item.workStatus === "Canceled"; }) ) { const dataId = data.map((itemId) => itemId.requestWork.id); await tx.task.updateMany({ where: { taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] }, requestWorkStep: { step: step, requestWorkId: { in: dataId }, workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] }, }, }, data: { taskStatus: TaskStatus.Canceled }, }); await Promise.all([ tx.quotation .updateManyAndReturn({ where: { quotationStatus: { not: QuotationStatus.Canceled }, requestData: { every: { requestDataStatus: RequestDataStatus.Canceled }, }, }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, }) .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, })), }); }), tx.taskOrder.updateMany({ where: { taskList: { every: { taskStatus: TaskStatus.Canceled }, }, }, data: { taskOrderStatus: TaskStatus.Canceled }, }), ]); } const requestList = await tx.requestData.findMany({ include: { requestWork: { include: { productService: { include: { product: true, service: true, work: { include: { productOnWork: true }, }, }, }, stepStatus: true, }, }, }, where: { requestWork: { some: { requestDataId: requestDataId, }, }, }, }); 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], }, requestData: { every: { requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed], }, }, }, }, 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, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, })), }); 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), }); }); // dataRecord.push(record); return data; }); } } @Route("/api/v1/request-work") @Tags("Request List") export class RequestListController 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; } @Get() @Security("keycloak") async getRequestWork( @Request() req: RequestWithUser, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() requestDataId?: string, @Query() workStatus?: RequestWorkStatus, @Query() readyToTask?: boolean, @Query() cancelOnly?: boolean, @Query() quotationId?: string, ) { let statusCondition: Prisma.RequestWorkWhereInput["stepStatus"] = {}; if (readyToTask) { statusCondition = { some: { OR: [ { workStatus: RequestWorkStatus.Ready }, { workStatus: { in: [RequestWorkStatus.Ready, RequestWorkStatus.InProgress] }, task: { some: { taskStatus: TaskStatus.Redo, requestWorkStep: { workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] }, }, }, }, }, ], }, }; } if (workStatus && !readyToTask && !cancelOnly) { statusCondition = { some: { workStatus }, }; } const where = { OR: cancelOnly ? [ { stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } }, }, { request: { requestDataStatus: RequestDataStatus.Canceled }, }, ] : undefined, stepStatus: readyToTask || workStatus ? statusCondition : undefined, creditNoteId: cancelOnly ? null : undefined, request: { id: requestDataId, requestDataStatus: readyToTask ? { notIn: [RequestDataStatus.Canceled, RequestDataStatus.Completed] } : undefined, quotationId, quotation: { registeredBranch: { OR: permissionCond(req.user) }, }, }, } satisfies Prisma.RequestWorkWhereInput; const [result, total] = await prisma.$transaction([ prisma.requestWork.findMany({ where, include: { request: { include: { quotation: readyToTask ? { include: { customerBranch: { include: { customer: true }, }, }, } : true, employee: true, }, }, stepStatus: cancelOnly ? true : { include: { task: { where: { taskStatus: TaskStatus.Complete } }, }, }, productService: { include: { service: { include: { workflow: { include: { step: { include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: true, responsibleGroup: true, }, }, }, }, }, }, work: true, product: { include: { document: true }, }, }, }, }, take: pageSize, skip: (page - 1) * pageSize, }), prisma.requestWork.count({ where }), ]); return { result: result.map((v) => { return Object.assign(v, { productService: Object.assign(v.productService, { product: Object.assign(v.productService.product, { document: v.productService.product.document.map((doc) => doc.name), }), }), }); }), page, pageSize, total, }; } @Get("{requestWorkId}") @Security("keycloak") async getRequestWorkById(@Path() requestWorkId: string) { const record = await prisma.requestWork.findFirst({ include: { request: { include: { quotation: true, employee: true, }, }, stepStatus: true, productService: { include: { service: { include: { workflow: { include: { step: { include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: true, responsibleGroup: true, }, }, }, }, }, }, work: true, product: { include: { document: true, }, }, }, }, }, where: { id: requestWorkId }, }); if (!record) throw notFoundError("Request Work"); return Object.assign(record, { productService: Object.assign(record.productService, { product: Object.assign(record.productService.product, { document: record.productService.product.document.map((doc) => doc.name), }), }), }); } @Put("{requestWorkId}") @Security("keycloak") async updateRequestWorkById( @Request() req: RequestWithUser, @Path() requestWorkId: string, @Body() payload: { attributes: Record }, ) { const record = await prisma.requestWork.update({ include: { request: { include: { quotation: true, employee: true, }, }, stepStatus: true, productService: { include: { service: true, work: true, product: { include: { document: true, }, }, }, }, }, where: { id: requestWorkId }, data: { attributes: payload.attributes }, }); return record; } @Put("{requestWorkId}/step-status/{step}") @Security("keycloak") async updateRequestWorkStepStatus( @Path() requestWorkId: string, @Path() step: number, @Body() payload: { workStatus?: RequestWorkStatus; attributes?: Record; customerDuty?: boolean | null; customerDutyCost?: number | null; companyDuty?: boolean | null; companyDutyCost?: number | null; individualDuty?: boolean | null; individualDutyCost?: number | null; responsibleUserLocal?: boolean | null; responsibleUserId?: string | null; }, ) { if (!payload.responsibleUserId) payload.responsibleUserId = undefined; return await prisma.$transaction(async (tx) => { const record = await tx.requestWorkStepStatus.upsert({ include: { requestWork: { include: { request: true, }, }, }, where: { step_requestWorkId: { step: step, requestWorkId, }, }, create: { ...payload, step: step, requestWorkId, }, update: payload, }); if (record.responsibleUserId === null) { await tx.requestWorkStepStatus.update({ where: { step_requestWorkId: { step: step, requestWorkId, }, responsibleUserId: null, }, data: { responsibleUserId: record.requestWork?.request.defaultMessengerId, }, }); } switch (payload.workStatus) { case "Ready": if (record.requestWork.request.requestDataStatus === "Pending") { await tx.requestData.updateMany({ where: { id: record.requestWork.requestDataId, requestDataStatus: "Pending", }, data: { requestDataStatus: "Ready" }, }); } break; case "InProgress": case "Waiting": case "Validate": case "Completed": case "Ended": await tx.requestData.update({ where: { id: record.requestWork.requestDataId, }, data: { requestDataStatus: "InProgress" }, }); break; case "Canceled": await tx.task.updateMany({ where: { taskStatus: { notIn: [TaskStatus.Complete, TaskStatus.Redo] }, requestWorkStep: { step: step, requestWorkId, workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] }, }, }, data: { taskStatus: TaskStatus.Canceled }, }); await Promise.all([ tx.quotation .updateManyAndReturn({ where: { quotationStatus: { not: QuotationStatus.Canceled }, requestData: { every: { requestDataStatus: RequestDataStatus.Canceled }, }, }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, }) .then(async (res) => { await tx.notification.createMany({ data: res.map((v) => ({ title: "สถานะใบเสนอราคาเปลี่ยนแปลง / Quotation Status Updated", detail: "รหัส / code : " + v.code + " Canceled", receiverId: v.createdByUserId, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, })), }); }), tx.taskOrder.updateMany({ where: { taskList: { every: { taskStatus: TaskStatus.Canceled }, }, }, data: { taskOrderStatus: TaskStatus.Canceled }, }), ]); break; } const requestList = await tx.requestData.findMany({ include: { requestWork: { include: { productService: { include: { product: true, service: true, work: { include: { productOnWork: true }, }, }, }, stepStatus: true, }, }, }, where: { requestWork: { some: { requestDataId: record.requestWork.requestDataId, }, }, }, }); 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, registeredBranchId: v.registeredBranchId, groupReceiver: { create: { name: "document_checker" } }, })), }); 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), }); }); return record; }); } } @Route("api/v1/request-work/{requestId}/step-status/{step}") @Tags("Request List") export class RequestListFileController extends Controller { private async checkPermission(user: RequestWithUser["user"], id: string) { const data = await prisma.requestWork.findUnique({ where: { id }, include: { request: { include: { quotation: { include: { registeredBranch: { include: branchRelationPermInclude(user) } }, }, }, }, }, }); if (!data) throw notFoundError("Request Work"); await permissionCheck(user, data.request.quotation.registeredBranch); } @Get("attachment") @Security("keycloak") async listAttachment( @Request() req: RequestWithUser, @Path() requestId: string, @Path() step: number, ) { await this.checkPermission(req.user, requestId); return await listFile(fileLocation.request.attachment(requestId, step)); } @Get("attachment/{name}") @Security("keycloak") async getAttachment( @Request() req: RequestWithUser, @Path() requestId: string, @Path() step: number, @Path() name: string, ) { await this.checkPermission(req.user, requestId); return await getFile(fileLocation.request.attachment(requestId, step, name)); } @Head("attachment/{name}") @Security("keycloak") async headAttachment( @Request() req: RequestWithUser, @Path() requestId: string, @Path() step: number, @Path() name: string, ) { await this.checkPermission(req.user, requestId); return await getPresigned("head", fileLocation.request.attachment(requestId, step, name)); } @Put("attachment/{name}") @Security("keycloak") async putAttachment( @Request() req: RequestWithUser, @Path() requestId: string, @Path() step: number, @Path() name: string, ) { await this.checkPermission(req.user, requestId); return await setFile(fileLocation.request.attachment(requestId, step, name)); } @Delete("attachment/{name}") @Security("keycloak") async delAttachment( @Request() req: RequestWithUser, @Path() requestId: string, @Path() step: number, @Path() name: string, ) { await this.checkPermission(req.user, requestId); return await deleteFile(fileLocation.request.attachment(requestId, step, name)); } }