import { PayCondition, Prisma, QuotationStatus, RequestDataStatus, Status } from "@prisma/client"; import config from "../config.json"; 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 { isSystem } from "../utils/keycloak"; import { isUsedError, notFoundError, relationError } from "../utils/error"; import { precisionRound } from "../utils/arithmetic"; import { queryOrNot } from "../utils/relation"; import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { RequestWorkStatus } from "../generated/kysely/types"; type QuotationCreate = { registeredBranchId: string; status?: Status; remark?: string | null; workName: string; contactName: string; contactTel: string; dueDate: Date; payCondition: PayCondition; paySplitCount?: number; paySplit?: { name?: string | null; amount: number }[]; payBillDate?: Date; // EmployeeId or Create new employee worker: ( | string | { dateOfBirth: Date; gender: string; nationality: string; namePrefix?: string; firstName: string; firstNameEN: string; middleName?: string; middleNameEN?: string; lastName: string; lastNameEN: string; } )[]; workerMax?: number | null; customerBranchId: string; urgent?: boolean; agentPrice?: boolean; discount?: number; productServiceList: { serviceId?: string; workId?: string; productId: string; amount: number; discount?: number; installmentNo?: number; workerIndex?: number[]; }[]; }; type QuotationUpdate = { registeredBranchId?: string; status?: "ACTIVE" | "INACTIVE"; quotationStatus?: "Accepted"; remark?: string | null; workName?: string; contactName?: string; contactTel?: string; dueDate?: Date; payCondition?: PayCondition; paySplitCount?: number; paySplit?: { name?: string | null; amount: number }[]; payBillDate?: Date; // EmployeeId or Create new employee worker?: ( | string | { dateOfBirth: Date; gender: string; nationality: string; namePrefix?: string; firstName: string; firstNameEN: string; middleName?: string; middleNameEN?: string; lastName: string; lastNameEN: string; } )[]; workerMax?: number | null; customerBranchId?: string; urgent?: boolean; discount?: number; productServiceList?: { serviceId?: string; workId?: string; productId: string; amount: number; discount?: number; installmentNo?: number; workerIndex?: number[]; }[]; }; const VAT_DEFAULT = config.vat; const MANAGE_ROLES = [ "system", "head_of_admin", "admin", "head_of_accountant", "accountant", "head_of_sale", "sale", ]; function globalAllow(user: RequestWithUser["user"]) { const allowList = ["system", "head_of_admin", "head_of_accountant", "head_of_sale"]; return allowList.some((v) => user.roles?.includes(v)); } const permissionCheckCompany = createPermCheck((_) => true); const permissionCheck = createPermCheck(globalAllow); const permissionCond = createPermCondition(globalAllow); @Route("/api/v1/quotation") @Tags("Quotation") export class QuotationController extends Controller { @Get("stats") @Security("keycloak") async getProductStats(@Request() req: RequestWithUser) { const result = await prisma.quotation.groupBy({ _count: true, by: "quotationStatus", where: { registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) }, isDebitNote: false, }, }); return result.reduce>((a, c) => { a[c.quotationStatus.charAt(0).toLowerCase() + c.quotationStatus.slice(1)] = c._count; return a; }, {}); } @Get() @Security("keycloak") async getQuotationList( @Request() req: RequestWithUser, @Query() page: number = 1, @Query() pageSize: number = 30, @Query() payCondition?: PayCondition, @Query() status?: QuotationStatus, @Query() urgentFirst?: boolean, @Query() includeRegisteredBranch?: boolean, @Query() hasCancel?: boolean, @Query() cancelIncludeDebitNote?: boolean, @Query() forDebitNote?: boolean, @Query() code?: string, @Query() query = "", ) { const where = { OR: queryOrNot(query, [ { code: { contains: query, mode: "insensitive" } }, { workName: { contains: query } }, { customerBranch: { OR: [ { code: { contains: query, mode: "insensitive" } }, { customerName: { contains: query } }, { firstName: { contains: query } }, { firstNameEN: { contains: query } }, { lastName: { contains: query } }, { lastNameEN: { contains: query } }, ], }, }, ]), isDebitNote: hasCancel && cancelIncludeDebitNote ? true : false, code, payCondition, registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) }, quotationStatus: forDebitNote ? { notIn: ["Issued", "Expired", "Accepted", "Canceled"] } : status, requestData: hasCancel ? { some: { OR: [ { requestDataStatus: RequestDataStatus.Canceled, requestWork: { some: { creditNoteId: null }, }, }, { requestWork: { some: { creditNoteId: null, stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } }, }, }, }, ], }, } : undefined, } satisfies Prisma.QuotationWhereInput; const [result, total] = await prisma.$transaction([ prisma.quotation.findMany({ where, include: { _count: { select: { worker: true }, }, registeredBranch: includeRegisteredBranch, customerBranch: { include: { customer: { include: { registeredBranch: true }, }, }, }, invoice: { include: { payment: true }, }, paySplit: { orderBy: { no: "asc" }, }, createdBy: true, updatedBy: true, }, orderBy: urgentFirst ? [{ urgent: "desc" }, { createdAt: "desc" }] : { createdAt: "desc" }, take: pageSize, skip: (page - 1) * pageSize, }), prisma.quotation.count({ where }), ]); if (hasCancel) { const canceled = await prisma.requestData.findMany({ include: { _count: { select: { requestWork: { where: { OR: [ { request: { requestDataStatus: RequestDataStatus.Canceled } }, { stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } } }, ], creditNoteId: null, }, }, }, }, }, where: { OR: [ { requestDataStatus: RequestDataStatus.Canceled, requestWork: { some: { creditNoteId: null }, }, }, { requestWork: { some: { creditNoteId: null, stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } }, }, }, }, ], quotationId: { in: result.map((v) => v.id) }, }, }); return { result: result.map((v) => { const canceledCount = canceled .filter((item) => item.quotationId === v.id) .reduce((a, c) => a + c._count.requestWork, 0); return Object.assign(v, { _count: { ...v._count, canceledWork: canceledCount }, }); }), page, pageSize, total, }; } return { result: result, page, pageSize, total }; } @Get("{quotationId}") @Security("keycloak") async getQuotationById(@Path() quotationId: string) { const record = await prisma.quotation.findUnique({ include: { _count: { select: { worker: true }, }, registeredBranch: true, customerBranch: { include: { province: true, district: true, subDistrict: true, }, }, worker: { include: { employee: { include: { employeePassport: { orderBy: { expireDate: "desc" }, }, }, }, }, }, debitNote: true, productServiceList: { include: { service: { include: { productGroup: true, work: { include: { productOnWork: { include: { product: true, }, }, }, }, }, }, work: true, product: { include: { productGroup: true }, }, worker: true, }, }, paySplit: { include: { invoice: true }, orderBy: { no: "asc" }, }, createdBy: true, updatedBy: true, }, where: { id: quotationId, isDebitNote: false }, }); if (!record) throw notFoundError("Quotation"); return record; } @Post() @Security("keycloak", MANAGE_ROLES) async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) { const ids = { employee: body.worker.filter((v) => typeof v === "string"), product: body.productServiceList .map((v) => v.productId) .filter((v, i, a) => a.findIndex((c) => c === v) === i), work: body.productServiceList .map((v) => v.workId || []) .filter((v, i, a) => a.findIndex((c) => c === v) === i) .flat(), service: body.productServiceList .map((v) => v.serviceId || []) .filter((v, i, a) => a.findIndex((c) => c === v) === i) .flat(), }; const [customerBranch, employee, product, work, service] = await prisma.$transaction( async (tx) => await Promise.all([ tx.customerBranch.findUnique({ include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, where: { id: body.customerBranchId }, }), tx.employee.findMany({ where: { id: { in: ids.employee } } }), tx.product.findMany({ where: { id: { in: ids.product } } }), ids.work.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null, ids.service.length ? tx.service.findMany({ where: { id: { in: ids.service } } }) : null, ]), ); if (!customerBranch) throw relationError("Customer Branch"); if (ids.employee.length !== employee.length) throw relationError("Worker"); if (ids.product.length !== product.length) throw relationError("Product"); if (ids.work.length && ids.work.length !== work?.length) throw relationError("Work"); if (ids.service.length && ids.service.length !== service?.length) { throw relationError("Service"); } await permissionCheckCompany(req.user, customerBranch.customer.registeredBranch); const { productServiceList: _productServiceList, worker: _worker, ...rest } = body; return await prisma.$transaction(async (tx) => { const nonExistEmployee = body.worker.filter((v) => typeof v !== "string"); const lastEmployee = await tx.runningNo.upsert({ where: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, }, create: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, value: nonExistEmployee.length, }, update: { value: { increment: nonExistEmployee.length } }, }); const newEmployee = await Promise.all( nonExistEmployee.map(async (v, i) => tx.employee.create({ data: { ...v, code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`, customerBranchId: customerBranch.id, }, }), ), ); const sortedEmployeeId: string[] = []; while (body.worker.length > 0) { const popExist = body.worker.shift(); if (typeof popExist === "string") sortedEmployeeId.push(popExist); else { const popNew = newEmployee.shift(); popNew && sortedEmployeeId.push(popNew.id); } } const currentYear = new Date().getFullYear(); const currentMonth = new Date().getMonth() + 1; const lastQuotation = await tx.runningNo.upsert({ where: { key: `QUOTATION_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`, }, create: { key: `QUOTATION_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`, value: 1, }, update: { value: { increment: 1 } }, }); const list = body.productServiceList.map((v, i) => { const p = product.find((p) => p.id === v.productId)!; const vatIncluded = body.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const originalPrice = body.agentPrice ? p.agentPrice : p.price; const finalPriceWithVat = precisionRound( originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), ); const price = finalPriceWithVat; const pricePerUnit = price / (1 + VAT_DEFAULT); const vat = (body.agentPrice ? p.agentPriceCalcVat : p.calcVat) ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT : 0; return { order: i + 1, productId: v.productId, workId: v.workId, serviceId: v.serviceId, pricePerUnit, amount: v.amount, discount: v.discount || 0, installmentNo: v.installmentNo, vat, worker: { create: sortedEmployeeId .filter((_, i) => !v.workerIndex || i in v.workerIndex) .map((employeeId) => ({ employeeId })), }, }; }); const price = list.reduce( (a, c) => { a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.vat = precisionRound(a.vat + c.vat); a.vatExcluded = c.vat === 0 ? precisionRound(a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0))) : a.vatExcluded; a.finalPrice = precisionRound( Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), ); return a; }, { totalPrice: 0, totalDiscount: 0, vat: 0, vatExcluded: 0, discount: body.discount, finalPrice: 0, }, ); await Promise.all([ tx.service.updateMany({ where: { id: { in: ids.service }, status: Status.CREATED }, data: { status: Status.ACTIVE }, }), tx.product.updateMany({ where: { id: { in: ids.product }, status: Status.CREATED }, data: { status: Status.ACTIVE }, }), ]); return await tx.quotation.create({ include: { productServiceList: { include: { service: { include: { productGroup: true, work: { include: { productOnWork: { include: { product: true, }, }, }, }, }, }, work: true, product: { include: { productGroup: true }, }, worker: true, }, }, paySplit: { include: { invoice: true }, }, worker: true, customerBranch: { include: { customer: true }, }, _count: { select: { productServiceList: true }, }, }, data: { ...rest, ...price, statusOrder: +(rest.status === "INACTIVE"), code: `QT${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(6, "0")}`, worker: { createMany: { data: sortedEmployeeId.map((v, i) => ({ no: i, employeeId: v, })), }, }, paySplit: { createMany: { data: (rest.paySplit || []).map((v, i) => ({ no: i + 1, ...v, })), }, }, productServiceList: { create: list, }, createdByUserId: req.user.sub, updatedByUserId: req.user.sub, }, }); }); } @Put("{quotationId}") @Security("keycloak", MANAGE_ROLES) async editQuotation( @Request() req: RequestWithUser, @Path() quotationId: string, @Body() body: QuotationUpdate, ) { const record = await prisma.quotation.findUnique({ include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, customerBranch: { include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, }, }, }, }, where: { id: quotationId, isDebitNote: false }, }); if (!record) throw notFoundError("Quotation"); await permissionCheckCompany(req.user, record.registeredBranch); const ids = { employee: body.worker?.filter((v) => typeof v === "string"), product: body.productServiceList ?.map((v) => v.productId) .filter((v, i, a) => a.findIndex((c) => c === v) === i), work: body.productServiceList ?.map((v) => v.workId || []) .filter((v, i, a) => a.findIndex((c) => c === v) === i) .flat(), service: body.productServiceList ?.map((v) => v.serviceId || []) .filter((v, i, a) => a.findIndex((c) => c === v) === i) .flat(), }; const [customerBranch, employee, product, work, service] = await prisma.$transaction( async (tx) => await Promise.all([ tx.customerBranch.findFirst({ include: { customer: { include: { registeredBranch: { include: branchRelationPermInclude(req.user), }, }, }, }, where: { id: body.customerBranchId }, }), tx.employee.findMany({ where: { id: { in: ids.employee } } }), tx.product.findMany({ where: { id: { in: ids.product } } }), ids.work?.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null, ids.service?.length ? tx.service.findMany({ include: { work: { include: { productOnWork: true } } }, where: { id: { in: ids.service } }, }) : null, ]), ); if (body.customerBranchId && !customerBranch) throw relationError("Customer Branch"); if (ids.employee && ids.employee.length !== employee.length) throw relationError("Worker"); if (ids.product && ids.product.length !== product.length) throw relationError("Product"); if (ids.work && ids.work.length && ids.work.length !== work?.length) { throw relationError("Work"); } if (ids.service && ids.service.length && ids.service.length !== service?.length) { throw relationError("Service"); } await permissionCheckCompany(req.user, record.customerBranch.customer.registeredBranch); if ( customerBranch && body.customerBranchId && record.customerBranchId !== body.customerBranchId ) { await permissionCheckCompany(req.user, customerBranch.customer.registeredBranch); } const { productServiceList: _productServiceList, worker: _worker, ...rest } = body; return await prisma.$transaction(async (tx) => { const sortedEmployeeId: string[] = []; const branchId = (customerBranch || record.customerBranch).id; const branchCode = (customerBranch || record.customerBranch).code; if (body.worker) { const nonExistEmployee = body.worker.filter((v) => typeof v !== "string"); const lastEmployee = await tx.runningNo.upsert({ where: { key: `EMPLOYEE_${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, }, create: { key: `EMPLOYEE_${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, value: 1, }, update: { value: { increment: nonExistEmployee.length } }, }); const newEmployee = await Promise.all( nonExistEmployee.map(async (v, i) => tx.employee.create({ data: { ...v, code: `${branchCode}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - nonExistEmployee.length + i + 1}`.padStart(7, "0")}`, customerBranchId: branchId, }, }), ), ); while (body.worker.length > 0) { const popExist = body.worker.shift(); if (typeof popExist === "string") sortedEmployeeId.push(popExist); else { const popNew = newEmployee.shift(); popNew && sortedEmployeeId.push(popNew.id); } } } const list = body.productServiceList?.map((v, i) => { const p = product.find((p) => p.id === v.productId)!; const vatIncluded = record.agentPrice ? p.agentPriceVatIncluded : p.vatIncluded; const originalPrice = record.agentPrice ? p.agentPrice : p.price; const finalPriceWithVat = precisionRound( originalPrice + (vatIncluded ? 0 : originalPrice * VAT_DEFAULT), ); const price = finalPriceWithVat; const pricePerUnit = price / (1 + VAT_DEFAULT); const vat = (record.agentPrice ? p.agentPriceCalcVat : p.calcVat) ? (pricePerUnit * v.amount - (v.discount || 0)) * VAT_DEFAULT : 0; return { order: i + 1, productId: v.productId, workId: v.workId, serviceId: v.serviceId, pricePerUnit, amount: v.amount, discount: v.discount || 0, installmentNo: v.installmentNo, vat, worker: { create: sortedEmployeeId .filter((_, i) => !v.workerIndex || i in v.workerIndex) .map((employeeId) => ({ employeeId })), }, }; }); const price = list?.reduce( (a, c) => { a.totalPrice = precisionRound(a.totalPrice + c.pricePerUnit * c.amount); a.totalDiscount = precisionRound(a.totalDiscount + c.discount); a.vat = precisionRound(a.vat + c.vat); a.vatExcluded = c.vat === 0 ? precisionRound( a.vatExcluded + (c.pricePerUnit * c.amount - (c.discount || 0)) * VAT_DEFAULT, ) : a.vatExcluded; a.finalPrice = precisionRound( Math.max(a.totalPrice - a.totalDiscount + a.vat - (body.discount || 0), 0), ); return a; }, { totalPrice: 0, totalDiscount: 0, vat: 0, vatExcluded: 0, discount: body.discount, finalPrice: 0, }, ); await Promise.all([ tx.service.updateMany({ where: { id: { in: ids.service }, status: Status.CREATED }, data: { status: Status.ACTIVE }, }), tx.product.updateMany({ where: { id: { in: ids.product }, status: Status.CREATED }, data: { status: Status.ACTIVE }, }), ]); return await tx.quotation.update({ include: { productServiceList: { include: { service: { include: { productGroup: true, work: { include: { productOnWork: { include: { product: true, }, }, }, }, }, }, work: true, product: { include: { productGroup: true }, }, worker: true, }, }, paySplit: true, worker: true, customerBranch: { include: { customer: true }, }, _count: { select: { productServiceList: true }, }, }, where: { id: quotationId, isDebitNote: false }, data: { ...rest, ...price, quotationStatus: record.quotationStatus === "Expired" ? "Issued" : rest.quotationStatus, statusOrder: +(rest.status === "INACTIVE"), worker: sortedEmployeeId.length > 0 ? { deleteMany: { id: { notIn: sortedEmployeeId } }, createMany: { skipDuplicates: true, data: sortedEmployeeId.map((v, i) => ({ no: i, employeeId: v, })), }, } : undefined, paySplit: rest.paySplit ? { deleteMany: {}, createMany: { data: (rest.paySplit || []).map((v, i) => ({ no: i + 1, ...v, })), }, } : undefined, productServiceList: list ? { deleteMany: {}, create: list, } : undefined, updatedByUserId: req.user.sub, }, }); }); } @Delete("{quotationId}") @Security("keycloak", MANAGE_ROLES) async deleteQuotationById(@Request() req: RequestWithUser, @Path() quotationId: string) { const record = await prisma.quotation.findUnique({ include: { registeredBranch: { include: branchRelationPermInclude(req.user) }, }, where: { id: quotationId, isDebitNote: false }, }); if (!record) throw notFoundError("Quotation"); await permissionCheck(req.user, record.registeredBranch); if (record.status !== Status.CREATED) throw isUsedError("Quotation"); return await prisma.quotation.delete({ include: { createdBy: true, updatedBy: true, }, where: { id: quotationId }, }); } } @Route("api/v1/quotation/{quotationId}") @Tags("Quotation") export class QuotationActionController extends Controller { @Post("add-worker") async addWorker( @Request() req: RequestWithUser, @Path() quotationId: string, @Body() body: ( | { workerId: string; productServiceId: string[]; } | { workerData: { dateOfBirth: Date; gender: string; nationality: string; namePrefix?: string; firstName: string; firstNameEN: string; middleName?: string; middleNameEN?: string; lastName: string; lastNameEN: string; }; productServiceId: string[]; } )[], ) { const { existsEmployee, newEmployee } = body.reduce<{ existsEmployee: { workerId: string; productServiceId: string[]; }[]; newEmployee: { workerData: { dateOfBirth: Date; gender: string; nationality: string; namePrefix?: string; firstName: string; firstNameEN: string; middleName?: string; middleNameEN?: string; lastName: string; lastNameEN: string; }; productServiceId: string[]; }[]; }>( (acc, current) => { if ("workerId" in current) { acc.existsEmployee.push(current); } else { acc.newEmployee.push(current); } return acc; }, { existsEmployee: [], newEmployee: [] }, ); const ids = { employee: existsEmployee.map((v) => v.workerId), productService: existsEmployee .flatMap((v) => v.productServiceId) .filter((lhs, i, a) => a.findIndex((rhs) => lhs === rhs) === i), }; const [quotation, employee, productService] = await prisma.$transaction( async (tx) => await Promise.all([ tx.quotation.findFirst({ include: { customerBranch: true, worker: true, _count: { select: { worker: true, }, }, }, where: { id: quotationId, isDebitNote: false }, }), tx.employee.findMany({ where: { id: { in: ids.employee } }, take: ids.employee.length + 1, // NOTE: Do not find further as it should be equal to input }), tx.quotationProductServiceList.findMany({ where: { id: { in: ids.productService }, quotationId }, take: ids.productService.length + 1, // NOTE: Do not find further as it should be equal to input }), ]), ); if (!quotation) throw relationError("Quotation"); if (ids.employee.length !== employee.length) throw relationError("Worker"); if (ids.productService.length !== productService.length) throw relationError("Product"); if ( quotation._count.worker + existsEmployee.filter( (lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId), ).length + newEmployee.length > (quotation.workerMax || 0) ) { if (existsEmployee.length === 0) return; throw new HttpError( HttpStatus.PRECONDITION_FAILED, "Worker exceed current quotation max worker.", "QuotationWorkerExceed", ); } await prisma.$transaction(async (tx) => { const customerBranch = quotation.customerBranch; const lastEmployee = await tx.runningNo.upsert({ where: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, }, create: { key: `EMPLOYEE_${customerBranch.id}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}`, value: newEmployee.length, }, update: { value: { increment: newEmployee.length } }, }); const newEmployeeWithId = await Promise.all( newEmployee.map(async (v, i) => { const data = await tx.employee.create({ data: { ...v.workerData, code: `${customerBranch.code}-${`${new Date().getFullYear()}`.slice(-2).padStart(2, "0")}${`${lastEmployee.value - newEmployee.length + i + 1}`.padStart(7, "0")}`, customerBranchId: customerBranch.id, }, }); return { workerId: data.id, productServiceId: v.productServiceId }; }), ); const rearrange: typeof existsEmployee = []; while (body.length > 0) { const item = body.shift(); if (item && "workerId" in item) { rearrange.push(item); } else { const popNew = newEmployeeWithId.shift(); popNew && rearrange.push(popNew); } } await tx.quotationProductServiceWorker.createMany({ data: existsEmployee .filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId)) .flatMap((lhs) => lhs.productServiceId.map((rhs) => ({ employeeId: lhs.workerId, productServiceId: rhs, })), ), skipDuplicates: true, }); const current = new Date(); const year = `${current.getFullYear()}`.slice(-2).padStart(2, "0"); const month = `${current.getMonth() + 1}`.padStart(2, "0"); const lastRequest = await tx.runningNo.upsert({ where: { key: `REQUEST_${year}${month}`, }, create: { key: `REQUEST_${year}${month}`, value: quotation.worker.length, }, update: { value: { increment: quotation.worker.length } }, }); await tx.quotation.update({ where: { id: quotationId, isDebitNote: false }, data: { quotationStatus: QuotationStatus.PaymentSuccess, // NOTE: change back if already complete or canceled worker: { createMany: { data: rearrange .filter((lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId)) .map((v, i) => ({ no: quotation._count.worker + i + 1, employeeId: v.workerId, })), }, }, requestData: quotation.quotationStatus === "PaymentInProcess" || quotation.quotationStatus === "PaymentSuccess" ? { create: rearrange .filter( (lhs) => !quotation.worker.find((rhs) => rhs.employeeId === lhs.workerId) && lhs.productServiceId.length > 0, ) .map((v, i) => ({ code: `TR${year}${month}${(lastRequest.value - quotation._count.worker + i + 1).toString().padStart(6, "0")}`, employeeId: v.workerId, requestWork: { create: v.productServiceId.map((v) => ({ productServiceId: v })), }, })), } : undefined, }, }); }); } } @Route("api/v1/quotation/{quotationId}/attachment") @Tags("Quotation") export class QuotationFileController extends Controller { async #checkPermission(user: RequestWithUser["user"], id: string) { const data = await prisma.quotation.findUnique({ include: { registeredBranch: { include: branchRelationPermInclude(user), }, }, where: { id, isDebitNote: false }, }); if (!data) throw notFoundError("Quotation"); await permissionCheck(user, data.registeredBranch); } @Get() @Security("keycloak") async listAttachment(@Request() req: RequestWithUser, @Path() quotationId: string) { await this.#checkPermission(req.user, quotationId); return await listFile(fileLocation.quotation.attachment(quotationId)); } @Head("{name}") async headAttachment( @Request() req: RequestWithUser, @Path() quotationId: string, @Path() name: string, ) { return req.res?.redirect( await getPresigned("head", fileLocation.quotation.attachment(quotationId, name)), ); } @Get("{name}") @Security("keycloak") async getAttachment( @Request() req: RequestWithUser, @Path() quotationId: string, @Path() name: string, ) { await this.#checkPermission(req.user, quotationId); return await getFile(fileLocation.quotation.attachment(quotationId, name)); } @Put("{name}") @Security("keycloak", MANAGE_ROLES) async putAttachment( @Request() req: RequestWithUser, @Path() quotationId: string, @Path() name: string, ) { await this.#checkPermission(req.user, quotationId); return await setFile(fileLocation.quotation.attachment(quotationId, name)); } @Delete("{name}") @Security("keycloak", MANAGE_ROLES) async deleteAttachment( @Request() req: RequestWithUser, @Path() quotationId: string, @Path() name: string, ) { await this.#checkPermission(req.user, quotationId); return await deleteFile(fileLocation.quotation.attachment(quotationId, name)); } }