From d490ad556faf91753f996e8307858c71bb9f0525 Mon Sep 17 00:00:00 2001 From: Florian Jud Date: Thu, 16 May 2024 10:01:35 +0200 Subject: [PATCH] Add more charts to home page --- package-lock.json | 391 +++++++++++++++++++++++++++++++ package.json | 1 + src/App.tsx | 8 +- src/components/Charts.tsx | 55 +++++ src/components/UserHomePage.tsx | 90 +++---- src/components/VacationGauge.tsx | 32 +++ src/interfaces/StoreTypes.ts | 28 +++ src/interfaces/Types.tsx | 2 + src/store/slices/timesSlice.ts | 7 +- src/store/thunks/timesThunks.ts | 87 ++++++- src/utils/DayjsUtils.ts | 43 +++- src/utils/HolidayUtils.ts | 4 +- src/utils/TimeUtils.ts | 50 +++- 13 files changed, 720 insertions(+), 78 deletions(-) create mode 100644 src/components/Charts.tsx create mode 100644 src/components/VacationGauge.tsx diff --git a/package-lock.json b/package-lock.json index be8e8ae..6c7ca2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@fontsource/roboto": "^5.0.13", "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.17", + "@mui/x-charts": "^7.4.0", "@mui/x-date-pickers": "^7.4.0", "@reduxjs/toolkit": "^2.2.4", "@types/node": "^20.12.12", @@ -4663,6 +4664,44 @@ } } }, + "node_modules/@mui/x-charts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.4.0.tgz", + "integrity": "sha512-W6A0ZJmfXLeAtuml0Yi7gvjxS6aW/2h6uO9PQNuE/rpV0iIEMU5bVfcJZGMVlh0WY+43FEicI1/8n6FzMcfZdg==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@react-spring/rafz": "^9.7.3", + "@react-spring/web": "^9.7.3", + "clsx": "^2.1.1", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, "node_modules/@mui/x-date-pickers": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.4.0.tgz", @@ -4908,6 +4947,71 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz", + "integrity": "sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.4.tgz", @@ -8509,6 +8613,111 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8639,6 +8848,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -11611,6 +11828,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", @@ -20671,6 +20896,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -26732,6 +26962,26 @@ "react-is": "^18.2.0" } }, + "@mui/x-charts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.4.0.tgz", + "integrity": "sha512-W6A0ZJmfXLeAtuml0Yi7gvjxS6aW/2h6uO9PQNuE/rpV0iIEMU5bVfcJZGMVlh0WY+43FEicI1/8n6FzMcfZdg==", + "requires": { + "@babel/runtime": "^7.24.0", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@react-spring/rafz": "^9.7.3", + "@react-spring/web": "^9.7.3", + "clsx": "^2.1.1", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "prop-types": "^15.8.1" + } + }, "@mui/x-date-pickers": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.4.0.tgz", @@ -26876,6 +27126,54 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "requires": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/rafz": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.3.tgz", + "integrity": "sha512-9vzW1zJPcC4nS3aCV+GgcsK/WLaB520Iyvm55ARHfM5AuyBqycjvh1wbmWmgCyJuX4VPoWigzemq1CaaeRSHhQ==" + }, + "@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "requires": { + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, "@reduxjs/toolkit": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.4.tgz", @@ -29526,6 +29824,81 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "requires": { + "delaunator": "5" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -29617,6 +29990,14 @@ "object-keys": "^1.1.1" } }, + "delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "requires": { + "robust-predicates": "^3.0.2" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -31809,6 +32190,11 @@ "side-channel": "^1.0.4" } }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", @@ -38418,6 +38804,11 @@ "glob": "^7.1.3" } }, + "robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", diff --git a/package.json b/package.json index 4398e19..0deeff6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.17", "@mui/x-date-pickers": "^7.4.0", + "@mui/x-charts": "^7.4.0", "@reduxjs/toolkit": "^2.2.4", "@types/node": "^20.12.12", "@types/react": "^18.3.2", diff --git a/src/App.tsx b/src/App.tsx index 20eaa48..a877867 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from './store/hooks'; import { setUser } from './store/slices/authSlice'; import { getAbsences } from './store/thunks/absencesThunks'; import { loadProfile } from './store/thunks/authThunks'; -import { getTimes } from './store/thunks/timesThunks'; +import { evaluateTimes, getTimes } from './store/thunks/timesThunks'; import A2HSInstaller from './utils/A2HSInstaller'; import AnalyticsTracker from './utils/AnalyticsTracker'; import { darkThemeOptions, lightThemeOptions } from './utils/MyThemeOptions'; @@ -72,6 +72,12 @@ const App = () => { return unsubscribe; }, []); + const times = useAppSelector((state) => state.times.times); + + useEffect(() => { + if (times && times.length > 0) dispatch(evaluateTimes(undefined)); + }, [times]); + return ( <> diff --git a/src/components/Charts.tsx b/src/components/Charts.tsx new file mode 100644 index 0000000..17d8c68 --- /dev/null +++ b/src/components/Charts.tsx @@ -0,0 +1,55 @@ +import { Stack, Typography } from '@mui/material'; +import { LineChart } from '@mui/x-charts'; +import { BarChart } from '@mui/x-charts/BarChart'; +import { useAppSelector } from '../store/hooks'; +import DayjsUtils from '../utils/DayjsUtils'; + +const Charts = () => { + const evaluateTimes = useAppSelector((state) => state.times.evaluateTimes); + + const dataset = evaluateTimes.map((item) => ({ + month: DayjsUtils.monthStringByNumber(item.month), + workingTime: (item.workingTime?.balance || 0) / 60, + availableTime: (item.availableTime?.balance || 0) / 60, + })); + + return ( + <> + {evaluateTimes && ( + + + Verfügungszeit-Saldo je Monat (Std.) + + + + Arbeitszeit-Saldo je Monat (Std.) + + + + )} + + ); +}; +export default Charts; diff --git a/src/components/UserHomePage.tsx b/src/components/UserHomePage.tsx index 73e9db4..8ba8c19 100644 --- a/src/components/UserHomePage.tsx +++ b/src/components/UserHomePage.tsx @@ -1,19 +1,18 @@ import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined'; -import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive'; import PsychologyAltOutlinedIcon from '@mui/icons-material/PsychologyAltOutlined'; import SickOutlinedIcon from '@mui/icons-material/SickOutlined'; import { Divider, Grid, Stack, Typography } from '@mui/material'; import { Dayjs } from 'dayjs'; -import locale from 'dayjs/locale/de'; import { useEffect, useState } from 'react'; import packageJson from '../../package.json'; import { useAppSelector } from '../store/hooks'; import { ReactComponent as HomeSvg } from '../svg/home.svg'; import AbsenceUtils from '../utils/AbsenceUtils'; import DayjsUtils from '../utils/DayjsUtils'; -import HolidayUtils from '../utils/HolidayUtils'; import TimeUtils from '../utils/TimeUtils'; +import Charts from './Charts'; import UserHomePageInfoCard from './UserHomePageInfoCard'; +import VacationGauge from './VacationGauge'; const UserHomePage = () => { const { user, profile } = useAppSelector((state) => state.auth); @@ -22,14 +21,13 @@ const UserHomePage = () => { const [noHolidays, setNoHolidays] = useState(0); const [noSickDays, setNoSickDays] = useState(0); - const [noAbsences, setNoAbsences] = useState(0); const [noAbsencesInMonth, setNoAbsencesInMonth] = useState(0); const [balanceWorkMinutes, setBalanceWorkMinutes] = useState(0); const [balanceAvailableMinutes, setBalanceAvailableMinutes] = useState(0); + const [workDaysThisMonth, setWorkDaysThisMonth] = useState(0); const [myStartfrom, setMyStartfrom] = useState(null); const to = DayjsUtils.endOfCurrentYear(); - const today = DayjsUtils.todayStart(); const firstDayOfMonth = DayjsUtils.firstDayOfCurrentMonth(); const startOfCurrentYear = DayjsUtils.startOfCurrentYear(); @@ -39,10 +37,10 @@ const UserHomePage = () => { Set the first day of the year as the start date */ useEffect(() => { - if(firstTimeDay){ - if(firstTimeDay.isAfter(startOfCurrentYear)){ + if (firstTimeDay) { + if (firstTimeDay.isAfter(startOfCurrentYear)) { setMyStartfrom(firstTimeDay); - }else{ + } else { setMyStartfrom(startOfCurrentYear); } } @@ -61,78 +59,55 @@ const UserHomePage = () => { setNoHolidays(profile.holidays); } - // get all absences - const resAbsences = AbsenceUtils.getAbsencesByTimestampRange(absences, myStartfrom.unix(), to.unix()); - if (resAbsences) setNoAbsences(resAbsences.length); - // absences in this month - const resAbsencesThisMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, firstDayOfMonth.unix(), today.unix()); + const resAbsencesThisMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, firstDayOfMonth.unix(), DayjsUtils.todayEnd().unix()); if (resAbsencesThisMonth) setNoAbsencesInMonth(resAbsencesThisMonth.length); // calculate sickness Days for this year const resSickDays = AbsenceUtils.getSickDaysByTimestampRange(sickDays, myStartfrom.unix(), to.unix()); setNoSickDays(resSickDays.length); + + // Calculate personal work days this month + const resWorkDaysMonth = TimeUtils.workDaysTillTodayInMonthByStartDate(firstDayOfMonth, profile); + setWorkDaysThisMonth(resWorkDaysMonth); } }, [profile, myStartfrom]); /* - Calculate Saldo Work Time + Calculate Saldo Work Time and Available Time */ useEffect(() => { - // calculate Saldo Work Time - if (sumMonth.workingTime && profile) { - const days: Dayjs[] = []; - const d = Math.ceil(today.diff(firstDayOfMonth, 'day', true)); - for (let i = 0; i < d; i++) { - const nextDay = firstDayOfMonth.add(i, 'day').locale({ ...locale }); - if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) { - days.push(nextDay); - } - } - const targetWorkingDays = days.length - noAbsences; - const targetWorkingMinutes = targetWorkingDays * profile.workingtime; - setBalanceWorkMinutes(sumMonth.workingTime - targetWorkingMinutes); - } - }, [sumMonth.workingTime, myStartfrom]); + if (sumMonth && profile) { + const targetDays = workDaysThisMonth - noAbsencesInMonth; - /* - Calculate Available Time - */ - useEffect(() => { - // calculate Available Time - if (sumMonth.availableTime && profile) { - const days: Dayjs[] = []; - const d = Math.ceil(today.diff(firstDayOfMonth, 'day', true)); - for (let i = 0; i < d; i++) { - const nextDay = firstDayOfMonth.add(i, 'day').locale({ ...locale }); - if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) { - days.push(nextDay); - } - } - const targetAvailableDays = days.length - noAbsencesInMonth; - const targetAvailableMinutes = targetAvailableDays * profile.availabletime; + const targetWorkingMinutes = targetDays * profile.workingtime; + const targetAvailableMinutes = targetDays * profile.availabletime; + + setBalanceWorkMinutes(sumMonth.workingTime - targetWorkingMinutes); setBalanceAvailableMinutes(sumMonth.availableTime - targetAvailableMinutes); } - }, [sumMonth.availableTime, myStartfrom]); + }, [sumMonth, workDaysThisMonth]); return ( - - Dashboard + + Hallo {user?.displayName} - Willkommen zuück, {user?.displayName}! Wir haben dich vermisst. 👋 + 👋 Willkommen zuück! Wir haben dich 😻 vermisst. - + { /> - - v{APPLICATION_VERSION} + + + v{APPLICATION_VERSION} ); diff --git a/src/components/VacationGauge.tsx b/src/components/VacationGauge.tsx new file mode 100644 index 0000000..137dc33 --- /dev/null +++ b/src/components/VacationGauge.tsx @@ -0,0 +1,32 @@ +import AirplanemodeActiveIcon from '@mui/icons-material/AirplanemodeActive'; +import { Card, CardContent, Stack, Typography } from '@mui/material'; +import { Gauge } from '@mui/x-charts'; + +interface VacationGaugePropertioes { + value: number; + valueMax: number; +} + +const VacationGauge = ({ value, valueMax }: VacationGaugePropertioes) => { + return ( + + + + + + Freie Urlaubstage + + + + + `${value} / ${valueMax}`} /> + + In diesem Jahr + + + + + ); +}; + +export default VacationGauge; diff --git a/src/interfaces/StoreTypes.ts b/src/interfaces/StoreTypes.ts index 6347258..f276ac1 100644 --- a/src/interfaces/StoreTypes.ts +++ b/src/interfaces/StoreTypes.ts @@ -9,6 +9,7 @@ export interface ITimesContext { sumMonth: ITimeDistribution; sumLastMonth: ITimeDistribution; firstTimeDay: Dayjs | null; + evaluateTimes: IBalance[]; } export interface ITimeDistribution { @@ -21,3 +22,30 @@ export interface IAbsencesContext { holidays: IAbsence[]; sickDays: IAbsence[]; } + +export interface IBalance { + year: number; + month: number; + range: { + from: number; + to: number; + }; + + days?: { + total?: number; + personal?: number; + absences?: number; + }; + + workingTime?: { + target?: number; + actual?: number; + balance?: number; + }; + + availableTime?: { + target?: number; + actual?: number; + balance?: number; + }; +} diff --git a/src/interfaces/Types.tsx b/src/interfaces/Types.tsx index f0d0875..a7b4925 100644 --- a/src/interfaces/Types.tsx +++ b/src/interfaces/Types.tsx @@ -116,3 +116,5 @@ export interface IBeforeInstallPromptEvent extends Event { }>; prompt(): Promise; } + +export type MonthName = 'Jan' | 'Feb' | 'Mar' | 'Apr' | 'May' | 'Jun' | 'Jul' | 'Aug' | 'Sep' | 'Oct' | 'Nov' | 'Dec'; diff --git a/src/store/slices/timesSlice.ts b/src/store/slices/timesSlice.ts index 2a710d5..3b7c12e 100644 --- a/src/store/slices/timesSlice.ts +++ b/src/store/slices/timesSlice.ts @@ -9,6 +9,7 @@ const initialState: ITimesContext = { sumMonth: { workingTime: 0, availableTime: 0 }, sumLastMonth: { workingTime: 0, availableTime: 0 }, firstTimeDay: null, + evaluateTimes: [], }; export const timesSlice = createSlice({ @@ -33,12 +34,16 @@ export const timesSlice = createSlice({ setFirstTimeDay: (state, action) => { state.firstTimeDay = action.payload; }, + setEvaluateTimes: (state, action) => { + state.evaluateTimes = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(getTimes.fulfilled, () => {}); }, }); -export const { setTimes, setTimesLoading, setSumYear, setSumMonth, setSumLastMonth, setFirstTimeDay } = timesSlice.actions; +export const { setTimes, setTimesLoading, setSumYear, setSumMonth, setSumLastMonth, setFirstTimeDay, setEvaluateTimes } = + timesSlice.actions; export default timesSlice.reducer; diff --git a/src/store/thunks/timesThunks.ts b/src/store/thunks/timesThunks.ts index cd7acaa..cb30387 100644 --- a/src/store/thunks/timesThunks.ts +++ b/src/store/thunks/timesThunks.ts @@ -1,9 +1,20 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { collection, deleteDoc, doc, getDocs, query, where, writeBatch } from 'firebase/firestore'; import { db } from '../../firebase/Firebase'; +import { IBalance } from '../../interfaces/StoreTypes'; import { ITime } from '../../interfaces/Types'; +import AbsenceUtils from '../../utils/AbsenceUtils'; +import DayjsUtils from '../../utils/DayjsUtils'; import TimeUtils from '../../utils/TimeUtils'; -import { setFirstTimeDay, setSumLastMonth, setSumMonth, setSumYear, setTimes, setTimesLoading } from '../slices/timesSlice'; +import { + setEvaluateTimes, + setFirstTimeDay, + setSumLastMonth, + setSumMonth, + setSumYear, + setTimes, + setTimesLoading, +} from '../slices/timesSlice'; export const getTimes = createAsyncThunk('times/all', async (action: any, thunkAPI: any) => { thunkAPI.dispatch(setTimesLoading(true)); @@ -30,7 +41,7 @@ export const getTimes = createAsyncThunk('times/all', async (action: any, thunkA // Find first day of time entries thunkAPI.dispatch(setFirstTimeDay(TimeUtils.findfirstDay(result))); - + // Set times and loading state thunkAPI.dispatch(setTimes(result)); thunkAPI.dispatch(setTimesLoading(false)); @@ -72,3 +83,75 @@ export const deleteTime = createAsyncThunk('time/delete', async (action: any, th thunkAPI.dispatch(getTimes(undefined)); }); }); + +export const evaluateTimes = createAsyncThunk('evaluate/times', async (action: any, thunkAPI: any) => { + const profile = thunkAPI.getState().auth.profile; + const times = thunkAPI.getState().times.times; + const absences = thunkAPI.getState().absences.absences; + + const dataset: IBalance[] = []; + + // iterate over each month + for (let i = 0; i < 12; i++) { + const year = DayjsUtils.startOfCurrentYear().year(); + const date = DayjsUtils.dateByMonthAndYear(i, year); + + const from = DayjsUtils.startOfMonthByDay(date).unix(); + const to = DayjsUtils.endOfMonthByDay(date).unix(); + + // result object for the current month + const result: IBalance = { + year: year, + month: i, + range: { + from: from, + to: to, + }, + }; + + // if >to< is in future, stop iteration + if (to > DayjsUtils.todayEnd().unix()) { + dataset.push(result); + continue; + } + + // calculate workdays in the current month + const workDaysInMonth = TimeUtils.workDaysInMonth(from, to, profile); + result['days'] = {}; + result['days']['total'] = workDaysInMonth; + + // filter absences for the current month + const absencesInMonth = AbsenceUtils.getAbsencesByTimestampRange(absences, from, to); + result['days']['absences'] = absencesInMonth.length; + + // calculate target values for the current month + const targetDaysInMonth = workDaysInMonth - absencesInMonth.length; + result['days']['personal'] = targetDaysInMonth; + + const targetWorkingMinutesInMonth = targetDaysInMonth * profile.workingtime; + result['workingTime'] = {}; + result['workingTime']['target'] = targetWorkingMinutesInMonth; + + const targetAvailableMinutesInMonth = targetDaysInMonth * profile.availabletime; + result['availableTime'] = {}; + result['availableTime']['target'] = targetAvailableMinutesInMonth; + + // filter times for the current month + const timesInMonth = TimeUtils.filterByTimestampRange(from, to, times); + + // calculate sums for the current month + const sumsOfMonth = TimeUtils.calculateSums(timesInMonth); + result['workingTime']['actual'] = sumsOfMonth.workingTime; + result['availableTime']['actual'] = sumsOfMonth.availableTime; + + // calculate balances for the current month + const workingTimeBalanceInMonth = sumsOfMonth.workingTime - targetWorkingMinutesInMonth; + result['workingTime']['balance'] = workingTimeBalanceInMonth; + + const availableTimeBalanceInMonth = sumsOfMonth.availableTime - targetAvailableMinutesInMonth; + result['availableTime']['balance'] = availableTimeBalanceInMonth; + + dataset.push(result); + } + thunkAPI.dispatch(setEvaluateTimes(dataset)); +}); diff --git a/src/utils/DayjsUtils.ts b/src/utils/DayjsUtils.ts index 70d7f36..4638b9f 100644 --- a/src/utils/DayjsUtils.ts +++ b/src/utils/DayjsUtils.ts @@ -2,9 +2,12 @@ import dayjs from 'dayjs'; import locale from 'dayjs/locale/de'; const today = () => { - return dayjs() - .locale({ ...locale }) -} + return dayjs().locale({ ...locale }); +}; + +const localize = (day: dayjs.Dayjs) => { + return day.locale({ ...locale }); +}; const endOfCurrentYear = () => { return today().endOf('year'); @@ -22,12 +25,12 @@ const todayEnd = () => { return today().endOf('day'); }; -const startOfLastMonth = ()=> { +const startOfLastMonth = () => { return today().subtract(1, 'month').startOf('month'); -} +}; const endOfLastMonth = () => { return today().subtract(1, 'month').endOf('month'); -} +}; const firstDayOfCurrentMonth = (date?: dayjs.Dayjs | undefined | null) => { const targetDate = date ? date : dayjs(); @@ -101,6 +104,27 @@ const firstDayOfWeek = (date?: dayjs.Dayjs | string | undefined | null) => { return targetDate.locale({ ...locale }).startOf('week'); }; +const startOfMonthByDay = (date: dayjs.Dayjs) => { + return date.startOf('month'); +}; + +const endOfMonthByDay = (date: dayjs.Dayjs) => { + return date.endOf('month'); +}; + +const dateByMonthAndYear = (month: number, year: number) => { + return dayjs().year(year).month(month); +}; + +const monthStringByNumber = (month: number) => { + return dayjs() + .locale({ + ...locale, + }) + .month(month) + .format('MMMM'); +}; + export default { todayStart, todayEnd, @@ -115,5 +139,10 @@ export default { firstDayOfWeek, startOfCurrentYear, startOfLastMonth, - endOfLastMonth + endOfLastMonth, + localize, + startOfMonthByDay, + endOfMonthByDay, + dateByMonthAndYear, + monthStringByNumber, }; diff --git a/src/utils/HolidayUtils.ts b/src/utils/HolidayUtils.ts index da8a444..70407dc 100644 --- a/src/utils/HolidayUtils.ts +++ b/src/utils/HolidayUtils.ts @@ -7,9 +7,7 @@ import TimeUtils from './TimeUtils'; export const checkIsWorkday = (workingdays: IWorkingdays, day: Dayjs, state: string) => { const workday = TimeUtils.checkWorkday(day, workingdays); const holiday = isHoliday(day, state); - if (holiday) return false; - if (workday) return true; - return false; + return workday && !holiday; }; export const isHolidayOrWeekend = (day: Dayjs): boolean => { diff --git a/src/utils/TimeUtils.ts b/src/utils/TimeUtils.ts index da8e38f..aad424d 100644 --- a/src/utils/TimeUtils.ts +++ b/src/utils/TimeUtils.ts @@ -19,13 +19,20 @@ export const numWorkdays = (workingdays: IWorkingdays): number => { }; export const checkWorkday = (day: Dayjs, workingdays: IWorkingdays): boolean => { - if (day.format('dddd') == 'Montag' && workingdays.monday) return true; - if (day.format('dddd') == 'Dienstag' && workingdays.tuesday) return true; - if (day.format('dddd') == 'Mittwoch' && workingdays.wednesday) return true; - if (day.format('dddd') == 'Donnerstag' && workingdays.thursday) return true; - if (day.format('dddd') == 'Freitag' && workingdays.friday) return true; - if (day.format('dddd') == 'Samstag' && workingdays.saturday) return true; - if (day.format('dddd') == 'Sonntag' && workingdays.sunday) return true; + const dayOfWeek = day.day(); // 0-6, represents Sunday-Saturday + + if ( + (dayOfWeek === 1 && workingdays.monday) || + (dayOfWeek === 2 && workingdays.tuesday) || + (dayOfWeek === 3 && workingdays.wednesday) || + (dayOfWeek === 4 && workingdays.thursday) || + (dayOfWeek === 5 && workingdays.friday) || + (dayOfWeek === 6 && workingdays.saturday) || + (dayOfWeek === 0 && workingdays.sunday) + ) { + return true; + } + return false; }; @@ -152,6 +159,33 @@ export const getDefaultTimeRange = () => { return { from: from, to: today }; }; +const workDaysTillTodayInMonthByStartDate = (startDay: dayjs.Dayjs, profile: IProfile): number => { + const days: Dayjs[] = []; + const today = DayjsUtils.todayStart(); + const d = Math.ceil(today.diff(startDay, 'day', true)); + for (let i = 0; i < d; i++) { + const nextDay = startDay.add(i, 'day').locale({ ...locale }); + if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) { + days.push(nextDay); + } + } + return days.length; +}; + +const workDaysInMonth = (fromTS: number, toTS: number, profile: IProfile): number => { + const from = dayjs.unix(fromTS); + const to = dayjs.unix(toTS); + let count = 0; + const d = Math.ceil(to.diff(from, 'day', true)); + for (let i = 0; i < d; i++) { + const nextDay = from.add(i, 'day'); + if (HolidayUtils.checkIsWorkday(profile.workingdays, nextDay, profile.state)) { + count++; + } + } + return count; +}; + export default { numWorkdays, checkWorkday, @@ -170,4 +204,6 @@ export default { findfirstDay, findTimeByTimestamp, getDefaultTimeRange, + workDaysTillTodayInMonthByStartDate, + workDaysInMonth, };