From 0f0075fa94f449049923b2aca490b3ad735df8bc Mon Sep 17 00:00:00 2001
From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
Date: Mon, 1 Sep 2025 09:27:53 +0700
Subject: [PATCH] chore: add html to text dep
---
package.json | 2 +
pnpm-lock.yaml | 109 +++++++++++++++++++++++++++++++++++
src/utils/string-template.ts | 68 ++++++++++++++++++++++
3 files changed, 179 insertions(+)
create mode 100644 src/utils/string-template.ts
diff --git a/package.json b/package.json
index 2f64c4f..13cd0e0 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@prisma/client": "^6.3.0",
"@scalar/express-api-reference": "^0.4.182",
"@tsoa/runtime": "^6.6.0",
+ "@types/html-to-text": "^9.0.4",
"canvas": "^3.1.0",
"cors": "^2.8.5",
"cron": "^3.3.1",
@@ -53,6 +54,7 @@
"exceljs": "^4.4.0",
"express": "^4.21.2",
"fast-jwt": "^5.0.5",
+ "html-to-text": "^9.0.5",
"jsbarcode": "^3.11.6",
"json-2-csv": "^5.5.8",
"kysely": "^0.27.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8471a8c..8d3df75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ importers:
'@tsoa/runtime':
specifier: ^6.6.0
version: 6.6.0
+ '@types/html-to-text':
+ specifier: ^9.0.4
+ version: 9.0.4
canvas:
specifier: ^3.1.0
version: 3.1.0
@@ -53,6 +56,9 @@ importers:
fast-jwt:
specifier: ^5.0.5
version: 5.0.5
+ html-to-text:
+ specifier: ^9.0.5
+ version: 9.0.5
jsbarcode:
specifier: ^3.11.6
version: 3.11.6
@@ -676,6 +682,9 @@ packages:
resolution: {integrity: sha512-4mQYkQJO0HHaoFd8Z+vSdQAvYcCJ2bRLN9ewE+GneB8kvoLG/oM3ynroqzGQdoytH8BmhnJwD3aEUagfbK2x5g==}
engines: {node: '>=18'}
+ '@selderee/plugin-htmlparser2@0.11.0':
+ resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
+
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -742,6 +751,9 @@ packages:
'@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
+ '@types/html-to-text@9.0.4':
+ resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==}
+
'@types/http-assert@1.5.6':
resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==}
@@ -1307,6 +1319,10 @@ packages:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
@@ -1347,6 +1363,19 @@ packages:
resolution: {integrity: sha512-tTmR3WhROYctuyVReQ+PfCU3zprmC45/VuSVzn8EjovzpRkXYUdXiDatB9M8pasj0V+wuuOyY8bcSHvlQ2GNag==}
engines: {node: '>=6'}
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
@@ -1398,6 +1427,10 @@ packages:
end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
@@ -1740,6 +1773,13 @@ packages:
resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==}
engines: {node: '>=14'}
+ html-to-text@9.0.5:
+ resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
+ engines: {node: '>=14'}
+
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -2040,6 +2080,9 @@ packages:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
+ leac@0.6.0:
+ resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
+
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
@@ -2467,6 +2510,9 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
+ parseley@0.12.1:
+ resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -2504,6 +2550,9 @@ packages:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
+ peberminta@0.9.0:
+ resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2746,6 +2795,9 @@ packages:
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+ selderee@0.11.0:
+ resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
+
semver@5.7.2:
resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==}
hasBin: true
@@ -4037,6 +4089,11 @@ snapshots:
'@scalar/openapi-types': 0.1.7
'@unhead/schema': 1.11.14
+ '@selderee/plugin-htmlparser2@0.11.0':
+ dependencies:
+ domhandler: 5.0.3
+ selderee: 0.11.0
+
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -4136,6 +4193,8 @@ snapshots:
'@types/qs': 6.9.17
'@types/serve-static': 1.15.7
+ '@types/html-to-text@9.0.4': {}
+
'@types/http-assert@1.5.6': {}
'@types/http-errors@2.0.4': {}
@@ -4781,6 +4840,8 @@ snapshots:
deep-extend@0.6.0: {}
+ deepmerge@4.3.1: {}
+
define-data-property@1.1.4:
dependencies:
es-define-property: 1.0.1
@@ -4823,6 +4884,24 @@ snapshots:
jszip: 3.10.1
sax: 1.3.0
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
dotenv@16.0.3: {}
dotenv@16.4.7: {}
@@ -4908,6 +4987,8 @@ snapshots:
dependencies:
once: 1.4.0
+ entities@4.5.0: {}
+
env-paths@2.2.1: {}
error-callsites@2.0.4:
@@ -5399,6 +5480,21 @@ snapshots:
hpagent@1.2.0: {}
+ html-to-text@9.0.5:
+ dependencies:
+ '@selderee/plugin-htmlparser2': 0.11.0
+ deepmerge: 4.3.1
+ dom-serializer: 2.0.0
+ htmlparser2: 8.0.2
+ selderee: 0.11.0
+
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ entities: 4.5.0
+
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -5689,6 +5785,8 @@ snapshots:
dependencies:
readable-stream: 2.3.8
+ leac@0.6.0: {}
+
lie@3.3.0:
dependencies:
immediate: 3.0.6
@@ -6096,6 +6194,11 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
+ parseley@0.12.1:
+ dependencies:
+ leac: 0.6.0
+ peberminta: 0.9.0
+
parseurl@1.3.3: {}
path-exists@4.0.0: {}
@@ -6119,6 +6222,8 @@ snapshots:
pathval@2.0.0: {}
+ peberminta@0.9.0: {}
+
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -6428,6 +6533,10 @@ snapshots:
secure-json-parse@2.7.0: {}
+ selderee@0.11.0:
+ dependencies:
+ parseley: 0.12.1
+
semver@5.7.2: {}
semver@6.3.1: {}
diff --git a/src/utils/string-template.ts b/src/utils/string-template.ts
new file mode 100644
index 0000000..462c6bf
--- /dev/null
+++ b/src/utils/string-template.ts
@@ -0,0 +1,68 @@
+export function formatNumberDecimal(num: number, point: number = 2): string {
+ return (num || 0).toLocaleString("eng", {
+ minimumFractionDigits: point,
+ maximumFractionDigits: point,
+ });
+}
+
+const templates = {
+ "quotation-labor": {
+ converter: (context?: { name: string[] }) => {
+ return context?.name.join("
") || "";
+ },
+ },
+ "quotation-payment": {
+ converter: (context?: {
+ paymentType: "Full" | "Split" | "SplitCustom" | "BillFull" | "BillSplit" | "BillSplitCustom";
+
+ amount?: number;
+ installments?: {
+ no: number;
+ amount: number;
+ }[];
+ }) => {
+ if (context?.paymentType === "Full") {
+ return [
+ "**** เงื่อนไขเพิ่มเติม",
+ "- เงื่อนไขการชำระเงิน แบบเต็มจำนวน",
+ ` จำนวน ${formatNumberDecimal(context?.amount || 0, 2)}`,
+ ].join("
");
+ } else {
+ return [
+ "**** เงื่อนไขเพิ่มเติม",
+ `- เงื่อนไขการชำระเงิน แบบแบ่งจ่าย${context?.paymentType === "SplitCustom" ? " กำหนดเอง " : " "}${context?.installments?.length} งวด`,
+ ...(context?.installments?.map(
+ (v) => ` งวดที่ ${v.no} จำนวน ${formatNumberDecimal(v.amount, 2)}`,
+ ) || []),
+ ].join("
");
+ }
+ },
+ },
+} as const;
+
+type Template = typeof templates;
+type TemplateName = keyof Template;
+type TemplateContext = {
+ [key in TemplateName]?: Parameters[0];
+};
+
+export function convertTemplate(
+ text: string,
+ context?: TemplateContext,
+ templateUse?: TemplateName[],
+) {
+ let ret = text;
+
+ for (const [name, template] of Object.entries(templates)) {
+ if (templateUse && !templateUse.includes(name as TemplateName)) continue;
+ ret = ret.replace(
+ new RegExp("\\#\\[" + name.replaceAll("-", "\\-") + "\\]", "g"),
+ typeof template.converter === "function"
+ ? template.converter(context?.[name as TemplateName] as any)
+ : template.converter,
+ );
+ // console.log(ret);
+ }
+
+ return ret;
+}