feat: doc template (#10)
* feat: doc-template * fix: empty not converted to dash * feat: also return province, district and sub district * refactor: move some relation to outer * feat: add more function * chore: deps * feat: add more relation * feat: count employee by gender * feat: count all employee * feat: add more function * feat: get employment office * fix: error --------- Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
This commit is contained in:
parent
67651eb213
commit
4e3c51d84b
4 changed files with 456 additions and 13 deletions
348
src/controllers/00-doc-template-controller.ts
Normal file
348
src/controllers/00-doc-template-controller.ts
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
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<Readable | Record<string, any>> {
|
||||
let record: Record<string, any>;
|
||||
|
||||
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<T>(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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { buffer } from "node:stream/consumers";
|
||||
import minio from "../services/minio";
|
||||
|
||||
if (!process.env.MINIO_BUCKET) {
|
||||
|
|
@ -26,6 +27,10 @@ export async function getFile(path: string, exp = 60 * 60) {
|
|||
return await minio.presignedGetObject(MINIO_BUCKET, path, exp);
|
||||
}
|
||||
|
||||
export async function getFileBuffer(path: string) {
|
||||
return await minio.getObject(MINIO_BUCKET, path).then(buffer);
|
||||
}
|
||||
|
||||
export async function setFile(path: string, exp = 6 * 60 * 60) {
|
||||
return await minio.presignedPutObject(MINIO_BUCKET, path, exp);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue