Merge branch 'develop'

This commit is contained in:
Methapon2001 2025-04-25 17:30:16 +07:00
commit 8a3a9e7eb3
42 changed files with 1295 additions and 226 deletions

View file

@ -24,6 +24,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/multer": "^1.4.12",
"@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17",
"nodemon": "^3.1.9",
@ -46,12 +47,14 @@
"dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0",
"dotenv": "^16.4.7",
"exceljs": "^4.4.0",
"express": "^4.21.2",
"fast-jwt": "^5.0.5",
"json-2-csv": "^5.5.8",
"kysely": "^0.27.5",
"minio": "^8.0.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0",
"prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6",

254
pnpm-lock.yaml generated
View file

@ -44,6 +44,9 @@ importers:
dotenv:
specifier: ^16.4.7
version: 16.4.7
exceljs:
specifier: ^4.4.0
version: 4.4.0
express:
specifier: ^4.21.2
version: 4.21.2
@ -62,6 +65,9 @@ importers:
morgan:
specifier: ^1.10.0
version: 1.10.0
multer:
specifier: ^1.4.5-lts.2
version: 1.4.5-lts.2
nodemailer:
specifier: ^6.10.0
version: 6.10.0
@ -99,6 +105,9 @@ importers:
'@types/morgan':
specifier: ^1.9.9
version: 1.9.9
'@types/multer':
specifier: ^1.4.12
version: 1.4.12
'@types/node':
specifier: ^20.17.10
version: 20.17.10
@ -177,6 +186,12 @@ packages:
resolution: {integrity: sha512-jasKNQeOb1vNf9aEYg+8zXmetaFjApDTSCC4QTl6aTixvyiRiSLcCiB8P6Q0lY9JIII/BhqNl8WbpFnsKitntw==}
engines: {node: '>=18'}
'@fast-csv/format@4.3.5':
resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
'@fast-csv/parse@4.3.6':
resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==}
'@fast-csv/parse@5.0.2':
resolution: {integrity: sha512-gMu1Btmm99TP+wc0tZnlH30E/F1Gw1Tah3oMDBHNPe9W8S68ixVHjt89Wg5lh7d9RuQMtwN+sGl5kxR891+fzw==}
@ -492,6 +507,9 @@ packages:
'@types/multer@1.4.12':
resolution: {integrity: sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==}
'@types/node@14.18.63':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
'@types/node@20.17.10':
resolution: {integrity: sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==}
@ -587,6 +605,9 @@ packages:
resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==}
hasBin: true
append-field@1.0.0:
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
archiver-utils@2.1.0:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
engines: {node: '>= 6'}
@ -682,6 +703,10 @@ packages:
resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==}
engines: {node: '>= 0.8'}
big-integer@1.6.52:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@ -689,12 +714,18 @@ packages:
binary-search@1.3.6:
resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==}
binary@0.3.0:
resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
block-stream2@2.1.0:
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
bluebird@3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
bn.js@4.12.1:
resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==}
@ -725,9 +756,24 @@ packages:
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
engines: {node: '>=8.0.0'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer-indexof-polyfill@1.0.2:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buffers@0.1.1:
resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==}
engines: {node: '>=0.2.0'}
busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@ -744,6 +790,9 @@ packages:
resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==}
engines: {node: '>= 0.4'}
chainsaw@0.1.0:
resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==}
chalk-template@0.4.0:
resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
engines: {node: '>=12'}
@ -821,6 +870,10 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
concat-stream@1.6.2:
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
engines: {'0': node >= 0.8}
console-log-level@1.4.1:
resolution: {integrity: sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ==}
@ -985,6 +1038,9 @@ packages:
resolution: {integrity: sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==}
engines: {node: '>= 0.4'}
duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -1087,6 +1143,10 @@ packages:
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
exceljs@4.4.0:
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
engines: {node: '>=8.3.0'}
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@ -1095,6 +1155,10 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
fast-csv@4.3.6:
resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==}
engines: {node: '>=10.0.0'}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@ -1203,6 +1267,11 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
fstream@1.0.12:
resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==}
engines: {node: '>=0.6'}
deprecated: This package is no longer supported.
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@ -1623,6 +1692,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
listenercount@1.0.1:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@ -1649,6 +1721,13 @@ packages:
lodash.groupby@4.6.0:
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.isfunction@3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
@ -1796,6 +1875,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
mnemonist@0.39.8:
resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
@ -1818,6 +1901,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
multer@1.4.5-lts.2:
resolution: {integrity: sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==}
engines: {node: '>= 6.0.0'}
negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -2190,6 +2277,11 @@ packages:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rimraf@2.7.1:
resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
@ -2225,6 +2317,10 @@ packages:
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
saxes@5.0.1:
resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==}
engines: {node: '>=10'}
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
@ -2372,6 +2468,10 @@ packages:
resolution: {integrity: sha512-LsvisgE3iThboRqA+XLmtnY9ktPLVPOj3zZxXMhlezeCcAh0RhomquXJgB8H+lb/RR/pPcbNVGHVKFUwjpoRtw==}
engines: {node: '>= 0.8'}
streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
@ -2498,6 +2598,9 @@ packages:
tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
traverse@0.3.9:
resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
@ -2567,6 +2670,9 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
typescript@5.7.2:
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
engines: {node: '>=14.17'}
@ -2616,6 +2722,9 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
unzipper@0.10.14:
resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -2626,6 +2735,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
uuid@9.0.0:
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
hasBin: true
@ -2718,6 +2831,13 @@ packages:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@ -2826,6 +2946,25 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@fast-csv/format@4.3.5':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.isboolean: 3.0.3
lodash.isequal: 4.5.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
'@fast-csv/parse@4.3.6':
dependencies:
'@types/node': 14.18.63
lodash.escaperegexp: 4.1.2
lodash.groupby: 4.6.0
lodash.isfunction: 3.0.9
lodash.isnil: 4.0.0
lodash.isundefined: 3.0.1
lodash.uniq: 4.5.0
'@fast-csv/parse@5.0.2':
dependencies:
lodash.escaperegexp: 4.1.2
@ -3341,6 +3480,8 @@ snapshots:
dependencies:
'@types/express': 4.17.21
'@types/node@14.18.63': {}
'@types/node@20.17.10':
dependencies:
undici-types: 6.19.8
@ -3440,6 +3581,8 @@ snapshots:
json-bignum: 0.0.3
tslib: 2.8.1
append-field@1.0.0: {}
archiver-utils@2.1.0:
dependencies:
glob: 7.2.3
@ -3563,11 +3706,18 @@ snapshots:
dependencies:
safe-buffer: 5.1.2
big-integer@1.6.52: {}
binary-extensions@2.3.0: {}
binary-search@1.3.6:
optional: true
binary@0.3.0:
dependencies:
buffers: 0.1.1
chainsaw: 0.1.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -3578,6 +3728,8 @@ snapshots:
dependencies:
readable-stream: 3.6.2
bluebird@3.4.7: {}
bn.js@4.12.1: {}
body-parser@1.20.3:
@ -3621,11 +3773,21 @@ snapshots:
buffer-crc32@1.0.0: {}
buffer-from@1.1.2: {}
buffer-indexof-polyfill@1.0.2: {}
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffers@0.1.1: {}
busboy@1.6.0:
dependencies:
streamsearch: 1.1.0
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.1:
@ -3645,6 +3807,10 @@ snapshots:
call-bind-apply-helpers: 1.0.1
get-intrinsic: 1.2.6
chainsaw@0.1.0:
dependencies:
traverse: 0.3.9
chalk-template@0.4.0:
dependencies:
chalk: 4.1.2
@ -3756,6 +3922,13 @@ snapshots:
concat-map@0.0.1: {}
concat-stream@1.6.2:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 2.3.8
typedarray: 0.0.6
console-log-level@1.4.1:
optional: true
@ -3899,6 +4072,10 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexer2@0.1.4:
dependencies:
readable-stream: 2.3.8
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
@ -4088,6 +4265,18 @@ snapshots:
eventemitter3@5.0.1: {}
exceljs@4.4.0:
dependencies:
archiver: 5.3.2
dayjs: 1.11.13
fast-csv: 4.3.6
jszip: 3.10.1
readable-stream: 3.6.2
saxes: 5.0.1
tmp: 0.2.1
unzipper: 0.10.14
uuid: 8.3.2
execa@5.1.1:
dependencies:
cross-spawn: 7.0.6
@ -4136,6 +4325,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
fast-csv@4.3.6:
dependencies:
'@fast-csv/format': 4.3.5
'@fast-csv/parse': 4.3.6
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -4258,6 +4452,13 @@ snapshots:
fsevents@2.3.3:
optional: true
fstream@1.0.12:
dependencies:
graceful-fs: 4.2.11
inherits: 2.0.4
mkdirp: 0.5.6
rimraf: 2.7.1
function-bind@1.1.2: {}
function.prototype.name@1.1.7:
@ -4693,6 +4894,8 @@ snapshots:
lines-and-columns@1.2.4: {}
listenercount@1.0.1: {}
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@ -4713,6 +4916,10 @@ snapshots:
lodash.groupby@4.6.0: {}
lodash.isboolean@3.0.3: {}
lodash.isequal@4.5.0: {}
lodash.isfunction@3.0.9: {}
lodash.isnil@4.0.0: {}
@ -4853,6 +5060,10 @@ snapshots:
minipass@7.1.2: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
mnemonist@0.39.8:
dependencies:
obliterator: 2.0.4
@ -4879,6 +5090,16 @@ snapshots:
ms@2.1.3: {}
multer@1.4.5-lts.2:
dependencies:
append-field: 1.0.0
busboy: 1.6.0
concat-stream: 1.6.2
mkdirp: 0.5.6
object-assign: 4.1.1
type-is: 1.6.18
xtend: 4.0.2
negotiator@0.6.3: {}
neo-async@2.6.2: {}
@ -5273,6 +5494,10 @@ snapshots:
reusify@1.0.4: {}
rimraf@2.7.1:
dependencies:
glob: 7.2.3
rimraf@3.0.2:
dependencies:
glob: 7.2.3
@ -5307,6 +5532,10 @@ snapshots:
sax@1.4.1: {}
saxes@5.0.1:
dependencies:
xmlchars: 2.2.0
secure-json-parse@2.7.0: {}
semver@5.7.2: {}
@ -5478,6 +5707,8 @@ snapshots:
stream-to-buffer@0.0.1: {}
streamsearch@1.1.0: {}
strict-uri-encode@2.0.0: {}
string-width@4.2.3:
@ -5618,6 +5849,8 @@ snapshots:
punycode: 2.3.1
optional: true
traverse@0.3.9: {}
triple-beam@1.4.1: {}
ts-deepmerge@7.0.2: {}
@ -5697,6 +5930,8 @@ snapshots:
possible-typed-array-names: 1.0.0
reflect.getprototypeof: 1.0.8
typedarray@0.0.6: {}
typescript@5.7.2: {}
typical@4.0.0: {}
@ -5736,6 +5971,19 @@ snapshots:
unpipe@1.0.0: {}
unzipper@0.10.14:
dependencies:
big-integer: 1.6.52
binary: 0.3.0
bluebird: 3.4.7
buffer-indexof-polyfill: 1.0.2
duplexer2: 0.1.4
fstream: 1.0.12
graceful-fs: 4.2.11
listenercount: 1.0.1
readable-stream: 2.3.8
setimmediate: 1.0.5
util-deprecate@1.0.2: {}
util@0.12.5:
@ -5748,6 +5996,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@8.3.2: {}
uuid@9.0.0: {}
v8-compile-cache-lib@3.0.1: {}
@ -5888,6 +6138,10 @@ snapshots:
xmlbuilder@11.0.1: {}
xmlchars@2.2.0: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
yallist@2.1.2:

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "contactName" TEXT,
ADD COLUMN "contactTel" TEXT;

