diff --git a/package-lock.json b/package-lock.json index 2c7e7e8..272ee43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,14 @@ "@supabase/supabase-js": "^2.105.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "d3-force": "^3.0.0", "lucide-react": "^1.16.0", "next-themes": "^0.4.6", - "pg": "^8.21.0", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", + "recharts": "^3.8.1", "shadcn": "^4.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", @@ -35,6 +36,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "pg": "^8.21.0", "tailwindcss": "^4.3.0", "vite": "^8.0.12", "vitest": "^4.1.7" @@ -2736,6 +2738,42 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", @@ -3022,7 +3060,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, "node_modules/@supabase/auth-js": { @@ -3414,6 +3457,69 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3486,6 +3592,12 @@ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", @@ -4276,6 +4388,159 @@ "devOptional": true, "license": "MIT" }, + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4302,6 +4567,12 @@ } } }, + "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==", + "license": "MIT" + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -4545,6 +4816,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4787,6 +5068,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -5523,6 +5810,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5555,6 +5852,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -6830,6 +7136,7 @@ "version": "8.21.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "dev": true, "license": "MIT", "dependencies": { "pg-connection-string": "^2.13.0", @@ -6857,6 +7164,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "dev": true, "license": "MIT", "optional": true }, @@ -6864,12 +7172,14 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "dev": true, "license": "MIT" }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, "license": "ISC", "engines": { "node": ">=4.0.0" @@ -6879,6 +7189,7 @@ "version": "3.14.0", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "dev": true, "license": "MIT", "peerDependencies": { "pg": ">=8.0" @@ -6888,12 +7199,14 @@ "version": "1.14.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "dev": true, "license": "MIT" }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, "license": "MIT", "dependencies": { "pg-int8": "1.0.1", @@ -6910,6 +7223,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, "license": "MIT", "dependencies": { "split2": "^4.1.0" @@ -6987,6 +7301,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6996,6 +7311,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7005,6 +7321,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7014,6 +7331,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, "license": "MIT", "dependencies": { "xtend": "^4.0.0" @@ -7261,6 +7579,36 @@ "react": "^19.2.6" } }, + "node_modules/react-is": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -7384,6 +7732,51 @@ "node": ">= 4" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7402,6 +7795,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7820,6 +8219,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, "license": "ISC", "engines": { "node": ">= 10.x" @@ -8339,6 +8739,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "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": "8.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", @@ -8663,6 +9085,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/package.json b/package.json index d739861..6d495ef 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "@supabase/supabase-js": "^2.105.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "d3-force": "^3.0.0", "lucide-react": "^1.16.0", "next-themes": "^0.4.6", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", + "recharts": "^3.8.1", "shadcn": "^4.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", @@ -38,9 +40,9 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "pg": "^8.21.0", "tailwindcss": "^4.3.0", "vite": "^8.0.12", - "pg": "^8.21.0", "vitest": "^4.1.7" } } diff --git a/src/components/matrix/BarChartView.jsx b/src/components/matrix/BarChartView.jsx new file mode 100644 index 0000000..c1d910d --- /dev/null +++ b/src/components/matrix/BarChartView.jsx @@ -0,0 +1,83 @@ +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, +} from 'recharts' + +const levelLabels = { + 1: { label: 'Débutant', color: '#9ca3af' }, + 2: { label: 'Intermédiaire', color: '#60a5fa' }, + 3: { label: 'Avancé', color: '#fbbf24' }, + 4: { label: 'Expert', color: '#34d399' }, +} + +export default function BarChartView({ + categories, skills: allSkills, members, levels, filterCat, + onBarClick, +}) { + const cats = filterCat === 'all' ? categories : categories.filter((c) => c.id === filterCat) + + const data = cats.map((cat) => { + const catSkills = allSkills.filter((s) => s.category_id === cat.id) + const buckets = { 1: 0, 2: 0, 3: 0, 4: 0 } + members.forEach((m) => { + catSkills.forEach((s) => { + const key = `${m.id}-${s.id}` + const lvl = levels[key]?.level + if (lvl) buckets[lvl]++ + }) + }) + return { + name: cat.name, + color: cat.color, + id: cat.id, + ...buckets, + } + }) + + if (data.length === 0) { + return
Aucune donnée à afficher
+ } + + function handleClick(entry, _index, level) { + if (onBarClick && entry?.id) { + onBarClick(entry.id, level) + } + } + + return ( +
+ + + + + + + ({ + value: `${l} - ${levelLabels[l].label}`, + type: 'rect', + color: levelLabels[l].color, + }))} + /> + {[1, 2, 3, 4].map((lvl) => ( + handleClick(entry, null, lvl)} + /> + ))} + + +
+ ) +} diff --git a/src/components/matrix/GraphView.jsx b/src/components/matrix/GraphView.jsx new file mode 100644 index 0000000..eba46b9 --- /dev/null +++ b/src/components/matrix/GraphView.jsx @@ -0,0 +1,251 @@ +import { useEffect, useRef, useState } from 'react' +import { forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide } from 'd3-force' +import { Button } from '@/components/ui/button' + +export default function GraphView({ + categories, skills: allSkills, levels, + filteredSkills, filteredMembers, +}) { + const canvasRef = useRef(null) + const [aggregated, setAggregated] = useState(false) + const [selectedNode, setSelectedNode] = useState(null) + const width = 900 + const height = 600 + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + const catMap = {} + categories.forEach((c) => { catMap[c.id] = c }) + + const nodes = [] + const links = [] + const nodeMap = new Map() + + filteredMembers.forEach((m) => { + const node = { id: m.id, type: 'member', label: m.full_name || m.email, r: 10, x: width / 2, y: height / 2 } + nodes.push(node) + nodeMap.set(m.id, node) + }) + + if (aggregated) { + categories + .filter((cat) => filteredSkills.length === 0 || filteredSkills.some((s) => s.category_id === cat.id)) + .forEach((cat) => { + const nid = `cat-${cat.id}` + const node = { id: nid, type: 'category', label: cat.name, r: 14, color: cat.color, catId: cat.id, x: width / 2, y: height / 2 } + nodes.push(node) + nodeMap.set(nid, node) + + filteredMembers.forEach((m) => { + const catSkillIds = allSkills.filter((s) => s.category_id === cat.id).map((s) => s.id) + let total = 0 + let count = 0 + catSkillIds.forEach((sid) => { + const key = `${m.id}-${sid}` + const lvl = levels[key]?.level + if (lvl) { total += lvl; count++ } + }) + if (count > 0) { + links.push({ source: m.id, target: nid, strength: total / count / 4 }) + } + }) + }) + } else { + filteredSkills.forEach((s) => { + const nid = `skill-${s.id}` + const node = { id: nid, type: 'skill', label: s.name, r: 8, color: catMap[s.category_id]?.color || '#888', skillId: s.id, x: width / 2, y: height / 2 } + nodes.push(node) + nodeMap.set(nid, node) + }) + + filteredMembers.forEach((m) => { + filteredSkills.forEach((s) => { + const key = `${m.id}-${s.id}` + const lvl = levels[key]?.level + if (lvl) { + links.push({ source: m.id, target: `skill-${s.id}`, strength: lvl / 4 }) + } + }) + }) + } + + if (nodes.length === 0) return + + const dpi = window.devicePixelRatio || 1 + canvas.width = width * dpi + canvas.height = height * dpi + ctx.scale(dpi, dpi) + + const borderColor = getComputedStyle(canvas).getPropertyValue('--border').trim() || '#e5e7eb' + const fgColor = getComputedStyle(canvas).getPropertyValue('--foreground').trim() || '#000' + + const simulation = forceSimulation(nodes) + .force('link', forceLink(links).id((d) => d.id).distance(120).strength((d) => d.strength || 0.3)) + .force('charge', forceManyBody().strength(-150)) + .force('center', forceCenter(width / 2, height / 2)) + .force('collide', forceCollide(20)) + .on('tick', ticked) + + function ticked() { + ctx.clearRect(0, 0, width, height) + + ctx.strokeStyle = borderColor + ctx.lineWidth = 1 + links.forEach((l) => { + const s = l.strength || 0.3 + ctx.globalAlpha = s * 0.5 + 0.15 + ctx.beginPath() + ctx.moveTo(l.source.x, l.source.y) + ctx.lineTo(l.target.x, l.target.y) + ctx.stroke() + }) + + ctx.globalAlpha = 1 + nodes.forEach((n) => { + const isSel = selectedNode === n.id + const isNeighbor = isSel + ? links.some((l) => { + const sid = typeof l.source === 'object' ? l.source.id : l.source + const tid = typeof l.target === 'object' ? l.target.id : l.target + return (sid === selectedNode && tid === n.id) || (tid === selectedNode && sid === n.id) + }) + : false + + ctx.globalAlpha = selectedNode && !isSel && !isNeighbor ? 0.12 : 1 + + if (n.type === 'member') { + ctx.beginPath() + ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI) + ctx.fillStyle = '#6366f1' + ctx.fill() + if (isSel) { + ctx.strokeStyle = '#fff' + ctx.lineWidth = 2 + ctx.stroke() + } + } else if (n.type === 'category') { + ctx.beginPath() + ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI) + ctx.fillStyle = n.color || '#888' + ctx.fill() + if (isSel) { + ctx.strokeStyle = '#fff' + ctx.lineWidth = 2 + ctx.stroke() + } + } else { + const s = 6 + ctx.fillStyle = n.color || '#888' + ctx.fillRect(n.x - s / 2, n.y - s / 2, s, s) + if (isSel) { + ctx.strokeStyle = '#fff' + ctx.lineWidth = 2 + ctx.strokeRect(n.x - s / 2, n.y - s / 2, s, s) + } + } + + ctx.fillStyle = fgColor + ctx.font = '10px sans-serif' + ctx.textAlign = 'center' + ctx.fillText(n.label, n.x, n.y + n.r + 12) + }) + + ctx.globalAlpha = 1 + } + + let dragNode = null + + function getCanvasPos(e) { + const rect = canvas.getBoundingClientRect() + return { + x: (e.clientX - rect.left) * (width / rect.width), + y: (e.clientY - rect.top) * (height / rect.height), + } + } + + function findHit(px, py) { + for (let i = nodes.length - 1; i >= 0; i--) { + const n = nodes[i] + const dx = px - n.x + const dy = py - n.y + if (dx * dx + dy * dy <= (n.r + 8) * (n.r + 8)) return n + } + return null + } + + function onPointerDown(e) { + const pos = getCanvasPos(e) + const hit = findHit(pos.x, pos.y) + if (hit) { + dragNode = hit + setSelectedNode(hit.id) + hit.fx = hit.x + hit.fy = hit.y + simulation.alphaTarget(0.3).restart() + } + } + + function onPointerMove(e) { + if (!dragNode) return + const pos = getCanvasPos(e) + dragNode.fx = Math.max(0, Math.min(width, pos.x)) + dragNode.fy = Math.max(0, Math.min(height, pos.y)) + } + + function onPointerUp() { + if (dragNode) { + dragNode.fx = null + dragNode.fy = null + dragNode = null + simulation.alphaTarget(0) + } + } + + canvas.addEventListener('pointerdown', onPointerDown) + window.addEventListener('pointermove', onPointerMove) + window.addEventListener('pointerup', onPointerUp) + + return () => { + simulation.stop() + canvas.removeEventListener('pointerdown', onPointerDown) + window.removeEventListener('pointermove', onPointerMove) + window.removeEventListener('pointerup', onPointerUp) + } + }, [filteredSkills, filteredMembers, levels, categories, aggregated, selectedNode, allSkills]) + + return ( +
+
+
+ + Membre + + + Compétence + + + Catégorie + +
+ +
+ +
+ +
+ Cliquez sur un nœud pour le sélectionner • Glissez pour déplacer +
+
+
+ ) +} diff --git a/src/components/matrix/HeatmapView.jsx b/src/components/matrix/HeatmapView.jsx new file mode 100644 index 0000000..1501b70 --- /dev/null +++ b/src/components/matrix/HeatmapView.jsx @@ -0,0 +1,157 @@ +import { useState } from 'react' +import { ChevronDown, ChevronRight, ListCollapse } from 'lucide-react' +import { SkillLevelSelect } from '@/components/SkillLevelBadge' + +const heatColors = { + 0: 'bg-gray-50 dark:bg-gray-900', + 1: 'bg-gray-200 dark:bg-gray-700', + 2: 'bg-blue-200 dark:bg-blue-900', + 3: 'bg-amber-200 dark:bg-amber-900', + 4: 'bg-green-200 dark:bg-green-900', +} + +export function HeatmapView({ + categories, levels, + isAdmin, currentUserId, + editing, onEdit, onUpdate, onCancel, + filteredSkills, visibleMembers, +}) { + const [collapsed, setCollapsed] = useState(new Set()) + + const grouped = categories + .map((cat) => ({ + ...cat, + catSkills: filteredSkills.filter((s) => s.category_id === cat.id), + })) + .filter((g) => g.catSkills.length > 0) + + const allCollapsed = grouped.every((g) => collapsed.has(g.id)) + + function toggleCategory(catId) { + setCollapsed((prev) => { + const next = new Set(prev) + if (next.has(catId)) next.delete(catId) + else next.add(catId) + return next + }) + } + + function toggleAll() { + if (allCollapsed) { + setCollapsed(new Set()) + } else { + setCollapsed(new Set(grouped.map((g) => g.id))) + } + } + + if (visibleMembers.length === 0) { + return ( +
+ Aucun résultat +
+ ) + } + + return ( +
+ + + + + {visibleMembers.map((m) => ( + + ))} + + + + {grouped.map((g) => { + const isCollapsed = collapsed.has(g.id) + return ( + <> + + + {visibleMembers.map((m) => { + const dotColors = ['bg-gray-200', 'bg-blue-200', 'bg-amber-200', 'bg-green-200'] + const counts = [0, 0, 0, 0] + g.catSkills.forEach((s) => { + const lvl = levels[`${m.id}-${s.id}`]?.level + if (lvl) counts[lvl - 1]++ + }) + return ( + + ) + })} + + {!isCollapsed && g.catSkills.map((s) => ( + + + {visibleMembers.map((m) => { + const key = `${m.id}-${s.id}` + const level = levels[key] + const lvl = level?.level || 0 + const canEditCell = isAdmin || currentUserId === m.id + const isEditing = editing === key + return ( + + ) + })} + + ))} + + ) + })} + +
+ + Compétence + + {m.full_name || m.email} +
toggleCategory(g.id)}> + + {isCollapsed ? : } + + {g.name} + + + + {counts.map((c, i) => + c > 0 ? ( + + + {c} + + ) : null + )} + +
+ {s.name} + + {isEditing && canEditCell ? ( + onUpdate(m.id, s.id, v)} + /> + ) : ( + canEditCell && onEdit(key)} + > + {lvl || '—'} + + )} + {isEditing && canEditCell && ( + + )} +
+
+ ) +} diff --git a/src/components/matrix/RadarView.jsx b/src/components/matrix/RadarView.jsx new file mode 100644 index 0000000..c1ef33b --- /dev/null +++ b/src/components/matrix/RadarView.jsx @@ -0,0 +1,104 @@ +import { useState } from 'react' +import { + RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, Tooltip, ResponsiveContainer, Legend, +} from 'recharts' + +export default function RadarView({ + categories, skills: allSkills, members, levels, filterMember, +}) { + const [selectedMember, setSelectedMember] = useState(filterMember !== 'all' ? filterMember : (members[0]?.id || '')) + + function getCategoryAvg(catId) { + const catSkills = allSkills.filter((s) => s.category_id === catId) + if (catSkills.length === 0) return 0 + let total = 0 + let count = 0 + members.forEach((m) => { + catSkills.forEach((s) => { + const key = `${m.id}-${s.id}` + const lvl = levels[key]?.level + if (lvl) { total += lvl; count++ } + }) + }) + return count > 0 ? +(total / count).toFixed(1) : 0 + } + + function getMemberAvg(catId, memberId) { + const catSkills = allSkills.filter((s) => s.category_id === catId) + if (catSkills.length === 0) return 0 + let total = 0 + let count = 0 + catSkills.forEach((s) => { + const key = `${memberId}-${s.id}` + const lvl = levels[key]?.level + if (lvl) { total += lvl; count++ } + }) + return count > 0 ? +(total / count).toFixed(1) : 0 + } + + const data = categories.map((cat) => { + const teamAvg = getCategoryAvg(cat.id) + const memberAvg = selectedMember ? getMemberAvg(cat.id, selectedMember) : 0 + return { + category: cat.name, + color: cat.color, + teamAvg, + memberAvg, + } + }) + + const selectedMemberName = members.find((m) => m.id === selectedMember)?.full_name || 'Membre' + + if (data.length === 0) { + return
Aucune donnée à afficher
+ } + + return ( +
+
+ + +
+ + + + + + + + + + + + +
+ ) +} diff --git a/src/components/matrix/ScatterView.jsx b/src/components/matrix/ScatterView.jsx new file mode 100644 index 0000000..290266f --- /dev/null +++ b/src/components/matrix/ScatterView.jsx @@ -0,0 +1,126 @@ +import { + ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, + Tooltip, ResponsiveContainer, Legend, +} from 'recharts' + +export default function ScatterView({ + members, levels, categories, + filterMember, + filteredSkills, onMemberSelect, +}) { + const data = [] + const catGaps = {} + let xPos = 0 + + categories.forEach((cat) => { + const catSkills = filteredSkills.filter((s) => s.category_id === cat.id) + if (catSkills.length === 0) return + xPos += 1 + catGaps[cat.id] = { start: xPos, end: xPos + catSkills.length - 1 } + catSkills.forEach((s) => { + const x = xPos++ + members.forEach((m) => { + if (filterMember !== 'all' && m.id !== filterMember) return + const key = `${m.id}-${s.id}` + const lvl = levels[key]?.level + if (!lvl) return + data.push({ + x, + y: lvl + (Math.random() - 0.5) * 0.3, + memberName: m.full_name || m.email, + skillName: s.name, + level: lvl, + category: cat.name, + categoryColor: cat.color, + memberId: m.id, + }) + }) + }) + }) + + const ticks = categories + .filter((cat) => catGaps[cat.id]) + .map((cat) => ({ + value: (catGaps[cat.id].start + catGaps[cat.id].end) / 2, + label: cat.name, + })) + + if (data.length === 0) { + return
Aucune donnée à afficher
+ } + + return ( +
+ + + + t.value)} + tickFormatter={(v) => ticks.find((t) => t.value === v)?.label || ''} + stroke="var(--foreground)" + tick={{ fontSize: 12 }} + /> + ['', '1 - Débutant', '2 - Intermédiaire', '3 - Avancé', '4 - Expert'][v]} + stroke="var(--foreground)" + tick={{ fontSize: 12 }} + /> + + { + if (name === 'y') return null + return [val, name] + }} + labelFormatter={() => ''} + content={({ active, payload }) => { + if (!active || !payload?.[0]) return null + const d = payload[0].payload + return ( +
+

{d.memberName}

+

{d.skillName}

+

Niveau : {d.level}

+

{d.category}

+
+ ) + }} + /> + ({ + id: c.id, + value: c.name, + type: 'circle', + color: c.color, + }))} + /> + {categories.map((cat) => { + const catData = data.filter((d) => d.category === cat.name) + return ( + onMemberSelect?.(point.memberId)} + style={{ cursor: 'pointer' }} + /> + ) + })} +
+
+
+ ) +} diff --git a/src/components/matrix/SkillMatrixFilters.jsx b/src/components/matrix/SkillMatrixFilters.jsx index a37e744..c69a255 100644 --- a/src/components/matrix/SkillMatrixFilters.jsx +++ b/src/components/matrix/SkillMatrixFilters.jsx @@ -1,4 +1,4 @@ -export function SkillMatrixFilters({ categories, members, filterCat, filterMember, filterMinLevel, onFilterChange }) { +export function SkillMatrixFilters({ categories, members, filterCat, filterMember, filterMinLevel, onFilterChange, hideMember }) { return (
@@ -14,19 +14,21 @@ export function SkillMatrixFilters({ categories, members, filterCat, filterMembe ))}
-
- - -
+ {!hideMember && ( +
+ + +
+ )}