diff --git a/package-lock.json b/package-lock.json index 4ee606c..1180132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.17.1", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -54,7 +54,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.0", + "prisma": "^6.17.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", @@ -1751,6 +1751,15 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", + "integrity": "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@inquirer/checkbox": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.9.tgz", @@ -1797,14 +1806,14 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.14", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", - "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.0.tgz", + "integrity": "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA==", "dev": true, "dependencies": { - "@inquirer/figures": "^1.0.12", - "@inquirer/type": "^3.0.7", - "ansi-escapes": "^4.3.2", + "@inquirer/ansi": "^1.0.1", + "@inquirer/figures": "^1.0.14", + "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", @@ -1824,14 +1833,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.14.tgz", - "integrity": "sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.21.tgz", + "integrity": "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ==", "dev": true, "dependencies": { - "@inquirer/core": "^10.1.14", - "@inquirer/type": "^3.0.7", - "external-editor": "^3.1.0" + "@inquirer/core": "^10.3.0", + "@inquirer/external-editor": "^1.0.2", + "@inquirer/type": "^3.0.9" }, "engines": { "node": ">=18" @@ -1867,10 +1876,47 @@ } } }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz", + "integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==", + "dev": true, + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.14.tgz", + "integrity": "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ==", "dev": true, "engines": { "node": ">=18" @@ -2039,9 +2085,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", - "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.9.tgz", + "integrity": "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w==", "dev": true, "engines": { "node": ">=18" @@ -3222,9 +3268,9 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.3.tgz", - "integrity": "sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "dependencies": { "file-type": "21.0.0", "iterare": "1.2.1", @@ -3266,9 +3312,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", - "integrity": "sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", + "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==", "hasInstallScript": true, "dependencies": { "@nuxt/opencollective": "0.4.1", @@ -3306,11 +3352,11 @@ } }, "node_modules/@nestjs/jwt": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", - "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.1.tgz", + "integrity": "sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==", "dependencies": { - "@types/jsonwebtoken": "9.0.7", + "@types/jsonwebtoken": "9.0.10", "jsonwebtoken": "9.0.2" }, "peerDependencies": { @@ -3366,11 +3412,11 @@ } }, "node_modules/@nestjs/schedule": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.0.tgz", - "integrity": "sha512-aQySMw6tw2nhitELXd3EiRacQRgzUKD9mFcUZVOJ7jPLqIBvXOyvRWLsK9SdurGA+jjziAlMef7iB5ZEFFoQpw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.0.1.tgz", + "integrity": "sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q==", "dependencies": { - "cron": "4.3.0" + "cron": "4.3.3" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -3470,16 +3516,16 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", - "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.1.tgz", + "integrity": "sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==", "dependencies": { "@microsoft/tsdoc": "0.15.1", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "8.2.0", - "swagger-ui-dist": "5.21.0" + "path-to-regexp": "8.3.0", + "swagger-ui-dist": "5.29.4" }, "peerDependencies": { "@fastify/static": "^8.0.0", @@ -3501,10 +3547,19 @@ } } }, + "node_modules/@nestjs/swagger/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@nestjs/testing": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.3.tgz", - "integrity": "sha512-CeXG6/eEqgFIkPkmU00y18Dd3DLOIDFhPItzJK1SWckKo6IhcnfoRJzGx75bmuvUMjb51j6An96S/+MJ2ty9jA==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz", + "integrity": "sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==", "dev": true, "dependencies": { "tslib": "2.8.1" @@ -3612,9 +3667,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", - "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.17.1.tgz", + "integrity": "sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==", "hasInstallScript": true, "engines": { "node": ">=18.18" @@ -3633,9 +3688,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.0.tgz", - "integrity": "sha512-k8tuChKpkO/Vj7ZEzaQMNflNGbaW4X0r8+PC+W2JaqVRdiS2+ORSv1SrDwNxsb8YyzIQJucXqLGZbgxD97ZhsQ==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.17.1.tgz", + "integrity": "sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==", "devOptional": true, "dependencies": { "c12": "3.1.0", @@ -3645,48 +3700,48 @@ } }, "node_modules/@prisma/debug": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.0.tgz", - "integrity": "sha512-eE2CB32nr1hRqyLVnOAVY6c//iSJ/PN+Yfoa/2sEzLGpORaCg61d+nvdAkYSh+6Y2B8L4BVyzkRMANLD6nnC2g==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.17.1.tgz", + "integrity": "sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.0.tgz", - "integrity": "sha512-XhE9v3hDQTNgCYMjogcCYKi7HCEkZf9WwTGuXy8cmY8JUijvU0ap4M7pGLx4pBblkp5EwUsYzw1YLtH7yi0GZw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.17.1.tgz", + "integrity": "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "6.17.0", - "@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "@prisma/fetch-engine": "6.17.0", - "@prisma/get-platform": "6.17.0" + "@prisma/debug": "6.17.1", + "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", + "@prisma/fetch-engine": "6.17.1", + "@prisma/get-platform": "6.17.1" } }, "node_modules/@prisma/engines-version": { - "version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a.tgz", - "integrity": "sha512-G0VU4uFDreATgTz4sh3dTtU2C+jn+J6c060ixavWZaUaSRZsNQhSPW26lbfez7GHzR02RGCdqs5UcSuGBC3yLw==", + "version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac.tgz", + "integrity": "sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.0.tgz", - "integrity": "sha512-YSl5R3WIAPrmshYPkaaszOsBIWRAovOCHn3y7gkTNGG51LjYW4pi6PFNkGouW6CA06qeTjTbGrDRCgFjnmVWDg==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.17.1.tgz", + "integrity": "sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.0", - "@prisma/engines-version": "6.17.0-16.c0aafc03b8ef6cdced8654b9a817999e02457d6a", - "@prisma/get-platform": "6.17.0" + "@prisma/debug": "6.17.1", + "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", + "@prisma/get-platform": "6.17.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.0.tgz", - "integrity": "sha512-3tEKChrnlmLXPd870oiVfRvj7vVKuxqP349hYaMDsbV4TZd3+lFqw8KTI2Tbq5DopamfNuNqhVCj+R6ZxKKYGQ==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.17.1.tgz", + "integrity": "sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==", "devOptional": true, "dependencies": { - "@prisma/debug": "6.17.0" + "@prisma/debug": "6.17.1" } }, "node_modules/@scarf/scarf": { @@ -4268,17 +4323,18 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", - "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "dependencies": { + "@types/ms": "*", "@types/node": "*" } }, "node_modules/@types/luxon": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", - "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==" }, "node_modules/@types/methods": { "version": "1.1.4", @@ -4292,6 +4348,11 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/multer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", @@ -6169,9 +6230,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "dev": true }, "node_modules/chokidar": { @@ -6614,12 +6675,12 @@ "dev": true }, "node_modules/cron": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.0.tgz", - "integrity": "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz", + "integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==", "dependencies": { - "@types/luxon": "~3.6.0", - "luxon": "~3.6.0" + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" }, "engines": { "node": ">=18.x" @@ -7493,32 +7554,6 @@ "node": ">=4" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -9648,9 +9683,9 @@ } }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "engines": { "node": ">=12" } @@ -10231,15 +10266,6 @@ "node": ">=8" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-cancelable": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", @@ -10677,14 +10703,14 @@ } }, "node_modules/prisma": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.0.tgz", - "integrity": "sha512-rcvldz98r+2bVCs0MldQCBaaVJRCj9Ew4IqphLATF89OJcSzwRQpwnKXR+W2+2VjK7/o2x3ffu5+2N3Muu6Dbw==", + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.17.1.tgz", + "integrity": "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/config": "6.17.0", - "@prisma/engines": "6.17.0" + "@prisma/config": "6.17.1", + "@prisma/engines": "6.17.1" }, "bin": { "prisma": "build/index.js" @@ -11791,9 +11817,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", - "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "version": "5.29.4", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.4.tgz", + "integrity": "sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==", "dependencies": { "@scarf/scarf": "=1.4.0" } @@ -12055,18 +12081,6 @@ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "devOptional": true }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index 609801c..c8db806 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@nestjs/platform-express": "^11.1.6", "@nestjs/schedule": "^6.0.0", "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.17.1", "bullmq": "^5.58.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", @@ -86,7 +86,7 @@ "globals": "^16.0.0", "jest": "^29.7.0", "prettier": "^3.4.2", - "prisma": "^6.17.0", + "prisma": "^6.17.1", "source-map-support": "^0.5.21", "supertest": "^7.0.0", "ts-jest": "^29.2.5", diff --git a/src/modules/leave-requests/utils/leave-request.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts index 53b1ba4..129742f 100644 --- a/src/modules/leave-requests/utils/leave-request.util.ts +++ b/src/modules/leave-requests/utils/leave-request.util.ts @@ -1,6 +1,6 @@ import { hhmmFromLocal, toDateOnly, toStringFromDate } from "src/modules/shared/helpers/date-time.helpers"; import { BadRequestException, Injectable } from "@nestjs/common"; -import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service"; +import { ShiftsCommandService } from "src/modules/shifts/_deprecated-files/shifts-command.service"; import { PrismaService } from "src/prisma/prisma.service"; import { LeaveTypes } from "@prisma/client"; import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; diff --git a/src/modules/pay-periods/pay-periods.module.ts b/src/modules/pay-periods/pay-periods.module.ts index f85d416..ef8345f 100644 --- a/src/modules/pay-periods/pay-periods.module.ts +++ b/src/modules/pay-periods/pay-periods.module.ts @@ -6,11 +6,11 @@ import { PayPeriodsQueryService } from "./services/pay-periods-query.service"; import { TimesheetsModule } from "../timesheets/timesheets.module"; import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service"; import { ExpensesCommandService } from "../expenses/services/expenses-command.service"; -import { ShiftsCommandService } from "../shifts/services/shifts-command.service"; +import { ShiftsCommandService } from "../shifts/_deprecated-files/shifts-command.service"; import { SharedModule } from "../shared/shared.module"; import { PrismaService } from "src/prisma/prisma.service"; import { BusinessLogicsModule } from "../business-logics/business-logics.module"; -import { ShiftsHelpersService } from "../shifts/helpers/shifts.helpers"; +import { ShiftsHelpersService } from "../shifts/_deprecated-files/shifts.helpers"; @Module({ imports: [PrismaModule, TimesheetsModule, SharedModule, BusinessLogicsModule], diff --git a/src/modules/shared/classes/timesheet.dto.ts b/src/modules/shared/classes/timesheet.dto.ts index 4a3f307..6024048 100644 --- a/src/modules/shared/classes/timesheet.dto.ts +++ b/src/modules/shared/classes/timesheet.dto.ts @@ -2,10 +2,14 @@ export class Session { user_id: number; } - - export class Timesheets { + employee_fullname: string; + timesheets: Timesheet[]; +} + +export class Timesheet { timesheet_id: number; + is_approved: boolean; days: TimesheetDay[]; weekly_hours: TotalHours[]; weekly_expenses: TotalExpenses[]; @@ -36,9 +40,9 @@ export class TotalExpenses { } export class Shift { - date: Date; - start_time: Date; - end_time: Date; + date: string; + start_time: string; + end_time: string; type: string; is_remote: boolean; is_approved: boolean; diff --git a/src/modules/shifts/controllers/shift.controller.ts b/src/modules/shifts/controllers/shift.controller.ts index c9b1b96..e57c233 100644 --- a/src/modules/shifts/controllers/shift.controller.ts +++ b/src/modules/shifts/controllers/shift.controller.ts @@ -1,13 +1,17 @@ //newer version that uses Express session data - -import { Controller } from "@nestjs/common"; -import { ShiftService } from "../services/shift.service"; +import { Controller, Delete, Param } from "@nestjs/common"; +import { ShiftsUpsertService } from "../services/shifts-upsert.service"; +import { Shifts } from "@prisma/client"; - -@Controller('shifts') +@Controller('shift') export class ShiftController { - constructor(private readonly service: ShiftService){} - + constructor( + private readonly upser_service: ShiftsUpsertService, + ){} + @Delete(':shift_id') + remove(@Param('shift_id') shift_id: number): Promise{ + return this.upser_service.deleteShift(shift_id); + } } \ No newline at end of file diff --git a/src/modules/shifts/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts deleted file mode 100644 index 26918dc..0000000 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Put, Query, } from "@nestjs/common"; -import { RolesAllowed } from "src/common/decorators/roles.decorators"; -import { Roles as RoleEnum } from '.prisma/client'; -import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; -import { ShiftsCommandService } from "../services/shifts-command.service"; -import { ShiftsQueryService } from "../services/shifts-query.service"; -import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto"; -import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; - -@ApiTags('Shifts') -@ApiBearerAuth('access-token') -// @UseGuards() -@Controller('shifts') -export class ShiftsController { - constructor( - private readonly shiftsService: ShiftsQueryService, - private readonly shiftsCommandService: ShiftsCommandService, - ){} - - @Put('upsert/:email') - async upsert_by_date( - @Param('email') email_param: string, - @Query('action') action: UpsertAction, - @Body() payload: UpsertShiftDto, - ) { - return this.shiftsCommandService.upsertShifts(email_param, action, payload); - } - - @Delete('delete/:email/:date') - async remove( - @Param('email') email: string, - @Param('date') date: string, - @Body() payload: UpsertShiftDto, - ) { - return this.shiftsCommandService.deleteShift(email, date, payload); - } - - @Patch('approval/:id') - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { - return this.shiftsCommandService.updateApproval(id, isApproved); - } - - @Get('summary') - async getSummary( @Query() query: GetShiftsOverviewDto): Promise { - return this.shiftsService.getSummary(query.period_id); - } - - @Get('export.csv') - @Header('Content-Type', 'text/csv; charset=utf-8') - @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') - async exportCsv(@Query() query: GetShiftsOverviewDto): Promise{ - const rows = await this.shiftsService.getSummary(query.period_id); - //CSV Headers - const header = [ - 'full_name', - 'supervisor', - 'total_regular_hrs', - 'total_evening_hrs', - 'total_overtime_hrs', - 'total_expenses', - 'total_mileage', - 'is_validated' - ].join(',') + '\n'; - - //CSV rows - const body = rows.map(r => { - const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; - - return [ - esc(r.full_name), - esc(r.supervisor), - r.total_regular_hrs.toFixed(2), - r.total_evening_hrs.toFixed(2), - r.total_overtime_hrs.toFixed(2), - r.total_expenses.toFixed(2), - r.total_mileage.toFixed(2), - r.is_approved, - ].join(','); - }).join('\n'); - - return Buffer.from('\uFEFF' + header + body, 'utf8'); - } - -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/create-shift.dto.ts b/src/modules/shifts/dtos/create-shift.dto.ts deleted file mode 100644 index ae393c7..0000000 --- a/src/modules/shifts/dtos/create-shift.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -//newer version that uses Express session data - -import { IsBoolean, IsOptional, IsString, MaxLength } from "class-validator"; - -export class createShift { - timesheet_id: number; - bank_code_id: number; - date: string; - start_time: string; - end_time: string; - - @IsBoolean() - is_remote: boolean; - - @IsBoolean() - is_approved: boolean; - - @IsOptional() - @IsString() - @MaxLength(280) - comment?: string; -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/get-shift-overview.dto.ts b/src/modules/shifts/dtos/get-shift-overview.dto.ts deleted file mode 100644 index e8ccdd2..0000000 --- a/src/modules/shifts/dtos/get-shift-overview.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Type } from "class-transformer"; -import { IsInt, Min, Max } from "class-validator"; - -export class GetShiftsOverviewDto { - @Type(()=> Number) - @IsInt() - @Min(1) - @Max(26) - period_id: number; -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/get-shift.dto.ts b/src/modules/shifts/dtos/get-shift.dto.ts index 4e4b17d..0c9f499 100644 --- a/src/modules/shifts/dtos/get-shift.dto.ts +++ b/src/modules/shifts/dtos/get-shift.dto.ts @@ -1,14 +1,12 @@ //newer version that uses Express session data - -export class getShift { - shift_id?: number; - timesheet_id?: number; - bank_code_id?: number; - date?: string; - start_time?: string; - end_time?: string; - is_remote?: boolean; - is_approved?: boolean; +export class GetShiftDto { + timesheet_id: number; + bank_code_id: number; + date: string; + start_time: string; + end_time: string; + is_remote: boolean; + is_approved: boolean; comment?: string; +} -} \ No newline at end of file diff --git a/src/modules/shifts/dtos/shift.dto.ts b/src/modules/shifts/dtos/shift.dto.ts new file mode 100644 index 0000000..70afc62 --- /dev/null +++ b/src/modules/shifts/dtos/shift.dto.ts @@ -0,0 +1,17 @@ +//newer version that uses Express session data + +import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator"; + +export class ShiftDto { + @IsInt() timesheet_id!: number; + @IsInt() bank_code_id!: number; + + @IsString() date!: string; + @IsString() start_time!: string; + @IsString() end_time!: string; + + @IsBoolean() is_approved!: boolean; + @IsBoolean() is_remote!: boolean; + + @IsOptional() @IsString() @MaxLength(280) comment?: string; +} \ No newline at end of file diff --git a/src/modules/shifts/dtos/update-shift.dto.ts b/src/modules/shifts/dtos/update-shift.dto.ts index db3e934..a458afd 100644 --- a/src/modules/shifts/dtos/update-shift.dto.ts +++ b/src/modules/shifts/dtos/update-shift.dto.ts @@ -1,9 +1,9 @@ //newer version that uses Express session data -export class updateShift { +import { PartialType, OmitType } from "@nestjs/swagger"; +import { ShiftDto } from "./shift.dto"; - date!: string; - start_time!: string; - end_time!: string; - -} \ No newline at end of file +export class updateShiftDto extends PartialType ( + //allows update using ShiftDto and preventing OmitType variables to be modified + OmitType(ShiftDto, [ 'is_approved', 'timesheet_id'] as const) +){} \ No newline at end of file diff --git a/src/modules/shifts/dtos/upsert-shift.dto.ts b/src/modules/shifts/dtos/upsert-shift.dto.ts deleted file mode 100644 index 7809571..0000000 --- a/src/modules/shifts/dtos/upsert-shift.dto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Type } from "class-transformer"; -import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator"; - -export const COMMENT_MAX_LENGTH = 280; - -export class ShiftPayloadDto { - - @Matches(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/) - date!: string; - - @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) - start_time!: string; - - @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) - end_time!: string; - - @IsString() - type!: string; - - @IsBoolean() - is_remote!: boolean; - - @IsBoolean() - is_approved!: boolean; - - @IsOptional() - @IsString() - @MaxLength(COMMENT_MAX_LENGTH) - comment?: string; -}; - -export class UpsertShiftDto { - - @IsOptional() - @ValidateNested() - @Type(()=> ShiftPayloadDto) - old_shift?: ShiftPayloadDto; - - @IsOptional() - @ValidateNested() - @Type(()=> ShiftPayloadDto) - new_shift?: ShiftPayloadDto; -}; \ No newline at end of file diff --git a/src/modules/shifts/helpers/shifts-date-time-helpers.ts b/src/modules/shifts/helpers/shifts-date-time-helpers.ts index 8bcd610..1ed0854 100644 --- a/src/modules/shifts/helpers/shifts-date-time-helpers.ts +++ b/src/modules/shifts/helpers/shifts-date-time-helpers.ts @@ -1,15 +1,3 @@ -export function timeFromHHMM(hhmm: string): Date { - const [h, m] = hhmm.split(':').map(Number); - return new Date(1970, 0, 1, h, m, 0, 0); -} - -export function toDateOnly(ymd: string): Date { - const y = Number(ymd.slice(0, 4)); - const m = Number(ymd.slice(5, 7)) - 1; - const d = Number(ymd.slice(8, 10)); - return new Date(y, m, d, 0, 0, 0, 0); -} - export function weekStartSunday(date_local: Date): Date { const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate())); const dow = start.getDay(); // 0 = dimanche @@ -18,8 +6,26 @@ export function weekStartSunday(date_local: Date): Date { return start; } -export function formatHHmm(t: Date): string { - const hh = String(t.getHours()).padStart(2, '0'); - const mm = String(t.getMinutes()).padStart(2, '0'); +//converts string to HHmm format +export const toStringFromHHmm = (date: Date): string => { + const hh = date.getUTCHours().toString().padStart(2, '0'); + const mm = date.getUTCMinutes().toString().padStart(2, '0'); return `${hh}:${mm}`; } + +//converts string to Date format +export const toStringFromDate = (date: Date) => + date.toISOString().slice(0,10); + +//converts HHmm format to string +export const toHHmmFromString = (hhmm: string): Date => { + const [hh, mm] = hhmm.split(':').map(Number); + const date = new Date('1970-01-01T00:00:00.000Z'); + date.setUTCHours(hh, mm, 0, 0); + return new Date(date); +} + +//converts Date format to string +export const toDateFromString = (ymd: string): Date => { + return new Date(`${ymd}T00:00:00:000Z`); +} \ No newline at end of file diff --git a/src/modules/shifts/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts deleted file mode 100644 index 6fb216d..0000000 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common"; -import { Prisma, Shifts } from "@prisma/client"; -import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; -import { normalizeShiftPayload, overlaps } from "../utils/shifts.utils"; -import { weekStartSunday, formatHHmm } from "./shifts-date-time-helpers"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; -import { OvertimeService } from "src/modules/business-logics/services/overtime.service"; - - -export type Tx = Prisma.TransactionClient; -export type Normalized = Awaited>; - -export class ShiftsHelpersService { - - constructor( - private readonly bankTypeResolver: BankCodesResolver, - private readonly overtimeService: OvertimeService, - ) { } - - async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) { - const start_of_week = weekStartSunday(date_only); - return tx.timesheets.findUnique({ - where: { employee_id_start_date: { employee_id, start_date: start_of_week } }, - select: { id: true }, - }); - } - - async normalizeRequired( - raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null, - label: 'old_shift' | 'new_shift' = 'new_shift', - ): Promise { - if (!raw) throw new BadRequestException(`${label} is required`); - const norm = await normalizeShiftPayload(raw); - if (norm.end_time.getTime() <= norm.start_time.getTime()) { - throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`); - } - return norm; - } - - async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise { - const found = await this.bankTypeResolver.findByType(type, tx); - const id = found?.id; - if (typeof id !== 'number') { - throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`); - } - return id; - } - - async getDayShifts(tx: Tx, timesheet_id: number, date_only: Date) { - return tx.shifts.findMany({ - where: { timesheet_id, date: date_only }, - include: { bank_code: true }, - orderBy: { start_time: 'asc' }, - }); - } - - async assertNoOverlap( - day_shifts: Array, - new_norm: Normalized | undefined, - exclude_id?: number, - ) { - if (!new_norm) return; - const conflicts = day_shifts.filter((s) => { - if (exclude_id && s.id === exclude_id) return false; - return overlaps( - new_norm.start_time.getTime(), - new_norm.end_time.getTime(), - s.start_time.getTime(), - s.end_time.getTime(), - ); - }); - if (conflicts.length) { - const payload = conflicts.map((s) => ({ - start_time: formatHHmm(s.start_time), - end_time: formatHHmm(s.end_time), - type: s.bank_code?.type ?? 'UNKNOWN', - })); - throw new ConflictException({ - error_code: 'SHIFT_OVERLAP', - message: 'New shift overlaps with existing shift(s)', - conflicts: payload, - }); - } - } - - - async findExactOldShift( - tx: Tx, - params: { - timesheet_id: number; - date_only: Date; - norm: Normalized; - bank_code_id: number; - comment?: string; - }, - ) { - const { timesheet_id, date_only, norm, bank_code_id } = params; - return tx.shifts.findFirst({ - where: { - timesheet_id, - date: date_only, - start_time: norm.start_time, - end_time: norm.end_time, - is_remote: norm.is_remote, - is_approved: norm.is_approved, - comment: norm.comment ?? null, - bank_code_id, - }, - select: { id: true }, - }); - } - - async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { - // Switch regular → weekly overtime si > 40h - await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); - const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); - const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); - // const [daily, weekly] = await Promise.all([ - // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), - // this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), - // ]); - return { daily, weekly }; - } - - async mapDay( - fresh: Array, - ): Promise { - return fresh.map((s) => ({ - start_time: formatHHmm(s.start_time), - end_time: formatHHmm(s.end_time), - type: s.bank_code?.type ?? 'UNKNOWN', - is_remote: s.is_remote, - comment: s.comment ?? null, - })); - } -} - diff --git a/src/modules/shifts/services/shift.service.ts b/src/modules/shifts/services/shift.service.ts deleted file mode 100644 index 3c82b88..0000000 --- a/src/modules/shifts/services/shift.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -//newer version that uses Express session data - -import { Injectable } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; - -@Injectable() -export class ShiftService { - constructor(private readonly prisma: PrismaService){} -} \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-command.service.ts b/src/modules/shifts/services/shifts-command.service.ts deleted file mode 100644 index c925f4a0..0000000 --- a/src/modules/shifts/services/shifts-command.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; -import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; -import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { Prisma, Shifts } from "@prisma/client"; -import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; -import { BaseApprovalService } from "src/common/shared/base-approval.service"; -import { PrismaService } from "src/prisma/prisma.service"; -import { toDateOnly } from "../helpers/shifts-date-time-helpers"; -import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; -import { ShiftsHelpersService } from "../helpers/shifts.helpers"; -import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; - -@Injectable() -export class ShiftsCommandService extends BaseApprovalService { - private readonly logger = new Logger(ShiftsCommandService.name); - - constructor( - prisma: PrismaService, - private readonly emailResolver: EmailToIdResolver, - private readonly typeResolver: BankCodesResolver, - private readonly helpersService: ShiftsHelpersService, - ) { super(prisma); } - - //_____________________________________________________________________________________________ - // APPROVAL AND DELEGATE METHODS - //_____________________________________________________________________________________________ - protected get delegate() { - return this.prisma.shifts; - } - - protected delegateFor(transaction: Prisma.TransactionClient) { - return transaction.shifts; - } - - async updateApproval(id: number, is_approved: boolean): Promise { - return this.prisma.$transaction((transaction) => - this.updateApprovalWithTransaction(transaction, id, is_approved), - ); - } - - - //TODO: modifier le Master Crud pour recevoir l'ensemble des shifts de la pay-period et trier sur l'action 'create'| 'update' | 'delete' - //_____________________________________________________________________________________________ - // MASTER CRUD METHOD - //_____________________________________________________________________________________________ - async upsertShifts( - email: string, - action: UpsertAction, - dto: UpsertShiftDto, - ): Promise<{ - action: UpsertAction; - day: DayShiftResponse[]; - }> { - if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided'); - - const date = dto.new_shift?.date ?? dto.old_shift?.date; - if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); - if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) { - throw new BadRequestException('old_shift.date and new_shift.date must be identical'); - } - - const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email - - if(action === 'create') { - if(!dto.new_shift || dto.old_shift) { - throw new BadRequestException(`Only new_shift must be provided for create`); - } - return this.createShift(employee_id, date, dto); - } - if(action === 'update'){ - if(!dto.old_shift || !dto.new_shift) { - throw new BadRequestException(`Both new_shift and old_shift must be provided for update`); - } - return this.updateShift(employee_id, date, dto); - } - throw new BadRequestException(`Unknown action: ${action}`); - } - - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - private async createShift( - employee_id: number, - date_iso: string, - dto: UpsertShiftDto, - ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); - const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); - - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - - await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); - - await tx.shifts.create({ - data: { - timesheet_id: timesheet.id, - date: date_only, - start_time: new_norm_shift.start_time, - end_time: new_norm_shift.end_time, - is_remote: new_norm_shift.is_remote, - is_approved: new_norm_shift.is_approved, - comment: new_norm_shift.comment ?? null, - bank_code_id: new_bank_code_id, - }, - }); - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; - }); - } - - //_________________________________________________________________ - // UPDATE - //_________________________________________________________________ - private async updateShift( - employee_id: number, - date_iso: string, - dto: UpsertShiftDto, - ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - - const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); - const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); - - const old_bank_code = await this.typeResolver.findByType(old_norm_shift.type); - const new_bank_code = await this.typeResolver.findByType(new_norm_shift.type); - - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - const existing = await this.helpersService.findExactOldShift(tx, { - timesheet_id: timesheet.id, - date_only, - norm: old_norm_shift, - bank_code_id: old_bank_code.id, - }); - if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); - - await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id); - - await tx.shifts.update({ - where: { id: existing.id }, - data: { - start_time: new_norm_shift.start_time, - end_time: new_norm_shift.end_time, - is_remote: new_norm_shift.is_remote, - comment: new_norm_shift.comment ?? null, - bank_code_id: new_bank_code.id, - }, - }); - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; - }); - - } - - //_________________________________________________________________ - // DELETE - //_________________________________________________________________ - async deleteShift( - email: string, - date_iso: string, - dto: UpsertShiftDto, - ){ - return this.prisma.$transaction(async (tx) => { - const date_only = toDateOnly(date_iso); //converts to Date format - const employee_id = await this.emailResolver.findIdByEmail(email); - - const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); - if(!timesheet) throw new NotFoundException('Timesheet not found') - const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); - const bank_code_id = await this.typeResolver.findByType(norm_shift.type); - - const existing = await this.helpersService.findExactOldShift(tx, { - timesheet_id: timesheet.id, - date_only, - norm: norm_shift, - bank_code_id: bank_code_id.id, - }); - if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); - - await tx.shifts.delete({ where: { id: existing.id } }); - - await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - }); - } -} - diff --git a/src/modules/shifts/services/shifts-get.service.ts b/src/modules/shifts/services/shifts-get.service.ts new file mode 100644 index 0000000..2df51d5 --- /dev/null +++ b/src/modules/shifts/services/shifts-get.service.ts @@ -0,0 +1,50 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { GetShiftDto } from "../dtos/get-shift.dto"; +import { toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers"; +import { Shifts } from "@prisma/client"; + +@Injectable() +export class ShiftsGetService { + constructor( + private readonly prisma: PrismaService, + ){} + + //fetch a shift using shift_id and return all that shift's info + async getShiftByShiftId(shift_id: number): Promise { + const shift = await this.prisma.shifts.findUnique({ + where: { id: shift_id }, + select: { + timesheet_id: true, + bank_code_id: true, + date: true, + start_time: true, + end_time: true, + is_remote: true, + is_approved: true, + comment: true, + } + }); + if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`); + + return { + timesheet_id: shift.timesheet_id, + bank_code_id: shift.bank_code_id, + date: toStringFromDate(shift.date), + start_time: toStringFromHHmm(shift.start_time), + end_time: toStringFromHHmm(shift.end_time), + is_remote: shift.is_remote, + is_approved: shift.is_approved, + comment: shift.comment ?? undefined, + }; + } + + //finds all shifts in a single day to return an array of shifts + async loadShiftsFromSameDay( timesheet_id: number, date_only: Date, + ): Promise> { + return this.prisma.shifts.findMany({ + where: { timesheet_id, date: date_only }, + include: { bank_code: { select: { type: true } } }, + }); + } +} \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-query.service.ts b/src/modules/shifts/services/shifts-query.service.ts deleted file mode 100644 index 68006df..0000000 --- a/src/modules/shifts/services/shifts-query.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; -import { NotificationsService } from "src/modules/notifications/services/notifications.service"; -import { computeHours } from "src/common/utils/date-utils"; -import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; - -// const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12); - -@Injectable() -export class ShiftsQueryService { - constructor( - private readonly prisma: PrismaService, - private readonly notifs: NotificationsService, - ) {} - - async getSummary(period_id: number): Promise { - //fetch pay-period to display - const period = await this.prisma.payPeriods.findFirst({ - where: { pay_period_no: period_id }, - }); - if(!period) { - throw new NotFoundException(`pay-period ${period_id} not found`); - } - const { period_start, period_end } = period; - - //prepare shifts and expenses for display - const shifts = await this.prisma.shifts.findMany({ - where: { date: { gte: period_start, lte: period_end } }, - include: { - bank_code: true, - timesheet: { include: { - employee: { include: { - user:true, - supervisor: { include: { user: true } }, - } }, - } }, - }, - }); - - const expenses = await this.prisma.expenses.findMany({ - where: { date: { gte: period_start, lte: period_end } }, - include: { - bank_code: true, - timesheet: { include: { employee: { - include: { user:true, - supervisor: { include: { user:true } }, - } }, - } }, - }, - }); - - const mapRow = new Map(); - - for(const shift of shifts) { - const employeeId = shift.timesheet.employee.user_id; - const user = shift.timesheet.employee.user; - const sup = shift.timesheet.employee.supervisor?.user; - - let row = mapRow.get(employeeId); - if(!row) { - row = { - full_name: `${user.first_name} ${user.last_name}`, - supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', - total_regular_hrs: 0, - total_evening_hrs: 0, - total_overtime_hrs: 0, - total_expenses: 0, - total_mileage: 0, - is_approved: false, - }; - } - const hours = computeHours(shift.start_time, shift.end_time); - - switch(shift.bank_code.type) { - case 'regular' : row.total_regular_hrs += hours; - break; - case 'evening' : row.total_evening_hrs += hours; - break; - case 'overtime' : row.total_overtime_hrs += hours; - break; - default: row.total_regular_hrs += hours; - } - mapRow.set(employeeId, row); - } - - for(const exp of expenses) { - const employee_id = exp.timesheet.employee.user_id; - const user = exp.timesheet.employee.user; - const sup = exp.timesheet.employee.supervisor?.user; - - let row = mapRow.get(employee_id); - if(!row) { - row = { - full_name: `${user.first_name} ${user.last_name}`, - supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', - total_regular_hrs: 0, - total_evening_hrs: 0, - total_overtime_hrs: 0, - total_expenses: 0, - total_mileage: 0, - is_approved: false, - }; - } - const amount = Number(exp.amount); - row.total_expenses += amount; - if(exp.bank_code.type === 'mileage') { - row.total_mileage += amount; - } - mapRow.set(employee_id, row); - } - //return by default the list of employee in ascending alphabetical order - return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name)); - } -} \ No newline at end of file diff --git a/src/modules/shifts/services/shifts-upsert.service.ts b/src/modules/shifts/services/shifts-upsert.service.ts new file mode 100644 index 0000000..e3adc52 --- /dev/null +++ b/src/modules/shifts/services/shifts-upsert.service.ts @@ -0,0 +1,208 @@ +import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../helpers/shifts-date-time-helpers"; +import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"; +import { ShiftsGetService } from "./shifts-get.service"; +import { updateShiftDto } from "../dtos/update-shift.dto"; +import { PrismaService } from "src/prisma/prisma.service"; +import { GetShiftDto } from "../dtos/get-shift.dto"; +import { ShiftDto } from "../dtos/shift.dto"; +import { Shifts } from "@prisma/client"; + +type Normalized = { date: Date; start_time: Date; end_time: Date; }; + +@Injectable() +export class ShiftsUpsertService { + constructor( + private readonly prisma: PrismaService, + private readonly getService: ShiftsGetService, + ){} + + //converts all string hours and date to Date and HHmm formats + private normalizeShiftDto = (dto: ShiftDto): Normalized => { + const date = toDateFromString(dto.date); + const start_time = toHHmmFromString(dto.start_time); + const end_time = toHHmmFromString(dto.end_time); + return { date, start_time, end_time }; + } + + // used to compare shifts and detect overlaps between them + private overlaps = ( + a_start: number, + a_end: number, + b_start: number, + b_end: number, + ) => a_start < b_end && b_start < a_end; + + //checked if a new shift overlaps already existing shifts + private assertNoOverlap = async ( + day_shifts: Array, + new_norm: Normalized | undefined, + exclude_id?: number, + ) => { + if (!new_norm) return; + const conflicts = day_shifts.filter((shift) => { + if (exclude_id && shift.id === exclude_id) return false; + return this.overlaps( + new_norm.start_time.getTime(), + new_norm.end_time.getTime(), + shift.start_time.getTime(), + shift.end_time.getTime(), + ); + }); + if (conflicts.length) { + const payload = conflicts.map((shift) => ({ + start_time: toStringFromHHmm(shift.start_time), + end_time: toStringFromHHmm(shift.end_time), + type: shift.bank_code?.type ?? 'UNKNOWN', + })); + throw new ConflictException({ + error_code: 'SHIFT_OVERLAP', + message: 'New shift overlaps with existing shift(s)', + conflicts: payload, + }); + } + } + + //normalized frontend data to match DB + //loads all shift from a selected day to check for overlaping shifts + //checks for overlaping shifts + //create a new shifts + //return an object of type GetShiftDto for the frontend to display + async createShift(timesheet_id: number, dto: ShiftDto): Promise { + const normed_shift = await this.normalizeShiftDto(dto); + if(normed_shift.end_time <= normed_shift.start_time){ + throw new BadRequestException('end_time must be greater than start_time') + } + + //call to a function to load all shifts contain in single day + const day_shifts = await this.getService.loadShiftsFromSameDay(timesheet_id, normed_shift.date); + + //call to a function to detect overlaps between shifts + await this.assertNoOverlap( day_shifts, normed_shift ) + + //create the shift with normalized date and times + const shift = await this.prisma.shifts.create({ + data: { + timesheet_id, + bank_code_id: dto.bank_code_id, + date: normed_shift.date, + start_time: normed_shift.start_time, + end_time: normed_shift.end_time, + is_remote: dto.is_remote, + comment: dto.comment ?? undefined, + }, + select: { + timesheet_id: true, + bank_code_id: true, + date: true, + start_time: true, + end_time: true, + is_remote: true, + comment: true, + }, + }); + if(!shift) throw new BadRequestException(`a shift cannot be created, missing value(s).`); + + return { + timesheet_id: shift.timesheet_id, + bank_code_id: shift.bank_code_id, + date: toStringFromDate(shift.date), + start_time: toStringFromHHmm(shift.start_time), + end_time: toStringFromHHmm(shift.end_time), + is_remote: shift.is_remote, + is_approved: false, + comment: shift.comment ?? undefined, + }; + } + + //finds existing shift in DB + //verify if shift is already approved + //normalized Date and Time format to string + //check for valid start and end times + //check for overlaping possibility + //buil a set of data to manipulate modified data only + //update shift in DB and return an updated version to display + async updateShift(shift_id: number, dto: updateShiftDto): Promise { + //search for original shift using shift_id + const existing = await this.prisma.shifts.findUnique({ + where: { id: shift_id }, + select: { + id: true, + timesheet_id: true, + bank_code_id: true, + date: true, + start_time: true, + end_time: true, + is_remote: true, + is_approved: true, + comment: true, + }, + }); + if(!existing) throw new NotFoundException(`Shift with id: ${shift_id} not found`); + if(existing.is_approved) throw new BadRequestException('Approved shift cannot be updated'); + + const date_string = dto.date ?? toStringFromDate(existing.date); + const start_string = dto.start_time ?? toStringFromHHmm(existing.start_time); + const end_string = dto.end_time ?? toStringFromHHmm(existing.end_time); + + const norm: Normalized = { + date: toDateFromString(date_string), + start_time: toHHmmFromString(start_string), + end_time: toHHmmFromString(end_string), + }; + if(norm.end_time <= norm.start_time) throw new BadRequestException('end time must be greater than start time'); + + //call to a function to detect overlaps between shifts + const day_shifts = await this.getService.loadShiftsFromSameDay(existing.timesheet_id, norm.date); + + //call to a function to detect overlaps between shifts + await this.assertNoOverlap(day_shifts, norm, shift_id); + + //partial build, update only modified datas + const data: any = {}; + if(dto.date !== undefined) data.date = norm.date; + if(dto.start_time !== undefined) data.start_time = norm.start_time; + if(dto.end_time !== undefined) data.end_time = norm.end_time; + if(dto.bank_code_id !== undefined) data.bank_code_id = dto.bank_code_id; + if(dto.is_remote !== undefined) data.is_remote = dto.is_remote; + if(dto.comment !== undefined) data.comment = dto.comment ?? null; + + //sends updated data to DB + const updated_shift = await this.prisma.shifts.update({ + where: { id: shift_id }, + data, + select: { + timesheet_id: true, + bank_code_id: true, + date: true, + start_time: true, + end_time: true, + is_remote: true, + is_approved: true, + comment: true, + }, + }); + //returns updated shift to frontend + return { + timesheet_id: updated_shift.timesheet_id, + bank_code_id: updated_shift.bank_code_id, + date: toStringFromDate(updated_shift.date), + start_time: toStringFromHHmm(updated_shift.start_time), + end_time: toStringFromHHmm(updated_shift.end_time), + is_approved: updated_shift.is_approved, + is_remote: updated_shift.is_remote, + comment: updated_shift.comment ?? undefined, + }; + } + + async deleteShift(shift_id: number) { + const shift = await this.prisma.shifts.findUnique({ + where: { id: shift_id }, + select: { id: true }, + }); + if(!shift) throw new NotFoundException(`Shift with id #${shift_id} not found`); + + return this.prisma.shifts.delete({ + where: { id: shift.id } + }); + } +} \ No newline at end of file diff --git a/src/modules/shifts/shifts.module.ts b/src/modules/shifts/shifts.module.ts index d6df6c8..4c20e0d 100644 --- a/src/modules/shifts/shifts.module.ts +++ b/src/modules/shifts/shifts.module.ts @@ -1,30 +1,24 @@ -import { Module } from '@nestjs/common'; -import { ShiftsController } from './controllers/shifts.controller'; -import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; -import { ShiftsCommandService } from './services/shifts-command.service'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { ShiftsQueryService } from './services/shifts-query.service'; import { ShiftsArchivalService } from './services/shifts-archival.service'; +import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; +import { NotificationsModule } from '../notifications/notifications.module'; +import { ShiftsUpsertService } from './services/shifts-upsert.service'; +import { ShiftsGetService } from './services/shifts-get.service'; +import { ShiftController } from './controllers/shift.controller'; import { SharedModule } from '../shared/shared.module'; -import { ShiftsHelpersService } from './helpers/shifts.helpers'; +import { Module } from '@nestjs/common'; @Module({ imports: [ - BusinessLogicsModule, - NotificationsModule, + BusinessLogicsModule, + NotificationsModule, SharedModule, ], - controllers: [ShiftsController], - providers: [ - ShiftsQueryService, - ShiftsCommandService, + controllers: [ShiftController], + providers: [ ShiftsArchivalService, - ShiftsHelpersService, - ], - exports: [ - ShiftsQueryService, - ShiftsCommandService, - ShiftsArchivalService, + ShiftsGetService, + ShiftsUpsertService, ], + exports: [ ShiftsUpsertService, ShiftsGetService ], }) export class ShiftsModule {} diff --git a/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts b/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts deleted file mode 100644 index 145885b..0000000 --- a/src/modules/shifts/types-and-interfaces/shifts-overview-row.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface OverviewRow { - full_name: string; - supervisor: string; - total_regular_hrs: number; - total_evening_hrs: number; - total_overtime_hrs: number; - total_expenses: number; - total_mileage: number; - is_approved: boolean; -} \ No newline at end of file diff --git a/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts b/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts deleted file mode 100644 index 733ea72..0000000 --- a/src/modules/shifts/types-and-interfaces/shifts-upsert.types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type DayShiftResponse = { - start_time: string; - end_time: string; - type: string; - is_remote: boolean; - comment: string | null; -} - -export type ShiftPayload = { - date: string; - start_time: string; - end_time: string; - type: string; - is_remote: boolean; - is_approved: boolean; - comment?: string | null; -} \ No newline at end of file diff --git a/src/modules/shifts/utils/shifts.utils.ts b/src/modules/shifts/utils/shifts.utils.ts deleted file mode 100644 index 5262850..0000000 --- a/src/modules/shifts/utils/shifts.utils.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { NotFoundException } from "@nestjs/common"; - -export function overlaps( - a_start_ms: number, - a_end_ms: number, - b_start_ms: number, - b_end_ms: number, -): boolean { - return a_start_ms < b_end_ms && b_start_ms < a_end_ms; -} - -export function resolveBankCodeByType(type: string): Promise { - const bank = this.prisma.bankCodes.findFirst({ - where: { type }, - select: { id: true }, - }); - if (!bank) { - throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` }); - } - return bank.id; -} - -export function normalizeShiftPayload(payload: { - date: string, - start_time: string, - end_time: string, - type: string, - is_remote: boolean, - is_approved: boolean, - comment?: string | null, -}) { - //normalize shift's infos - const date = payload.date?.trim(); - const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date ?? ''); - if (!m) throw new Error(`Invalid date format (expected YYYY-MM-DD): "${payload.date}"`); - const year = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); - - const asLocalDateOn = (input: string): Date => { - // HH:mm ? - const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim()); - if (hm) return new Date(year, mo - 1, d, Number(hm[1]), Number(hm[2]), 0, 0); - const iso = new Date(input); - if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${input}"`); - return new Date(year, mo - 1, d, iso.getHours(), iso.getMinutes(), iso.getSeconds(), iso.getMilliseconds()); - }; - - const start_time = asLocalDateOn(payload.start_time); - const end_time = asLocalDateOn(payload.end_time); - - const type = (payload.type || '').trim().toUpperCase(); - const is_remote = payload.is_remote; - const is_approved = payload.is_approved; - //normalize comment - const trimmed = typeof payload.comment === 'string' ? payload.comment.trim() : null; - const comment = trimmed && trimmed.length > 0 ? trimmed : null; - - return { date, start_time, end_time, type, is_remote, is_approved, comment }; -} \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts b/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts new file mode 100644 index 0000000..0921a48 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/get-shift-overview.dto.ts @@ -0,0 +1,10 @@ +// import { Type } from "class-transformer"; +// import { IsInt, Min, Max } from "class-validator"; + +// export class GetShiftsOverviewDto { +// @Type(()=> Number) +// @IsInt() +// @Min(1) +// @Max(26) +// period_id: number; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts b/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts new file mode 100644 index 0000000..5544309 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-command.service.ts @@ -0,0 +1,194 @@ +// import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; +// import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; +// import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; +// import { Prisma, Shifts } from "@prisma/client"; +// import { UpsertShiftDto } from "../dtos/upsert-shift.dto"; +// import { BaseApprovalService } from "src/common/shared/base-approval.service"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { toDateOnly } from "../helpers/shifts-date-time-helpers"; +// import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; +// import { ShiftsHelpersService } from "../helpers/shifts.helpers"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; + +// @Injectable() +// export class ShiftsCommandService extends BaseApprovalService { +// private readonly logger = new Logger(ShiftsCommandService.name); + +// constructor( +// prisma: PrismaService, +// private readonly emailResolver: EmailToIdResolver, +// private readonly typeResolver: BankCodesResolver, +// private readonly helpersService: ShiftsHelpersService, +// ) { super(prisma); } + +// //_____________________________________________________________________________________________ +// // APPROVAL AND DELEGATE METHODS +// //_____________________________________________________________________________________________ +// protected get delegate() { +// return this.prisma.shifts; +// } + +// protected delegateFor(transaction: Prisma.TransactionClient) { +// return transaction.shifts; +// } + +// async updateApproval(id: number, is_approved: boolean): Promise { +// return this.prisma.$transaction((transaction) => +// this.updateApprovalWithTransaction(transaction, id, is_approved), +// ); +// } + + +// //TODO: modifier le Master Crud pour recevoir l'ensemble des shifts de la pay-period et trier sur l'action 'create'| 'update' | 'delete' +// //_____________________________________________________________________________________________ +// // MASTER CRUD METHOD +// //_____________________________________________________________________________________________ +// async upsertShifts( +// email: string, +// action: UpsertAction, +// dto: UpsertShiftDto, +// ): Promise<{ +// action: UpsertAction; +// day: DayShiftResponse[]; +// }> { +// if (!dto.old_shift && !dto.new_shift) throw new BadRequestException('At least one of old or new shift must be provided'); + +// const date = dto.new_shift?.date ?? dto.old_shift?.date; +// if (!date) throw new BadRequestException("A date (YYYY-MM-DD) must be provided in old_shift or new_shift"); +// if (dto.old_shift?.date && dto.new_shift?.date && dto.old_shift.date !== dto.new_shift.date) { +// throw new BadRequestException('old_shift.date and new_shift.date must be identical'); +// } + +// const employee_id = await this.emailResolver.findIdByEmail(email);//resolve employee id using email + +// if(action === 'create') { +// if(!dto.new_shift || dto.old_shift) { +// throw new BadRequestException(`Only new_shift must be provided for create`); +// } +// return this.createShift(employee_id, date, dto); +// } +// if(action === 'update'){ +// if(!dto.old_shift || !dto.new_shift) { +// throw new BadRequestException(`Both new_shift and old_shift must be provided for update`); +// } +// return this.updateShift(employee_id, date, dto); +// } +// throw new BadRequestException(`Unknown action: ${action}`); +// } + +// //_________________________________________________________________ +// // CREATE +// //_________________________________________________________________ +// private async createShift( +// employee_id: number, +// date_iso: string, +// dto: UpsertShiftDto, +// ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') +// const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); +// const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); + +// const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); + +// await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); + +// await tx.shifts.create({ +// data: { +// timesheet_id: timesheet.id, +// date: date_only, +// start_time: new_norm_shift.start_time, +// end_time: new_norm_shift.end_time, +// is_remote: new_norm_shift.is_remote, +// is_approved: new_norm_shift.is_approved, +// comment: new_norm_shift.comment ?? null, +// bank_code_id: new_bank_code_id, +// }, +// }); +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; +// }); +// } + +// //_________________________________________________________________ +// // UPDATE +// //_________________________________________________________________ +// private async updateShift( +// employee_id: number, +// date_iso: string, +// dto: UpsertShiftDto, +// ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') + +// const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); +// const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); + +// const old_bank_code = await this.typeResolver.findByType(old_norm_shift.type); +// const new_bank_code = await this.typeResolver.findByType(new_norm_shift.type); + +// const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// const existing = await this.helpersService.findExactOldShift(tx, { +// timesheet_id: timesheet.id, +// date_only, +// norm: old_norm_shift, +// bank_code_id: old_bank_code.id, +// }); +// if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); + +// await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift, existing.id); + +// await tx.shifts.update({ +// where: { id: existing.id }, +// data: { +// start_time: new_norm_shift.start_time, +// end_time: new_norm_shift.end_time, +// is_remote: new_norm_shift.is_remote, +// comment: new_norm_shift.comment ?? null, +// bank_code_id: new_bank_code.id, +// }, +// }); +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); +// return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; +// }); + +// } + +// //_________________________________________________________________ +// // DELETE +// //_________________________________________________________________ +// async deleteShift( +// email: string, +// date_iso: string, +// dto: UpsertShiftDto, +// ){ +// return this.prisma.$transaction(async (tx) => { +// const date_only = toDateOnly(date_iso); //converts to Date format +// const employee_id = await this.emailResolver.findIdByEmail(email); + +// const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); +// if(!timesheet) throw new NotFoundException('Timesheet not found') +// const norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); +// const bank_code_id = await this.typeResolver.findByType(norm_shift.type); + +// const existing = await this.helpersService.findExactOldShift(tx, { +// timesheet_id: timesheet.id, +// date_only, +// norm: norm_shift, +// bank_code_id: bank_code_id.id, +// }); +// if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); + +// await tx.shifts.delete({ where: { id: existing.id } }); + +// await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); +// }); +// } +// } + diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts b/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts new file mode 100644 index 0000000..b82576e --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-overview-row.interface.ts @@ -0,0 +1,10 @@ +// export interface OverviewRow { +// full_name: string; +// supervisor: string; +// total_regular_hrs: number; +// total_evening_hrs: number; +// total_overtime_hrs: number; +// total_expenses: number; +// total_mileage: number; +// is_approved: boolean; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts b/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts new file mode 100644 index 0000000..be978f0 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-query.service.ts @@ -0,0 +1,114 @@ +// import { Injectable, NotFoundException } from "@nestjs/common"; +// import { PrismaService } from "src/prisma/prisma.service"; +// import { NotificationsService } from "src/modules/notifications/services/notifications.service"; +// import { computeHours } from "src/common/utils/date-utils"; +// import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; + +// // const DAILY_LIMIT_HOURS = Number(process.env.DAILY_LIMIT_HOURS ?? 12); + +// @Injectable() +// export class ShiftsQueryService { +// constructor( +// private readonly prisma: PrismaService, +// private readonly notifs: NotificationsService, +// ) {} + +// async getSummary(period_id: number): Promise { +// //fetch pay-period to display +// const period = await this.prisma.payPeriods.findFirst({ +// where: { pay_period_no: period_id }, +// }); +// if(!period) { +// throw new NotFoundException(`pay-period ${period_id} not found`); +// } +// const { period_start, period_end } = period; + +// //prepare shifts and expenses for display +// const shifts = await this.prisma.shifts.findMany({ +// where: { date: { gte: period_start, lte: period_end } }, +// include: { +// bank_code: true, +// timesheet: { include: { +// employee: { include: { +// user:true, +// supervisor: { include: { user: true } }, +// } }, +// } }, +// }, +// }); + +// const expenses = await this.prisma.expenses.findMany({ +// where: { date: { gte: period_start, lte: period_end } }, +// include: { +// bank_code: true, +// timesheet: { include: { employee: { +// include: { user:true, +// supervisor: { include: { user:true } }, +// } }, +// } }, +// }, +// }); + +// const mapRow = new Map(); + +// for(const shift of shifts) { +// const employeeId = shift.timesheet.employee.user_id; +// const user = shift.timesheet.employee.user; +// const sup = shift.timesheet.employee.supervisor?.user; + +// let row = mapRow.get(employeeId); +// if(!row) { +// row = { +// full_name: `${user.first_name} ${user.last_name}`, +// supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', +// total_regular_hrs: 0, +// total_evening_hrs: 0, +// total_overtime_hrs: 0, +// total_expenses: 0, +// total_mileage: 0, +// is_approved: false, +// }; +// } +// const hours = computeHours(shift.start_time, shift.end_time); + +// switch(shift.bank_code.type) { +// case 'regular' : row.total_regular_hrs += hours; +// break; +// case 'evening' : row.total_evening_hrs += hours; +// break; +// case 'overtime' : row.total_overtime_hrs += hours; +// break; +// default: row.total_regular_hrs += hours; +// } +// mapRow.set(employeeId, row); +// } + +// for(const exp of expenses) { +// const employee_id = exp.timesheet.employee.user_id; +// const user = exp.timesheet.employee.user; +// const sup = exp.timesheet.employee.supervisor?.user; + +// let row = mapRow.get(employee_id); +// if(!row) { +// row = { +// full_name: `${user.first_name} ${user.last_name}`, +// supervisor: sup? `${sup.first_name} ${sup.last_name }` : '', +// total_regular_hrs: 0, +// total_evening_hrs: 0, +// total_overtime_hrs: 0, +// total_expenses: 0, +// total_mileage: 0, +// is_approved: false, +// }; +// } +// const amount = Number(exp.amount); +// row.total_expenses += amount; +// if(exp.bank_code.type === 'mileage') { +// row.total_mileage += amount; +// } +// mapRow.set(employee_id, row); +// } +// //return by default the list of employee in ascending alphabetical order +// return Array.from(mapRow.values()).sort((a,b) => a.full_name.localeCompare(b.full_name)); +// } +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts b/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts new file mode 100644 index 0000000..ea21afe --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts-upsert.types.ts @@ -0,0 +1,17 @@ +// export type DayShiftResponse = { +// start_time: string; +// end_time: string; +// type: string; +// is_remote: boolean; +// comment: string | null; +// } + +// export type ShiftPayload = { +// date: string; +// start_time: string; +// end_time: string; +// type: string; +// is_remote: boolean; +// is_approved: boolean; +// comment?: string | null; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts b/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts new file mode 100644 index 0000000..88a1637 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.controller.ts @@ -0,0 +1,87 @@ +// import { Body, Controller, Delete, Get, Header, Param, ParseBoolPipe, ParseIntPipe, Patch, Put, Query, } from "@nestjs/common"; +// import { RolesAllowed } from "src/common/decorators/roles.decorators"; +// import { Roles as RoleEnum } from '.prisma/client'; +// import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +// import { ShiftsCommandService } from "../services/shifts-command.service"; +// import { ShiftsQueryService } from "../services/shifts-query.service"; +// import { GetShiftsOverviewDto } from "../dtos/get-shift-overview.dto"; +// import { ShiftPayloadDto, UpsertShiftDto } from "../dtos/upsert-shift.dto"; +// import { OverviewRow } from "../types-and-interfaces/shifts-overview-row.interface"; +// import { UpsertAction } from "src/modules/shared/types/upsert-actions.types"; + +// @ApiTags('Shifts') +// @ApiBearerAuth('access-token') +// // @UseGuards() +// @Controller('shifts') +// export class ShiftsController { +// constructor( +// private readonly shiftsService: ShiftsQueryService, +// private readonly shiftsCommandService: ShiftsCommandService, +// ){} + +// @Put('upsert/:email') +// async upsert_by_date( +// @Param('email') email_param: string, +// @Query('action') action: UpsertAction, +// @Body() payload: UpsertShiftDto, +// ) { +// return this.shiftsCommandService.upsertShifts(email_param, action, payload); +// } + +// @Delete('delete/:email/:date') +// async remove( +// @Param('email') email: string, +// @Param('date') date: string, +// @Body() payload: UpsertShiftDto, +// ) { +// return this.shiftsCommandService.deleteShift(email, date, payload); +// } + +// @Patch('approval/:id') +// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) +// async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) { +// return this.shiftsCommandService.updateApproval(id, isApproved); +// } + +// @Get('summary') +// async getSummary( @Query() query: GetShiftsOverviewDto): Promise { +// return this.shiftsService.getSummary(query.period_id); +// } + +// @Get('export.csv') +// @Header('Content-Type', 'text/csv; charset=utf-8') +// @Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"') +// async exportCsv(@Query() query: GetShiftsOverviewDto): Promise{ +// const rows = await this.shiftsService.getSummary(query.period_id); +// //CSV Headers +// const header = [ +// 'full_name', +// 'supervisor', +// 'total_regular_hrs', +// 'total_evening_hrs', +// 'total_overtime_hrs', +// 'total_expenses', +// 'total_mileage', +// 'is_validated' +// ].join(',') + '\n'; + +// //CSV rows +// const body = rows.map(r => { +// const esc = (str: string) => `"${str.replace(/"/g, '""')}"`; + +// return [ +// esc(r.full_name), +// esc(r.supervisor), +// r.total_regular_hrs.toFixed(2), +// r.total_evening_hrs.toFixed(2), +// r.total_overtime_hrs.toFixed(2), +// r.total_expenses.toFixed(2), +// r.total_mileage.toFixed(2), +// r.is_approved, +// ].join(','); +// }).join('\n'); + +// return Buffer.from('\uFEFF' + header + body, 'utf8'); +// } + +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts new file mode 100644 index 0000000..4c417c2 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.helpers.ts @@ -0,0 +1,107 @@ +// import { BadRequestException, UnprocessableEntityException, NotFoundException, ConflictException } from "@nestjs/common"; +// import { Prisma, Shifts } from "@prisma/client"; +// import { UpsertShiftDto } from "../deprecated-files/upsert-shift.dto"; +// import { DayShiftResponse } from "../types-and-interfaces/shifts-upsert.types"; +// import { normalizeShiftPayload } from "../utils/shifts.utils"; +// import { toStringFromHHmm, weekStartSunday } from "./shifts-date-time-helpers"; +// import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; +// import { OvertimeService } from "src/modules/business-logics/services/overtime.service"; + +// export type Tx = Prisma.TransactionClient; +// export type Normalized = Awaited>; + +// export class ShiftsHelpersService { + +// constructor( +// private readonly bankTypeResolver: BankCodesResolver, +// private readonly overtimeService: OvertimeService, +// ) { } + +// async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) { +// const start_of_week = weekStartSunday(date_only); +// return tx.timesheets.findUnique({ +// where: { employee_id_start_date: { employee_id, start_date: start_of_week } }, +// select: { id: true }, +// }); +// } + +// async normalizeRequired( +// raw: UpsertShiftDto['new_shift'] | UpsertShiftDto['old_shift'] | undefined | null, +// label: 'old_shift' | 'new_shift' = 'new_shift', +// ): Promise { +// if (!raw) throw new BadRequestException(`${label} is required`); +// const norm = await normalizeShiftPayload(raw); +// if (norm.end_time.getTime() <= norm.start_time.getTime()) { +// throw new UnprocessableEntityException(` ${label}.end_time must be > ${label}.start_time`); +// } +// return norm; +// } + +// async resolveBankIdRequired(tx: Tx, type: string, label: 'old_shift' | 'new_shift'): Promise { +// const found = await this.bankTypeResolver.findByType(type, tx); +// const id = found?.id; +// if (typeof id !== 'number') { +// throw new NotFoundException(`bank code not found for ${label}.type: ${type ?? ''}`); +// } +// return id; +// } + +// async getDayShifts(tx: Tx, timesheet_id: number, date_only: Date) { +// return tx.shifts.findMany({ +// where: { timesheet_id, date: date_only }, +// include: { bank_code: true }, +// orderBy: { start_time: 'asc' }, +// }); +// } + +// async findExactOldShift( +// tx: Tx, +// params: { +// timesheet_id: number; +// date_only: Date; +// norm: Normalized; +// bank_code_id: number; +// comment?: string; +// }, +// ) { +// const { timesheet_id, date_only, norm, bank_code_id } = params; +// return tx.shifts.findFirst({ +// where: { +// timesheet_id, +// date: date_only, +// start_time: norm.start_time, +// end_time: norm.end_time, +// is_remote: norm.is_remote, +// is_approved: norm.is_approved, +// comment: norm.comment ?? null, +// bank_code_id, +// }, +// select: { id: true }, +// }); +// } + +// async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { +// // Switch regular → weekly overtime si > 40h +// await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); +// const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); +// const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); +// // const [daily, weekly] = await Promise.all([ +// // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), +// // this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), +// // ]); +// return { daily, weekly }; +// } + +// async mapDay( +// fresh: Array, +// ): Promise { +// return fresh.map((s) => ({ +// start_time: toStringFromHHmm(s.start_time), +// end_time: toStringFromHHmm(s.end_time), +// type: s.bank_code?.type ?? 'UNKNOWN', +// is_remote: s.is_remote, +// comment: s.comment ?? null, +// })); +// } +// } + diff --git a/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts b/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts new file mode 100644 index 0000000..bfa5402 --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/shifts.utils.ts @@ -0,0 +1,58 @@ +// import { NotFoundException } from "@nestjs/common"; + +// export function overlaps( +// a_start_ms: number, +// a_end_ms: number, +// b_start_ms: number, +// b_end_ms: number, +// ): boolean { +// return a_start_ms < b_end_ms && b_start_ms < a_end_ms; +// } + +// export function resolveBankCodeByType(type: string): Promise { +// const bank = this.prisma.bankCodes.findFirst({ +// where: { type }, +// select: { id: true }, +// }); +// if (!bank) { +// throw new NotFoundException({ error_code: 'SHIFT_TYPE_UNKNOWN', message: `unknown shift type: ${type}` }); +// } +// return bank.id; +// } + +// export function normalizeShiftPayload(payload: { +// date: string, +// start_time: string, +// end_time: string, +// type: string, +// is_remote: boolean, +// is_approved: boolean, +// comment?: string | null, +// }) { +// //normalize shift's infos +// const date = payload.date?.trim(); +// const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date ?? ''); +// if (!m) throw new Error(`Invalid date format (expected YYYY-MM-DD): "${payload.date}"`); +// const year = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); + +// const asLocalDateOn = (input: string): Date => { +// // HH:mm ? +// const hm = /^(\d{2}):(\d{2})$/.exec((input ?? '').trim()); +// if (hm) return new Date(year, mo - 1, d, Number(hm[1]), Number(hm[2]), 0, 0); +// const iso = new Date(input); +// if (isNaN(iso.getTime())) throw new Error(`Invalid time: "${input}"`); +// return new Date(year, mo - 1, d, iso.getHours(), iso.getMinutes(), iso.getSeconds(), iso.getMilliseconds()); +// }; + +// const start_time = asLocalDateOn(payload.start_time); +// const end_time = asLocalDateOn(payload.end_time); + +// const type = (payload.type || '').trim().toUpperCase(); +// const is_remote = payload.is_remote; +// const is_approved = payload.is_approved; +// //normalize comment +// const trimmed = typeof payload.comment === 'string' ? payload.comment.trim() : null; +// const comment = trimmed && trimmed.length > 0 ? trimmed : null; + +// return { date, start_time, end_time, type, is_remote, is_approved, comment }; +// } \ No newline at end of file diff --git a/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts b/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts new file mode 100644 index 0000000..eab1abe --- /dev/null +++ b/src/modules/shifts/~misc_deprecated-files/upsert-shift.dto.ts @@ -0,0 +1,43 @@ +// import { Type } from "class-transformer"; +// import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator"; + +// export const COMMENT_MAX_LENGTH = 280; + +// export class ShiftPayloadDto { + +// @Matches(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/) +// date!: string; + +// @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) +// start_time!: string; + +// @Matches(/^([01]\d|2[0-3]):([0-5]\d)$/) +// end_time!: string; + +// @IsString() +// type!: string; + +// @IsBoolean() +// is_remote!: boolean; + +// @IsBoolean() +// is_approved!: boolean; + +// @IsOptional() +// @IsString() +// @MaxLength(COMMENT_MAX_LENGTH) +// comment?: string; +// }; + +// export class UpsertShiftDto { + +// @IsOptional() +// @ValidateNested() +// @Type(()=> ShiftPayloadDto) +// old_shift?: ShiftPayloadDto; + +// @IsOptional() +// @ValidateNested() +// @Type(()=> ShiftPayloadDto) +// new_shift?: ShiftPayloadDto; +// }; \ No newline at end of file diff --git a/src/modules/timesheets/timesheets.module.ts b/src/modules/timesheets/timesheets.module.ts index e51c30f..baa507e 100644 --- a/src/modules/timesheets/timesheets.module.ts +++ b/src/modules/timesheets/timesheets.module.ts @@ -2,13 +2,13 @@ import { TimesheetsController } from './controllers/timesheets.controller'; import { TimesheetsQueryService } from './services/timesheets-query.service'; import { TimesheetArchiveService } from './services/timesheet-archive.service'; import { TimesheetsCommandService } from './services/timesheets-command.service'; -import { ShiftsCommandService } from '../shifts/services/shifts-command.service'; +import { ShiftsCommandService } from '../shifts/_deprecated-files/shifts-command.service'; import { ExpensesCommandService } from '../expenses/services/expenses-command.service'; import { BusinessLogicsModule } from 'src/modules/business-logics/business-logics.module'; import { SharedModule } from '../shared/shared.module'; import { Module } from '@nestjs/common'; import { TimesheetSelectorsService } from './utils-helpers-others/timesheet.selectors'; -import { ShiftsHelpersService } from '../shifts/helpers/shifts.helpers'; +import { ShiftsHelpersService } from '../shifts/_deprecated-files/shifts.helpers'; @Module({ imports: [