View file

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "firstName" DROP NOT NULL,
ALTER COLUMN "lastName" DROP NOT NULL;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TaskOrder" ADD COLUMN "codeProductReceived" TEXT;

View file

@ -0,0 +1,18 @@
-- AlterTable
ALTER TABLE "Institution" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "createdByUserId" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedByUserId" TEXT;
-- AlterTable
ALTER TABLE "Payment" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedByUserId" TEXT;
-- AddForeignKey
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Institution" ADD CONSTRAINT "Institution_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_updatedByUserId_fkey" FOREIGN KEY ("updatedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Employee" ALTER COLUMN "lastNameEN" DROP NOT NULL;

View file

@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "WorkflowTemplateStepGroup" (
"id" TEXT NOT NULL,
"group" TEXT NOT NULL,
"workflowTemplateStepId" TEXT NOT NULL,
CONSTRAINT "WorkflowTemplateStepGroup_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "WorkflowTemplateStepGroup" ADD CONSTRAINT "WorkflowTemplateStepGroup_workflowTemplateStepId_fkey" FOREIGN KEY ("workflowTemplateStepId") REFERENCES "WorkflowTemplateStep"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,20 @@
/*
Warnings:
- You are about to drop the column `importNationality` on the `User` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "User" DROP COLUMN "importNationality";
-- CreateTable
CREATE TABLE "UserImportNationality" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"userId" TEXT NOT NULL,
CONSTRAINT "UserImportNationality_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "UserImportNationality" ADD CONSTRAINT "UserImportNationality_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT;

View file

@ -366,16 +366,24 @@ enum UserType {
AGENCY
}
model UserImportNationality {
id String @id @default(cuid())
name String
user User @relation(fields: [userId], references: [id])
userId String
}
model User {
id String @id @default(cuid())
code String?
namePrefix String?
firstName String
firstName String?
firstNameEN String
middleName String?
middleNameEN String?
lastName String
lastName String?
lastNameEN String
username String
gender String
@ -424,7 +432,7 @@ model User {
licenseExpireDate DateTime? @db.Date
sourceNationality String?
importNationality String?
importNationality UserImportNationality[]
trainingPlace String?
responsibleArea UserResponsibleArea[]
@ -484,12 +492,15 @@ model User {
flowCreated WorkflowTemplate[] @relation("FlowCreatedByUser")
flowUpdated WorkflowTemplate[] @relation("FlowUpdatedByUser")
invoiceCreated Invoice[]
paymentCreated Payment[]
paymentCreated Payment[] @relation("PaymentCreatedByUser")
paymentUpdated Payment[] @relation("PaymentUpdatedByUser")
notificationReceive Notification[] @relation("NotificationReceiver")
notificationRead Notification[] @relation("NotificationRead")
notificationDelete Notification[] @relation("NotificationDelete")
taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser")
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
institutionCreated Institution[] @relation("InstitutionCreatedByUser")
institutionUpdated Institution[] @relation("InstitutionUpdatedByUser")
requestWorkStepStatus RequestWorkStepStatus[]
userTask UserTask[]
@ -497,6 +508,9 @@ model User {
remark String?
agencyStatus String?
contactName String?
contactTel String?
}
model UserResponsibleArea {
@ -771,11 +785,12 @@ model Employee {
middleName String?
middleNameEN String?
lastName String?
lastNameEN String
lastNameEN String?
dateOfBirth DateTime? @db.Date
gender String
nationality String
dateOfBirth DateTime? @db.Date
gender String
nationality String
otherNationality String?
address String?
addressEN String?
@ -850,18 +865,19 @@ model EmployeePassport {
issuePlace String
previousPassportRef String?
workerStatus String?
nationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
workerStatus String?
nationality String?
otherNationality String?
namePrefix String?
firstName String?
firstNameEN String?
middleName String?
middleNameEN String?
lastName String?
lastNameEN String?
gender String?
birthDate String?
birthCountry String?
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
employeeId String
@ -1012,6 +1028,13 @@ model Institution {
contactEmail String?
contactTel String?
createdAt DateTime @default(now())
createdBy User? @relation(name: "InstitutionCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @default(now()) @updatedAt
updatedBy User? @relation(name: "InstitutionUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
bank InstitutionBank[]
}
@ -1076,6 +1099,15 @@ model WorkflowTemplateStepInstitution {
workflowTemplateStepId String
}
model WorkflowTemplateStepGroup {
id String @id @default(cuid())
group String
workflowTemplateStep WorkflowTemplateStep @relation(fields: [workflowTemplateStepId], references: [id], onDelete: Cascade)
workflowTemplateStepId String
}
model WorkflowTemplateStep {
id String @id @default(cuid())
@ -1086,6 +1118,7 @@ model WorkflowTemplateStep {
value WorkflowTemplateStepValue[] // NOTE: For enum or options type
responsiblePerson WorkflowTemplateStepUser[]
responsibleInstitution WorkflowTemplateStepInstitution[]
responsibleGroup WorkflowTemplateStepGroup[]
messengerByArea Boolean @default(false)
attributes Json?
@ -1460,8 +1493,12 @@ model Payment {
date DateTime?
createdAt DateTime @default(now())
createdBy User? @relation(fields: [createdByUserId], references: [id], onDelete: SetNull)
createdBy User? @relation(name: "PaymentCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
createdByUserId String?
updatedAt DateTime @default(now()) @updatedAt
updatedBy User? @relation(name: "PaymentUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
updatedByUserId String?
}
enum RequestDataStatus {
@ -1614,7 +1651,8 @@ model TaskProduct {
model TaskOrder {
id String @id @default(cuid())
code String
code String
codeProductReceived String?
taskName String
taskOrderStatus TaskOrderStatus @default(Pending)

View file

@ -2,6 +2,7 @@ import { Body, Controller, Get, Path, Post, Query, Route, Tags } from "tsoa";
import prisma from "../db";
import { queryOrNot } from "../utils/relation";
import { notFoundError } from "../utils/error";
import { Prisma } from "@prisma/client";
@Route("/api/v1/employment-office")
@Tags("Employment Office")
@ -11,6 +12,39 @@ export class EmploymentOfficeController extends Controller {
return this.getEmploymentOfficeListByCriteria(districtId, query);
}
@Post("list-same-office-area")
async getSameOfficeArea(@Body() body: { districtId: string }) {
const office = await prisma.employmentOffice.findFirst({
include: {
province: {
include: {
district: true,
},
},
district: true,
},
where: {
OR: [
{
province: { district: { some: { id: body.districtId } } },
district: { none: {} },
},
{
district: {
some: { districtId: body.districtId },
},
},
],
},
});
if (!office) return [];
return [
...office.district.map((v) => v.districtId),
...office.province.district.map((v) => v.id),
];
}
@Post("list")
async getEmploymentOfficeListByCriteria(
@Query() districtId?: string,
@ -40,11 +74,14 @@ export class EmploymentOfficeController extends Controller {
],
[],
),
...queryOrNot(
...(queryOrNot(
query,
[{ name: { contains: query } }, { nameEN: { contains: query } }],
[
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
],
[],
),
) satisfies Prisma.EmploymentOfficeWhereInput["OR"]),
...queryOrNot(!!body?.id, [{ id: { in: body?.id } }], []),
]
: undefined,

View file

@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, Path, Post, Route, Security, Tags } from "tsoa";
import { addUserRoles, listRole, removeUserRoles } from "../services/keycloak";
import { Body, Controller, Delete, Get, Path, Post, Query, Route, Security, Tags } from "tsoa";
import { addUserRoles, getGroup, listRole, removeUserRoles } from "../services/keycloak";
@Route("api/v1/keycloak")
@Tags("Single-Sign On")
@ -44,4 +44,13 @@ export class KeycloakController extends Controller {
);
if (!result) throw new Error("Failed. Cannot remove user's role.");
}
@Get("group")
async getGroup(@Query() query: string = "") {
const querySearch = query === "" ? "q" : `search=${query}`;
const group = await getGroup(querySearch);
if (!Array.isArray(group)) throw new Error("Failed. Cannot get group(s) data from the server.");
return group;
}
}

View file

@ -36,8 +36,8 @@ export class NotificationController extends Controller {
AND: [
{
OR: queryOrNot<(typeof where)[]>(query, [
{ title: { contains: query } },
{ detail: { contains: query } },
{ title: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
]),
},
{

View file

@ -39,6 +39,7 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
@ -250,6 +251,8 @@ export class BranchController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -265,26 +268,27 @@ export class BranchController extends Controller {
},
OR: queryOrNot<Prisma.BranchWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
{
branch: {
some: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
],
},
},
},
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchWhereInput;
const [result, total] = await prisma.$transaction([
@ -309,12 +313,13 @@ export class BranchController extends Controller {
where: {
AND: { OR: permissionCond(req.user) },
OR: [
{ nameEN: { contains: query } },
{ name: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
{ nameEN: { contains: query, mode: "insensitive" } },
{ name: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
],
...whereDateQuery(startDate, endDate),
},
include: {
province: true,

View file

@ -18,7 +18,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { RequestWithUser } from "../interfaces/user";
import { branchRelationPermInclude, createPermCheck } from "../services/permission";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "branch_manager"];
@ -97,6 +97,8 @@ export class UserBranchController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -104,9 +106,10 @@ export class UserBranchController extends Controller {
userId,
},
OR: queryOrNot<Prisma.BranchUserWhereInput[]>(query, [
{ branch: { name: { contains: query } } },
{ branch: { nameEN: { contains: query } } },
{ branch: { name: { contains: query, mode: "insensitive" } } },
{ branch: { nameEN: { contains: query, mode: "insensitive" } } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([
@ -150,6 +153,8 @@ export class BranchUserController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
AND: {
@ -157,13 +162,14 @@ export class BranchUserController extends Controller {
branchId,
},
OR: [
{ user: { firstName: { contains: query } } },
{ user: { firstNameEN: { contains: query } } },
{ user: { lastName: { contains: query } } },
{ user: { lastNameEN: { contains: query } } },
{ user: { email: { contains: query } } },
{ user: { telephoneNo: { contains: query } } },
{ user: { firstName: { contains: query, mode: "insensitive" } } },
{ user: { firstNameEN: { contains: query, mode: "insensitive" } } },
{ user: { lastName: { contains: query, mode: "insensitive" } } },
{ user: { lastNameEN: { contains: query, mode: "insensitive" } } },
{ user: { email: { contains: query, mode: "insensitive" } } },
{ user: { telephoneNo: { contains: query, mode: "insensitive" } } },
],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.BranchUserWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -27,6 +27,7 @@ import {
listRole,
getUserRoles,
removeUserRoles,
getGroupUser,
} from "../services/keycloak";
import { isSystem } from "../utils/keycloak";
import {
@ -51,6 +52,7 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { retry } from "../utils/func";
@ -79,11 +81,11 @@ type UserCreate = {
citizenExpire?: Date | null;
namePrefix?: string | null;
firstName: string;
firstName?: string;
firstNameEN: string;
middleName?: string | null;
middleNameEN?: string | null;
lastName: string;
lastName?: string;
lastNameEN: string;
gender: string;
@ -97,7 +99,7 @@ type UserCreate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
importNationality?: string[] | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
@ -123,6 +125,9 @@ type UserCreate = {
remark?: string;
agencyStatus?: string;
contactName?: string;
contactTel?: string;
};
type UserUpdate = {
@ -139,9 +144,9 @@ type UserUpdate = {
namePrefix?: string | null;
firstName?: string;
firstNameEN?: string;
firstNameEN: string;
middleName?: string | null;
middleNameEN?: string | null;
middleNameEN: string | null;
lastName?: string;
lastNameEN?: string;
gender?: string;
@ -156,7 +161,7 @@ type UserUpdate = {
licenseIssueDate?: Date | null;
licenseExpireDate?: Date | null;
sourceNationality?: string | null;
importNationality?: string | null;
importNationality?: string[] | null;
trainingPlace?: string | null;
responsibleArea?: string[] | null;
birthDate?: Date | null;
@ -182,6 +187,9 @@ type UserUpdate = {
remark?: string;
agencyStatus?: string;
contactName?: string;
contactTel?: string;
};
const permissionCondCompany = createPermCondition((_) => true);
@ -273,6 +281,8 @@ export class UserController extends Controller {
@Query() status?: Status,
@Query() responsibleDistrictId?: string,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getUserByCriteria(
req,
@ -284,6 +294,8 @@ export class UserController extends Controller {
status,
responsibleDistrictId,
activeBranchOnly,
startDate,
endDate,
);
}
@ -299,6 +311,8 @@ export class UserController extends Controller {
@Query() status?: Status,
@Query() responsibleDistrictId?: string,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
userId?: string[];
@ -324,12 +338,12 @@ export class UserController extends Controller {
const where = {
OR: queryOrNot<Prisma.UserWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ email: { contains: query } },
{ telephoneNo: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ telephoneNo: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]),
AND: {
@ -362,12 +376,14 @@ export class UserController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.UserWhereInput;
const [result, total] = await prisma.$transaction([
prisma.user.findMany({
orderBy: [{ statusOrder: "asc" }, { createdAt: "asc" }],
include: {
importNationality: true,
responsibleArea: true,
province: true,
district: true,
@ -386,6 +402,7 @@ export class UserController extends Controller {
return {
result: result.map((v) => ({
...v,
importNationality: v.importNationality.map((v) => v.name),
responsibleArea: v.responsibleArea.map((v) => v.area),
branch: includeBranch ? v.branch.map((a) => a.branch) : undefined,
})),
@ -400,6 +417,7 @@ export class UserController extends Controller {
async getUserById(@Path() userId: string) {
const record = await prisma.user.findFirst({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -411,7 +429,11 @@ export class UserController extends Controller {
if (!record) throw notFoundError("User");
return record;
const { importNationality, ...rest } = record;
return Object.assign(rest, {
importNationality: importNationality.map((v) => v.name),
});
}
@Post()
@ -477,8 +499,8 @@ export class UserController extends Controller {
}
const userId = await createUser(username, username, {
firstName: body.firstName,
lastName: body.lastName,
firstName: body.firstNameEN,
lastName: body.lastNameEN,
email: body.email,
requiredActions: ["UPDATE_PASSWORD"],
enabled: rest.status !== "INACTIVE",
@ -513,6 +535,9 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
username,
userRole: role.name,
@ -668,6 +693,7 @@ export class UserController extends Controller {
const record = await prisma.user.update({
include: {
importNationality: true,
province: true,
district: true,
subDistrict: true,
@ -682,6 +708,10 @@ export class UserController extends Controller {
create: rest.responsibleArea.map((v) => ({ area: v })),
}
: undefined,
importNationality: {
deleteMany: {},
createMany: { data: rest.importNationality?.map((v) => ({ name: v })) || [] },
},
statusOrder: +(rest.status === "INACTIVE"),
userRole,
province: connectOrDisconnect(provinceId),
@ -933,3 +963,17 @@ export class UserSignatureController extends Controller {
await deleteFile(fileLocation.user.signature(userId));
}
}
@Route("api/v1/user/{userId}/group")
@Tags("User")
@Security("keycloak")
export class UserGroupController extends Controller {
@Get()
async getUserGroup(@Path() userId: string) {
const groupUser = await getGroupUser(userId);
if (!Array.isArray(groupUser))
throw new Error("Failed. Cannot get user group(s) data from the server.");
return groupUser;
}
}

View file

@ -30,6 +30,7 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import {
@ -195,18 +196,20 @@ export class CustomerBranchController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeRegisBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.CustomerBranchWhereInput[]>(query, [
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ email: { contains: query } },
{ code: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ email: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]),
AND: {
@ -229,6 +232,7 @@ export class CustomerBranchController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
...filterStatus(activeRegisBranchOnly ? Status.ACTIVE : status),
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerBranchWhereInput;
const [result, total] = await prisma.$transaction([
@ -285,13 +289,15 @@ export class CustomerBranchController extends Controller {
@Query() visa?: boolean,
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]),
AND: {
@ -300,6 +306,7 @@ export class CustomerBranchController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -36,7 +36,7 @@ import {
setFile,
} from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { connectOrNot, queryOrNot } from "../utils/relation";
import { connectOrNot, queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [
"system",
@ -165,17 +165,19 @@ export class CustomerController extends Controller {
@Query() includeBranch: boolean = false,
@Query() company: boolean = false,
@Query() activeBranchOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.CustomerWhereInput[]>(query, [
{ branch: { some: { namePrefix: { contains: query } } } },
{ branch: { some: { customerName: { contains: query } } } },
{ branch: { some: { registerName: { contains: query } } } },
{ branch: { some: { registerNameEN: { contains: query } } } },
{ branch: { some: { firstName: { contains: query } } } },
{ branch: { some: { firstNameEN: { contains: query } } } },
{ branch: { some: { lastName: { contains: query } } } },
{ branch: { some: { lastNameEN: { contains: query } } } },
{ branch: { some: { namePrefix: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { customerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { registerNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { firstNameEN: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { lastName: { contains: query, mode: "insensitive" } } } },
{ branch: { some: { lastNameEN: { contains: query, mode: "insensitive" } } } },
]),
AND: {
customerType,
@ -188,6 +190,7 @@ export class CustomerController extends Controller {
: permissionCond(req.user, { activeOnly: activeBranchOnly }),
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CustomerWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -30,6 +30,7 @@ import {
connectOrNot,
queryOrNot,
whereAddressQuery,
whereDateQuery,
} from "../utils/relation";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import {
@ -73,6 +74,7 @@ type EmployeeCreate = {
dateOfBirth?: Date | null;
gender: string;
nationality: string;
otherNationality?: string;
namePrefix?: string | null;
firstName?: string;
@ -109,6 +111,7 @@ type EmployeeUpdate = {
dateOfBirth?: Date;
gender?: string;
nationality?: string;
otherNationality?: string;
namePrefix?: string | null;
firstName?: string;
@ -116,7 +119,7 @@ type EmployeeUpdate = {
middleName?: string | null;
middleNameEN?: string | null;
lastName?: string;
lastNameEN: string;
lastNameEN?: string;
addressEN?: string;
address?: string;
@ -154,6 +157,8 @@ export class EmployeeController extends Controller {
@Query() customerBranchId?: string,
@Query() status?: Status,
@Query() query: string = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await prisma.employee
.groupBy({
@ -163,13 +168,13 @@ export class EmployeeController extends Controller {
OR: queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]),
AND: {
@ -183,6 +188,7 @@ export class EmployeeController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
},
})
.then((res) =>
@ -208,6 +214,8 @@ export class EmployeeController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.listByCriteria(
req,
@ -222,6 +230,8 @@ export class EmployeeController extends Controller {
page,
pageSize,
activeOnly,
startDate,
endDate,
);
}
@ -240,6 +250,8 @@ export class EmployeeController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
passport?: string[];
@ -252,13 +264,13 @@ export class EmployeeController extends Controller {
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]) ?? []),
...(queryOrNot<Prisma.EmployeeWhereInput[]>(!!body, [
@ -288,6 +300,7 @@ export class EmployeeController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -43,6 +43,7 @@ type EmployeePassportPayload = {
workerStatus: string;
nationality: string;
otherNationality: string;
namePrefix?: string | null;
firstName: string;
firstNameEN: string;

View file

@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
type WorkflowPayload = {
name: string;
@ -37,6 +37,7 @@ type WorkflowPayload = {
attributes?: { [key: string]: any };
responsiblePersonId?: string[];
responsibleInstitution?: string[];
responsibleGroup?: string[];
messengerByArea?: boolean;
}[];
registeredBranchId?: string;
@ -58,13 +59,15 @@ export class FlowTemplateController extends Controller {
@Query() status?: Status,
@Query() query = "",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot(query, [
{ name: { contains: query } },
{ name: { contains: query, mode: "insensitive" } },
{
step: {
some: { name: { contains: query } },
some: { name: { contains: query, mode: "insensitive" } },
},
},
]),
@ -74,6 +77,7 @@ export class FlowTemplateController extends Controller {
OR: permissionCondCompany(req.user, { activeOnly: true }),
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.WorkflowTemplateWhereInput;
const [result, total] = await prisma.$transaction([
prisma.workflowTemplate.findMany({
@ -86,6 +90,7 @@ export class FlowTemplateController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
orderBy: { order: "asc" },
},
@ -103,6 +108,7 @@ export class FlowTemplateController extends Controller {
step: r.step.map((v) => ({
...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})),
})),
page,
@ -123,6 +129,7 @@ export class FlowTemplateController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -137,6 +144,7 @@ export class FlowTemplateController extends Controller {
step: record.step.map((v) => ({
...v,
responsibleInstitution: v.responsibleInstitution.map((institution) => institution.group),
responsibleGroup: v.responsibleGroup.map((group) => group.group),
})),
};
}
@ -212,6 +220,9 @@ export class FlowTemplateController extends Controller {
responsibleInstitution: {
create: v.responsibleInstitution?.map((group) => ({ group })),
},
responsibleGroup: {
create: v.responsibleGroup?.map((group) => ({ group })),
},
})),
},
},
@ -292,6 +303,10 @@ export class FlowTemplateController extends Controller {
deleteMany: {},
create: v.responsibleInstitution?.map((group) => ({ group })),
},
responsibleGroup: {
deleteMany: {},
create: v.responsibleGroup?.map((group) => ({ group })),
},
},
})),
},

View file

@ -17,7 +17,7 @@ import {
} from "tsoa";
import prisma from "../db";
import { isUsedError, notFoundError } from "../utils/error";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { RequestWithUser } from "../interfaces/user";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
@ -108,8 +108,19 @@ export class InstitutionController extends Controller {
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getInstitutionListByCriteria(query, page, pageSize, status, activeOnly, group);
return this.getInstitutionListByCriteria(
query,
page,
pageSize,
status,
activeOnly,
group,
startDate,
endDate,
);
}
@Post("list")
@ -122,6 +133,8 @@ export class InstitutionController extends Controller {
@Query() status?: Status,
@Query() activeOnly?: boolean,
@Query() group?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body()
body?: {
group?: string[];
@ -131,9 +144,10 @@ export class InstitutionController extends Controller {
...filterStatus(activeOnly ? Status.ACTIVE : status),
group: body?.group ? { in: body.group } : group,
OR: queryOrNot<Prisma.InstitutionWhereInput[]>(query, [
{ name: { contains: query } },
{ name: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.InstitutionWhereInput;
const [result, total] = await prisma.$transaction([
@ -178,6 +192,7 @@ export class InstitutionController extends Controller {
body: InstitutionPayload & {
status?: Status;
},
@Request() req: RequestWithUser,
) {
return await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
@ -194,6 +209,8 @@ export class InstitutionController extends Controller {
return await tx.institution.create({
include: {
bank: true,
createdBy: true,
updatedBy: true,
},
data: {
...body,
@ -204,6 +221,8 @@ export class InstitutionController extends Controller {
data: body.bank ?? [],
},
},
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
},
});
});

View file

@ -21,6 +21,7 @@ import {
createPermCondition,
} from "../services/permission";
import { PaymentStatus } from "../generated/kysely/types";
import { whereDateQuery } from "../utils/relation";
type InvoicePayload = {
quotationId: string;
@ -95,23 +96,25 @@ export class InvoiceController extends Controller {
@Query() quotationId?: string,
@Query() debitNoteId?: string,
@Query() pay?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where: Prisma.InvoiceWhereInput = {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ quotation: { workName: { contains: query } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -132,6 +135,7 @@ export class InvoiceController extends Controller {
OR: permissionCondCompany(req.user),
},
},
...whereDateQuery(startDate, endDate),
};
const [result, total] = await prisma.$transaction([

View file

@ -11,6 +11,7 @@ import {
Security,
Tags,
Query,
UploadedFile,
} from "tsoa";
import { Prisma, Product, Status } from "@prisma/client";
@ -27,7 +28,8 @@ import { isSystem } from "../utils/keycloak";
import { filterStatus } from "../services/prisma";
import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import spreadsheet from "../utils/spreadsheet";
const MANAGE_ROLES = [
"system",
@ -139,6 +141,8 @@ export class ProductController extends Controller {
@Query() orderField?: keyof Product,
@Query() orderBy?: "asc" | "desc",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
const targetGroup =
@ -154,8 +158,8 @@ export class ProductController extends Controller {
const where = {
OR: queryOrNot<Prisma.ProductWhereInput[]>(query, [
{ name: { contains: query } },
{ detail: { contains: query } },
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: {
@ -194,6 +198,7 @@ export class ProductController extends Controller {
: []),
],
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ProductWhereInput;
const [result, total] = await prisma.$transaction([
@ -444,6 +449,146 @@ export class ProductController extends Controller {
where: { id: productId },
});
}
@Post("import-product")
@Security("keycloak", MANAGE_ROLES)
async importProduct(
@Request() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
@Query() productGroupId: string,
) {
if (!file?.buffer) throw notFoundError("File");
const buffer = new Uint8Array(file.buffer).buffer;
const dataFile = await spreadsheet.readExcel(buffer, {
header: true,
worksheet: "Sheet1",
});
let dataName: string[] = [];
const data = dataFile.map((item: any) => {
dataName.push(item.name);
return {
...item,
expenseType:
item.expenseType === "ค่าธรรมเนียม"
? "fee"
: item.expenseType === "ค่าบริการ"
? "serviceFee"
: "processingFee",
shared: item.shared === "ใช่" ? true : false,
price:
typeof item.price === "number"
? item.price
: +parseFloat(item.price?.replace(",", "") || "0").toFixed(6),
calcVat: item.calcVat === "ใช่" ? true : false,
vatIncluded: item.vatIncluded === "รวม" ? true : false,
agentPrice:
typeof item.agentPrice === "number"
? item.agentPrice
: +parseFloat(item.agentPrice?.replace(",", "") || "0").toFixed(6),
agentPriceCalcVat: item.agentPriceCalcVat === "ใช่" ? true : false,
agentPriceVatIncluded: item.agentPriceVatIncluded === "รวม" ? true : false,
serviceCharge:
typeof item.serviceCharge === "number"
? item.serviceCharge
: +parseFloat(item.serviceCharge?.replace(",", "") || "0").toFixed(6),
serviceChargeCalcVat: item.serviceChargeCalcVat === "ใช่" ? true : false,
serviceChargeVatIncluded: item.serviceChargeVatIncluded === "รวม" ? true : false,
};
});
const [productGroup, productSameName] = await prisma.$transaction([
prisma.productGroup.findFirst({
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
}),
prisma.product.findMany({
where: {
productGroup: {
id: productGroupId,
registeredBranch: {
OR: permissionCondCompany(req.user),
},
},
name: { in: dataName },
},
}),
]);
if (!productGroup) throw relationError("Product Group");
await permissionCheck(req.user, productGroup.registeredBranch);
let dataProduct: ProductCreate[] = [];
const record = await prisma.$transaction(
async (tx) => {
const branch = productGroup.registeredBranch;
const company = (branch.headOffice || branch).code;
await Promise.all(
data.map(async (item) => {
const dataDuplicate = productSameName.some(
(v) => v.code.slice(0, -3) === item.code.toUpperCase() && v.name === item.name,
);
if (!dataDuplicate) {
const last = await tx.runningNo.upsert({
where: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
},
create: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
dataProduct.push({
...item,
code: `${item.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
productGroupId: productGroupId,
});
}
}),
);
return await prisma.product.createManyAndReturn({
data: dataProduct,
include: {
createdBy: true,
updatedBy: true,
},
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
},
);
if (productGroup.status === "CREATED") {
await prisma.productGroup.update({
include: {
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
return record;
}
}
@Route("api/v1/product/{productId}")

View file

@ -27,7 +27,7 @@ import {
} from "../services/permission";
import { filterStatus } from "../services/prisma";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
type ProductGroupCreate = {
name: string;
@ -90,11 +90,13 @@ export class ProductGroup extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.ProductGroupWhereInput[]>(query, [
{ name: { contains: query } },
{ detail: { contains: query } },
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: [
@ -105,6 +107,7 @@ export class ProductGroup extends Controller {
: { OR: permissionCond(req.user, { activeOnly }) },
},
],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ProductGroupWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -24,7 +24,7 @@ import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { notFoundError } from "../utils/error";
import { filterStatus } from "../services/prisma";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
type PropertyPayload = {
name: string;
@ -49,15 +49,21 @@ export class PropertiesController extends Controller {
@Query() status?: Status,
@Query() query = "",
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot(query, [{ name: { contains: query } }, { nameEN: { contains: query } }]),
OR: queryOrNot(query, [
{ name: { contains: query, mode: "insensitive" } },
{ nameEN: { contains: query, mode: "insensitive" } },
]),
AND: {
...filterStatus(activeOnly ? Status.ACTIVE : status),
registeredBranch: {
OR: permissionCondCompany(req.user, { activeOnly: true }),
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.PropertyWhereInput;
const [result, total] = await prisma.$transaction([
prisma.property.findMany({

View file

@ -4,6 +4,7 @@ import { Prisma } from "@prisma/client";
import { notFoundError } from "../utils/error";
import { RequestWithUser } from "../interfaces/user";
import { createPermCondition } from "../services/permission";
import { whereDateQuery } from "../utils/relation";
const permissionCondCompany = createPermCondition((_) => true);
@ -21,6 +22,8 @@ export class ReceiptController extends Controller {
@Query() quotationId?: string,
@Query() debitNoteId?: string,
@Query() debitNoteOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where: Prisma.PaymentWhereInput = {
paymentStatus: "PaymentSuccess",
@ -33,6 +36,7 @@ export class ReceiptController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
};
const [result, total] = await prisma.$transaction([

View file

@ -36,7 +36,7 @@ import {
listFile,
setFile,
} from "../utils/minio";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = [
"system",
@ -164,6 +164,8 @@ export class ServiceController extends Controller {
@Query() fullDetail?: boolean,
@Query() activeOnly?: boolean,
@Query() shared?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
// NOTE: will be used to scope product within product group that is shared between branch but not company when select shared product if user is system
const targetGroup =
@ -179,8 +181,8 @@ export class ServiceController extends Controller {
const where = {
OR: queryOrNot<Prisma.ServiceWhereInput[]>(query, [
{ name: { contains: query } },
{ detail: { contains: query } },
{ name: { contains: query, mode: "insensitive" } },
{ detail: { contains: query, mode: "insensitive" } },
{ code: { contains: query, mode: "insensitive" } },
]),
AND: {
@ -219,6 +221,7 @@ export class ServiceController extends Controller {
: []),
],
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.ServiceWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -18,6 +18,7 @@ import prisma from "../db";
import { RequestWithUser } from "../interfaces/user";
import HttpStatus from "../interfaces/http-status";
import { isUsedError, notFoundError } from "../utils/error";
import { whereDateQuery } from "../utils/relation";
type WorkCreate = {
order: number;
@ -45,9 +46,12 @@ export class WorkController extends Controller {
@Query() query: string = "",
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: [{ name: { contains: query }, serviceId: baseOnly ? null : undefined }],
...whereDateQuery(startDate, endDate),
} satisfies Prisma.WorkWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -105,6 +105,7 @@ export class QuotationPayment extends Controller {
async updatePayment(
@Path() paymentId: string,
@Body() body: { amount?: number; date?: Date; paymentStatus?: PaymentStatus },
@Request() req: RequestWithUser,
) {
const record = await prisma.payment.findUnique({
where: { id: paymentId },
@ -164,6 +165,7 @@ export class QuotationPayment extends Controller {
code: lastReceipt
? `RE${year}${month}${lastReceipt.value.toString().padStart(6, "0")}`
: undefined,
updatedByUserId: req.user.sub,
},
});

View file

@ -25,7 +25,7 @@ import {
import { isSystem } from "../utils/keycloak";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { precisionRound } from "../utils/arithmetic";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
@ -55,13 +55,14 @@ type QuotationCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string;
namePrefix?: string;
firstName: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName: string;
lastNameEN: string;
lastNameEN?: string;
}
)[];
@ -112,14 +113,15 @@ type QuotationUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string;
namePrefix?: string;
firstName: string;
firstName?: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName: string;
lastNameEN: string;
lastName?: string;
lastNameEN?: string;
}
)[];
@ -206,20 +208,22 @@ export class QuotationController extends Controller {
@Query() forDebitNote?: boolean,
@Query() code?: string,
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -253,6 +257,7 @@ export class QuotationController extends Controller {
},
}
: undefined,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -1005,6 +1010,7 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -1027,6 +1033,7 @@ export class QuotationActionController extends Controller {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality?: string;
namePrefix?: string;
firstName: string;
firstNameEN: string;

View file

@ -27,11 +27,12 @@ import {
createPermCheck,
createPermCondition,
} from "../services/permission";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { notFoundError } from "../utils/error";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
import { getGroupUser } from "../services/keycloak";
// User in company can edit.
const permissionCheck = createPermCheck((_) => true);
@ -80,6 +81,9 @@ export class RequestDataController extends Controller {
@Query() requestDataStatus?: RequestDataStatus,
@Query() quotationId?: string,
@Query() code?: string,
@Query() incomplete?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
@ -91,34 +95,40 @@ export class RequestDataController extends Controller {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
},
{
employee: {
OR: [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
]),
code,
requestDataStatus,
requestDataStatus: incomplete
? {
notIn: [RequestDataStatus.Completed, RequestDataStatus.Canceled],
}
: requestDataStatus,
requestWork: responsibleOnly
? {
some: {
@ -127,9 +137,24 @@ export class RequestDataController extends Controller {
workflow: {
step: {
some: {
responsiblePerson: {
some: { userId: req.user.sub },
},
OR: [
{
responsiblePerson: {
some: { userId: req.user.sub },
},
},
{
responsibleGroup: {
some: {
group: {
in: await getGroupUser(req.user.sub).then((r) =>
r.map(({ name }: { name: string }) => name),
),
},
},
},
},
],
},
},
},
@ -142,6 +167,7 @@ export class RequestDataController extends Controller {
id: quotationId,
registeredBranch: { OR: permissionCond(req.user) },
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.RequestDataWhereInput;
const [result, total] = await prisma.$transaction([
@ -164,6 +190,7 @@ export class RequestDataController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -182,6 +209,20 @@ export class RequestDataController extends Controller {
employeePassport: {
orderBy: { expireDate: "desc" },
},
customerBranch: {
include: {
province: {
include: {
employmentOffice: true,
},
},
district: {
include: {
employmentOffice: true,
},
},
},
},
},
},
},
@ -192,7 +233,24 @@ export class RequestDataController extends Controller {
prisma.requestData.count({ where }),
]);
return { result, page, pageSize, total };
const dataRequestData = result.map((item) => {
const employee = item.employee;
const dataOffice =
employee.customerBranch.district?.employmentOffice.at(0) ??
employee.customerBranch.province?.employmentOffice.at(0);
return {
...item,
dataOffice,
};
});
return {
result: dataRequestData,
page,
pageSize,
total,
};
}
@Get("{requestDataId}")
@ -231,7 +289,7 @@ export class RequestDataController extends Controller {
return record;
}
@Post("updata-messenger")
@Post("update-messenger")
@Security("keycloak")
async updateRequestData(
@Request() req: RequestWithUser,
@ -748,6 +806,7 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},
@ -808,6 +867,7 @@ export class RequestListController extends Controller {
include: { user: true },
},
responsibleInstitution: true,
responsibleGroup: true,
},
},
},

View file

@ -42,7 +42,7 @@ import {
listFile,
setFile,
} from "../utils/minio";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
const MANAGE_ROLES = ["system", "head_of_admin", "admin", "document_checker"];
@ -86,6 +86,8 @@ export class TaskController extends Controller {
@Query() pageSize = 30,
@Query() assignedByUserId?: string,
@Query() taskOrderStatus?: TaskOrderStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return this.getTaskOrderListByCriteria(
req,
@ -94,6 +96,8 @@ export class TaskController extends Controller {
pageSize,
assignedByUserId,
taskOrderStatus,
startDate,
endDate,
);
}
@ -106,6 +110,8 @@ export class TaskController extends Controller {
@Query() pageSize = 30,
@Query() assignedUserId?: string,
@Query() taskOrderStatus?: TaskOrderStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: { code?: string[] },
) {
const where = {
@ -121,10 +127,11 @@ export class TaskController extends Controller {
code: body?.code ? { in: body.code } : undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
{ taskName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ contactTel: { contains: query, mode: "insensitive" } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([
@ -193,6 +200,7 @@ export class TaskController extends Controller {
step: {
include: {
value: true,
responsibleGroup: true,
responsiblePerson: {
include: { user: true },
},
@ -690,12 +698,31 @@ export class TaskActionController extends Controller {
if (!record) throw notFoundError("Task Order");
await prisma.$transaction(async (tx) => {
const last = await tx.runningNo.upsert({
where: {
key: "TASK_RI",
},
create: {
key: "TASK_RI",
value: 1,
},
update: {
value: { increment: 1 },
},
});
const current = new Date();
const year = `${current.getFullYear()}`.padStart(2, "0");
const month = `${current.getMonth() + 1}`.padStart(2, "0");
const code = `RI${year}${month}${last.value.toString().padStart(6, "0")}`;
await Promise.all([
tx.taskOrder.update({
where: { id: taskOrderId },
data: {
urgent: false,
taskOrderStatus: TaskOrderStatus.Complete,
codeProductReceived: code,
userTask: {
updateMany: {
where: { taskOrderId },
@ -979,6 +1006,8 @@ export class UserTaskController extends Controller {
@Query() page = 1,
@Query() pageSize = 30,
@Query() userTaskStatus?: UserTaskStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
taskList: {
@ -1021,10 +1050,11 @@ export class UserTaskController extends Controller {
: undefined,
OR: queryOrNot(query, [
{ code: { contains: query, mode: "insensitive" } },
{ taskName: { contains: query } },
{ contactName: { contains: query } },
{ contactTel: { contains: query } },
{ taskName: { contains: query, mode: "insensitive" } },
{ contactName: { contains: query, mode: "insensitive" } },
{ contactTel: { contains: query, mode: "insensitive" } },
]),
...whereDateQuery(startDate, endDate),
} satisfies Prisma.TaskOrderWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -35,7 +35,7 @@ import {
} from "../utils/minio";
import { notFoundError } from "../utils/error";
import { CreditNotePaybackType, CreditNoteStatus, Prisma, RequestDataStatus } from "@prisma/client";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
const MANAGE_ROLES = [
@ -121,6 +121,8 @@ export class CreditNoteController extends Controller {
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await this.getCreditNoteListByCriteria(
req,
@ -129,6 +131,8 @@ export class CreditNoteController extends Controller {
query,
quotationId,
creditNoteStatus,
startDate,
endDate,
);
}
@ -142,6 +146,8 @@ export class CreditNoteController extends Controller {
@Query() query: string = "",
@Query() quotationId?: string,
@Query() creditNoteStatus?: CreditNoteStatus,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: {},
) {
const where = {
@ -153,17 +159,17 @@ export class CreditNoteController extends Controller {
request: {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
{ quotation: { code: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -171,14 +177,14 @@ export class CreditNoteController extends Controller {
OR: [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -199,6 +205,7 @@ export class CreditNoteController extends Controller {
},
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.CreditNoteWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -36,7 +36,7 @@ import {
setFile,
} from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot } from "../utils/relation";
import { queryOrNot, whereDateQuery } from "../utils/relation";
import { isSystem } from "../utils/keycloak";
import { precisionRound } from "../utils/arithmetic";
@ -76,6 +76,7 @@ type DebitNoteCreate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality: string;
namePrefix?: string;
firstName: string;
firstNameEN: string;
@ -111,13 +112,14 @@ type DebitNoteUpdate = {
dateOfBirth: Date;
gender: string;
nationality: string;
otherNationality: string;
namePrefix?: string;
firstName: string;
firstName?: string;
firstNameEN: string;
middleName?: string;
middleNameEN?: string;
lastName: string;
lastNameEN: string;
lastName?: string;
lastNameEN?: string;
}
)[];
@ -168,6 +170,8 @@ export class DebitNoteController extends Controller {
@Query() payCondition?: PayCondition,
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
return await this.getDebitNoteListByCriteria(
req,
@ -179,6 +183,8 @@ export class DebitNoteController extends Controller {
payCondition,
includeRegisteredBranch,
code,
startDate,
endDate,
);
}
@ -195,21 +201,23 @@ export class DebitNoteController extends Controller {
@Query() payCondition?: PayCondition,
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
@Body() body?: {},
) {
const where = {
OR: queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -220,6 +228,7 @@ export class DebitNoteController extends Controller {
debitNoteQuotationId: quotationId,
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
quotationStatus: status,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([

View file

@ -25,7 +25,7 @@ import {
TaskStatus,
RequestWorkStatus,
} from "@prisma/client";
import { queryOrNot, whereAddressQuery } from "../utils/relation";
import { queryOrNot, whereAddressQuery, whereDateQuery } from "../utils/relation";
import { filterStatus } from "../services/prisma";
// import { RequestWorkStatus } from "../generated/kysely/types";
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
@ -51,6 +51,8 @@ export class LineController extends Controller {
@Query() page: number = 1,
@Query() pageSize: number = 30,
@Query() activeOnly?: boolean,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: !!query
@ -58,13 +60,13 @@ export class LineController extends Controller {
...(queryOrNot<Prisma.EmployeeWhereInput[]>(query, [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
...whereAddressQuery(query),
]) ?? []),
]
@ -87,6 +89,7 @@ export class LineController extends Controller {
subDistrict: zipCode ? { zipCode } : undefined,
gender,
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.EmployeeWhereInput;
const [result, total] = await prisma.$transaction([
@ -173,24 +176,26 @@ export class LineController extends Controller {
@Query() requestDataStatus?: RequestDataStatus,
@Query() quotationId?: string,
@Query() code?: string,
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR: queryOrNot<Prisma.RequestDataWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ quotation: { code: { contains: query, mode: "insensitive" } } },
{ quotation: { workName: { contains: query } } },
{ quotation: { workName: { contains: query, mode: "insensitive" } } },
{
quotation: {
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ registerName: { contains: query } },
{ registerNameEN: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ registerName: { contains: query, mode: "insensitive" } },
{ registerNameEN: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -198,14 +203,14 @@ export class LineController extends Controller {
OR: [
{
employeePassport: {
some: { number: { contains: query } },
some: { number: { contains: query, mode: "insensitive" } },
},
},
{ code: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -247,6 +252,7 @@ export class LineController extends Controller {
],
},
},
...whereDateQuery(startDate, endDate),
} satisfies Prisma.RequestDataWhereInput;
const [result, total] = await prisma.$transaction([
@ -604,6 +610,8 @@ export class LineController extends Controller {
@Query() includeRegisteredBranch?: boolean,
@Query() code?: string,
@Query() query = "",
@Query() startDate?: Date,
@Query() endDate?: Date,
) {
const where = {
OR:
@ -611,16 +619,16 @@ export class LineController extends Controller {
? [
...(queryOrNot<Prisma.QuotationWhereInput[]>(query, [
{ code: { contains: query, mode: "insensitive" } },
{ workName: { contains: query } },
{ workName: { contains: query, mode: "insensitive" } },
{
customerBranch: {
OR: [
{ code: { contains: query, mode: "insensitive" } },
{ customerName: { contains: query } },
{ firstName: { contains: query } },
{ firstNameEN: { contains: query } },
{ lastName: { contains: query } },
{ lastNameEN: { contains: query } },
{ customerName: { contains: query, mode: "insensitive" } },
{ firstName: { contains: query, mode: "insensitive" } },
{ firstNameEN: { contains: query, mode: "insensitive" } },
{ lastName: { contains: query, mode: "insensitive" } },
{ lastNameEN: { contains: query, mode: "insensitive" } },
],
},
},
@ -660,6 +668,7 @@ export class LineController extends Controller {
},
}
: undefined,
...whereDateQuery(startDate, endDate),
} satisfies Prisma.QuotationWhereInput;
const [result, total] = await prisma.$transaction([
@ -1368,3 +1377,65 @@ export class LineQuotationFileController extends Controller {
return await deleteFile(fileLocation.quotation.attachment(quotationId, name));
}
}
@Route("api/v1/line/payment/{paymentId}/attachment")
@Tags("Line")
export class PaymentFileLineController extends Controller {
private async checkPermission(_user: RequestWithUser["user"], id: string) {
const data = await prisma.payment.findUnique({
include: {
invoice: {
include: {
quotation: true,
},
},
},
where: { id },
});
if (!data) throw notFoundError("Payment");
return { paymentId: id, quotationId: data.invoice.quotationId };
}
@Get()
@Security("line")
async listAttachment(@Request() req: RequestWithUser, @Path() paymentId: string) {
const { quotationId } = await this.checkPermission(req.user, paymentId);
return await listFile(fileLocation.quotation.payment(quotationId, paymentId));
}
@Head("{name}")
async headAttachment(
@Request() req: RequestWithUser,
@Path() paymentId: string,
@Path() name: string,
) {
const data = await prisma.payment.findUnique({
where: { id: paymentId },
include: { invoice: true },
});
if (!data) throw notFoundError("Payment");
return req.res?.redirect(
await getPresigned(
"head",
fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name),
),
);
}
@Get("{name}")
async getAttachment(
@Request() req: RequestWithUser,
@Path() paymentId: string,
@Path() name: string,
) {
const data = await prisma.payment.findUnique({
where: { id: paymentId },
include: { invoice: true },
});
if (!data) throw notFoundError("Payment");
return req.res?.redirect(
await getFile(fileLocation.quotation.payment(data.invoice.quotationId, paymentId, name)),
);
}
}

View file

@ -0,0 +1,25 @@
import express from "express";
import { Controller, Get, Path, Request, Route } from "tsoa";
import { getFile } from "../utils/minio";
@Route("api/v1/troubleshooting")
export class TroubleshootingController extends Controller {
@Get()
async get(@Request() req: express.Request) {
return req.res?.redirect(await getFile(".troubleshooting/toc.json"));
}
@Get("{category}/assets/{name}")
async getAsset(@Request() req: express.Request, @Path() category: string, @Path() name: string) {
return req.res?.redirect(await getFile(`.troubleshooting/${category}/assets/${name}`));
}
@Get("{category}/page/{page}")
async getContent(
@Request() req: express.Request,
@Path() category: string,
@Path() page: string,
) {
return req.res?.redirect(await getFile(`.troubleshooting/${category}/${page}.md`));
}
}

View file

@ -346,6 +346,64 @@ export async function removeUserRoles(userId: string, roles: { id: string; name:
return true;
}
export async function getGroup(query: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/groups?${query}`, {
headers: {
authorization: `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "GET",
});
const dataMainGroup = await res.json();
const fetchSubGroups = async (group: any) => {
let fullSubGroup = await Promise.all(
group.subGroups.map((subGroupsData: any) => {
if (group.subGroupCount > 0) {
return fetchSubGroups(subGroupsData);
} else {
return {
id: subGroupsData.id,
name: subGroupsData.name,
path: subGroupsData.path,
subGroupCount: subGroupsData.subGroupCount,
subGroups: [],
};
}
}),
);
return {
id: group.id,
name: group.name,
path: group.path,
subGroupCount: group.subGroupCount,
subGroups: fullSubGroup,
};
};
const fullMainGroup = await Promise.all(dataMainGroup.map(fetchSubGroups));
return fullMainGroup;
}
export async function getGroupUser(userId: string) {
const res = await fetch(`${KC_URL}/admin/realms/${KC_REALM}/users/${userId}/groups`, {
headers: {
authorization: `Bearer ${await getToken()}`,
"content-type": `application/json`,
},
method: "GET",
});
const data = await res.json();
return data.map((item: any) => {
return {
id: item.id,
name: item.name,
path: item.path,
};
});
}
export default {
createUser,
listRole,

View file

@ -10,26 +10,35 @@ export function connectOrDisconnect(id?: string | null) {
export function whereAddressQuery(query: string) {
return [
{ address: { contains: query } },
{ addressEN: { contains: query } },
{ soi: { contains: query } },
{ soiEN: { contains: query } },
{ moo: { contains: query } },
{ mooEN: { contains: query } },
{ street: { contains: query } },
{ streetEN: { contains: query } },
{ province: { name: { contains: query } } },
{ province: { nameEN: { contains: query } } },
{ district: { name: { contains: query } } },
{ district: { nameEN: { contains: query } } },
{ subDistrict: { name: { contains: query } } },
{ subDistrict: { nameEN: { contains: query } } },
{ subDistrict: { zipCode: { contains: query } } },
];
{ address: { contains: query, mode: "insensitive" } },
{ addressEN: { contains: query, mode: "insensitive" } },
{ soi: { contains: query, mode: "insensitive" } },
{ soiEN: { contains: query, mode: "insensitive" } },
{ moo: { contains: query, mode: "insensitive" } },
{ mooEN: { contains: query, mode: "insensitive" } },
{ street: { contains: query, mode: "insensitive" } },
{ streetEN: { contains: query, mode: "insensitive" } },
{ province: { name: { contains: query, mode: "insensitive" } } },
{ province: { nameEN: { contains: query, mode: "insensitive" } } },
{ district: { name: { contains: query, mode: "insensitive" } } },
{ district: { nameEN: { contains: query, mode: "insensitive" } } },
{ subDistrict: { name: { contains: query, mode: "insensitive" } } },
{ subDistrict: { nameEN: { contains: query, mode: "insensitive" } } },
{ subDistrict: { zipCode: { contains: query, mode: "insensitive" } } },
] as const;
}
export function queryOrNot<T>(query: string | boolean, where: T): T | undefined;
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback: U): T | U;
export function queryOrNot<T, U>(query: string | boolean, where: T, fallback?: U) {
export function queryOrNot<T>(query: any, where: T): T | undefined;
export function queryOrNot<T, U>(query: any, where: T, fallback: U): T | U;
export function queryOrNot<T, U>(query: any, where: T, fallback?: U) {
return !!query ? where : fallback;
}
export function whereDateQuery(startDate: Date | undefined, endDate: Date | undefined) {
return {
createdAt: {
gte: startDate,
lte: endDate,
},
};
}

105
src/utils/spreadsheet.ts Normal file
View file

@ -0,0 +1,105 @@
import Excel from "exceljs";
export default class spreadsheet {
static async readCsv() {
// TODO: read csv
}
/**
* This function read data from excel file.
*
* @param buffer - Excel file.
* @param opts.header - Interprets the first row as the names of the fields.
* @param opts.worksheet - Specifies the worksheet to read. Can be the worksheet's name or its 1-based index.
*
* @returns
*/
static async readExcel<T extends unknown>(
buffer: Excel.Buffer,
opts?: { header?: boolean; worksheet?: number | string },
): Promise<T[]> {
const workbook = new Excel.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1);
if (!worksheet) return [];
const header: Record<number, string | number> = {};
const values: any[] = [];
worksheet.eachRow((row, rowId) => {
if (rowId === 1 && opts?.header !== false) {
row.eachCell((cell, cellId) => {
if (typeof cell.value === "string") {
header[cellId] = nameValue(cell.value);
} else {
header[cellId] = cellId.toString();
}
});
} else {
const data: Record<string | number, Excel.CellValue> = {};
row.eachCell((cell, cellId) => {
data[opts?.header !== false ? header[cellId] : cellId - 1] = cell.value;
});
values.push(opts?.header !== false ? data : Object.values(data));
}
});
return values;
}
}
function nameValue(value: string) {
let code: string;
switch (value) {
case "ชื่อสินค้าและบริการ":
code = "name";
break;
case "ระยะเวลาดำเนินการ":
code = "process";
break;
case "ประเภทค่าใช้จ่าย":
code = "expenseType";
break;
case "รายละเอียด":
code = "detail";
break;
case "หมายเหตุ":
code = "remark";
break;
case "ใช้งานร่วมกัน":
code = "shared";
break;
case "คำนวณภาษีราคาขาย":
code = "calcVat";
break;
case "รวม VAT ราคาขาย":
code = "vatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาขาย":
code = "price";
break;
case "คำนวณภาษีราคาตัวแทน":
code = "agentPriceCalcVat";
break;
case "รวม VAT ราคาตัวแทน":
code = "agentPriceVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาตัวแทน":
code = "agentPrice";
break;
case "คำนวณภาษีราคาดำเนินการ":
code = "serviceChargeCalcVat";
break;
case "รวม VAT ราคาดำเนินการ":
code = "serviceChargeVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาดำเนินการ":
code = "serviceCharge";
break;
default:
code = "code";
break;
}
return code;
}