GWM : error message handling
This commit is contained in:
parent
f04b917b76
commit
fe7b35a730
209
package-lock.json
generated
209
package-lock.json
generated
@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "erp-backend",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
@ -22,6 +23,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"puppeteer": "^25.2.0",
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
@ -1362,6 +1364,91 @@
|
||||
"@prisma/debug": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz",
|
||||
"integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==",
|
||||
"dependencies": {
|
||||
"modern-tar": "^0.7.6",
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"proxy-agent": ">=8.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"proxy-agent": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"dependencies": {
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/strip-ansi": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"dependencies": {
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers/node_modules/yargs-parser": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
@ -2190,6 +2277,21 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
||||
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@ -2667,6 +2769,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1638949",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz",
|
||||
"integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA=="
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
@ -2755,8 +2862,7 @@
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
@ -2838,7 +2944,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -3504,7 +3609,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
@ -3513,7 +3617,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
||||
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -4848,7 +4951,6 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@ -5483,6 +5585,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@ -5494,6 +5601,14 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/modern-tar": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
|
||||
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
@ -6189,6 +6304,42 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "25.2.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.0.tgz",
|
||||
"integrity": "sha512-JPMPd/2+lgdkLhEyPqH895oR3ccMt1wSra6oewgjjTuLmo2s9zPZpKXQTFEIiA/fMKpiL01kjU3+2zPEReRWNg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "3.0.5",
|
||||
"chromium-bidi": "16.0.1",
|
||||
"devtools-protocol": "0.0.1638949",
|
||||
"lilconfig": "^3.1.3",
|
||||
"puppeteer-core": "25.2.0",
|
||||
"typed-query-selector": "^2.12.2"
|
||||
},
|
||||
"bin": {
|
||||
"puppeteer": "lib/puppeteer/node/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "25.2.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.0.tgz",
|
||||
"integrity": "sha512-jGhuGAlkgOcbyGRc0Cm9b/y4vvqoxhyAyl6a1diVe8F3sHsgTaQ60QQT5F3rGegTZV3prysgHVc+0LsvPZo3GA==",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "3.0.5",
|
||||
"chromium-bidi": "16.0.1",
|
||||
"devtools-protocol": "0.0.1638949",
|
||||
"typed-query-selector": "^2.12.2",
|
||||
"webdriver-bidi-protocol": "0.4.2",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
@ -6817,7 +6968,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
@ -6834,7 +6984,6 @@
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -6846,7 +6995,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
@ -7247,6 +7395,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
|
||||
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
@ -7376,6 +7529,11 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
|
||||
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -7529,7 +7687,6 @@
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
@ -7546,7 +7703,6 @@
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -7558,7 +7714,6 @@
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -7570,7 +7725,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
|
||||
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.2.2"
|
||||
},
|
||||
@ -7599,6 +7753,26 @@
|
||||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
@ -7611,7 +7785,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@ -7732,6 +7905,14 @@
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:deploy": "prisma migrate deploy",
|
||||
"prisma:seed": "node prisma/seed.js",
|
||||
"pdf:preview": "node scripts/preview-pdf-templates.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"lint-staged": {
|
||||
@ -37,6 +38,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"puppeteer": "^25.2.0",
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
32
scripts/preview-pdf-templates.js
Normal file
32
scripts/preview-pdf-templates.js
Normal file
@ -0,0 +1,32 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { generatePdf } = require('../src/utils/pdf/pdfGenerator');
|
||||
const { generatePoHtml } = require('../src/utils/pdf/templates/po.template');
|
||||
const { generateGrnHtml } = require('../src/utils/pdf/templates/grn.template');
|
||||
const { generateAssetHtml } = require('../src/utils/pdf/templates/asset.template');
|
||||
const { generateAmcHtml } = require('../src/utils/pdf/templates/amc.template');
|
||||
|
||||
const OUTPUT_DIR = path.join(__dirname, '..', 'uploads', 'pdf-previews');
|
||||
|
||||
const templates = [
|
||||
{ name: 'purchase-order', html: generatePoHtml() },
|
||||
{ name: 'grn', html: generateGrnHtml() },
|
||||
{ name: 'asset', html: generateAssetHtml() },
|
||||
{ name: 'amc', html: generateAmcHtml() },
|
||||
];
|
||||
|
||||
const run = async () => {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
for (const template of templates) {
|
||||
const outputPath = path.join(OUTPUT_DIR, `${template.name}.pdf`);
|
||||
const pdfBuffer = await generatePdf(template.html);
|
||||
fs.writeFileSync(outputPath, pdfBuffer);
|
||||
console.log(`Generated: ${outputPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
console.error('PDF preview generation failed:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,31 +1,52 @@
|
||||
const ApiError = require('../utils/ApiError');
|
||||
const logger = require('../config/logger');
|
||||
const env = require('../config/env');
|
||||
const { mapDbError } = require('../utils/mapDbError');
|
||||
|
||||
module.exports = (err, req, res, _next) => {
|
||||
let statusCode = err.statusCode || 500;
|
||||
let message = err.message || 'Internal server error';
|
||||
let errors = err.errors || [];
|
||||
const mapped = mapDbError(err);
|
||||
const isOperational = err instanceof ApiError || mapped instanceof ApiError;
|
||||
|
||||
if (!(err instanceof ApiError)) {
|
||||
statusCode = 500;
|
||||
message = env.NODE_ENV === 'production' ? 'Internal server error' : err.message;
|
||||
errors = [];
|
||||
let statusCode = 500;
|
||||
let message = 'Internal server error';
|
||||
let errors = [];
|
||||
|
||||
if (mapped instanceof ApiError) {
|
||||
statusCode = mapped.statusCode;
|
||||
message = mapped.message;
|
||||
errors = mapped.errors || [];
|
||||
} else if (err instanceof ApiError) {
|
||||
statusCode = err.statusCode;
|
||||
message = err.message;
|
||||
errors = err.errors || [];
|
||||
} else if (env.NODE_ENV !== 'production') {
|
||||
message = err.message || message;
|
||||
}
|
||||
|
||||
logger.error(message, {
|
||||
logger.error(isOperational ? message : err.message || message, {
|
||||
requestId: req.id,
|
||||
statusCode,
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
user: req.user ? req.user.id.toString() : undefined,
|
||||
stack: err.stack,
|
||||
user: req.user?.id?.toString(),
|
||||
errorName: err?.name,
|
||||
prismaCode: err?.code,
|
||||
pgCode: err?.cause?.code,
|
||||
constraint: err?.cause?.constraint,
|
||||
prismaMeta: err?.meta,
|
||||
clientMessage: message,
|
||||
stack: err?.stack,
|
||||
});
|
||||
|
||||
return res.status(statusCode).json({
|
||||
const payload = {
|
||||
success: false,
|
||||
message,
|
||||
errors,
|
||||
...(env.NODE_ENV === 'development' && { stack: err.stack }),
|
||||
});
|
||||
};
|
||||
|
||||
if (env.NODE_ENV === 'development' && !isOperational && err?.stack) {
|
||||
payload.stack = err.stack;
|
||||
}
|
||||
|
||||
return res.status(statusCode).json(payload);
|
||||
};
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const BRAND_TYPES = ['OWN', 'OEM', 'THIRD_PARTY'];
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
brand_type: Joi.string().allow('').optional(),
|
||||
contact_person: Joi.string().allow('').optional(),
|
||||
phone: Joi.string().allow('').optional(),
|
||||
email: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
code: Joi.string().max(30).required(),
|
||||
name: Joi.string().max(150).required(),
|
||||
brand_type: Joi.string()
|
||||
.valid(...BRAND_TYPES)
|
||||
.default('OWN'),
|
||||
contact_person: Joi.string().max(100).allow(null, '').optional(),
|
||||
phone: Joi.string().max(20).allow(null, '').optional(),
|
||||
email: Joi.string().email().max(200).allow(null, '').optional(),
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
const updateSchema = createSchema
|
||||
.fork(['code', 'name', 'brand_type'], (schema) => schema.optional())
|
||||
.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
@ -19,4 +25,4 @@ const listQuerySchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, BRAND_TYPES };
|
||||
|
||||
113
src/utils/mapDbError.js
Normal file
113
src/utils/mapDbError.js
Normal file
@ -0,0 +1,113 @@
|
||||
const ApiError = require('./ApiError');
|
||||
|
||||
const PG_CODES = {
|
||||
UNIQUE_VIOLATION: '23505',
|
||||
FOREIGN_KEY_VIOLATION: '23503',
|
||||
CHECK_VIOLATION: '23514',
|
||||
NOT_NULL_VIOLATION: '23502',
|
||||
};
|
||||
|
||||
const CONSTRAINT_MESSAGES = {
|
||||
brands_brand_type_check: 'Invalid brand type. Allowed values: OWN, OEM, THIRD_PARTY.',
|
||||
};
|
||||
|
||||
const TABLE_MESSAGES = {
|
||||
brands: 'Invalid brand data. Please check required fields and allowed values.',
|
||||
};
|
||||
|
||||
const extractPgCode = (err) => {
|
||||
const fromCause = err?.cause?.code;
|
||||
if (fromCause) return String(fromCause);
|
||||
|
||||
const match = String(err?.message || '').match(/code: "(\d{5})"/);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const extractConstraint = (err) => {
|
||||
const fromCause = err?.cause?.constraint;
|
||||
if (fromCause) return fromCause;
|
||||
|
||||
const match = String(err?.message || '').match(/constraint "([^"]+)"/i);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const extractTable = (err) => {
|
||||
const fromCause = err?.cause?.table;
|
||||
if (fromCause) return fromCause;
|
||||
|
||||
const match = String(err?.message || '').match(/relation "([^"]+)"/i);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const mapPrismaKnownError = (err) => {
|
||||
switch (err.code) {
|
||||
case 'P2002': {
|
||||
const target = err.meta?.target;
|
||||
const fields = Array.isArray(target) ? target.join(', ') : 'value';
|
||||
return new ApiError(409, `A record with this ${fields} already exists`);
|
||||
}
|
||||
case 'P2003':
|
||||
return new ApiError(422, 'Referenced record does not exist or is invalid');
|
||||
case 'P2025':
|
||||
return new ApiError(404, 'Record not found');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const mapPostgresError = (err) => {
|
||||
const pgCode = extractPgCode(err);
|
||||
const constraint = extractConstraint(err);
|
||||
const table = extractTable(err);
|
||||
|
||||
if (constraint && CONSTRAINT_MESSAGES[constraint]) {
|
||||
return new ApiError(422, CONSTRAINT_MESSAGES[constraint]);
|
||||
}
|
||||
|
||||
if (pgCode === PG_CODES.UNIQUE_VIOLATION) {
|
||||
return new ApiError(409, 'A record with this value already exists');
|
||||
}
|
||||
|
||||
if (pgCode === PG_CODES.FOREIGN_KEY_VIOLATION) {
|
||||
return new ApiError(422, 'Referenced record does not exist or is invalid');
|
||||
}
|
||||
|
||||
if (pgCode === PG_CODES.NOT_NULL_VIOLATION) {
|
||||
return new ApiError(422, 'A required field is missing');
|
||||
}
|
||||
|
||||
if (pgCode === PG_CODES.CHECK_VIOLATION) {
|
||||
if (table && TABLE_MESSAGES[table]) {
|
||||
return new ApiError(422, TABLE_MESSAGES[table]);
|
||||
}
|
||||
return new ApiError(422, 'Invalid value. Please check allowed options and required fields.');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const mapDbError = (err) => {
|
||||
if (err instanceof ApiError) return err;
|
||||
if (!err) return null;
|
||||
|
||||
if (err.name === 'PrismaClientKnownRequestError' || err.code?.startsWith?.('P')) {
|
||||
const mapped = mapPrismaKnownError(err);
|
||||
if (mapped) return mapped;
|
||||
}
|
||||
|
||||
if (
|
||||
err.name === 'PrismaClientUnknownRequestError' ||
|
||||
err.name === 'PrismaClientRustPanicError' ||
|
||||
extractPgCode(err)
|
||||
) {
|
||||
const mapped = mapPostgresError(err);
|
||||
if (mapped) return mapped;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
mapDbError,
|
||||
PG_CODES,
|
||||
};
|
||||
12
src/utils/pdf/helpers/formatCurrency.js
Normal file
12
src/utils/pdf/helpers/formatCurrency.js
Normal file
@ -0,0 +1,12 @@
|
||||
const formatCurrency = (value = 0) => {
|
||||
const amount = Number(value) || 0;
|
||||
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
module.exports = { formatCurrency };
|
||||
16
src/utils/pdf/helpers/formatDate.js
Normal file
16
src/utils/pdf/helpers/formatDate.js
Normal file
@ -0,0 +1,16 @@
|
||||
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '-';
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = MONTHS[date.getMonth()];
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}-${month}-${year}`;
|
||||
};
|
||||
|
||||
module.exports = { formatDate };
|
||||
76
src/utils/pdf/helpers/numberToWords.js
Normal file
76
src/utils/pdf/helpers/numberToWords.js
Normal file
@ -0,0 +1,76 @@
|
||||
const ONES = [
|
||||
'',
|
||||
'One',
|
||||
'Two',
|
||||
'Three',
|
||||
'Four',
|
||||
'Five',
|
||||
'Six',
|
||||
'Seven',
|
||||
'Eight',
|
||||
'Nine',
|
||||
'Ten',
|
||||
'Eleven',
|
||||
'Twelve',
|
||||
'Thirteen',
|
||||
'Fourteen',
|
||||
'Fifteen',
|
||||
'Sixteen',
|
||||
'Seventeen',
|
||||
'Eighteen',
|
||||
'Nineteen',
|
||||
];
|
||||
|
||||
const TENS = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'];
|
||||
|
||||
const twoDigitsToWords = (num) => {
|
||||
if (num < 20) return ONES[num];
|
||||
|
||||
const ten = Math.floor(num / 10);
|
||||
const one = num % 10;
|
||||
return `${TENS[ten]}${one ? ` ${ONES[one]}` : ''}`.trim();
|
||||
};
|
||||
|
||||
const threeDigitsToWords = (num) => {
|
||||
const hundred = Math.floor(num / 100);
|
||||
const remainder = num % 100;
|
||||
|
||||
const hundredPart = hundred ? `${ONES[hundred]} Hundred` : '';
|
||||
const remainderPart = remainder ? twoDigitsToWords(remainder) : '';
|
||||
|
||||
return `${hundredPart}${hundredPart && remainderPart ? ' ' : ''}${remainderPart}`.trim();
|
||||
};
|
||||
|
||||
const integerToIndianWords = (num) => {
|
||||
if (num === 0) return 'Zero';
|
||||
|
||||
const crore = Math.floor(num / 10000000);
|
||||
const lakh = Math.floor((num % 10000000) / 100000);
|
||||
const thousand = Math.floor((num % 100000) / 1000);
|
||||
const rest = num % 1000;
|
||||
|
||||
const chunks = [];
|
||||
if (crore) chunks.push(`${twoDigitsToWords(crore)} Crore`);
|
||||
if (lakh) chunks.push(`${twoDigitsToWords(lakh)} Lakh`);
|
||||
if (thousand) chunks.push(`${twoDigitsToWords(thousand)} Thousand`);
|
||||
if (rest) chunks.push(threeDigitsToWords(rest));
|
||||
|
||||
return chunks.join(' ').trim();
|
||||
};
|
||||
|
||||
const numberToWords = (value = 0) => {
|
||||
const amount = Number(value) || 0;
|
||||
const integerPart = Math.floor(amount);
|
||||
const decimalPart = Math.round((amount - integerPart) * 100);
|
||||
|
||||
const rupeesWords = integerToIndianWords(integerPart);
|
||||
const paiseWords = decimalPart ? twoDigitsToWords(decimalPart) : '';
|
||||
|
||||
if (decimalPart) {
|
||||
return `${rupeesWords} Rupees and ${paiseWords} Paise Only`;
|
||||
}
|
||||
|
||||
return `${rupeesWords} Rupees Only`;
|
||||
};
|
||||
|
||||
module.exports = { numberToWords };
|
||||
258
src/utils/pdf/helpers/templateBase.js
Normal file
258
src/utils/pdf/helpers/templateBase.js
Normal file
@ -0,0 +1,258 @@
|
||||
const escapeHtml = (value = '') =>
|
||||
String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const getCompanyInitials = (name = '') =>
|
||||
String(name)
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0].toUpperCase())
|
||||
.join('') || 'CO';
|
||||
|
||||
const getBaseStyles = () => `
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #111827;
|
||||
background: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.document { width: 100%; padding: 0; }
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
.logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.company h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.company .meta {
|
||||
margin-top: 4px;
|
||||
color: #374151;
|
||||
}
|
||||
.title-block {
|
||||
text-align: right;
|
||||
min-width: 280px;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
color: #1d4ed8;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
.doc-meta {
|
||||
margin-top: 6px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.party-grid {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-title {
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
padding: 8px 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card-body { padding: 10px; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.items {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.items thead th {
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
padding: 8px 6px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
.items tbody td {
|
||||
padding: 8px 6px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.items tbody tr:nth-child(even) { background: #f9fafb; }
|
||||
.text-right { text-align: right; }
|
||||
.item-name { width: 34%; }
|
||||
.info-grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #e5e7eb;
|
||||
}
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label {
|
||||
color: #6b7280;
|
||||
min-width: 130px;
|
||||
}
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
.summary-wrap {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.summary-box {
|
||||
width: 340px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.summary-box table td { padding: 8px 10px; }
|
||||
.summary-box tr:nth-child(even) { background: #f9fafb; }
|
||||
.grand-total {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.amount-words {
|
||||
margin-top: 8px;
|
||||
border: 1px dashed #93c5fd;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
}
|
||||
.terms {
|
||||
margin-top: 14px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.terms h3 {
|
||||
margin: 0 0 8px;
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
}
|
||||
.footnote {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #6b7280;
|
||||
font-size: 10px;
|
||||
}
|
||||
.page-number:before {
|
||||
content: "Page " counter(page);
|
||||
}
|
||||
`;
|
||||
|
||||
const renderCompanyHeader = ({ company, title, docMetaHtml }) => {
|
||||
const initials = getCompanyInitials(company.name);
|
||||
|
||||
return `
|
||||
<div class="header">
|
||||
<div class="brand">
|
||||
<div class="logo">${escapeHtml(initials)}</div>
|
||||
<div class="company">
|
||||
<h1>${escapeHtml(company.name)}</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(company.address)}, ${escapeHtml(company.city)}, ${escapeHtml(company.state)} - ${escapeHtml(company.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(company.gstin)} | Phone: ${escapeHtml(company.phone)}<br />
|
||||
Email: ${escapeHtml(company.email)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
<h2 class="title">${escapeHtml(title)}</h2>
|
||||
<div class="doc-meta">${docMetaHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderPartyCard = (title, bodyHtml) => `
|
||||
<div class="card">
|
||||
<div class="card-title">${escapeHtml(title)}</div>
|
||||
<div class="card-body">${bodyHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const renderFootnote = () => `
|
||||
<div class="footnote">
|
||||
<span>This is a computer generated document.</span>
|
||||
<span class="page-number"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const getDummyCompany = () => ({
|
||||
name: 'Acme Infra & Engineering Pvt. Ltd.',
|
||||
address: '3rd Floor, Business Tower, Sector 62',
|
||||
city: 'Noida',
|
||||
state: 'Uttar Pradesh',
|
||||
pincode: '201301',
|
||||
gstin: '09AABCA1234K1Z7',
|
||||
phone: '+91 120 4400 221',
|
||||
email: 'procurement@acmeinfra.in',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
escapeHtml,
|
||||
getBaseStyles,
|
||||
renderCompanyHeader,
|
||||
renderPartyCard,
|
||||
renderFootnote,
|
||||
getDummyCompany,
|
||||
getCompanyInitials,
|
||||
};
|
||||
24
src/utils/pdf/pdfGenerator.js
Normal file
24
src/utils/pdf/pdfGenerator.js
Normal file
@ -0,0 +1,24 @@
|
||||
const generatePdf = async (htmlContent) => {
|
||||
const puppeteer = await import('puppeteer');
|
||||
const browser = await puppeteer.default.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
|
||||
});
|
||||
|
||||
return pdfBuffer;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { generatePdf };
|
||||
191
src/utils/pdf/templates/amc.template.js
Normal file
191
src/utils/pdf/templates/amc.template.js
Normal file
@ -0,0 +1,191 @@
|
||||
const { formatCurrency } = require('../helpers/formatCurrency');
|
||||
const { formatDate } = require('../helpers/formatDate');
|
||||
const { numberToWords } = require('../helpers/numberToWords');
|
||||
const {
|
||||
escapeHtml,
|
||||
getBaseStyles,
|
||||
renderCompanyHeader,
|
||||
renderPartyCard,
|
||||
renderFootnote,
|
||||
getDummyCompany,
|
||||
} = require('../helpers/templateBase');
|
||||
|
||||
const getDummyAmcData = () => ({
|
||||
company: getDummyCompany(),
|
||||
asset: {
|
||||
asset_code: 'AST-PLT-2026-0042',
|
||||
asset_name: 'CNC Lathe Machine - Model XL-500',
|
||||
brand_model: 'XL-500 Pro Series',
|
||||
serial_number: 'PTI-XL500-2024-8891',
|
||||
plant_name: 'Noida Manufacturing Plant',
|
||||
department_name: 'Production Engineering',
|
||||
},
|
||||
contract: {
|
||||
contract_no: 'AMC-2026-PTI-0042',
|
||||
contract_type: 'COMPREHENSIVE',
|
||||
start_date: '2026-04-01',
|
||||
end_date: '2027-03-31',
|
||||
renewal_date: '2027-02-15',
|
||||
annual_cost: 125000,
|
||||
payment_frequency: 'ANNUAL',
|
||||
service_frequency: 'QUARTERLY',
|
||||
visits_per_year: 4,
|
||||
is_active: true,
|
||||
contact_person: 'Suresh Kulkarni',
|
||||
contact_phone: '+91 98765 43210',
|
||||
contact_email: 'amc.support@precisiontools.in',
|
||||
scope_of_work:
|
||||
'Preventive maintenance, breakdown support, spare parts labour, and annual health check of CNC lathe including spindle, coolant system, and control panel.',
|
||||
exclusions:
|
||||
'Major spindle replacement, accidental damage, consumables (coolant, lubricants), and third-party calibration charges.',
|
||||
remarks: 'Response SLA: 24 hours for breakdown calls. Next scheduled visit due 15-Jul-2026.',
|
||||
},
|
||||
vendor: {
|
||||
vendor_code: 'VND-PTI-01',
|
||||
vendor_name: 'Precision Tools India',
|
||||
address: '12, Industrial Area, Pimpri',
|
||||
city: 'Pune',
|
||||
state: 'Maharashtra',
|
||||
pincode: '411018',
|
||||
gstin: '27AABCP1234M1Z5',
|
||||
},
|
||||
service_visits: [
|
||||
{
|
||||
visit_date: '2026-04-10',
|
||||
visit_type: 'PREVENTIVE',
|
||||
status: 'COMPLETED',
|
||||
engineer_name: 'Anil Deshmukh',
|
||||
work_done: 'Quarterly PM — lubrication, belt tension check, control panel inspection.',
|
||||
},
|
||||
{
|
||||
visit_date: '2026-01-08',
|
||||
visit_type: 'BREAKDOWN',
|
||||
status: 'COMPLETED',
|
||||
engineer_name: 'Rohit Jadhav',
|
||||
work_done: 'Replaced coolant pump seal and recalibrated spindle speed sensor.',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const generateAmcHtml = (inputData = getDummyAmcData()) => {
|
||||
const data = inputData || getDummyAmcData();
|
||||
|
||||
const visitRows = (data.service_visits || [])
|
||||
.map(
|
||||
(visit, index) => `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${formatDate(visit.visit_date)}</td>
|
||||
<td>${escapeHtml(visit.visit_type)}</td>
|
||||
<td>${escapeHtml(visit.status)}</td>
|
||||
<td>${escapeHtml(visit.engineer_name)}</td>
|
||||
<td>${escapeHtml(visit.work_done)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const docMetaHtml = `
|
||||
<div><strong>Contract No:</strong> ${escapeHtml(data.contract.contract_no)}</div>
|
||||
<div><strong>Valid From:</strong> ${formatDate(data.contract.start_date)}</div>
|
||||
<div><strong>Valid To:</strong> ${formatDate(data.contract.end_date)}</div>
|
||||
<div style="margin-top: 4px;"><span class="status">${data.contract.is_active ? 'ACTIVE' : 'INACTIVE'}</span></div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>AMC Contract Summary</title>
|
||||
<style>${getBaseStyles()}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
${renderCompanyHeader({ company: data.company, title: 'AMC CONTRACT SUMMARY', docMetaHtml })}
|
||||
|
||||
<div class="party-grid">
|
||||
${renderPartyCard(
|
||||
'Asset Owner',
|
||||
`
|
||||
<strong>${escapeHtml(data.company.name)}</strong><br />
|
||||
Plant: ${escapeHtml(data.asset.plant_name)}<br />
|
||||
Department: ${escapeHtml(data.asset.department_name)}
|
||||
`
|
||||
)}
|
||||
${renderPartyCard(
|
||||
'AMC Vendor',
|
||||
`
|
||||
<strong>${escapeHtml(data.vendor.vendor_name)}</strong> (${escapeHtml(data.vendor.vendor_code)})<br />
|
||||
${escapeHtml(data.vendor.address)}, ${escapeHtml(data.vendor.city)}<br />
|
||||
${escapeHtml(data.vendor.state)} - ${escapeHtml(data.vendor.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(data.vendor.gstin)}<br />
|
||||
Contact: ${escapeHtml(data.contract.contact_person)} (${escapeHtml(data.contract.contact_phone)})
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px;">
|
||||
<div class="card-title">Covered Asset</div>
|
||||
<div class="card-body">
|
||||
<strong>${escapeHtml(data.asset.asset_name)}</strong> (${escapeHtml(data.asset.asset_code)})<br />
|
||||
${escapeHtml(data.asset.brand_model)} | S/N: ${escapeHtml(data.asset.serial_number)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">Contract Terms</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row"><span class="info-label">Contract Type</span><span class="info-value">${escapeHtml(data.contract.contract_type)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Payment Frequency</span><span class="info-value">${escapeHtml(data.contract.payment_frequency)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Service Frequency</span><span class="info-value">${escapeHtml(data.contract.service_frequency)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Visits Per Year</span><span class="info-value">${escapeHtml(data.contract.visits_per_year)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Renewal Date</span><span class="info-value">${formatDate(data.contract.renewal_date)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Annual Cost</span><span class="info-value">${formatCurrency(data.contract.annual_cost)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Vendor Contact</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row"><span class="info-label">Contact Person</span><span class="info-value">${escapeHtml(data.contract.contact_person)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Phone</span><span class="info-value">${escapeHtml(data.contract.contact_phone)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Email</span><span class="info-value">${escapeHtml(data.contract.contact_email)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Contract Period</span><span class="info-value">${formatDate(data.contract.start_date)} to ${formatDate(data.contract.end_date)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amount-words">Annual Cost in Words: ${escapeHtml(numberToWords(data.contract.annual_cost))}</div>
|
||||
|
||||
<div class="terms">
|
||||
<h3>Scope of Work</h3>
|
||||
<div>${escapeHtml(data.contract.scope_of_work)}</div>
|
||||
<h3 style="margin-top:10px;">Exclusions</h3>
|
||||
<div>${escapeHtml(data.contract.exclusions)}</div>
|
||||
<h3 style="margin-top:10px;">Remarks</h3>
|
||||
<div>${escapeHtml(data.contract.remarks)}</div>
|
||||
</div>
|
||||
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%;">S.No</th>
|
||||
<th style="width:12%;">Visit Date</th>
|
||||
<th style="width:14%;">Type</th>
|
||||
<th style="width:12%;">Status</th>
|
||||
<th style="width:16%;">Engineer</th>
|
||||
<th>Work Done</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${visitRows}</tbody>
|
||||
</table>
|
||||
|
||||
${renderFootnote()}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = { generateAmcHtml, getDummyAmcData };
|
||||
179
src/utils/pdf/templates/asset.template.js
Normal file
179
src/utils/pdf/templates/asset.template.js
Normal file
@ -0,0 +1,179 @@
|
||||
const { formatCurrency } = require('../helpers/formatCurrency');
|
||||
const { formatDate } = require('../helpers/formatDate');
|
||||
const {
|
||||
escapeHtml,
|
||||
getBaseStyles,
|
||||
renderCompanyHeader,
|
||||
renderFootnote,
|
||||
getDummyCompany,
|
||||
} = require('../helpers/templateBase');
|
||||
|
||||
const getDummyAssetData = () => ({
|
||||
company: getDummyCompany(),
|
||||
asset: {
|
||||
asset_code: 'AST-PLT-2026-0042',
|
||||
asset_name: 'CNC Lathe Machine - Model XL-500',
|
||||
status: 'IN_USE',
|
||||
condition: 'GOOD',
|
||||
brand_model: 'XL-500 Pro Series',
|
||||
manufacturer: 'Precision Tools India',
|
||||
serial_number: 'PTI-XL500-2024-8891',
|
||||
part_number: 'XL500-PRO-3AX',
|
||||
purchase_date: '2024-03-15',
|
||||
purchase_cost: 1850000,
|
||||
useful_life_years: 10,
|
||||
depreciation_method: 'WDV',
|
||||
salvage_value: 185000,
|
||||
warranty_expiry_date: '2027-03-14',
|
||||
location_detail: 'Bay 3, Production Floor',
|
||||
qr_code_value: 'AST-PLT-2026-0042',
|
||||
remarks: 'Annual calibration due in September. AMC contract active.',
|
||||
},
|
||||
category: {
|
||||
code: 'PLT',
|
||||
name: 'Plant & Machinery',
|
||||
},
|
||||
plant: {
|
||||
code: 'PLT-NOI-01',
|
||||
name: 'Noida Manufacturing Plant',
|
||||
},
|
||||
department: {
|
||||
name: 'Production Engineering',
|
||||
},
|
||||
warehouse: {
|
||||
code: 'WH-NOI-01',
|
||||
name: 'Noida Main Warehouse',
|
||||
},
|
||||
assigned_to: {
|
||||
full_name: 'Vikram Singh',
|
||||
employee_code: 'EMP-1042',
|
||||
},
|
||||
vendor: {
|
||||
vendor_code: 'VND-PTI-01',
|
||||
vendor_name: 'Precision Tools India',
|
||||
},
|
||||
purchase_order: {
|
||||
po_number: 'PO-2024-00112',
|
||||
},
|
||||
grn: {
|
||||
grn_number: 'GRN-2024-00089',
|
||||
},
|
||||
});
|
||||
|
||||
const renderInfoRows = (rows) =>
|
||||
rows
|
||||
.map(
|
||||
([label, value]) => `
|
||||
<div class="info-row">
|
||||
<span class="info-label">${escapeHtml(label)}</span>
|
||||
<span class="info-value">${value}</span>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const generateAssetHtml = (inputData = getDummyAssetData()) => {
|
||||
const data = inputData || getDummyAssetData();
|
||||
|
||||
const docMetaHtml = `
|
||||
<div><strong>Asset Code:</strong> ${escapeHtml(data.asset.asset_code)}</div>
|
||||
<div><strong>Registered:</strong> ${formatDate(data.asset.purchase_date)}</div>
|
||||
<div><strong>Category:</strong> ${escapeHtml(data.category.name)}</div>
|
||||
<div style="margin-top: 4px;"><span class="status">${escapeHtml(data.asset.status)}</span></div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Asset Detail Sheet</title>
|
||||
<style>${getBaseStyles()}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
${renderCompanyHeader({ company: data.company, title: 'ASSET DETAIL SHEET', docMetaHtml })}
|
||||
|
||||
<div class="card" style="margin-top:14px;">
|
||||
<div class="card-title">Asset Identification</div>
|
||||
<div class="card-body">
|
||||
<strong style="font-size:14px;">${escapeHtml(data.asset.asset_name)}</strong>
|
||||
<div style="margin-top:6px;color:#374151;">
|
||||
${escapeHtml(data.asset.brand_model)} | S/N: ${escapeHtml(data.asset.serial_number)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">Asset Details</div>
|
||||
<div class="card-body">
|
||||
${renderInfoRows([
|
||||
['Asset Code', escapeHtml(data.asset.asset_code)],
|
||||
['Category', `${escapeHtml(data.category.code)} - ${escapeHtml(data.category.name)}`],
|
||||
['Manufacturer', escapeHtml(data.asset.manufacturer)],
|
||||
['Brand / Model', escapeHtml(data.asset.brand_model)],
|
||||
['Serial Number', escapeHtml(data.asset.serial_number)],
|
||||
['Part Number', escapeHtml(data.asset.part_number)],
|
||||
['Condition', escapeHtml(data.asset.condition)],
|
||||
['QR Code', escapeHtml(data.asset.qr_code_value)],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Location & Assignment</div>
|
||||
<div class="card-body">
|
||||
${renderInfoRows([
|
||||
['Plant', `${escapeHtml(data.plant.code)} - ${escapeHtml(data.plant.name)}`],
|
||||
['Department', escapeHtml(data.department.name)],
|
||||
['Warehouse', `${escapeHtml(data.warehouse.code)} - ${escapeHtml(data.warehouse.name)}`],
|
||||
['Location Detail', escapeHtml(data.asset.location_detail)],
|
||||
[
|
||||
'Assigned To',
|
||||
`${escapeHtml(data.assigned_to.full_name)} (${escapeHtml(data.assigned_to.employee_code)})`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">Financial & Depreciation</div>
|
||||
<div class="card-body">
|
||||
${renderInfoRows([
|
||||
['Purchase Date', formatDate(data.asset.purchase_date)],
|
||||
['Purchase Cost', formatCurrency(data.asset.purchase_cost)],
|
||||
['Useful Life', `${escapeHtml(data.asset.useful_life_years)} years`],
|
||||
['Depreciation Method', escapeHtml(data.asset.depreciation_method)],
|
||||
['Salvage Value', formatCurrency(data.asset.salvage_value)],
|
||||
['Warranty Expiry', formatDate(data.asset.warranty_expiry_date)],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Procurement Reference</div>
|
||||
<div class="card-body">
|
||||
${renderInfoRows([
|
||||
['Vendor', `${escapeHtml(data.vendor.vendor_code)} - ${escapeHtml(data.vendor.vendor_name)}`],
|
||||
['Purchase Order', escapeHtml(data.purchase_order.po_number)],
|
||||
['GRN Reference', escapeHtml(data.grn.grn_number)],
|
||||
['Status', escapeHtml(data.asset.status)],
|
||||
])}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="terms">
|
||||
<h3>Remarks</h3>
|
||||
<div>${escapeHtml(data.asset.remarks)}</div>
|
||||
</div>
|
||||
|
||||
${renderFootnote()}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = { generateAssetHtml, getDummyAssetData };
|
||||
235
src/utils/pdf/templates/grn.template.js
Normal file
235
src/utils/pdf/templates/grn.template.js
Normal file
@ -0,0 +1,235 @@
|
||||
const { formatCurrency } = require('../helpers/formatCurrency');
|
||||
const { formatDate } = require('../helpers/formatDate');
|
||||
const { numberToWords } = require('../helpers/numberToWords');
|
||||
const {
|
||||
escapeHtml,
|
||||
getBaseStyles,
|
||||
renderCompanyHeader,
|
||||
renderPartyCard,
|
||||
renderFootnote,
|
||||
getDummyCompany,
|
||||
} = require('../helpers/templateBase');
|
||||
|
||||
const getDummyGrnData = () => ({
|
||||
company: getDummyCompany(),
|
||||
grn: {
|
||||
grn_number: 'GRN-2026-00018',
|
||||
grn_date: '2026-06-22',
|
||||
status: 'POSTED',
|
||||
po_number: 'PO-2026-00047',
|
||||
vendor_invoice_no: 'SIS/INV/2026/441',
|
||||
vendor_invoice_date: '2026-06-21',
|
||||
vendor_invoice_amount: 254743.5,
|
||||
vehicle_no: 'MH-12-AB-4521',
|
||||
lr_no: 'LR-77821',
|
||||
lr_date: '2026-06-21',
|
||||
received_by: 'Amit Sharma',
|
||||
quality_checked_by: 'Priya Nair',
|
||||
remarks: 'All items inspected and accepted except partial rejection on line 3.',
|
||||
},
|
||||
vendor: {
|
||||
vendor_name: 'Shree Industrial Supplies',
|
||||
gstin: '27AAECS7788B1Z2',
|
||||
address: 'Plot 17, MIDC Industrial Area',
|
||||
city: 'Pune',
|
||||
state: 'Maharashtra',
|
||||
pincode: '411019',
|
||||
contact_name: 'Rajesh Patil',
|
||||
phone: '+91 98220 12345',
|
||||
},
|
||||
warehouse: {
|
||||
code: 'WH-NOI-01',
|
||||
name: 'Noida Main Warehouse',
|
||||
address: 'Plot 9, Industrial Estate, Sector 63, Noida',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
line_no: 1,
|
||||
item_code: 'ITM-MS-CH-100',
|
||||
item_name: 'MS Channel 100x50 mm',
|
||||
uom: 'KG',
|
||||
ordered_qty: 1250,
|
||||
previously_received_qty: 0,
|
||||
current_qty: 1250,
|
||||
accepted_qty: 1250,
|
||||
rejected_qty: 0,
|
||||
rate: 72.5,
|
||||
line_total: 90625,
|
||||
},
|
||||
{
|
||||
line_no: 2,
|
||||
item_code: 'ITM-FST-M12',
|
||||
item_name: 'Industrial Fasteners Set M12',
|
||||
uom: 'BOX',
|
||||
ordered_qty: 40,
|
||||
previously_received_qty: 0,
|
||||
current_qty: 40,
|
||||
accepted_qty: 40,
|
||||
rejected_qty: 0,
|
||||
rate: 1425,
|
||||
line_total: 57000,
|
||||
},
|
||||
{
|
||||
line_no: 3,
|
||||
item_code: 'ITM-WLD-E6013',
|
||||
item_name: 'Welding Rod E6013',
|
||||
uom: 'BOX',
|
||||
ordered_qty: 30,
|
||||
previously_received_qty: 0,
|
||||
current_qty: 30,
|
||||
accepted_qty: 28,
|
||||
rejected_qty: 2,
|
||||
rate: 985,
|
||||
line_total: 27580,
|
||||
},
|
||||
{
|
||||
line_no: 4,
|
||||
item_code: 'ITM-SFT-GLV',
|
||||
item_name: 'Safety Gloves (Heat Resistant)',
|
||||
uom: 'PAIR',
|
||||
ordered_qty: 200,
|
||||
previously_received_qty: 0,
|
||||
current_qty: 200,
|
||||
accepted_qty: 200,
|
||||
rejected_qty: 0,
|
||||
rate: 210,
|
||||
line_total: 42000,
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
sub_total: 217205,
|
||||
grand_total: 217205,
|
||||
},
|
||||
});
|
||||
|
||||
const generateGrnHtml = (inputData = getDummyGrnData()) => {
|
||||
const data = inputData || getDummyGrnData();
|
||||
|
||||
const itemsRows = (data.items || [])
|
||||
.map(
|
||||
(item, index) => `
|
||||
<tr>
|
||||
<td>${item.line_no || index + 1}</td>
|
||||
<td class="item-name">
|
||||
${escapeHtml(item.item_name)}<br />
|
||||
<span style="color:#6b7280;font-size:10px;">${escapeHtml(item.item_code)}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(item.uom)}</td>
|
||||
<td class="text-right">${Number(item.ordered_qty || 0).toFixed(2)}</td>
|
||||
<td class="text-right">${Number(item.accepted_qty || 0).toFixed(2)}</td>
|
||||
<td class="text-right">${Number(item.rejected_qty || 0).toFixed(2)}</td>
|
||||
<td class="text-right">${formatCurrency(item.rate)}</td>
|
||||
<td class="text-right">${formatCurrency(item.line_total)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const docMetaHtml = `
|
||||
<div><strong>GRN No:</strong> ${escapeHtml(data.grn.grn_number)}</div>
|
||||
<div><strong>Date:</strong> ${formatDate(data.grn.grn_date)}</div>
|
||||
<div><strong>PO Ref:</strong> ${escapeHtml(data.grn.po_number)}</div>
|
||||
<div style="margin-top: 4px;"><span class="status">${escapeHtml(data.grn.status)}</span></div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Goods Receipt Note</title>
|
||||
<style>${getBaseStyles()}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
${renderCompanyHeader({ company: data.company, title: 'GOODS RECEIPT NOTE', docMetaHtml })}
|
||||
|
||||
<div class="party-grid">
|
||||
${renderPartyCard(
|
||||
'Received At',
|
||||
`
|
||||
<strong>${escapeHtml(data.warehouse.name)}</strong> (${escapeHtml(data.warehouse.code)})<br />
|
||||
${escapeHtml(data.warehouse.address)}<br />
|
||||
<strong>${escapeHtml(data.company.name)}</strong>
|
||||
`
|
||||
)}
|
||||
${renderPartyCard(
|
||||
'Supplier',
|
||||
`
|
||||
<strong>${escapeHtml(data.vendor.vendor_name)}</strong><br />
|
||||
${escapeHtml(data.vendor.address)}, ${escapeHtml(data.vendor.city)}<br />
|
||||
${escapeHtml(data.vendor.state)} - ${escapeHtml(data.vendor.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(data.vendor.gstin)}<br />
|
||||
Contact: ${escapeHtml(data.vendor.contact_name)} (${escapeHtml(data.vendor.phone)})
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">Invoice & Transport</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row"><span class="info-label">Vendor Invoice No</span><span class="info-value">${escapeHtml(data.grn.vendor_invoice_no)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Invoice Date</span><span class="info-value">${formatDate(data.grn.vendor_invoice_date)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Invoice Amount</span><span class="info-value">${formatCurrency(data.grn.vendor_invoice_amount)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Vehicle No</span><span class="info-value">${escapeHtml(data.grn.vehicle_no)}</span></div>
|
||||
<div class="info-row"><span class="info-label">LR No / Date</span><span class="info-value">${escapeHtml(data.grn.lr_no)} / ${formatDate(data.grn.lr_date)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">Receipt Details</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row"><span class="info-label">Received By</span><span class="info-value">${escapeHtml(data.grn.received_by)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Quality Checked By</span><span class="info-value">${escapeHtml(data.grn.quality_checked_by)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Warehouse</span><span class="info-value">${escapeHtml(data.warehouse.code)}</span></div>
|
||||
<div class="info-row"><span class="info-label">PO Reference</span><span class="info-value">${escapeHtml(data.grn.po_number)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:5%;">S.No</th>
|
||||
<th>Item</th>
|
||||
<th style="width:8%;">UOM</th>
|
||||
<th style="width:10%;" class="text-right">Ordered</th>
|
||||
<th style="width:10%;" class="text-right">Accepted</th>
|
||||
<th style="width:9%;" class="text-right">Rejected</th>
|
||||
<th style="width:12%;" class="text-right">Rate</th>
|
||||
<th style="width:14%;" class="text-right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${itemsRows}</tbody>
|
||||
</table>
|
||||
|
||||
<div class="summary-wrap">
|
||||
<div class="summary-box">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Accepted Value</td>
|
||||
<td class="text-right">${formatCurrency(data.totals.sub_total)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grand-total">Total Received Value</td>
|
||||
<td class="text-right grand-total">${formatCurrency(data.totals.grand_total)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amount-words">Amount in Words: ${escapeHtml(numberToWords(data.totals.grand_total))}</div>
|
||||
|
||||
<div class="terms">
|
||||
<h3>Remarks</h3>
|
||||
<div>${escapeHtml(data.grn.remarks)}</div>
|
||||
</div>
|
||||
|
||||
${renderFootnote()}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = { generateGrnHtml, getDummyGrnData };
|
||||
424
src/utils/pdf/templates/po.template.js
Normal file
424
src/utils/pdf/templates/po.template.js
Normal file
@ -0,0 +1,424 @@
|
||||
const { formatCurrency } = require('../helpers/formatCurrency');
|
||||
const { formatDate } = require('../helpers/formatDate');
|
||||
const { numberToWords } = require('../helpers/numberToWords');
|
||||
|
||||
const getDummyPoData = () => ({
|
||||
company: {
|
||||
name: 'Acme Infra & Engineering Pvt. Ltd.',
|
||||
address: '3rd Floor, Business Tower, Sector 62',
|
||||
city: 'Noida',
|
||||
state: 'Uttar Pradesh',
|
||||
pincode: '201301',
|
||||
gstin: '09AABCA1234K1Z7',
|
||||
phone: '+91 120 4400 221',
|
||||
email: 'procurement@acmeinfra.in',
|
||||
},
|
||||
po: {
|
||||
po_number: 'PO-2026-00047',
|
||||
po_date: '2026-06-20',
|
||||
expected_delivery_date: '2026-06-30',
|
||||
status: 'Approved',
|
||||
payment_term: '30 Days from Invoice Date',
|
||||
delivery_term: 'Door Delivery - Main Plant',
|
||||
terms_and_conditions:
|
||||
'Material must conform to approved specifications. Any variation should be approved in writing before dispatch.',
|
||||
remarks: 'Please mention PO number in invoice and delivery challan.',
|
||||
},
|
||||
vendor: {
|
||||
vendor_name: 'Shree Industrial Supplies',
|
||||
gstin: '27AAECS7788B1Z2',
|
||||
address: 'Plot 17, MIDC Industrial Area',
|
||||
city: 'Pune',
|
||||
state: 'Maharashtra',
|
||||
pincode: '411019',
|
||||
contact_name: 'Rajesh Patil',
|
||||
phone: '+91 98220 12345',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
line_no: 1,
|
||||
item_name: 'MS Channel 100x50 mm',
|
||||
uom: 'KG',
|
||||
ordered_qty: 1250,
|
||||
rate: 72.5,
|
||||
gst_rate_pct: 18,
|
||||
taxable_amount: 90625,
|
||||
tax_amount: 16312.5,
|
||||
line_total: 106937.5,
|
||||
},
|
||||
{
|
||||
line_no: 2,
|
||||
item_name: 'Industrial Fasteners Set M12',
|
||||
uom: 'BOX',
|
||||
ordered_qty: 40,
|
||||
rate: 1425,
|
||||
gst_rate_pct: 18,
|
||||
taxable_amount: 57000,
|
||||
tax_amount: 10260,
|
||||
line_total: 67260,
|
||||
},
|
||||
{
|
||||
line_no: 3,
|
||||
item_name: 'Welding Rod E6013',
|
||||
uom: 'BOX',
|
||||
ordered_qty: 30,
|
||||
rate: 985,
|
||||
gst_rate_pct: 12,
|
||||
taxable_amount: 29550,
|
||||
tax_amount: 3546,
|
||||
line_total: 33096,
|
||||
},
|
||||
{
|
||||
line_no: 4,
|
||||
item_name: 'Safety Gloves (Heat Resistant)',
|
||||
uom: 'PAIR',
|
||||
ordered_qty: 200,
|
||||
rate: 210,
|
||||
gst_rate_pct: 5,
|
||||
taxable_amount: 42000,
|
||||
tax_amount: 2100,
|
||||
line_total: 44100,
|
||||
},
|
||||
],
|
||||
totals: {
|
||||
sub_total: 219175,
|
||||
tax_total: 32218.5,
|
||||
freight_charges: 2500,
|
||||
other_charges: 850,
|
||||
grand_total: 254743.5,
|
||||
},
|
||||
});
|
||||
|
||||
const escapeHtml = (value = '') =>
|
||||
String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
const calculateTaxSplit = (taxTotal) => {
|
||||
const halfTax = Number((Number(taxTotal || 0) / 2).toFixed(2));
|
||||
return { cgst: halfTax, sgst: halfTax };
|
||||
};
|
||||
|
||||
const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
const data = inputData || getDummyPoData();
|
||||
const taxSplit = calculateTaxSplit(data.totals.tax_total);
|
||||
const itemsRows = (data.items || [])
|
||||
.map(
|
||||
(item, index) => `
|
||||
<tr>
|
||||
<td>${item.line_no || index + 1}</td>
|
||||
<td class="item-name">${escapeHtml(item.item_name)}</td>
|
||||
<td>${escapeHtml(item.uom)}</td>
|
||||
<td class="text-right">${Number(item.ordered_qty || 0).toFixed(2)}</td>
|
||||
<td class="text-right">${formatCurrency(item.rate)}</td>
|
||||
<td class="text-right">${Number(item.gst_rate_pct || 0).toFixed(2)}%</td>
|
||||
<td class="text-right">${formatCurrency(item.line_total)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Purchase Order</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #111827;
|
||||
background: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.document {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
}
|
||||
.logo {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.company h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.company .meta {
|
||||
margin-top: 4px;
|
||||
color: #374151;
|
||||
}
|
||||
.title-block {
|
||||
text-align: right;
|
||||
min-width: 280px;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
color: #1d4ed8;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
.doc-meta {
|
||||
margin-top: 6px;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.party-grid {
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-title {
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
padding: 8px 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card-body {
|
||||
padding: 10px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.items {
|
||||
margin-top: 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.items thead th {
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
padding: 8px 6px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
.items tbody td {
|
||||
padding: 8px 6px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.items tbody tr:nth-child(even) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.item-name {
|
||||
width: 34%;
|
||||
}
|
||||
.summary-wrap {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.summary-box {
|
||||
width: 340px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.summary-box table td {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.summary-box tr:nth-child(even) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
.grand-total {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.amount-words {
|
||||
margin-top: 8px;
|
||||
border: 1px dashed #93c5fd;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
font-weight: 600;
|
||||
color: #1e40af;
|
||||
}
|
||||
.terms {
|
||||
margin-top: 14px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
.terms h3 {
|
||||
margin: 0 0 8px;
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
}
|
||||
.footnote {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #6b7280;
|
||||
font-size: 10px;
|
||||
}
|
||||
.page-number:before {
|
||||
content: "Page " counter(page);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
<div class="header">
|
||||
<div class="brand">
|
||||
<div class="logo">AI</div>
|
||||
<div class="company">
|
||||
<h1>${escapeHtml(data.company.name)}</h1>
|
||||
<div class="meta">
|
||||
${escapeHtml(data.company.address)}, ${escapeHtml(data.company.city)}, ${escapeHtml(data.company.state)} - ${escapeHtml(data.company.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(data.company.gstin)} | Phone: ${escapeHtml(data.company.phone)}<br />
|
||||
Email: ${escapeHtml(data.company.email)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title-block">
|
||||
<h2 class="title">PURCHASE ORDER</h2>
|
||||
<div class="doc-meta">
|
||||
<div><strong>PO No:</strong> ${escapeHtml(data.po.po_number)}</div>
|
||||
<div><strong>Date:</strong> ${formatDate(data.po.po_date)}</div>
|
||||
<div><strong>Delivery:</strong> ${formatDate(data.po.expected_delivery_date)}</div>
|
||||
<div style="margin-top: 4px;"><span class="status">${escapeHtml(data.po.status)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="party-grid">
|
||||
<div class="card">
|
||||
<div class="card-title">From</div>
|
||||
<div class="card-body">
|
||||
<strong>${escapeHtml(data.company.name)}</strong><br />
|
||||
${escapeHtml(data.company.address)}, ${escapeHtml(data.company.city)}<br />
|
||||
${escapeHtml(data.company.state)} - ${escapeHtml(data.company.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(data.company.gstin)}<br />
|
||||
Phone: ${escapeHtml(data.company.phone)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">To</div>
|
||||
<div class="card-body">
|
||||
<strong>${escapeHtml(data.vendor.vendor_name)}</strong><br />
|
||||
${escapeHtml(data.vendor.address)}, ${escapeHtml(data.vendor.city)}<br />
|
||||
${escapeHtml(data.vendor.state)} - ${escapeHtml(data.vendor.pincode)}<br />
|
||||
GSTIN: ${escapeHtml(data.vendor.gstin)}<br />
|
||||
Contact: ${escapeHtml(data.vendor.contact_name)} (${escapeHtml(data.vendor.phone)})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 6%;">S.No</th>
|
||||
<th>Item</th>
|
||||
<th style="width: 9%;">UOM</th>
|
||||
<th style="width: 11%;" class="text-right">Qty</th>
|
||||
<th style="width: 14%;" class="text-right">Rate</th>
|
||||
<th style="width: 10%;" class="text-right">GST%</th>
|
||||
<th style="width: 16%;" class="text-right">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${itemsRows}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="summary-wrap">
|
||||
<div class="summary-box">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Sub Total</td>
|
||||
<td class="text-right">${formatCurrency(data.totals.sub_total)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CGST</td>
|
||||
<td class="text-right">${formatCurrency(taxSplit.cgst)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SGST</td>
|
||||
<td class="text-right">${formatCurrency(taxSplit.sgst)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Freight</td>
|
||||
<td class="text-right">${formatCurrency(data.totals.freight_charges)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Other Charges</td>
|
||||
<td class="text-right">${formatCurrency(data.totals.other_charges)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="grand-total">Grand Total</td>
|
||||
<td class="text-right grand-total">${formatCurrency(data.totals.grand_total)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amount-words">Amount in Words: ${escapeHtml(numberToWords(data.totals.grand_total))}</div>
|
||||
|
||||
<div class="terms">
|
||||
<h3>Terms & Conditions</h3>
|
||||
<div><strong>Payment Term:</strong> ${escapeHtml(data.po.payment_term)}</div>
|
||||
<div><strong>Delivery Term:</strong> ${escapeHtml(data.po.delivery_term)}</div>
|
||||
<div style="margin-top: 6px;">${escapeHtml(data.po.terms_and_conditions)}</div>
|
||||
<div style="margin-top: 6px;"><strong>Remarks:</strong> ${escapeHtml(data.po.remarks)}</div>
|
||||
</div>
|
||||
|
||||
<div class="footnote">
|
||||
<span>This is a computer generated document.</span>
|
||||
<span class="page-number"></span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = { generatePoHtml, getDummyPoData };
|
||||
Loading…
Reference in New Issue
Block a user