diff --git a/package.json b/package.json index b576e46..6294f71 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb7ddd5..9ad8f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/prisma/migrations/20250410102415_add/migration.sql b/prisma/migrations/20250410102415_add/migration.sql new file mode 100644 index 0000000..eaf0561 --- /dev/null +++ b/prisma/migrations/20250410102415_add/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "contactName" TEXT, +ADD COLUMN "contactTel" TEXT; diff --git a/prisma/migrations/20250410104307_change/migration.sql b/prisma/migrations/20250410104307_change/migration.sql new file mode 100644 index 0000000..3239108 --- /dev/null +++ b/prisma/migrations/20250410104307_change/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "firstName" DROP NOT NULL, +ALTER COLUMN "lastName" DROP NOT NULL; diff --git a/prisma/migrations/20250418095201_add/migration.sql b/prisma/migrations/20250418095201_add/migration.sql new file mode 100644 index 0000000..be3e4d0 --- /dev/null +++ b/prisma/migrations/20250418095201_add/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "TaskOrder" ADD COLUMN "codeProductReceived" TEXT; diff --git a/prisma/migrations/20250418103300_add/migration.sql b/prisma/migrations/20250418103300_add/migration.sql new file mode 100644 index 0000000..2e63034 --- /dev/null +++ b/prisma/migrations/20250418103300_add/migration.sql @@ -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; diff --git a/prisma/migrations/20250422024534_remove_lastname_requirement_employee/migration.sql b/prisma/migrations/20250422024534_remove_lastname_requirement_employee/migration.sql new file mode 100644 index 0000000..bd03e24 --- /dev/null +++ b/prisma/migrations/20250422024534_remove_lastname_requirement_employee/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Employee" ALTER COLUMN "lastNameEN" DROP NOT NULL; diff --git a/prisma/migrations/20250424042834_add/migration.sql b/prisma/migrations/20250424042834_add/migration.sql new file mode 100644 index 0000000..13999bc --- /dev/null +++ b/prisma/migrations/20250424042834_add/migration.sql @@ -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; diff --git a/prisma/migrations/20250424094947_change_import_nationality_to_relation/migration.sql b/prisma/migrations/20250424094947_change_import_nationality_to_relation/migration.sql new file mode 100644 index 0000000..c961d96 --- /dev/null +++ b/prisma/migrations/20250424094947_change_import_nationality_to_relation/migration.sql @@ -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; diff --git a/prisma/migrations/20250425040315_add/migration.sql b/prisma/migrations/20250425040315_add/migration.sql new file mode 100644 index 0000000..9393a9a --- /dev/null +++ b/prisma/migrations/20250425040315_add/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Employee" ADD COLUMN "otherNationality" TEXT; diff --git a/prisma/migrations/20250425041426_add/migration.sql b/prisma/migrations/20250425041426_add/migration.sql new file mode 100644 index 0000000..a004873 --- /dev/null +++ b/prisma/migrations/20250425041426_add/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EmployeePassport" ADD COLUMN "otherNationality" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cb38c88..73be520 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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) diff --git a/src/controllers/00-employment-office-controller.ts b/src/controllers/00-employment-office-controller.ts index e7efc24..9c6995b 100644 --- a/src/controllers/00-employment-office-controller.ts +++ b/src/controllers/00-employment-office-controller.ts @@ -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, diff --git a/src/controllers/00-keycloak-controller.ts b/src/controllers/00-keycloak-controller.ts index 87ca8d0..1d5d9b9 100644 --- a/src/controllers/00-keycloak-controller.ts +++ b/src/controllers/00-keycloak-controller.ts @@ -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; + } } diff --git a/src/controllers/00-notification-controller.ts b/src/controllers/00-notification-controller.ts index 2bc4bf0..da40b93 100644 --- a/src/controllers/00-notification-controller.ts +++ b/src/controllers/00-notification-controller.ts @@ -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" } }, ]), }, { diff --git a/src/controllers/01-branch-controller.ts b/src/controllers/01-branch-controller.ts index acb204f..da4c958 100644 --- a/src/controllers/01-branch-controller.ts +++ b/src/controllers/01-branch-controller.ts @@ -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(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, diff --git a/src/controllers/01-branch-user-controller.ts b/src/controllers/01-branch-user-controller.ts index 2b0ec23..05177d2 100644 --- a/src/controllers/01-branch-user-controller.ts +++ b/src/controllers/01-branch-user-controller.ts @@ -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(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([ diff --git a/src/controllers/02-user-controller.ts b/src/controllers/02-user-controller.ts index f1dca32..2c864e6 100644 --- a/src/controllers/02-user-controller.ts +++ b/src/controllers/02-user-controller.ts @@ -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(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; + } +} diff --git a/src/controllers/03-customer-branch-controller.ts b/src/controllers/03-customer-branch-controller.ts index 772c4df..5c94a2f 100644 --- a/src/controllers/03-customer-branch-controller.ts +++ b/src/controllers/03-customer-branch-controller.ts @@ -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(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(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([ diff --git a/src/controllers/03-customer-controller.ts b/src/controllers/03-customer-controller.ts index bef51e3..122726f 100644 --- a/src/controllers/03-customer-controller.ts +++ b/src/controllers/03-customer-controller.ts @@ -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(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([ diff --git a/src/controllers/03-employee-controller.ts b/src/controllers/03-employee-controller.ts index eb08db7..03ab1cc 100644 --- a/src/controllers/03-employee-controller.ts +++ b/src/controllers/03-employee-controller.ts @@ -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(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(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(!!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([ diff --git a/src/controllers/03-employee-passport-controller.ts b/src/controllers/03-employee-passport-controller.ts index 8f8c253..5509373 100644 --- a/src/controllers/03-employee-passport-controller.ts +++ b/src/controllers/03-employee-passport-controller.ts @@ -43,6 +43,7 @@ type EmployeePassportPayload = { workerStatus: string; nationality: string; + otherNationality: string; namePrefix?: string | null; firstName: string; firstNameEN: string; diff --git a/src/controllers/04-flow-template-controller.ts b/src/controllers/04-flow-template-controller.ts index 54510d5..940fd1d 100644 --- a/src/controllers/04-flow-template-controller.ts +++ b/src/controllers/04-flow-template-controller.ts @@ -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 })), + }, }, })), }, diff --git a/src/controllers/04-institution-controller.ts b/src/controllers/04-institution-controller.ts index 5e21445..21611a7 100644 --- a/src/controllers/04-institution-controller.ts +++ b/src/controllers/04-institution-controller.ts @@ -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(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, }, }); }); diff --git a/src/controllers/04-invoice-controller.ts b/src/controllers/04-invoice-controller.ts index 20dd91a..fb3fdf0 100644 --- a/src/controllers/04-invoice-controller.ts +++ b/src/controllers/04-invoice-controller.ts @@ -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([ diff --git a/src/controllers/04-product-controller.ts b/src/controllers/04-product-controller.ts index 3646479..3f2ff4e 100644 --- a/src/controllers/04-product-controller.ts +++ b/src/controllers/04-product-controller.ts @@ -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(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}") diff --git a/src/controllers/04-product-group-controller.ts b/src/controllers/04-product-group-controller.ts index 2fdcb50..ecd4ac6 100644 --- a/src/controllers/04-product-group-controller.ts +++ b/src/controllers/04-product-group-controller.ts @@ -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(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([ diff --git a/src/controllers/04-properties-controller.ts b/src/controllers/04-properties-controller.ts index 46cea13..6203ec6 100644 --- a/src/controllers/04-properties-controller.ts +++ b/src/controllers/04-properties-controller.ts @@ -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({ diff --git a/src/controllers/04-receipt-controller.ts b/src/controllers/04-receipt-controller.ts index caad04b..55a52c6 100644 --- a/src/controllers/04-receipt-controller.ts +++ b/src/controllers/04-receipt-controller.ts @@ -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([ diff --git a/src/controllers/04-service-controller.ts b/src/controllers/04-service-controller.ts index a60c555..ed46c18 100644 --- a/src/controllers/04-service-controller.ts +++ b/src/controllers/04-service-controller.ts @@ -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(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([ diff --git a/src/controllers/04-work-controller.ts b/src/controllers/04-work-controller.ts index 9b875b6..c8a140b 100644 --- a/src/controllers/04-work-controller.ts +++ b/src/controllers/04-work-controller.ts @@ -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([ diff --git a/src/controllers/05-payment-controller.ts b/src/controllers/05-payment-controller.ts index a4b7339..e5bf0a4 100644 --- a/src/controllers/05-payment-controller.ts +++ b/src/controllers/05-payment-controller.ts @@ -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, }, }); diff --git a/src/controllers/05-quotation-controller.ts b/src/controllers/05-quotation-controller.ts index 0369627..9fddd04 100644 --- a/src/controllers/05-quotation-controller.ts +++ b/src/controllers/05-quotation-controller.ts @@ -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(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; diff --git a/src/controllers/06-request-list-controller.ts b/src/controllers/06-request-list-controller.ts index 4484ce3..ca5398f 100644 --- a/src/controllers/06-request-list-controller.ts +++ b/src/controllers/06-request-list-controller.ts @@ -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(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, }, }, }, diff --git a/src/controllers/07-task-controller.ts b/src/controllers/07-task-controller.ts index 2da73b2..5e9977c 100644 --- a/src/controllers/07-task-controller.ts +++ b/src/controllers/07-task-controller.ts @@ -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([ diff --git a/src/controllers/08-credit-note-controller.ts b/src/controllers/08-credit-note-controller.ts index e5d2759..4a6f622 100644 --- a/src/controllers/08-credit-note-controller.ts +++ b/src/controllers/08-credit-note-controller.ts @@ -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(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([ diff --git a/src/controllers/09-debit-note-controller.ts b/src/controllers/09-debit-note-controller.ts index bca4057..118575b 100644 --- a/src/controllers/09-debit-note-controller.ts +++ b/src/controllers/09-debit-note-controller.ts @@ -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(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([ diff --git a/src/controllers/09-line-controller.ts b/src/controllers/09-line-controller.ts index cf8d7f3..91c06bb 100644 --- a/src/controllers/09-line-controller.ts +++ b/src/controllers/09-line-controller.ts @@ -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(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(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(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)), + ); + } +} diff --git a/src/controllers/10-troubleshooting-controller.ts b/src/controllers/10-troubleshooting-controller.ts new file mode 100644 index 0000000..2c5418a --- /dev/null +++ b/src/controllers/10-troubleshooting-controller.ts @@ -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`)); + } +} diff --git a/src/services/keycloak.ts b/src/services/keycloak.ts index db2d15d..494e19e 100644 --- a/src/services/keycloak.ts +++ b/src/services/keycloak.ts @@ -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, diff --git a/src/utils/relation.ts b/src/utils/relation.ts index 3e96f11..aebb744 100644 --- a/src/utils/relation.ts +++ b/src/utils/relation.ts @@ -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(query: string | boolean, where: T): T | undefined; -export function queryOrNot(query: string | boolean, where: T, fallback: U): T | U; -export function queryOrNot(query: string | boolean, where: T, fallback?: U) { +export function queryOrNot(query: any, where: T): T | undefined; +export function queryOrNot(query: any, where: T, fallback: U): T | U; +export function queryOrNot(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, + }, + }; +} diff --git a/src/utils/spreadsheet.ts b/src/utils/spreadsheet.ts new file mode 100644 index 0000000..b04a4f2 --- /dev/null +++ b/src/utils/spreadsheet.ts @@ -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( + buffer: Excel.Buffer, + opts?: { header?: boolean; worksheet?: number | string }, + ): Promise { + const workbook = new Excel.Workbook(); + await workbook.xlsx.load(buffer); + const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1); + + if (!worksheet) return []; + + const header: Record = {}; + 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 = {}; + 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; +}