diff --git a/package.json b/package.json index b576e46..6294f71 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", + "@types/multer": "^1.4.12", "@types/node": "^20.17.10", "@types/nodemailer": "^6.4.17", "nodemon": "^3.1.9", @@ -46,12 +47,14 @@ "dayjs-plugin-utc": "^0.1.2", "docx-templates": "^4.13.0", "dotenv": "^16.4.7", + "exceljs": "^4.4.0", "express": "^4.21.2", "fast-jwt": "^5.0.5", "json-2-csv": "^5.5.8", "kysely": "^0.27.5", "minio": "^8.0.2", "morgan": "^1.10.0", + "multer": "^1.4.5-lts.2", "nodemailer": "^6.10.0", "prisma-extension-kysely": "^3.0.0", "promise.any": "^2.0.6", diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts index 09dce42..146f6d1 100644 --- a/src/controllers/04-product-controller.ts +++ b/src/controllers/04-product-controller.ts @@ -11,6 +11,7 @@ import { Security, Tags, Query, + UploadedFile, } from "tsoa"; import { Prisma, Product, Status } from "@prisma/client"; @@ -28,6 +29,7 @@ import { filterStatus } from "../services/prisma"; import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio"; import { isUsedError, notFoundError, relationError } from "../utils/error"; import { queryOrNot, whereDateQuery } from "../utils/relation"; +import spreadsheet from "../utils/spreadsheet"; const MANAGE_ROLES = [ "system", @@ -447,6 +449,132 @@ export class ProductController extends Controller { where: { id: productId }, }); } + + @Post("uploadedFile") + @Security("keycloak", MANAGE_ROLES) + async importProduct( + @Request() req: RequestWithUser, + @UploadedFile() file: Express.Multer.File, + @Query() productGroupId: string, + ) { + if (!file?.buffer) throw notFoundError("File"); + + const buffer = new Uint8Array(file.buffer).buffer; + const dataFile = await spreadsheet.readExcel(buffer, { + header: true, + worksheet: "Sheet1", + }); + + let dataName: string[] = []; + const data = await dataFile.map((item: any) => { + dataName.push(item.name); + return { + ...item, + expenseType: + item.expenseType === "ค่าธรรมเนียม" + ? "fee" + : item.expenseType === "ค่าบริการ" + ? "serviceFee" + : "processingFee", + shared: item.shared === "ใช่" ? true : false, + calcVat: item.calcVat === "ใช่" ? true : false, + vatIncluded: item.vatIncluded === "รวม" ? true : false, + agentPriceCalcVat: item.agentPriceCalcVat === "ใช่" ? true : false, + agentPriceVatIncluded: item.agentPriceVatIncluded === "รวม" ? true : false, + serviceChargeCalcVat: item.serviceChargeCalcVat === "ใช่" ? true : false, + serviceChargeVatIncluded: item.serviceChargeVatIncluded === "รวม" ? true : false, + }; + }); + + const [productGroup, productSameName] = await prisma.$transaction([ + prisma.productGroup.findFirst({ + include: { + registeredBranch: { + include: branchRelationPermInclude(req.user), + }, + createdBy: true, + updatedBy: true, + }, + where: { id: productGroupId }, + }), + prisma.product.findMany({ + where: { + productGroup: { + registeredBranch: { + OR: permissionCondCompany(req.user), + }, + }, + name: { in: dataName }, + }, + }), + ]); + + if (!productGroup) throw relationError("Product Group"); + + await permissionCheck(req.user, productGroup.registeredBranch); + let dataProduct: ProductCreate[] = []; + + const record = await prisma.$transaction( + async (tx) => { + const branch = productGroup.registeredBranch; + const company = (branch.headOffice || branch).code; + console.log(branch, company); + for (const item of data) { + const dataDuplicate = productSameName.some( + (v) => v.code.slice(0, -3) === item.code.toUpperCase() && v.name === item.name, + ); + + if (!dataDuplicate) { + const last = await tx.runningNo.upsert({ + where: { + key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`, + }, + create: { + key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`, + value: 1, + }, + update: { value: { increment: 1 } }, + }); + + dataProduct.push({ + ...item, + code: `${item.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`, + createdByUserId: req.user.sub, + updatedByUserId: req.user.sub, + productGroupId: productGroupId, + }); + } + } + console.log("dataProduct", dataProduct); + + return await prisma.product.createManyAndReturn({ + data: dataProduct, + include: { + createdBy: true, + updatedBy: true, + }, + }); + }, + { + isolationLevel: Prisma.TransactionIsolationLevel.Serializable, + }, + ); + + if (productGroup.status === "CREATED") { + await prisma.productGroup.update({ + include: { + createdBy: true, + updatedBy: true, + }, + where: { id: productGroupId }, + data: { status: Status.ACTIVE }, + }); + } + + this.setStatus(HttpStatus.CREATED); + + return record; + } } @Route("api/v1/product/{productId}") diff --git a/src/utils/spreadsheet.ts b/src/utils/spreadsheet.ts new file mode 100644 index 0000000..b04a4f2 --- /dev/null +++ b/src/utils/spreadsheet.ts @@ -0,0 +1,105 @@ +import Excel from "exceljs"; + +export default class spreadsheet { + static async readCsv() { + // TODO: read csv + } + + /** + * This function read data from excel file. + * + * @param buffer - Excel file. + * @param opts.header - Interprets the first row as the names of the fields. + * @param opts.worksheet - Specifies the worksheet to read. Can be the worksheet's name or its 1-based index. + * + * @returns + */ + static async readExcel( + buffer: Excel.Buffer, + opts?: { header?: boolean; worksheet?: number | string }, + ): Promise { + const workbook = new Excel.Workbook(); + await workbook.xlsx.load(buffer); + const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1); + + if (!worksheet) return []; + + const header: Record = {}; + const values: any[] = []; + + worksheet.eachRow((row, rowId) => { + if (rowId === 1 && opts?.header !== false) { + row.eachCell((cell, cellId) => { + if (typeof cell.value === "string") { + header[cellId] = nameValue(cell.value); + } else { + header[cellId] = cellId.toString(); + } + }); + } else { + const data: Record = {}; + row.eachCell((cell, cellId) => { + data[opts?.header !== false ? header[cellId] : cellId - 1] = cell.value; + }); + values.push(opts?.header !== false ? data : Object.values(data)); + } + }); + + return values; + } +} + +function nameValue(value: string) { + let code: string; + switch (value) { + case "ชื่อสินค้าและบริการ": + code = "name"; + break; + case "ระยะเวลาดำเนินการ": + code = "process"; + break; + case "ประเภทค่าใช้จ่าย": + code = "expenseType"; + break; + case "รายละเอียด": + code = "detail"; + break; + case "หมายเหตุ": + code = "remark"; + break; + case "ใช้งานร่วมกัน": + code = "shared"; + break; + case "คำนวณภาษีราคาขาย": + code = "calcVat"; + break; + case "รวม VAT ราคาขาย": + code = "vatIncluded"; + break; + case "ราคาต่อหน่วย (บาท) ราคาขาย": + code = "price"; + break; + case "คำนวณภาษีราคาตัวแทน": + code = "agentPriceCalcVat"; + break; + case "รวม VAT ราคาตัวแทน": + code = "agentPriceVatIncluded"; + break; + case "ราคาต่อหน่วย (บาท) ราคาตัวแทน": + code = "agentPrice"; + break; + case "คำนวณภาษีราคาดำเนินการ": + code = "serviceChargeCalcVat"; + break; + case "รวม VAT ราคาดำเนินการ": + code = "serviceChargeVatIncluded"; + break; + case "ราคาต่อหน่วย (บาท) ราคาดำเนินการ": + code = "serviceCharge"; + break; + default: + code = "code"; + break; + } + return code; +}