import createReport from "docx-templates"; import ThaiBahtText from "thai-baht-text"; import { District, Province, SubDistrict } from "@prisma/client"; import { Readable } from "node:stream"; import { Controller, Get, Path, Query, Route } from "tsoa"; import prisma from "../db"; import { notFoundError } from "../utils/error"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; import { getFileBuffer, listFile } from "../utils/minio"; const quotationData = (id: string) => prisma.quotation.findFirst({ where: { id, isDebitNote: false }, include: { registeredBranch: { include: { province: true, district: true, subDistrict: true, headOffice: { include: { province: true, district: true, subDistrict: true, }, }, }, }, customerBranch: { include: { customer: true, province: true, district: true, subDistrict: true, }, }, worker: { include: { employee: { include: { province: true, district: true, subDistrict: true, employeePassport: { orderBy: { expireDate: "desc" }, }, employeeWork: true, }, }, }, }, productServiceList: { include: { work: true, product: true, service: true, }, }, }, }); @Route("api/v1/doc-template") export class DocTemplateController extends Controller { @Get() async getTemplate() { return await listFile(`doc-template/`); } @Get("{documentTemplate}") async getDocument( @Path() documentTemplate: string, @Query() data: string, @Query() dataId: string, @Query() dataOnly?: boolean, ): Promise> { let record: Record; switch (data) { case "quotation": record = await quotationData(dataId).then(async (quotation) => replaceEmptyField({ quotation, customerBranch: quotation?.customerBranch, registeredBranch: quotation?.registeredBranch, employee: quotation?.worker.map((item) => item.employee), employeeCount: { all: quotation?.worker.length, male: quotation?.worker.filter((item) => item.employee.gender === "male").length, female: quotation?.worker.filter((item) => item.employee.gender === "female").length, }, employmentOffice: quotation && quotation.customerBranch.districtId ? await prisma.employmentOffice.findFirst({ where: { OR: [ { province: { district: { some: { id: quotation.customerBranch.districtId } }, }, district: { none: {} }, }, { district: { some: { districtId: quotation.customerBranch.districtId }, }, }, ], }, orderBy: [{ provinceId: "asc" }, { id: "asc" }], }) : undefined, }), ); break; default: throw new HttpError(HttpStatus.BAD_REQUEST, "No data for template", "noDataTemplate"); } if (!data) throw notFoundError("Data"); if (dataOnly) return record; const template = await getFileBuffer(`doc-template/${documentTemplate}`); if (!data) Readable.from(template); const report = await createReport({ template, data: record, additionalJsContext: { addressFull, addressFullTH: (addr: FullAddress) => addressFull(addr, "th"), addressFullEN: (addr: FullAddress) => addressFull(addr, "en"), gender, genderTH: (text: string) => gender(text, "th"), genderEN: (text: string) => gender(text, "en"), businessType, businessTypeEN: (text: string) => businessType(text, "en"), businessTypeTH: (text: string) => businessType(text, "th"), namePrefix, namePrefixEN: (text: string) => namePrefix(text, "en"), namePrefixTH: (text: string) => namePrefix(text, "th"), jobPosition, jobPositionEN: (text: string) => jobPosition(text, "en"), jobPositionTH: (text: string) => jobPosition(text, "th"), nationality, nationalityEN: (text: string) => nationality(text, "en"), nationalityTH: (text: string) => nationality(text, "th"), thaiBahtText: (input: string | number) => { ThaiBahtText(typeof input === "string" ? input.replaceAll(",", "") : input); }, }, }).then(Buffer.from); return Readable.from(report); } } function replaceEmptyField(data: T): T { return JSON.parse(JSON.stringify(data).replace(/null|\"\"/g, '"\-"')); } type FullAddress = { address: string; addressEN: string; moo?: string; mooEN?: string; soi?: string; soiEN?: string; street?: string; streetEN?: string; province?: Province | null; district?: District | null; subDistrict?: SubDistrict | null; en?: boolean; }; function addressFull(addr: FullAddress, lang: "th" | "en" = "en") { let fragments: string[]; switch (lang) { case "th": fragments = [`${addr.address},`]; if (addr.moo) fragments.push(`หมู่ ${addr.moo},`); if (addr.soi) fragments.push(`ซอย ${addr.soi},`); if (addr.street) fragments.push(`ถนน${addr.street},`); if (addr.subDistrict) { fragments.push(`${addr.province?.id === "10" ? "แขวง" : "ตำบล"}${addr.subDistrict.name},`); } if (addr.district) { fragments.push(`${addr.province?.id === "10" ? "เขต" : "อำเภอ"}${addr.district.name},`); } if (addr.province) fragments.push(`จังหวัด${addr.province.name},`); break; default: fragments = [`${addr.addressEN},`]; if (addr.mooEN) fragments.push(`Moo ${addr.mooEN},`); if (addr.soiEN) fragments.push(`Soi ${addr.soiEN},`); if (addr.streetEN) fragments.push(`${addr.streetEN} Rd.`); if (addr.subDistrict) { fragments.push(`${addr.subDistrict.nameEN} sub-district,`); } if (addr.district) fragments.push(`${addr.district.nameEN} district,`); if (addr.province) fragments.push(`${addr.province.nameEN},`); break; } return fragments.join(" "); } function gender(text: string, lang: "th" | "en" = "en") { switch (lang) { case "th": return { male: "ชาย", female: "หญิง" }[text] || text; default: text.charAt(0).toUpperCase() + text.slice(1); } } function businessType(text: string, lang: "th" | "en" = "en") { switch (lang) { case "th": return ( { ["fisheries"]: "ประมง", ["continuous-fisheries"]: "ต่อเนื่องประมงทะเล", ["agriculture"]: "เกษตรและปศุสัตว์", ["construction"]: "กิจการก่อสร้าง", ["domesticHelper"]: "ผู้รับใช้ในบ้าน", ["continuousAgriculture"]: "กิจการต่อเนื่องการเกษตร", ["continuousButchery"]: "ต่อเนื่องปศุสัตว์โรงฆ่าสัตว์ ชำแหละ", ["recycling"]: "กิจการรีไซเคิล", ["mining"]: "เหมืองแร่/เหมืองหิน", ["metal"]: "จำหน่ายผลิตภัณฑ์โลหะ", ["food"]: "จำหน่ายอาหารและเครื่องดื่ม", ["soilBasedProducts"]: "ผลิตหรือจำหน่ายผลิตภัณฑ์จากดิน", ["constructionMaterials"]: "ผลิตหรือจำหน่ายวัสดุก่อสร้าง", ["stone"]: "แปรรูปหิน", ["cloth"]: "ผลิตหรือจำหน่ายเสื้อผ้าสำเร็จรูป", ["plastic"]: "ผลิตหรือจำหน่ายผลิตภัณฑ์พลาสติก", ["paper"]: "ผลิตหรือจำหน่ายผลิตภัณฑ์กระดาษ", ["electronics"]: "ผลิตหรือจำหน่ายผลิตภัณฑ์อิเล็กทรอนิกส์", ["transport"]: "ขนถ่ายสินค้าทางบก น้ำ คลังสินค้า", ["market"]: "ค้าส่ง ค้าปลีก แผงลอย", ["car"]: "อู่ซ่อมรถ ล้าง อัดฉีด", ["fuel"]: "สถานีบริการน้ำมัน แก้ส เชื้อเพลิง", ["institution"]: "สถานศึกษา มูลนิธิ สมาคม สถานพยาบาล", ["service"]: "การให้บริการต่างๆ", ["coordinator"]: "งานผู้ประสานงานด้านภาษากัมพูชา ลาว หรือเมียนมา", ["seafood"]: "แปรรูปสัตว์น้ำ", }[text] || text ); default: return ( { ["fisheries"]: "Fisheries", ["continuous-fisheries"]: "Continuous fisheries", ["agriculture"]: "Agriculture and livestock", ["construction"]: "Construction business", ["domesticHelper"]: "Domestic helper", ["continuousAgriculture"]: "Continuous agricultural operation", ["continuousButchery"]: "Continuous livestock slaughter and processing ", ["recycling"]: "Recycling business", ["mining"]: "Mining/quarry", ["metal"]: "Metal products distribution", ["food"]: "Food and beverage distribution", ["soilBasedProducts"]: "Manufacture or sell products made from soil", ["constructionMaterials"]: "Manufacture or sell construction materials", ["stone"]: "Stone processing", ["cloth"]: "Manufacture or sell ready-to-wear clothing", ["plastic"]: "Manufacture or sell plastic products", ["paper"]: "Manufacture or sell paper products", ["electronics"]: "Manufacture or sell electronic products", ["transport"]: "Transport goods by land, water, and operate warehouses", ["market"]: "Wholesale, retail, floating panels", ["car"]: "Auto repair shop, car wash, and detailing", ["fuel"]: "Gas station, fuel station, and service station", ["institution"]: "Educational institution, foundation, association, hospital", ["service"]: "Various services", ["coordinator"]: "Coordinator for Khmer, Laos, or Myanmar language services", ["seafood"]: "Processing seafood", }[text] || text ); } } function namePrefix(text: string, lang: "th" | "en" = "en") { switch (lang) { case "th": return { mr: "นาย", mrs: "นาง", miss: "นางสาว" }[text] || text; default: text.charAt(0).toUpperCase() + text.slice(1); } } function nationality(text: string, lang: "th" | "en" = "en") { switch (lang) { case "th": return ( { ["THA"]: "ไทย", ["MMR"]: "เมียนมา", ["LAO"]: "ลาว", ["KHM"]: "กัมพูชา", ["VNM"]: "เวียดนาม", ["PHL"]: "ฟิลิปปินส์", ["CHN"]: "จีน", }[text] || text ); default: return ( { ["THA"]: "Thai", ["MMR"]: "Myanmar", ["LAO"]: "Laos", ["KHM"]: "Khmer", ["VNM"]: "Vietnam", ["PHL"]: "Philippines", ["CHN"]: "China", }[text] || text ); } } function jobPosition(text: string, lang: "th" | "en" = "en") { switch (lang) { case "th": return ( { ["labourer"]: "กรรมกร", ["boatsMechanic"]: "ช่างเครื่องยนต์ในเรือประมงทะเล", ["domesticHelper"]: "ผู้รับใช้ในบ้าน", ["coordinator"]: "งานผู้ประสานงานด้านภาษากัมพูชา ลาว หรือเมียนมา", }[text] || text ); default: return ( { labourer: "Labourer", boatsMechanic: "Marine engine mechanic on fishing boats", domesticHelper: "Domestic helper", coordinator: "Coordinator for Khmer, Laos, or Myanmar language services", }[text] || text ); } }