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 } from "../utils/relation"; import { notFoundError } from "../utils/error"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; // 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: { customerBranch: { customer: { 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, ) { const where = { OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { quotation: { code: { contains: query, mode: "insensitive" } } }, { quotation: { workName: { contains: query } } }, { quotation: { customerBranch: { OR: [ { code: { contains: query, mode: "insensitive" } }, { customerName: { contains: query } }, { registerName: { contains: query } }, { registerNameEN: { contains: query } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ], }, }, employee: { OR: [ { employeePassport: { some: { number: { contains: query } }, }, }, { code: { contains: query, mode: "insensitive" } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ], }, }, ]), code, requestDataStatus, requestWork: responsibleOnly ? { some: { productService: { service: { workflow: { step: { some: { responsiblePerson: { some: { userId: req.user.sub }, }, }, }, }, }, }, }, } : undefined, quotation: { id: quotationId, registeredBranch: { OR: permissionCond(req.user) }, }, } 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, }, }, }, }, }, }, }, }, customerBranch: { include: { customer: true }, }, }, }, employee: { include: { employeePassport: { orderBy: { expireDate: "desc" }, }, }, }, }, take: pageSize, skip: (page - 1) * pageSize, }), prisma.requestData.count({ where }), ]); return { result, page, pageSize, total }; } @Get("{requestDataId}") @Security("keycloak") async getRequestData(@Path() requestDataId: string) { return await prisma.requestData.findFirst({ where: { id: requestDataId }, include: { quotation: { include: { customerBranch: { include: { customer: true } }, invoice: { include: { installments: true, payment: true, }, }, createdBy: true, }, }, employee: { include: { employeePassport: { orderBy: { expireDate: "desc" }, }, }, }, }, }); } } @Route("/api/v1/request-data/{requestDataId}") @Tags("Request List") export class RequestDataActionController extends Controller { @Post("cancel") @Security("keycloak") async cancelRequestData(@Path() requestDataId: string) { 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.updateMany({ where: { requestData: { every: { requestDataStatus: RequestDataStatus.Canceled }, }, }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, }), tx.taskOrder.updateMany({ where: { taskList: { every: { taskStatus: TaskStatus.Canceled }, }, }, data: { taskOrderStatus: TaskStatus.Canceled }, }), ]); }); } } @Route("/api/v1/request-work") @Tags("Request List") export class RequestListController extends Controller { @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 }, { task: { some: { taskStatus: TaskStatus.Redo, requestWorkStep: { workStatus: { notIn: [RequestWorkStatus.Completed, RequestWorkStatus.Ended] }, }, }, }, }, ], }, }; } if (cancelOnly) { statusCondition = { some: { workStatus: RequestWorkStatus.Canceled }, }; } if (workStatus && !readyToTask && !cancelOnly) { statusCondition = { some: { workStatus }, }; } const where = { stepStatus: readyToTask || cancelOnly || workStatus ? statusCondition : undefined, creditNote: 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: true, employee: true, }, }, stepStatus: { include: { task: { where: { taskStatus: TaskStatus.Complete } }, }, }, productService: { include: { service: { include: { workflow: { include: { step: { include: { value: true, responsiblePerson: { include: { user: true }, }, responsibleInstitution: 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, }, }, }, }, }, }, 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, }); 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.updateMany({ where: { requestData: { every: { requestDataStatus: RequestDataStatus.Canceled }, }, }, data: { quotationStatus: QuotationStatus.Canceled, urgent: false }, }), 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.updateMany({ where: { quotationStatus: { notIn: [QuotationStatus.Canceled, QuotationStatus.ProcessComplete], }, requestData: { every: { requestDataStatus: { in: [RequestDataStatus.Canceled, RequestDataStatus.Completed] }, }, }, }, data: { quotationStatus: QuotationStatus.ProcessComplete, urgent: false }, }); 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)); } }