From c30291a5c5675698d8a63aeb2b6a448922b6a5d3 Mon Sep 17 00:00:00 2001 From: Heisjabo Date: Wed, 31 Jul 2024 11:09:40 +0200 Subject: [PATCH] feat(seller-stats): Implement seller statistics in the dashboard --- .eslintrc.cjs | 1 + package-lock.json | 269 ++++++++++++++++++++++- package.json | 1 + src/__test__/sellerStats.test.tsx | 46 ++++ src/assets/Icon.png | Bin 0 -> 290 bytes src/assets/solar_bag-3-linear.png | Bin 0 -> 440 bytes src/assets/solar_graph-up-linear.png | Bin 0 -> 486 bytes src/components/dashboard/Header.tsx | 12 +- src/components/dashboard/SalesChart.tsx | 137 ++++++++++++ src/dashboard/sellers/Index.tsx | 106 ++++++++- src/dashboard/sellers/RecentProducts.tsx | 94 ++++++++ src/redux/reducers/sellerStats.ts | 59 +++++ src/redux/store.ts | 2 + src/utils/transformData.ts | 61 +++++ 14 files changed, 772 insertions(+), 16 deletions(-) create mode 100644 src/__test__/sellerStats.test.tsx create mode 100644 src/assets/Icon.png create mode 100644 src/assets/solar_bag-3-linear.png create mode 100644 src/assets/solar_graph-up-linear.png create mode 100644 src/components/dashboard/SalesChart.tsx create mode 100644 src/dashboard/sellers/RecentProducts.tsx create mode 100644 src/redux/reducers/sellerStats.ts create mode 100644 src/utils/transformData.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6413569..fa65cb2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -61,6 +61,7 @@ module.exports = { "no-console": "off", "no-undef": "off", "@typescript-eslint/no-unused-vars": "off", + "react/no-unstable-nested-components": "off", "jsx-a11y/mouse-events-have-key-events": "off", "react/button-has-type": "off", "react/no-array-index-key": "off", diff --git a/package-lock.json b/package-lock.json index 17bc653..2ee6e76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "react-router-dom": "^6.23.1", "react-spinners": "^0.14.1", "react-toastify": "^10.0.5", + "recharts": "^2.12.7", "redux": "^5.0.1", "redux-logger": "^3.0.6", "redux-thunk": "^3.1.0", @@ -3967,6 +3968,60 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -5789,6 +5844,116 @@ "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-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "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/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5908,6 +6073,11 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -7217,6 +7387,14 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8113,6 +8291,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/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -10381,8 +10567,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -14281,6 +14466,20 @@ "react-dom": ">=16.8" } }, + "node_modules/react-smooth": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-spinners": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.14.1.tgz", @@ -14336,6 +14535,46 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/recharts/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -15433,6 +15672,11 @@ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15880,6 +16124,27 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", diff --git a/package.json b/package.json index 7b88bb6..a9eb3d6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "react-router-dom": "^6.23.1", "react-spinners": "^0.14.1", "react-toastify": "^10.0.5", + "recharts": "^2.12.7", "redux": "^5.0.1", "redux-logger": "^3.0.6", "redux-thunk": "^3.1.0", diff --git a/src/__test__/sellerStats.test.tsx b/src/__test__/sellerStats.test.tsx new file mode 100644 index 0000000..dc24383 --- /dev/null +++ b/src/__test__/sellerStats.test.tsx @@ -0,0 +1,46 @@ +import { configureStore } from "@reduxjs/toolkit"; + +import sellerStatsReducer, { fetchStats } from "../redux/reducers/sellerStats"; + +describe("sellerStats slice", () => { + let store; + + beforeEach(() => { + store = configureStore({ + reducer: { + sellerStatistics: sellerStatsReducer, + }, + }); + }); + + it("should handle initial state", () => { + const { sellerStatistics } = store.getState(); + expect(sellerStatistics).toEqual({ + loading: false, + data: [], + error: null, + }); + }); + + it("should handle fetchStats.pending", () => { + store.dispatch(fetchStats.pending("sellerStatistics")); + const { sellerStatistics } = store.getState(); + expect(sellerStatistics.loading).toBe(true); + }); + + it("should handle fetchStats.fulfilled", () => { + const stats = [{ id: 1 }, { id: 2 }]; + store.dispatch(fetchStats.fulfilled(stats, "sellerStatistics")); + const { sellerStatistics } = store.getState(); + expect(sellerStatistics.loading).toBe(false); + expect(sellerStatistics.data).toEqual(stats); + }); + + it("should handle fetchStats.rejected", () => { + const error = new Error("Error message"); + store.dispatch(fetchStats.rejected(error, "sellerStatistics")); + const { sellerStatistics } = store.getState(); + expect(sellerStatistics.loading).toBe(false); + expect(sellerStatistics.error).toBe(error.message); + }); +}); diff --git a/src/assets/Icon.png b/src/assets/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b14cf099871aa3f8ce7aff52a92d61a1b435e5 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W;!3HFgc;@~FQk(@Ik;M!Q+`=Ht$S`Y;1W=H% zILO_JVcj{Imp~3nx}&cn1H;CC?mvmFK>l`57srqa#-$TB@*Xk}aQ)B9e?VhP&Owod zE?XL0G+qR6b~RhTyseS7=#Hg|(xmfcQ}l1z-Ms&PpYMq)-zsl0?&++Q$bCFPlTBDM zT{l^^-P?)rkk8chha;;(qqyJfJHW$haBOLRd_>w`<9bErvgr8Lb^AA*`INpjj>Azm zLf}XKyJHek%`0P875}t2^=9E*O(tp8EmpsOO*dhc7FsrMVYQv+L)}tYCc_Iayfklw jST@$C+>M^Q{1vzR<*F5Vi#NIfy};n<>gTe~DWM4ff)#2s literal 0 HcmV?d00001 diff --git a/src/assets/solar_bag-3-linear.png b/src/assets/solar_bag-3-linear.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1fd11252d6a6dcaff525a4493c0e37617c5552 GIT binary patch literal 440 zcmV;p0Z0CcP)8~^s$nE7(rvxs**@Fk@$sI5M1bBmN>2UOvso*FG1-Qk`u`WC+_88pAMjN%Mih^nwf6$eVY7b3r{rno@P)TAwDG0onms-vkQFA-{9c0e?F>+ukRmR*xthA@mvH- z%yq1KIJyP8sv=1AAZU`M-CCLta%GoLK+A3}SVCt|0a=(aZ8`=TrUZKqou;*qj}y>0 z!Nwcf$B$#$Hp;b#OFMotWh2K@pUYGewA5UQ=5R0N+$x}v@UW$(&#Wn({EC{2k+ void; } -const Header: React.FC = ({ toggleSidebar }) => { +const Header = ({ toggleSidebar }: any) => { const [target, setTarget] = useState(null); - const { profile } = useSelector((state: RootState) => state.usersProfile); + const { profile } = useSelector((state: any) => state.usersProfile); const dispatch = useAppDispatch(); const { unreadCount } = useAppSelector((state) => state.notifications); const [showDropdown, setShowDropdown] = useState(false); @@ -93,7 +91,7 @@ const Header: React.FC = ({ toggleSidebar }) => { onClick={() => setShowDropdown(!showDropdown)} /> ) : ( - setShowDropdown(!showDropdown)} /> diff --git a/src/components/dashboard/SalesChart.tsx b/src/components/dashboard/SalesChart.tsx new file mode 100644 index 0000000..7cd645a --- /dev/null +++ b/src/components/dashboard/SalesChart.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; +import { FaCircle } from "react-icons/fa"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const SalesChart = ({ data }) => { + const isMobile = useMediaQuery("(max-width:600px)"); + + const CustomBar = (props) => { + const { + x, y, width, height, fill, + } = props; + const barWidth = isMobile ? 10 : 20; + const centeredX = x + (width - barWidth) / 2; + const radius = 7; + return ( + + ); + }; + + const CustomTooltip = ({ active, payload, coordinate }: any) => { + if (active && payload && payload.length) { + const sales = payload.find((entry) => entry.dataKey === "sales")?.value; + const products = payload.find( + (entry) => entry.dataKey === "products", + )?.value; + const top = coordinate.y - 200; + const left = coordinate.x - 48; + + const salesValue = sales; + const productsValue = products; + + return ( +
+
+ + Sales +
+
+ {salesValue} +
+
+ + Products +
+
+ {productsValue} +
+
+ ); + } + + return null; + }; + + return ( +
+
+
+

+ Trades Overview +

+
+ + + Sales + +
+
+ + + Products + +
+
+
+ + + + + + } + cursor={{ fill: "transparent" }} + /> + } + /> + } + /> + + +
+ ); +}; + +export default SalesChart; diff --git a/src/dashboard/sellers/Index.tsx b/src/dashboard/sellers/Index.tsx index f0f5468..0e74dca 100644 --- a/src/dashboard/sellers/Index.tsx +++ b/src/dashboard/sellers/Index.tsx @@ -1,11 +1,103 @@ +import { Link } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useEffect, useState } from "react"; +import { RiCoinsLine } from "react-icons/ri"; +import { GoArrowDownRight } from "react-icons/go"; + import Layout from "../../components/layouts/SellerLayout"; +import SalesChart from "../../components/dashboard/SalesChart"; +import Button from "../../components/dashboard/Button"; +import { useAppDispatch } from "../../redux/hooks"; +import { fetchStats } from "../../redux/reducers/sellerStats"; +import { transformStatsData } from "../../utils/transformData"; +import solarGraph from "../../assets/solar_graph-up-linear.png"; +import solarBag from "../../assets/solar_bag-3-linear.png"; + +import RecentProducts from "./RecentProducts"; + +const Index = () => { + const sellerStats = useSelector((state: any) => state.sellerStats.data); + const dispatch = useAppDispatch(); + const [transformedData, setTransformedData] = useState({}); + + useEffect(() => { + dispatch(fetchStats()); + }, [dispatch]); + + useEffect(() => { + const transformedData = transformStatsData(sellerStats); + setTransformedData(transformedData); + }, [sellerStats]); + + console.log(transformedData); -const Index = () => ( - -
-

Dashboard

-
-
-); + return ( + +
+
+
+
+
+
+ solar graph +
+

+ Total Sales +

+
+

{transformedData.totalSales}

+
+
+
+
+ +
+

+ Total Revenue +

+
+

+ RWF + {transformedData.totalRevenue} +

+
+
+
+
+ solar graph +
+

+ Expired products +

+
+

+ {transformedData.expiredProducts?.length} +

+
+
+
+
+ +
+

+ Total loss +

+
+

+ RWF + {' '} + {transformedData.totalLosses} +

+
+
+
+ + +
+
+
+
+ ); +}; export default Index; diff --git a/src/dashboard/sellers/RecentProducts.tsx b/src/dashboard/sellers/RecentProducts.tsx new file mode 100644 index 0000000..dc227aa --- /dev/null +++ b/src/dashboard/sellers/RecentProducts.tsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from "react"; +import { useSelector } from "react-redux"; + +import { useAppDispatch } from "../../redux/hooks"; +import Spinner from "../../components/dashboard/Spinner"; +import { fetchProducts } from "../../redux/reducers/productsSlice"; + +const Table: React.FC = () => { + const dispatch = useAppDispatch(); + const products = useSelector((state: any) => state.products.data); + const loading = useSelector((state: any) => state.products.loading); + + useEffect(() => { + dispatch(fetchProducts()); + }, [dispatch]); + + const currentItems = products + .slice(0, 5) + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + + return ( +
+
+

+ Recent products +

+
+
+
+ + + + + + + + + + + + {loading ? ( + + + + ) : currentItems.length === 0 ? ( + + + + ) : ( + currentItems.map((item: any) => ( + + + + + + + )) + )} + +
+ Product + + category + + CreatedAt + + Price +
+

Loading best selling stocks...

+ +
+ No stocks found. +
+ {item.name} + {item.name} + + {item.category.name} + + {new Date(item.createdAt).toDateString()} + {item.price}
+
+
+
+ ); +}; + +export default Table; diff --git a/src/redux/reducers/sellerStats.ts b/src/redux/reducers/sellerStats.ts new file mode 100644 index 0000000..1f0f13b --- /dev/null +++ b/src/redux/reducers/sellerStats.ts @@ -0,0 +1,59 @@ +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import { AxiosError } from "axios"; + +import api from "../api/api"; + +export const fetchStats = createAsyncThunk( + "sellerStatistics", + async (_, { rejectWithValue }) => { + try { + const response = await api.get("/stats", { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + }, + }); + return response.data; + } catch (err: any) { + const error = err as AxiosError; + return rejectWithValue(error.response?.data); + } + }, +); + +interface IStatistics { + loading: boolean; + data: any[]; + error: string | null; +} + +const initialState: IStatistics = { + loading: false, + data: [], + error: null, +}; + +const sellerStatsSlice = createSlice({ + name: "sellerStatistics", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(fetchStats.pending, (state) => { + if (state.data.length === 0) { + state.loading = true; + } + }) + .addCase(fetchStats.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.data = action.payload; + }) + .addCase(fetchStats.rejected, (state, action) => { + state.loading = false; + // @ts-ignore + state.error = action.payload || action.error.message; + }); + }, +}); + +export default sellerStatsSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 5dccda1..ba35c54 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -20,6 +20,7 @@ import wishListSlice from "./reducers/wishListSlice"; import ordersReducer from "./reducers/ordersSlice"; import PaymentSlice from "./reducers/payment"; import adsReducer from "./reducers/listAddSlice"; +import sellerStatsReducer from "./reducers/sellerStats"; const store = configureStore({ reducer: { @@ -43,6 +44,7 @@ const store = configureStore({ order: ordersReducer, payment: PaymentSlice, ads: adsReducer, + sellerStats: sellerStatsReducer, }, }); export type RootState = ReturnType; diff --git a/src/utils/transformData.ts b/src/utils/transformData.ts new file mode 100644 index 0000000..ce30811 --- /dev/null +++ b/src/utils/transformData.ts @@ -0,0 +1,61 @@ +export const transformStatsData = (stats) => { + const monthNames = { + January: "Jan", + February: "Feb", + March: "Mar", + April: "Apr", + May: "May", + June: "Jun", + July: "Jul", + August: "Aug", + September: "Sep", + October: "Oct", + November: "Nov", + December: "Dec", + }; + + const monthOrder = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + const currentMonthIndex = new Date().getMonth(); + + const chartData = stats + .map((stat) => ({ + name: monthNames[stat.month], + sales: stat.monthlySales, + products: stat.totalProducts, + })) + .filter((data) => monthOrder.indexOf(data.name) <= currentMonthIndex) + .sort((a, b) => monthOrder.indexOf(a.name) - monthOrder.indexOf(b.name)); + + const totalLosses = stats.reduce((acc, stat) => acc + stat.losses, 0); + const expiredProducts = stats.reduce( + (acc, stat) => acc.concat(stat.expiredProducts), + [], + ); + const totalSales = stats.reduce((acc, stat) => acc + stat.monthlySales, 0); + const totalRevenue = stats.reduce( + (acc, stat) => acc + stat.monthlyRevenue, + 0, + ); + + return { + chartData, + totalLosses, + expiredProducts, + totalSales, + totalRevenue, + }; +};