Tâches Récentes
@@ -103,7 +104,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
>
{(() => {
try {
- return getPriorityConfig(task.priority as any).label;
+ return getPriorityConfig(task.priority as TaskPriority).label;
} catch {
return task.priority;
}
diff --git a/package-lock.json b/package-lock.json
index 28f51c7..ed82429 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "towercontrol-temp",
+ "name": "towercontrol",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "towercontrol-temp",
+ "name": "towercontrol",
"version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -18,6 +18,7 @@
"prisma": "^6.16.1",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "recharts": "^3.2.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1"
},
@@ -1149,6 +1150,32 @@
"@prisma/debug": "6.16.1"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
+ "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "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/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1169,6 +1196,12 @@
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"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/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1475,6 +1508,69 @@
"tslib": "^2.4.0"
}
},
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "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/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1510,7 +1606,7 @@
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -1526,6 +1622,12 @@
"@types/react": "^19.0.0"
}
},
+ "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/@typescript-eslint/eslint-plugin": {
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
@@ -2887,9 +2989,130 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "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-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-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "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-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/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -2979,6 +3202,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/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -3376,6 +3605,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.39.10",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
+ "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3862,6 +4101,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -4515,6 +4760,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
+ "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
+ "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",
@@ -4598,6 +4853,15 @@
"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==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
@@ -6560,9 +6824,31 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+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/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -6590,6 +6876,48 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/recharts": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz",
+ "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 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/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6634,6 +6962,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "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": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -7618,6 +7952,12 @@
"node": ">=18"
}
},
+ "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==",
+ "license": "MIT"
+ },
"node_modules/tinyexec": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
@@ -7925,12 +8265,43 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "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/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index ac836f7..384aa8f 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"prisma": "^6.16.1",
"react": "19.1.0",
"react-dom": "19.1.0",
+ "recharts": "^3.2.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1"
},
diff --git a/services/analytics.ts b/services/analytics.ts
new file mode 100644
index 0000000..61878b4
--- /dev/null
+++ b/services/analytics.ts
@@ -0,0 +1,292 @@
+import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
+import { prisma } from './database';
+
+export interface ProductivityMetrics {
+ completionTrend: Array<{
+ date: string;
+ completed: number;
+ created: number;
+ total: number;
+ }>;
+ velocityData: Array<{
+ week: string;
+ completed: number;
+ average: number;
+ }>;
+ priorityDistribution: Array<{
+ priority: string;
+ count: number;
+ percentage: number;
+ }>;
+ statusFlow: Array<{
+ status: string;
+ count: number;
+ percentage: number;
+ }>;
+ weeklyStats: {
+ thisWeek: number;
+ lastWeek: number;
+ change: number;
+ changePercent: number;
+ };
+}
+
+export interface TimeRange {
+ start: Date;
+ end: Date;
+}
+
+export class AnalyticsService {
+ /**
+ * Calcule les métriques de productivité pour une période donnée
+ */
+ static async getProductivityMetrics(timeRange?: TimeRange): Promise
{
+ try {
+ const now = new Date();
+ const defaultStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 jours
+
+ const start = timeRange?.start || defaultStart;
+ const end = timeRange?.end || now;
+
+ // Récupérer toutes les tâches depuis la base de données avec leurs tags
+ const dbTasks = await prisma.task.findMany({
+ include: {
+ taskTags: {
+ include: {
+ tag: true
+ }
+ }
+ }
+ });
+
+ // Convertir en format Task
+ const tasks: Task[] = dbTasks.map(task => ({
+ id: task.id,
+ title: task.title,
+ description: task.description || undefined,
+ status: task.status as TaskStatus,
+ priority: task.priority as TaskPriority,
+ source: task.source as TaskSource,
+ sourceId: task.sourceId || undefined,
+ tags: task.taskTags.map(taskTag => taskTag.tag.name),
+ dueDate: task.dueDate || undefined,
+ completedAt: task.completedAt || undefined,
+ createdAt: task.createdAt,
+ updatedAt: task.updatedAt,
+ jiraProject: task.jiraProject || undefined,
+ jiraKey: task.jiraKey || undefined,
+ jiraType: task.jiraType || undefined,
+ assignee: task.assignee || undefined
+ }));
+
+ return {
+ completionTrend: this.calculateCompletionTrend(tasks, start, end),
+ velocityData: this.calculateVelocity(tasks, start, end),
+ priorityDistribution: this.calculatePriorityDistribution(tasks),
+ statusFlow: this.calculateStatusFlow(tasks),
+ weeklyStats: this.calculateWeeklyStats(tasks)
+ };
+ } catch (error) {
+ console.error('Erreur lors du calcul des métriques:', error);
+ throw new Error('Impossible de calculer les métriques de productivité');
+ }
+ }
+
+ /**
+ * Calcule la tendance de completion des tâches par jour
+ */
+ private static calculateCompletionTrend(tasks: Task[], start: Date, end: Date) {
+ const trend: Array<{ date: string; completed: number; created: number; total: number }> = [];
+
+ // Générer les dates pour la période
+ const currentDate = new Date(start);
+ while (currentDate <= end) {
+ const dateStr = currentDate.toISOString().split('T')[0];
+
+ // Compter les tâches terminées ce jour
+ const completedThisDay = tasks.filter(task =>
+ task.completedAt &&
+ task.completedAt.toISOString().split('T')[0] === dateStr
+ ).length;
+
+ // Compter les tâches créées ce jour
+ const createdThisDay = tasks.filter(task =>
+ task.createdAt.toISOString().split('T')[0] === dateStr
+ ).length;
+
+ // Total cumulé jusqu'à ce jour
+ const totalUntilThisDay = tasks.filter(task =>
+ new Date(task.createdAt) <= currentDate
+ ).length;
+
+ trend.push({
+ date: dateStr,
+ completed: completedThisDay,
+ created: createdThisDay,
+ total: totalUntilThisDay
+ });
+
+ currentDate.setDate(currentDate.getDate() + 1);
+ }
+
+ return trend;
+ }
+
+ /**
+ * Calcule la vélocité (tâches terminées par semaine)
+ */
+ private static calculateVelocity(tasks: Task[], start: Date, end: Date) {
+ const weeklyData: Array<{ week: string; completed: number; average: number }> = [];
+ const completedTasks = tasks.filter(task => task.completedAt);
+
+ // Grouper par semaine
+ const weekGroups = new Map();
+
+ completedTasks.forEach(task => {
+ if (task.completedAt && task.completedAt >= start && task.completedAt <= end) {
+ const weekStart = this.getWeekStart(task.completedAt);
+ const weekKey = weekStart.toISOString().split('T')[0];
+ weekGroups.set(weekKey, (weekGroups.get(weekKey) || 0) + 1);
+ }
+ });
+
+ // Calculer la moyenne mobile
+ const values = Array.from(weekGroups.values());
+ const average = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
+
+ // Convertir en format pour le graphique
+ weekGroups.forEach((count, weekKey) => {
+ const weekDate = new Date(weekKey);
+ weeklyData.push({
+ week: `Sem. ${this.getWeekNumber(weekDate)}`,
+ completed: count,
+ average: Math.round(average * 10) / 10
+ });
+ });
+
+ return weeklyData.sort((a, b) => a.week.localeCompare(b.week));
+ }
+
+ /**
+ * Calcule la distribution des priorités
+ */
+ private static calculatePriorityDistribution(tasks: Task[]) {
+ const priorityCounts = new Map();
+ const total = tasks.length;
+
+ tasks.forEach(task => {
+ const priority = task.priority || 'non-définie';
+ priorityCounts.set(priority, (priorityCounts.get(priority) || 0) + 1);
+ });
+
+ return Array.from(priorityCounts.entries()).map(([priority, count]) => ({
+ priority: this.getPriorityLabel(priority),
+ count,
+ percentage: Math.round((count / total) * 100)
+ }));
+ }
+
+ /**
+ * Calcule la distribution des statuts
+ */
+ private static calculateStatusFlow(tasks: Task[]) {
+ const statusCounts = new Map();
+ const total = tasks.length;
+
+ tasks.forEach(task => {
+ const status = task.status;
+ statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
+ });
+
+ return Array.from(statusCounts.entries()).map(([status, count]) => ({
+ status: this.getStatusLabel(status),
+ count,
+ percentage: Math.round((count / total) * 100)
+ }));
+ }
+
+ /**
+ * Calcule les statistiques hebdomadaires
+ */
+ private static calculateWeeklyStats(tasks: Task[]) {
+ const now = new Date();
+ const thisWeekStart = this.getWeekStart(now);
+ const lastWeekStart = new Date(thisWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000);
+ const lastWeekEnd = new Date(thisWeekStart.getTime() - 1);
+
+ const thisWeekCompleted = tasks.filter(task =>
+ task.completedAt &&
+ task.completedAt >= thisWeekStart &&
+ task.completedAt <= now
+ ).length;
+
+ const lastWeekCompleted = tasks.filter(task =>
+ task.completedAt &&
+ task.completedAt >= lastWeekStart &&
+ task.completedAt <= lastWeekEnd
+ ).length;
+
+ const change = thisWeekCompleted - lastWeekCompleted;
+ const changePercent = lastWeekCompleted > 0
+ ? Math.round((change / lastWeekCompleted) * 100)
+ : thisWeekCompleted > 0 ? 100 : 0;
+
+ return {
+ thisWeek: thisWeekCompleted,
+ lastWeek: lastWeekCompleted,
+ change,
+ changePercent
+ };
+ }
+
+ /**
+ * Obtient le début de la semaine pour une date
+ */
+ private static getWeekStart(date: Date): Date {
+ const d = new Date(date);
+ const day = d.getDay();
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Lundi = début de semaine
+ return new Date(d.setDate(diff));
+ }
+
+ /**
+ * Obtient le numéro de la semaine
+ */
+ private static getWeekNumber(date: Date): number {
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
+ const dayNum = d.getUTCDay() || 7;
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
+ }
+
+ /**
+ * Convertit le code priorité en label français
+ */
+ private static getPriorityLabel(priority: string): string {
+ const labels: Record = {
+ 'low': 'Faible',
+ 'medium': 'Moyenne',
+ 'high': 'Élevée',
+ 'urgent': 'Urgente',
+ 'non-définie': 'Non définie'
+ };
+ return labels[priority] || priority;
+ }
+
+ /**
+ * Convertit le code statut en label français
+ */
+ private static getStatusLabel(status: string): string {
+ const labels: Record = {
+ 'backlog': 'Backlog',
+ 'todo': 'À faire',
+ 'in_progress': 'En cours',
+ 'done': 'Terminé',
+ 'cancelled': 'Annulé',
+ 'freeze': 'Gelé',
+ 'archived': 'Archivé'
+ };
+ return labels[status] || status;
+ }
+}
diff --git a/src/actions/analytics.ts b/src/actions/analytics.ts
new file mode 100644
index 0000000..dc1e957
--- /dev/null
+++ b/src/actions/analytics.ts
@@ -0,0 +1,25 @@
+'use server';
+
+import { AnalyticsService, ProductivityMetrics, TimeRange } from '@/services/analytics';
+
+export async function getProductivityMetrics(timeRange?: TimeRange): Promise<{
+ success: boolean;
+ data?: ProductivityMetrics;
+ error?: string;
+}> {
+ try {
+ const metrics = await AnalyticsService.getProductivityMetrics(timeRange);
+
+ return {
+ success: true,
+ data: metrics
+ };
+ } catch (error) {
+ console.error('Erreur lors de la récupération des métriques:', error);
+
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Erreur inconnue'
+ };
+ }
+}