From 1e4c673c1cc611d8fbdb76e2eedf006f9ae0ad56 Mon Sep 17 00:00:00 2001
From: Aye <117931758+ayetza@users.noreply.github.com>
Date: Tue, 14 Apr 2026 17:38:46 -0600
Subject: [PATCH 01/23] frontend sprint 2
---
.../src/main/frontend/package-lock.json | 514 +++++++++++-
.../backend/src/main/frontend/package.json | 1 +
.../backend/src/main/frontend/src/App.js | 442 +++++-----
.../src/main/frontend/src/Dashboard.js | 269 ++++++
.../backend/src/main/frontend/src/NewItem.js | 60 +-
.../backend/src/main/frontend/src/index.css | 794 +++++++++++++++---
6 files changed, 1690 insertions(+), 390 deletions(-)
create mode 100644 MtdrSpring/backend/src/main/frontend/src/Dashboard.js
diff --git a/MtdrSpring/backend/src/main/frontend/package-lock.json b/MtdrSpring/backend/src/main/frontend/package-lock.json
index 382891ec0..8ff1f4283 100644
--- a/MtdrSpring/backend/src/main/frontend/package-lock.json
+++ b/MtdrSpring/backend/src/main/frontend/package-lock.json
@@ -18,6 +18,7 @@
"react-dom": "^17.0.2",
"react-moment": "^1.1.2",
"react-scripts": "5.0.0",
+ "recharts": "^2.1.16",
"typescript": "^4.6.4"
}
},
@@ -4087,6 +4088,51 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-color": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz",
+ "integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz",
+ "integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "^2"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.4.tgz",
+ "integrity": "sha512-jjZVLBjEX4q6xneKMmv62UocaFJFOTQSb/1aTzs3m3ICTOFoVaqGBHpNLm/4dVi0/FTltfBKgmOK1ECj3/gGjA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.5.tgz",
+ "integrity": "sha512-YOpKj0kIEusRf7ofeJcSZQsvKbnTwpe1DUF+P2qsotqG53kEsjm7EzzliqQxMkAWdkZcHrg5rRhB4JiDOQPX+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "^2"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.7.tgz",
+ "integrity": "sha512-HedHlfGHdwzKqX9+PiQVXZrdmGlwo7naoefJP7kCNk4Y7qcpQt1tUaoRa6qn0kbTdlaIHGO7111qLtb/6J8uuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "^2"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.4.tgz",
+ "integrity": "sha512-BTfLsxTeo7yFxI/haOOf1ZwJ6xKgQLT9dCp+EcmQv87Gox6X+oKl4mLKfO6fnWm3P22+A6DknMNEZany8ql2Rw==",
+ "license": "MIT"
+ },
"node_modules/@types/eslint": {
"version": "8.40.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz",
@@ -5803,6 +5849,12 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
"node_modules/clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
@@ -6319,6 +6371,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/css-unit-converter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
+ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
+ "license": "MIT"
+ },
"node_modules/css-vendor": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
@@ -6496,9 +6554,86 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
"node_modules/csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
+ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-format": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
+ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-interpolate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
+ "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-color": "1 - 2"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
+ "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-scale": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
+ "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "^2.3.0",
+ "d3-format": "1 - 2",
+ "d3-interpolate": "1.2.0 - 2",
+ "d3-time": "^2.1.1",
+ "d3-time-format": "2 - 3"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
+ "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1 - 2"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
+ "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "2"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
+ "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-time": "1 - 2"
+ }
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -6539,6 +6674,12 @@
"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==",
+ "license": "MIT"
+ },
"node_modules/dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -7906,6 +8047,15 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
+ "node_modules/fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@@ -9112,6 +9262,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
"node_modules/ipaddr.js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
@@ -14396,6 +14552,12 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
+ "node_modules/react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
+ "license": "MIT"
+ },
"node_modules/react-moment": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz",
@@ -14414,6 +14576,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-resize-detector": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz",
+ "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@@ -14486,6 +14661,46 @@
}
}
},
+ "node_modules/react-smooth": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz",
+ "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-equals": "^5.0.0",
+ "react-transition-group": "2.9.0"
+ },
+ "peerDependencies": {
+ "prop-types": "^15.6.0",
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-smooth/node_modules/dom-helpers": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
+ "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
+ "node_modules/react-smooth/node_modules/react-transition-group": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
+ "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "dom-helpers": "^3.4.0",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2",
+ "react-lifecycles-compat": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react": ">=15.0.0",
+ "react-dom": ">=15.0.0"
+ }
+ },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -14533,6 +14748,51 @@
"node": ">=8.10.0"
}
},
+ "node_modules/recharts": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.16.tgz",
+ "integrity": "sha512-aYn1plTjYzRCo3UGxtWsduslwYd+Cuww3h/YAAEoRdGe0LRnBgYgaXSlVrNFkWOOSXrBavpmnli9h7pvRuk5wg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "^2.0.0",
+ "@types/d3-scale": "^3.0.0",
+ "@types/d3-shape": "^2.0.0",
+ "classnames": "^2.2.5",
+ "d3-interpolate": "^2.0.0",
+ "d3-scale": "^3.0.0",
+ "d3-shape": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.19",
+ "react-is": "^16.10.2",
+ "react-resize-detector": "^7.1.2",
+ "react-smooth": "^2.0.1",
+ "recharts-scale": "^0.4.4",
+ "reduce-css-calc": "^2.1.8"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "prop-types": "^15.6.0",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
"node_modules/recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -14544,6 +14804,22 @@
"node": ">=6.0.0"
}
},
+ "node_modules/reduce-css-calc": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
+ "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
+ "license": "MIT",
+ "dependencies": {
+ "css-unit-converter": "^1.1.1",
+ "postcss-value-parser": "^3.3.0"
+ }
+ },
+ "node_modules/reduce-css-calc/node_modules/postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
+ "license": "MIT"
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -20029,6 +20305,45 @@
"@types/node": "*"
}
},
+ "@types/d3-color": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz",
+ "integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg=="
+ },
+ "@types/d3-interpolate": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz",
+ "integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==",
+ "requires": {
+ "@types/d3-color": "^2"
+ }
+ },
+ "@types/d3-path": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.4.tgz",
+ "integrity": "sha512-jjZVLBjEX4q6xneKMmv62UocaFJFOTQSb/1aTzs3m3ICTOFoVaqGBHpNLm/4dVi0/FTltfBKgmOK1ECj3/gGjA=="
+ },
+ "@types/d3-scale": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.5.tgz",
+ "integrity": "sha512-YOpKj0kIEusRf7ofeJcSZQsvKbnTwpe1DUF+P2qsotqG53kEsjm7EzzliqQxMkAWdkZcHrg5rRhB4JiDOQPX+A==",
+ "requires": {
+ "@types/d3-time": "^2"
+ }
+ },
+ "@types/d3-shape": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.7.tgz",
+ "integrity": "sha512-HedHlfGHdwzKqX9+PiQVXZrdmGlwo7naoefJP7kCNk4Y7qcpQt1tUaoRa6qn0kbTdlaIHGO7111qLtb/6J8uuw==",
+ "requires": {
+ "@types/d3-path": "^2"
+ }
+ },
+ "@types/d3-time": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.4.tgz",
+ "integrity": "sha512-BTfLsxTeo7yFxI/haOOf1ZwJ6xKgQLT9dCp+EcmQv87Gox6X+oKl4mLKfO6fnWm3P22+A6DknMNEZany8ql2Rw=="
+ },
"@types/eslint": {
"version": "8.40.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz",
@@ -21335,6 +21650,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
},
+ "classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ },
"clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
@@ -21702,6 +22022,11 @@
}
}
},
+ "css-unit-converter": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
+ "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA=="
+ },
"css-vendor": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
@@ -21828,9 +22153,76 @@
}
},
"csstype": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
- "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
+ },
+ "d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "requires": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "d3-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
+ "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
+ },
+ "d3-format": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz",
+ "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
+ },
+ "d3-interpolate": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
+ "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
+ "requires": {
+ "d3-color": "1 - 2"
+ }
+ },
+ "d3-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
+ "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA=="
+ },
+ "d3-scale": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz",
+ "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==",
+ "requires": {
+ "d3-array": "^2.3.0",
+ "d3-format": "1 - 2",
+ "d3-interpolate": "1.2.0 - 2",
+ "d3-time": "^2.1.1",
+ "d3-time-format": "2 - 3"
+ }
+ },
+ "d3-shape": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
+ "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
+ "requires": {
+ "d3-path": "1 - 2"
+ }
+ },
+ "d3-time": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz",
+ "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==",
+ "requires": {
+ "d3-array": "2"
+ }
+ },
+ "d3-time-format": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
+ "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
+ "requires": {
+ "d3-time": "1 - 2"
+ }
},
"damerau-levenshtein": {
"version": "1.0.8",
@@ -21860,6 +22252,11 @@
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
},
+ "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=="
+ },
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -22872,6 +23269,11 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
+ "fast-equals": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="
+ },
"fast-glob": {
"version": "3.2.12",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
@@ -23736,6 +24138,11 @@
"side-channel": "^1.0.4"
}
},
+ "internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+ },
"ipaddr.js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
@@ -27396,6 +27803,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
+ "react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+ },
"react-moment": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz",
@@ -27407,6 +27819,14 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
+ "react-resize-detector": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz",
+ "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==",
+ "requires": {
+ "lodash": "^4.17.21"
+ }
+ },
"react-scripts": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@@ -27462,6 +27882,36 @@
"workbox-webpack-plugin": "^6.4.1"
}
},
+ "react-smooth": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz",
+ "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==",
+ "requires": {
+ "fast-equals": "^5.0.0",
+ "react-transition-group": "2.9.0"
+ },
+ "dependencies": {
+ "dom-helpers": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
+ "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
+ "react-transition-group": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
+ "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
+ "requires": {
+ "dom-helpers": "^3.4.0",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2",
+ "react-lifecycles-compat": "^3.0.4"
+ }
+ }
+ }
+ },
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -27499,6 +27949,42 @@
"picomatch": "^2.2.1"
}
},
+ "recharts": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.1.16.tgz",
+ "integrity": "sha512-aYn1plTjYzRCo3UGxtWsduslwYd+Cuww3h/YAAEoRdGe0LRnBgYgaXSlVrNFkWOOSXrBavpmnli9h7pvRuk5wg==",
+ "requires": {
+ "@types/d3-interpolate": "^2.0.0",
+ "@types/d3-scale": "^3.0.0",
+ "@types/d3-shape": "^2.0.0",
+ "classnames": "^2.2.5",
+ "d3-interpolate": "^2.0.0",
+ "d3-scale": "^3.0.0",
+ "d3-shape": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.19",
+ "react-is": "^16.10.2",
+ "react-resize-detector": "^7.1.2",
+ "react-smooth": "^2.0.1",
+ "recharts-scale": "^0.4.4",
+ "reduce-css-calc": "^2.1.8"
+ },
+ "dependencies": {
+ "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=="
+ }
+ }
+ },
+ "recharts-scale": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+ "requires": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
"recursive-readdir": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz",
@@ -27507,6 +27993,22 @@
"minimatch": "^3.0.5"
}
},
+ "reduce-css-calc": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
+ "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
+ "requires": {
+ "css-unit-converter": "^1.1.1",
+ "postcss-value-parser": "^3.3.0"
+ },
+ "dependencies": {
+ "postcss-value-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
+ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
+ }
+ }
+ },
"regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
diff --git a/MtdrSpring/backend/src/main/frontend/package.json b/MtdrSpring/backend/src/main/frontend/package.json
index f92323cbe..1ec5bfd76 100644
--- a/MtdrSpring/backend/src/main/frontend/package.json
+++ b/MtdrSpring/backend/src/main/frontend/package.json
@@ -13,6 +13,7 @@
"react-dom": "^17.0.2",
"react-moment": "^1.1.2",
"react-scripts": "5.0.0",
+ "recharts": "^2.1.16",
"typescript": "^4.6.4"
},
"scripts": {
diff --git a/MtdrSpring/backend/src/main/frontend/src/App.js b/MtdrSpring/backend/src/main/frontend/src/App.js
index 21462dd91..6bcc9f457 100644
--- a/MtdrSpring/backend/src/main/frontend/src/App.js
+++ b/MtdrSpring/backend/src/main/frontend/src/App.js
@@ -1,240 +1,250 @@
- /*
-## MyToDoReact version 1.0.
-##
-## Copyright (c) 2022 Oracle, Inc.
-## Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
-*/
-/*
- * This is the application main React component. We're using "function"
- * components in this application. No "class" components should be used for
- * consistency.
- * @author jean.de.lavarene@oracle.com
- */
import React, { useState, useEffect } from 'react';
import NewItem from './NewItem';
+import Dashboard from './Dashboard';
import API_LIST from './API';
import DeleteIcon from '@mui/icons-material/Delete';
-import { Button, TableBody, CircularProgress } from '@mui/material';
+import TaskAltIcon from '@mui/icons-material/TaskAlt';
+import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
+import BarChartIcon from '@mui/icons-material/BarChart';
+import { CircularProgress } from '@mui/material';
import Moment from 'react-moment';
-/* In this application we're using Function Components with the State Hooks
- * to manage the states. See the doc: https://reactjs.org/docs/hooks-state.html
- * This App component represents the entire app. It renders a NewItem component
- * and two tables: one that lists the todo items that are to be done and another
- * one with the items that are already done.
- */
+const CARD_COLORS = ['#7C3AED', '#F59E0B', '#14B8A6', '#EC4899', '#3B82F6', '#EF4444'];
+
function App() {
- // isLoading is true while waiting for the backend to return the list
- // of items. We use this state to display a spinning circle:
- const [isLoading, setLoading] = useState(false);
- // Similar to isLoading, isInserting is true while waiting for the backend
- // to insert a new item:
- const [isInserting, setInserting] = useState(false);
- // The list of todo items is stored in this state. It includes the "done"
- // "not-done" items:
- const [items, setItems] = useState([]);
- // In case of an error during the API call:
- const [error, setError] = useState();
-
- function deleteItem(deleteId) {
- // console.log("deleteItem("+deleteId+")")
- fetch(API_LIST+"/"+deleteId, {
- method: 'DELETE',
+ const [activeTab, setActiveTab] = useState('tasks');
+ const [isLoading] = useState(false);
+ const [isInserting, setInserting] = useState(false);
+ const [items, setItems] = useState([]);
+ const [, setError] = useState();
+
+ function deleteItem(deleteId) {
+ fetch(API_LIST + "/" + deleteId, { method: 'DELETE' })
+ .then(response => {
+ if (response.ok) return response;
+ throw new Error('Something went wrong ...');
})
+ .then(
+ () => { setItems(prev => prev.filter(item => item.id !== deleteId)); },
+ (err) => { setError(err); }
+ );
+ }
+
+ function toggleDone(event, id, description, done) {
+ event.preventDefault();
+ modifyItem(id, description, done).then(
+ () => { reloadOneItem(id); },
+ (err) => { setError(err); }
+ );
+ }
+
+ function reloadOneItem(id) {
+ fetch(API_LIST + "/" + id)
.then(response => {
- // console.log("response=");
- // console.log(response);
- if (response.ok) {
- // console.log("deleteItem FETCH call is ok");
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
+ if (response.ok) return response.json();
+ throw new Error('Something went wrong ...');
})
.then(
(result) => {
- const remainingItems = items.filter(item => item.id !== deleteId);
- setItems(remainingItems);
+ setItems(prev => prev.map(x =>
+ x.id === id ? { ...x, description: result.description, done: result.done } : x
+ ));
},
- (error) => {
- setError(error);
- }
+ (err) => { setError(err); }
);
- }
- function toggleDone(event, id, description, done) {
- event.preventDefault();
- modifyItem(id, description, done).then(
- (result) => { reloadOneIteam(id); },
- (error) => { setError(error); }
- );
- }
- function reloadOneIteam(id){
- fetch(API_LIST+"/"+id)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Something went wrong ...');
- }
- })
- .then(
- (result) => {
- const items2 = items.map(
- x => (x.id === id ? {
- ...x,
- 'description':result.description,
- 'done': result.done
- } : x));
- setItems(items2);
- },
- (error) => {
- setError(error);
- });
- }
- function modifyItem(id, description, done) {
- // console.log("deleteItem("+deleteId+")")
- var data = {"description": description, "done": done};
- return fetch(API_LIST+"/"+id, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(data)
- })
+ }
+
+ function modifyItem(id, description, done) {
+ return fetch(API_LIST + "/" + id, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ description, done }),
+ }).then(response => {
+ if (response.ok) return response;
+ throw new Error('Something went wrong ...');
+ });
+ }
+
+ useEffect(() => {
+ setItems([
+ { id: 1, description: 'Design new dashboard layout', createdAt: '2026-04-14T09:00:00', done: false },
+ { id: 2, description: 'Fix login bug on mobile', createdAt: '2026-04-14T10:30:00', done: false },
+ { id: 3, description: 'Write unit tests for API', createdAt: '2026-04-13T15:00:00', done: false },
+ { id: 4, description: 'Deploy to staging environment', createdAt: '2026-04-13T11:00:00', done: true },
+ { id: 5, description: 'Review pull request #42', createdAt: '2026-04-12T08:00:00', done: true },
+ ]);
+ }, []);
+
+ function addItem(text) {
+ setInserting(true);
+ fetch(API_LIST, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ description: text }),
+ })
.then(response => {
- // console.log("response=");
- // console.log(response);
- if (response.ok) {
- // console.log("deleteItem FETCH call is ok");
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
- });
- }
- /*
- To simulate slow network, call sleep before making API calls.
- const sleep = (milliseconds) => {
- return new Promise(resolve => setTimeout(resolve, milliseconds))
- }
- */
- useEffect(() => {
- setLoading(true);
- // sleep(5000).then(() => {
- fetch(API_LIST)
- .then(response => {
- if (response.ok) {
- return response.json();
- } else {
- throw new Error('Something went wrong ...');
- }
- })
- .then(
- (result) => {
- setLoading(false);
- setItems(result);
- },
- (error) => {
- setLoading(false);
- setError(error);
- });
-
- //})
- },
- // https://en.reactjs.org/docs/faq-ajax.html
- [] // empty deps array [] means
- // this useEffect will run once
- // similar to componentDidMount()
- );
- function addItem(text){
- console.log("addItem("+text+")")
- setInserting(true);
- var data = {};
- console.log(data);
- data.description = text;
- fetch(API_LIST, {
- method: 'POST',
- // We convert the React state to JSON and send it as the POST body
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(data),
- }).then((response) => {
- // This API doens't return a JSON document
- console.log(response);
- console.log();
- console.log(response.headers.location);
- // return response.json();
- if (response.ok) {
- return response;
- } else {
- throw new Error('Something went wrong ...');
- }
- }).then(
+ if (response.ok) return response;
+ throw new Error('Something went wrong ...');
+ })
+ .then(
(result) => {
- var id = result.headers.get('location');
- var newItem = {"id": id, "description": text}
- setItems([newItem, ...items]);
+ const id = result.headers.get('location');
+ setItems(prev => [{ id, description: text }, ...prev]);
setInserting(false);
},
- (error) => {
- setInserting(false);
- setError(error);
- }
+ (err) => { setInserting(false); setError(err); }
);
- }
- return (
-
-
MY TODO LIST
-
- { error &&
- Error: {error.message}
- }
- { isLoading &&
-
- }
- { !isLoading &&
-
-
-
- {items.map(item => (
- !item.done && (
-
- {item.description}
- { /*{JSON.stringify(item, null, 2) } */ }
- {item.createdAt}
- toggleDone(event, item.id, item.description, !item.done)} size="small">
- Done
-
-
- )))}
-
-
-
- Done items
-
-
-
- {items.map(item => (
- item.done && (
-
-
- {item.description}
- {item.createdAt}
- toggleDone(event, item.id, item.description, !item.done)} size="small">
- Undo
-
- } variant="contained" className="DeleteButton" onClick={() => deleteItem(item.id)} size="small">
- Delete
-
-
- )))}
-
-
+ }
+
+ const todoItems = items.filter(item => !item.done);
+ const doneItems = items.filter(item => item.done);
+ const donePercent = items.length > 0 ? Math.round((doneItems.length / items.length) * 100) : 0;
+
+ return (
+
+
+
+ {/* Left panel — header + stats */}
+
+
+
+
+
+ My Tasks
+ Stay organized, stay focused
+
+
+
+ {todoItems.length} pending
+
+
+
+ {doneItems.length} completed
+
+
+
+
+ {items.length > 0 && (
+
+
+ {doneItems.length} of {items.length} tasks completed
+ {donePercent}%
+
+
+
+ )}
+
+
+ {/* Right panel — tasks */}
+
+
+ setActiveTab('tasks')}
+ >
+
+ Tasks
+
+ setActiveTab('analytics')}
+ >
+
+ Analytics
+
+
+
+ {activeTab === 'analytics' ? (
+
+ ) : (
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+
To Do
+
+ {todoItems.length}
+
+
+ {todoItems.length === 0 ? (
+ All caught up — nothing left to do!
+ ) : (
+ todoItems.map((item, i) => (
+
+
toggleDone(e, item.id, item.description, true)}
+ title="Mark as done"
+ />
+
+ {item.description}
+ {item.createdAt && (
+
+ {item.createdAt}
+
+ )}
+
+
+ ))
+ )}
+
+
+ {doneItems.length > 0 && (
+
+
+
Completed
+ {doneItems.length}
+
+ {doneItems.map((item) => (
+
+
toggleDone(e, item.id, item.description, false)}
+ title="Mark as to do"
+ />
+
+ {item.description}
+ {item.createdAt && (
+
+ {item.createdAt}
+
+ )}
+
+
+ deleteItem(item.id)}
+ title="Delete task"
+ >
+
+
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+ )}
- }
- );
+
+ );
}
+
export default App;
diff --git a/MtdrSpring/backend/src/main/frontend/src/Dashboard.js b/MtdrSpring/backend/src/main/frontend/src/Dashboard.js
new file mode 100644
index 000000000..4cf63bc5b
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/Dashboard.js
@@ -0,0 +1,269 @@
+import React from 'react';
+import {
+ BarChart, Bar, XAxis, YAxis, CartesianGrid,
+ Tooltip, Legend, ResponsiveContainer
+} from 'recharts';
+
+// ── Mock data (swap for real API when backend is ready) ──────────────────────
+const SPRINT_DATA = [
+ { dev: 'Ana G.', s1: 5, s2: 6, s3: 4, h1: 14, h2: 16, h3: 12 },
+ { dev: 'Carlos L.', s1: 3, s2: 5, s3: 6, h1: 11, h2: 14, h3: 13 },
+ { dev: 'Maria R.', s1: 7, s2: 4, s3: 5, h1: 18, h2: 15, h3: 18 },
+ { dev: 'Jorge M.', s1: 4, s2: 7, s3: 3, h1: 10, h2: 16, h3: 9 },
+ { dev: 'Sofia C.', s1: 6, s2: 5, s3: 8, h1: 15, h2: 14, h3: 17 },
+];
+
+// ── Derived KPIs ─────────────────────────────────────────────────────────────
+const totalTasks = SPRINT_DATA.reduce((acc, d) => acc + d.s1 + d.s2 + d.s3, 0);
+const totalHours = SPRINT_DATA.reduce((acc, d) => acc + d.h1 + d.h2 + d.h3, 0);
+const avgTasksDev = (totalTasks / SPRINT_DATA.length).toFixed(1);
+const avgHoursDev = (totalHours / SPRINT_DATA.length).toFixed(1);
+
+const KPI_CARDS = [
+ { label: '# Tasks', value: totalTasks, color: '#7C3AED', bg: '#EDE9FE' },
+ { label: 'Real Hours', value: `${totalHours}h`, color: '#F59E0B', bg: '#FEF3C7' },
+ { label: 'Tasks / Dev', value: avgTasksDev, color: '#14B8A6', bg: '#CCFBF1' },
+ { label: 'Hours / Dev', value: `${avgHoursDev}h`,color: '#EC4899', bg: '#FCE7F3' },
+];
+
+// ── Auto-generated insights from data ────────────────────────────────────────
+function generateInsights() {
+ const withTotals = SPRINT_DATA.map(d => ({
+ ...d,
+ totalTasks: d.s1 + d.s2 + d.s3,
+ totalHours: d.h1 + d.h2 + d.h3,
+ efficiency: (d.s1 + d.s2 + d.s3) / (d.h1 + d.h2 + d.h3),
+ trend: d.s3 - d.s1,
+ }));
+
+ const topTasks = [...withTotals].sort((a, b) => b.totalTasks - a.totalTasks)[0];
+ const topEff = [...withTotals].sort((a, b) => b.efficiency - a.efficiency)[0];
+ const lowEff = [...withTotals].sort((a, b) => a.efficiency - b.efficiency)[0];
+ const mostImproved = [...withTotals].sort((a, b) => b.trend - a.trend)[0];
+ const declining = [...withTotals].sort((a, b) => a.trend - b.trend)[0];
+ const mostHours = [...withTotals].sort((a, b) => b.totalHours - a.totalHours)[0];
+ const leastHours = [...withTotals].sort((a, b) => a.totalHours - b.totalHours)[0];
+
+ const taskVariance = Math.max(...withTotals.map(d => d.totalTasks)) -
+ Math.min(...withTotals.map(d => d.totalTasks));
+
+ const insights = [
+ {
+ type: 'success',
+ tag: 'Top Performer',
+ title: `${topTasks.dev} leads in productivity`,
+ body: `Completed ${topTasks.totalTasks} tasks in total — the highest count on the team.`,
+ },
+ {
+ type: 'info',
+ tag: 'Efficiency',
+ title: `${topEff.dev} is the most efficient`,
+ body: `Achieves ${topEff.efficiency.toFixed(2)} tasks/hour — the best output-to-time ratio on the team.`,
+ },
+ {
+ type: 'warning',
+ tag: 'Watch',
+ title: `${lowEff.dev} has the lowest efficiency`,
+ body: `Only ${lowEff.efficiency.toFixed(2)} tasks/hour. May be facing technical blockers or handling higher-complexity work.`,
+ },
+ mostImproved.trend > 0 ? {
+ type: 'success',
+ tag: 'Positive Trend',
+ title: `${mostImproved.dev} is improving sprint over sprint`,
+ body: `Increased by ${mostImproved.trend} tasks from Sprint 1 to Sprint 3 — a strong learning curve.`,
+ } : null,
+ declining.trend < 0 ? {
+ type: 'danger',
+ tag: 'Declining Trend',
+ title: `${declining.dev} shows a drop in output`,
+ body: `Down ${Math.abs(declining.trend)} tasks from Sprint 1 to Sprint 3. Needs follow-up.`,
+ } : null,
+ taskVariance >= 4 ? {
+ type: 'warning',
+ tag: 'Imbalance',
+ title: `High variance across developers`,
+ body: `There is a ${taskVariance}-task gap between the highest and lowest contributor. Workload may not be evenly distributed.`,
+ } : null,
+ {
+ type: 'info',
+ tag: 'Workload',
+ title: `${mostHours.dev} is logging the most hours`,
+ body: `${mostHours.totalHours}h total vs ${leastHours.totalHours}h for ${leastHours.dev} — a ${mostHours.totalHours - leastHours.totalHours}h gap that may signal uneven task assignment.`,
+ },
+ ].filter(Boolean);
+
+ const actions = [
+ {
+ priority: 'High',
+ color: '#EF4444',
+ bg: '#FEF2F2',
+ text: `Set up pair programming sessions between ${topEff.dev} and ${lowEff.dev} to share best practices and unblock bottlenecks.`,
+ },
+ {
+ priority: 'High',
+ color: '#EF4444',
+ bg: '#FEF2F2',
+ text: declining.trend < 0
+ ? `Schedule a 1-on-1 with ${declining.dev} to identify what caused the drop from Sprint 1 to Sprint 3 before the next sprint begins.`
+ : `Review task distribution — ensure no developer is assigned more than 130% of the team average.`,
+ },
+ {
+ priority: 'Medium',
+ color: '#F59E0B',
+ bg: '#FEF3C7',
+ text: `Rebalance workload between ${mostHours.dev} and ${leastHours.dev} in the next sprint — the ${mostHours.totalHours - leastHours.totalHours}h difference is a burnout risk.`,
+ },
+ {
+ priority: 'Medium',
+ color: '#F59E0B',
+ bg: '#FEF3C7',
+ text: `Use ${topTasks.dev}'s estimates as a baseline reference when assigning story points to the team.`,
+ },
+ {
+ priority: 'Low',
+ color: '#14B8A6',
+ bg: '#CCFBF1',
+ text: `Publicly acknowledge ${mostImproved.dev}'s progress in the retrospective — reinforces a culture of continuous improvement.`,
+ },
+ ];
+
+ return { insights, actions };
+}
+
+// ── Tooltips ─────────────────────────────────────────────────────────────────
+const CustomTooltip = ({ active, payload, label }) => {
+ if (!active || !payload?.length) return null;
+ return (
+
+
{label}
+ {payload.map(p => (
+
{p.name}: {p.value} tasks
+ ))}
+
+ );
+};
+
+const HoursTooltip = ({ active, payload, label }) => {
+ if (!active || !payload?.length) return null;
+ return (
+
+
{label}
+ {payload.map(p => (
+
{p.name}: {p.value}h
+ ))}
+
+ );
+};
+
+const INSIGHT_STYLES = {
+ success: { border: '#22C55E', bg: '#F0FDF4', tag: '#16A34A' },
+ info: { border: '#3B82F6', bg: '#EFF6FF', tag: '#1D4ED8' },
+ warning: { border: '#F59E0B', bg: '#FFFBEB', tag: '#B45309' },
+ danger: { border: '#EF4444', bg: '#FEF2F2', tag: '#B91C1C' },
+};
+
+// ── Component ─────────────────────────────────────────────────────────────────
+function Dashboard() {
+ const { insights, actions } = generateInsights();
+
+ return (
+
+
+ {/* KPI Cards */}
+
+ {KPI_CARDS.map(card => (
+
+ {card.value}
+ {card.label}
+
+ ))}
+
+
+ {/* Chart 1 — Tasks by developer/sprint */}
+
+
+
Completed Tasks by Developer
+
Comparative analysis per sprint
+
+
+
+
+
+
+
+ } cursor={{ fill: 'rgba(124,58,237,0.04)' }} />
+
+
+
+
+
+
+
+
+
+ {/* Chart 2 — Real hours by developer/sprint */}
+
+
+
Real Hours by Developer
+
Comparative analysis per sprint
+
+
+
+
+
+
+
+ } cursor={{ fill: 'rgba(124,58,237,0.04)' }} />
+
+
+
+
+
+
+
+
+
+ {/* Insights */}
+
+
+
Insights
+
Patterns automatically detected from the data
+
+
+ {insights.map((ins, i) => {
+ const s = INSIGHT_STYLES[ins.type];
+ return (
+
+
{ins.tag}
+
{ins.title}
+
{ins.body}
+
+ );
+ })}
+
+
+
+ {/* Improvement Actions */}
+
+
+
Improvement Actions
+
Concrete recommendations for the next sprint
+
+
+ {actions.map((action, i) => (
+
+
{action.priority}
+
{action.text}
+
+ ))}
+
+
+
+
+ );
+}
+
+export default Dashboard;
diff --git a/MtdrSpring/backend/src/main/frontend/src/NewItem.js b/MtdrSpring/backend/src/main/frontend/src/NewItem.js
index c52158419..565e86411 100644
--- a/MtdrSpring/backend/src/main/frontend/src/NewItem.js
+++ b/MtdrSpring/backend/src/main/frontend/src/NewItem.js
@@ -1,64 +1,36 @@
-/*
-## MyToDoReact version 1.0.
-##
-## Copyright (c) 2022 Oracle, Inc.
-## Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
-*/
-/*
- * Component that supports creating a new todo item.
- * @author jean.de.lavarene@oracle.com
- */
-
import React, { useState } from "react";
-import Button from '@mui/material/Button';
-
function NewItem(props) {
const [item, setItem] = useState('');
+
function handleSubmit(e) {
- // console.log("NewItem.handleSubmit("+e+")");
- if (!item.trim()) {
- return;
- }
- // addItem makes the REST API call:
+ e.preventDefault();
+ if (!item.trim()) return;
props.addItem(item);
setItem("");
- e.preventDefault();
- }
- function handleChange(e) {
- setItem(e.target.value);
}
+
return (
-
My Tasks
+
Is this orange?
+
Is this orange?
Stay organized, stay focused
diff --git a/MtdrSpring/backend/src/main/frontend/src/index.css b/MtdrSpring/backend/src/main/frontend/src/index.css
index ae5ada450..fef409449 100644
--- a/MtdrSpring/backend/src/main/frontend/src/index.css
+++ b/MtdrSpring/backend/src/main/frontend/src/index.css
@@ -1,680 +1,7 @@
-@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
-/* ── Design tokens ─────────────────────────────────────────────────────────── */
-:root {
- --purple: #7C3AED;
- --purple-dark: #5B21B6;
- --purple-mid: #6D28D9;
- --purple-light: #EDE9FE;
- --purple-soft: #F5F3FF;
- --yellow: #FCD34D;
- --teal: #14B8A6;
- --green: #22C55E;
- --danger: #EF4444;
- --danger-light: #FEF2F2;
- --text-primary: #1F1D2E;
- --text-secondary: #9CA3AF;
- --text-light: #C4B5FD;
- --surface: #FFFFFF;
- --bg: #F5F3FF;
- --border: #EDE9FE;
- --shadow-card: 0 2px 12px rgba(124,58,237,0.08);
- --shadow-lg: 0 8px 32px rgba(124,58,237,0.18);
- --radius-xl: 24px;
- --radius-lg: 16px;
- --radius-md: 12px;
- --radius-sm: 8px;
- --radius-pill: 99px;
-
- /* Fluid spacing scale */
- --space-xs: clamp(6px, 1vw, 8px);
- --space-sm: clamp(10px, 2vw, 14px);
- --space-md: clamp(14px, 3vw, 20px);
- --space-lg: clamp(20px, 4vw, 32px);
- --space-xl: clamp(28px, 5vw, 48px);
-
- /* Fluid type scale */
- --text-xs: clamp(10px, 1.5vw, 11px);
- --text-sm: clamp(11px, 1.8vw, 13px);
- --text-base: clamp(13px, 2vw, 15px);
- --text-lg: clamp(16px, 2.5vw, 20px);
- --text-xl: clamp(20px, 3.5vw, 28px);
- --text-2xl: clamp(22px, 4vw, 32px);
-}
-
-/* ── Reset ─────────────────────────────────────────────────────────────────── */
-*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
-
-html { -webkit-text-size-adjust: 100%; }
-
-body {
- background: var(--bg);
- font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- color: var(--text-primary);
- min-height: 100vh;
- overflow-x: hidden;
-}
-
-/* ── Layout shell ───────────────────────────────────────────────────────────── */
-.app-wrapper { min-height: 100vh; }
-
-/* Default: mobile — full-width stacked */
-.layout {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-.left-panel { flex-shrink: 0; }
-
-.right-panel {
- flex: 1;
- min-width: 0;
- padding: clamp(14px, 4vw, 20px) clamp(14px, 4vw, 16px) 60px;
-}
-
-/* Remove right-panel padding on tablet+ since layout handles it */
-@media (min-width: 640px) {
- .right-panel { padding: 0; }
-}
-
-/* ─ Tablet portrait (≥ 640px) ─ */
-@media (min-width: 640px) {
- .layout {
- max-width: 600px;
- margin: 0 auto;
- padding: var(--space-lg) var(--space-md) 60px;
- }
- .app-header {
- border-radius: var(--radius-xl) !important;
- padding: var(--space-lg) var(--space-lg) var(--space-md) !important;
- }
- .progress-wrap { margin: var(--space-sm) 0 0 !important; border-radius: var(--radius-lg); }
- .app-body { padding: var(--space-md) 0 0 !important; }
-}
-
-/* ─ Tablet landscape (≥ 768px) ─ */
-@media (min-width: 768px) {
- .layout { max-width: 720px; }
-}
-
-/* ─ Small desktop (≥ 1024px) — switch to two-column ─ */
-@media (min-width: 1024px) {
- .layout {
- flex-direction: row;
- align-items: flex-start;
- max-width: 1000px;
- padding: var(--space-xl) var(--space-lg) 80px;
- gap: var(--space-lg);
- }
- .left-panel {
- width: 290px;
- position: sticky;
- top: var(--space-xl);
- }
- .app-header {
- border-radius: var(--radius-xl) !important;
- padding: var(--space-lg) var(--space-md) var(--space-md) !important;
- }
- .progress-wrap { margin: var(--space-sm) 0 0 !important; border-radius: var(--radius-lg); }
- .app-body { padding: 0 !important; }
-}
-
-/* ─ Large desktop (≥ 1280px) ─ */
-@media (min-width: 1280px) {
- .layout {
- max-width: 1160px;
- gap: 40px;
- }
- .left-panel { width: 320px; }
-}
-
-/* ─ Extra large (≥ 1536px) ─ */
-@media (min-width: 1536px) {
- .layout { max-width: 1360px; }
- .left-panel { width: 360px; }
-}
-
-/* ── Header ─────────────────────────────────────────────────────────────────── */
-.app-header {
- background: linear-gradient(140deg, #7C3AED 0%, #5B21B6 100%);
- border-radius: 0 0 28px 28px;
- padding: clamp(40px, 8vw, 52px) clamp(20px, 5vw, 28px) clamp(24px, 5vw, 36px);
- color: white;
- position: relative;
- overflow: hidden;
-}
-
-.app-header::before {
- content: '';
- position: absolute;
- top: -40px; right: -40px;
- width: clamp(120px, 20vw, 180px);
- height: clamp(120px, 20vw, 180px);
- background: rgba(255,255,255,0.06);
- border-radius: 50%;
-}
-
-.app-header::after {
- content: '';
- position: absolute;
- bottom: -60px; right: 40px;
- width: clamp(80px, 14vw, 120px);
- height: clamp(80px, 14vw, 120px);
- background: rgba(255,255,255,0.04);
- border-radius: 50%;
-}
-
-.header-logo {
- width: clamp(44px, 7vw, 52px);
- height: clamp(44px, 7vw, 52px);
- background: rgba(255,255,255,0.18);
- border-radius: var(--radius-md);
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: var(--space-md);
- backdrop-filter: blur(8px);
- flex-shrink: 0;
-}
-
-.header-logo svg { font-size: clamp(22px, 4vw, 28px) !important; color: var(--yellow); }
-
-.app-header h1 {
- font-size: var(--text-2xl);
- font-weight: 700;
- letter-spacing: -0.5px;
- margin-bottom: 4px;
- line-height: 1.2;
-}
-
-.app-header .subtitle {
- font-size: var(--text-sm);
- font-weight: 400;
- color: var(--text-light);
- margin-bottom: var(--space-md);
-}
-
-/* ── Stats row ──────────────────────────────────────────────────────────────── */
-.stats-row { display: flex; gap: 10px; flex-wrap: wrap; }
-
-.stat-chip {
- background: rgba(255,255,255,0.14);
- border-radius: var(--radius-pill);
- padding: 5px 12px;
- font-size: var(--text-sm);
- font-weight: 600;
- color: white;
- display: flex;
- align-items: center;
- gap: 6px;
- backdrop-filter: blur(8px);
-}
-
-.stat-chip .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
-.dot-pending { background: var(--yellow); }
-.dot-done { background: var(--green); }
-
-/* ── Progress ───────────────────────────────────────────────────────────────── */
-.progress-wrap {
- margin: clamp(14px, 3vw, 20px) clamp(14px, 4vw, 16px) 0;
- background: var(--surface);
- border-radius: var(--radius-lg);
- padding: clamp(12px, 2.5vw, 16px) clamp(14px, 3vw, 20px);
- box-shadow: var(--shadow-card);
-}
-
-@media (min-width: 640px) {
- .progress-wrap { margin: clamp(14px, 3vw, 20px) 0 0; }
-}
-
-.progress-label {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: var(--text-sm);
- font-weight: 600;
- color: var(--text-secondary);
- margin-bottom: 10px;
-}
-
-.progress-label .pct { color: var(--purple); }
-
-.progress-track {
- height: clamp(5px, 1vw, 8px);
- background: var(--border);
- border-radius: var(--radius-pill);
- overflow: hidden;
-}
-
-.progress-fill {
- height: 100%;
- background: linear-gradient(90deg, var(--purple), #A78BFA);
- border-radius: var(--radius-pill);
- transition: width 0.5s cubic-bezier(0.4,0,0.2,1);
-}
-
-/* ── App body ───────────────────────────────────────────────────────────────── */
-.app-body {
- padding: clamp(14px, 3vw, 20px) 0 0;
-}
-
-/* On tablet+ the right-panel has no padding, so app-body needs its own horizontal */
-@media (min-width: 640px) {
- .app-body {
- padding: clamp(16px, 3vw, 20px) 0 0;
- }
-}
-
-/* ── New item form ──────────────────────────────────────────────────────────── */
-.new-item-wrap {
- background: var(--surface);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-card);
- border: 1.5px solid var(--border);
- margin-bottom: clamp(16px, 3vw, 24px);
- display: flex;
- align-items: center;
- overflow: hidden;
- transition: border-color 0.2s, box-shadow 0.2s;
-}
-
-.new-item-wrap:focus-within {
- border-color: var(--purple);
- box-shadow: 0 0 0 4px rgba(124,58,237,0.1);
-}
-
-.new-item-prefix {
- padding: 0 4px 0 clamp(12px, 3vw, 18px);
- color: var(--purple);
- font-size: clamp(18px, 3vw, 22px);
- font-weight: 300;
- display: flex;
- align-items: center;
- flex-shrink: 0;
- user-select: none;
-}
-
-.new-item-input {
- flex: 1;
- min-width: 0;
- border: none;
- outline: none;
- background: transparent;
- font-family: inherit;
- font-size: var(--text-base);
- font-weight: 500;
- color: var(--text-primary);
- padding: clamp(12px, 2.5vw, 15px) 8px;
-}
-
-.new-item-input::placeholder { color: var(--text-secondary); font-weight: 400; }
-
-.add-btn {
- flex-shrink: 0;
- margin: 6px;
- padding: clamp(7px, 1.5vw, 9px) clamp(14px, 3vw, 20px);
- background: linear-gradient(135deg, var(--purple), var(--purple-mid));
- color: white;
- border: none;
- border-radius: var(--radius-md);
- font-family: inherit;
- font-size: var(--text-sm);
- font-weight: 600;
- cursor: pointer;
- transition: opacity 0.15s, transform 0.1s, box-shadow 0.15s;
- white-space: nowrap;
- box-shadow: 0 4px 12px rgba(124,58,237,0.3);
- /* min touch target */
- min-height: 36px;
-}
-
-.add-btn:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); box-shadow: 0 6px 16px rgba(124,58,237,0.4); }
-.add-btn:active:not(:disabled) { transform: translateY(0); }
-.add-btn:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
-
-/* ── Task sections ──────────────────────────────────────────────────────────── */
-.tasks-section { margin-bottom: clamp(18px, 4vw, 28px); }
-
-.section-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: clamp(8px, 2vw, 12px);
-}
-
-.section-header h2 {
- font-size: var(--text-xs);
- font-weight: 700;
- color: var(--text-secondary);
- text-transform: uppercase;
- letter-spacing: 0.8px;
-}
-
-.count-badge {
- background: var(--purple-light);
- color: var(--purple);
- font-size: var(--text-xs);
- font-weight: 700;
- padding: 2px 9px;
- border-radius: var(--radius-pill);
-}
-
-/* ── Task card ──────────────────────────────────────────────────────────────── */
-.task-card {
- background: var(--surface);
- border-radius: var(--radius-md);
- padding: clamp(10px, 2vw, 14px) clamp(12px, 2.5vw, 16px);
- margin-bottom: clamp(6px, 1.5vw, 10px);
- box-shadow: var(--shadow-card);
- display: flex;
- align-items: center;
- gap: clamp(10px, 2vw, 14px);
- border: 1.5px solid transparent;
- transition: box-shadow 0.15s, border-color 0.15s, transform 0.1s;
- position: relative;
- overflow: hidden;
-}
-
-.task-card::before {
- content: '';
- position: absolute;
- left: 0; top: 0; bottom: 0;
- width: 4px;
- background: var(--card-accent, var(--purple));
- border-radius: 4px 0 0 4px;
-}
-
-.task-card:hover { box-shadow: var(--shadow-lg); border-color: var(--border); transform: translateY(-1px); }
-.task-card:hover .task-actions { opacity: 1; }
-
-.task-card.is-done { background: #FAFAFA; box-shadow: none; border-color: var(--border); }
-.task-card.is-done::before { background: var(--green); }
-.task-card.is-done:hover { transform: none; box-shadow: var(--shadow-card); }
-
-/* On touch devices always show actions (no hover) */
-@media (hover: none) {
- .task-actions { opacity: 1; }
- .task-card:hover { transform: none; box-shadow: var(--shadow-card); }
-}
-
-/* ── Checkbox ───────────────────────────────────────────────────────────────── */
-.task-checkbox {
- flex-shrink: 0;
- width: clamp(20px, 3.5vw, 24px);
- height: clamp(20px, 3.5vw, 24px);
- border-radius: 50%;
- border: 2px solid #D1D5DB;
- background: transparent;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- padding: 0;
- outline: none;
- /* min touch target */
- position: relative;
-}
-
-.task-checkbox::after {
- content: '';
- position: absolute;
- inset: -8px;
-}
-
-.task-checkbox:hover { border-color: var(--purple); background: var(--purple-light); }
-
-.task-checkbox.checked { background: var(--green); border-color: var(--green); }
-.task-checkbox.checked::after {
- content: '✓';
- position: static;
- color: white;
- font-size: clamp(10px, 1.8vw, 13px);
- font-weight: 700;
- line-height: 1;
-}
-
-/* ── Task body ──────────────────────────────────────────────────────────────── */
-.task-body { flex: 1; min-width: 0; }
-
-.task-description {
- font-size: var(--text-base);
- font-weight: 500;
- color: var(--text-primary);
- display: block;
- line-height: 1.4;
- word-break: break-word;
-}
-
-.task-card.is-done .task-description {
- color: #9CA3AF;
- text-decoration: line-through;
- font-weight: 400;
-}
-
-.task-date {
- font-size: var(--text-xs);
- color: var(--text-secondary);
- display: block;
- margin-top: 3px;
- font-weight: 400;
-}
-
-/* ── Task actions ───────────────────────────────────────────────────────────── */
-.task-actions {
- display: flex;
- align-items: center;
- gap: 4px;
- opacity: 0;
- transition: opacity 0.15s;
- flex-shrink: 0;
-}
-
-.action-btn {
- width: clamp(30px, 5vw, 34px);
- height: clamp(30px, 5vw, 34px);
- border-radius: var(--radius-sm);
- border: none;
- background: transparent;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-secondary);
- transition: background 0.12s, color 0.12s;
- outline: none;
-}
-
-.action-btn:hover { background: var(--border); color: var(--text-primary); }
-.action-btn.delete:hover { background: var(--danger-light); color: var(--danger); }
-
-/* ── Empty state ────────────────────────────────────────────────────────────── */
-.empty-state {
- text-align: center;
- padding: clamp(24px, 5vw, 36px) 0 clamp(20px, 4vw, 28px);
- color: var(--text-secondary);
- font-size: var(--text-base);
- font-weight: 500;
-}
-
-/* ── Tabs ───────────────────────────────────────────────────────────────────── */
-.tabs {
- display: flex;
- gap: 5px;
- margin-bottom: clamp(14px, 3vw, 20px);
- background: var(--surface);
- border-radius: var(--radius-lg);
- padding: 5px;
- box-shadow: var(--shadow-card);
-}
-
-.tab-btn {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- padding: clamp(8px, 1.8vw, 10px) clamp(10px, 2.5vw, 16px);
- border: none;
- border-radius: var(--radius-md);
- background: transparent;
- font-family: inherit;
- font-size: var(--text-sm);
- font-weight: 600;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 0.18s;
- min-height: 40px;
-}
-
-.tab-btn svg { flex-shrink: 0; }
-
-.tab-btn:hover { background: var(--purple-soft); color: var(--purple); }
-.tab-btn.active { background: var(--purple); color: white; box-shadow: 0 4px 12px rgba(124,58,237,0.3); }
-
-/* Hide icon labels on very small screens */
-@media (max-width: 359px) {
- .tab-btn span { display: none; }
-}
-
-/* ── Dashboard ──────────────────────────────────────────────────────────────── */
-.dashboard {
- display: flex;
- flex-direction: column;
- gap: clamp(12px, 2.5vw, 16px);
-}
-
-/* KPI grid: 2 cols on mobile → 4 cols on sm+ */
-.kpi-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: clamp(8px, 2vw, 12px);
-}
-
-@media (min-width: 480px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } }
-
-.kpi-card {
- background: var(--kpi-bg);
- border-radius: var(--radius-lg);
- padding: clamp(14px, 2.5vw, 18px) clamp(12px, 2.5vw, 16px);
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.kpi-value {
- font-size: clamp(20px, 4vw, 28px);
- font-weight: 700;
- color: var(--kpi-color);
- line-height: 1;
-}
-
-.kpi-label {
- font-size: var(--text-xs);
- font-weight: 600;
- color: var(--kpi-color);
- opacity: 0.7;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-/* ── Chart card ─────────────────────────────────────────────────────────────── */
-.chart-card {
- background: var(--surface);
- border-radius: var(--radius-lg);
- padding: clamp(14px, 3vw, 20px);
- box-shadow: var(--shadow-card);
- /* prevent chart overflow on small screens */
- overflow: hidden;
-}
-
-.chart-header { margin-bottom: clamp(10px, 2vw, 16px); }
-
-.chart-header h3 {
- font-size: var(--text-base);
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 2px;
-}
-
-.chart-header p { font-size: var(--text-xs); color: var(--text-secondary); font-weight: 400; }
-
-.chart-wrap { width: 100%; overflow: hidden; }
-
-.chart-tooltip {
- background: white;
- border-radius: 12px;
- padding: 10px 14px;
- box-shadow: 0 8px 32px rgba(124,58,237,0.15);
- font-family: 'Poppins', sans-serif;
- font-size: 12px;
- border: none;
-}
-
-.tooltip-label { font-weight: 700; color: var(--text-primary); margin-bottom: 6px; font-size: 13px; }
-
-/* ── Insights ───────────────────────────────────────────────────────────────── */
-.insights-section { margin-top: 4px; }
-
-.insights-header { margin-bottom: clamp(8px, 2vw, 12px); }
-
-.insights-header h3 { font-size: var(--text-base); font-weight: 700; color: var(--text-primary); margin-bottom: 2px; }
-.insights-header p { font-size: var(--text-xs); color: var(--text-secondary); }
-
-/* 1 col mobile → 2 cols sm+ */
-.insights-grid {
- display: grid;
- grid-template-columns: 1fr;
- gap: clamp(8px, 2vw, 10px);
-}
-
-@media (min-width: 560px) { .insights-grid { grid-template-columns: repeat(2, 1fr); } }
-
-.insight-card {
- background: var(--ins-bg);
- border-left: 4px solid var(--ins-border);
- border-radius: var(--radius-md);
- padding: clamp(10px, 2vw, 14px) clamp(12px, 2.5vw, 16px);
- display: flex;
- flex-direction: column;
- gap: 5px;
-}
-
-.insight-tag { font-size: var(--text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: var(--ins-tag); }
-.insight-title { font-size: var(--text-sm); font-weight: 700; color: var(--text-primary); line-height: 1.3; }
-.insight-body { font-size: var(--text-xs); color: var(--text-secondary); line-height: 1.5; }
-
-/* ── Actions ────────────────────────────────────────────────────────────────── */
-.actions-list { display: flex; flex-direction: column; gap: clamp(6px, 1.5vw, 8px); }
-
-.action-item {
- background: var(--act-bg);
- border-radius: var(--radius-md);
- padding: clamp(10px, 2vw, 12px) clamp(12px, 2.5vw, 16px);
- display: flex;
- align-items: flex-start;
- gap: clamp(8px, 2vw, 12px);
-}
-
-.action-priority {
- flex-shrink: 0;
- font-size: var(--text-xs);
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: var(--act-color);
- background: white;
- border: 1.5px solid var(--act-color);
- border-radius: var(--radius-pill);
- padding: 2px 8px;
- margin-top: 2px;
- white-space: nowrap;
-}
-
-.action-text { font-size: var(--text-sm); color: var(--text-primary); line-height: 1.5; }
-
-/* ── Loading ────────────────────────────────────────────────────────────────── */
-.loading-wrap { display: flex; justify-content: center; padding: clamp(32px, 6vw, 48px) 0; }
+.test-tailwind {
+ @apply bg-orange-500 p-10 text-white font-bold;
+}
\ No newline at end of file
From 41d495de38a59e7c579661fad22706a7b9860428 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago
Date: Wed, 15 Apr 2026 13:51:07 -0600
Subject: [PATCH 11/23] Work item components
---
.../backend/src/main/frontend/craco.config.js | 7 +
.../src/main/frontend/package-lock.json | 16 ++
.../backend/src/main/frontend/package.json | 1 +
.../backend/src/main/frontend/src/App.js | 252 ------------------
.../backend/src/main/frontend/src/App.tsx | 45 ++++
.../work-items/components/comment-item.tsx | 0
.../work-items/components/comment-thread.tsx | 0
.../components/shared/metric-card.tsx | 28 ++
.../components/shared/person-avatar.tsx | 24 ++
.../components/shared/person-stack.tsx | 29 ++
.../components/shared/work-item-badge-row.tsx | 44 +++
.../components/work-item-activity-panel.tsx | 0
.../components/work-item-comments-panel.tsx | 0
.../components/work-item-context-card.tsx | 45 ++++
.../components/work-item-detail-header.tsx | 93 +++++++
.../components/work-item-detail-summary.tsx | 0
.../components/work-item-links-panel.tsx | 0
.../components/work-item-meta-card.tsx | 25 ++
.../components/work-item-metrics.tsx | 41 +++
.../components/work-item-progress-card.tsx | 34 +++
.../facades/work-item-detail.facade.impl.ts | 0
.../facades/work-item-detail.facade.ts | 0
.../features/work-items/lib/work-item-ui.ts | 65 +++++
.../model/work-item-detail-screen-data.ts | 0
.../model/work-item-detail-state.ts | 0
.../pages/work-item-detail-page.tsx | 0
.../pages/work-item-detail-ui-prototype.tsx | 43 +++
.../work-items/types/work-item-ui.types.ts | 31 +++
.../backend/src/main/frontend/tsconfig.json | 4 +-
29 files changed, 573 insertions(+), 254 deletions(-)
delete mode 100644 MtdrSpring/backend/src/main/frontend/src/App.js
create mode 100644 MtdrSpring/backend/src/main/frontend/src/App.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-item.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-thread.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/metric-card.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-avatar.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-stack.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/work-item-badge-row.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-activity-panel.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-comments-panel.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-context-card.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-header.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-summary.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-links-panel.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-meta-card.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-metrics.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-progress-card.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.impl.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/work-item-ui.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-screen-data.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-state.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-page.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-ui-prototype.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/types/work-item-ui.types.ts
diff --git a/MtdrSpring/backend/src/main/frontend/craco.config.js b/MtdrSpring/backend/src/main/frontend/craco.config.js
index c92370e1d..34ff10e49 100644
--- a/MtdrSpring/backend/src/main/frontend/craco.config.js
+++ b/MtdrSpring/backend/src/main/frontend/craco.config.js
@@ -1,4 +1,11 @@
+const path = require('path');
+
module.exports = {
+ webpack: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
style: {
postcss: {
mode: "extends",
diff --git a/MtdrSpring/backend/src/main/frontend/package-lock.json b/MtdrSpring/backend/src/main/frontend/package-lock.json
index 5330eaa09..8a45b839d 100644
--- a/MtdrSpring/backend/src/main/frontend/package-lock.json
+++ b/MtdrSpring/backend/src/main/frontend/package-lock.json
@@ -15,6 +15,7 @@
"@mui/material": "^5.8.0",
"@mui/styles": "^5.7.0",
"@tailwindcss/vite": "^4.2.2",
+ "lucide-react": "^1.8.0",
"moment": "^2.29.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
@@ -13195,6 +13196,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@@ -28191,6 +28201,12 @@
"yallist": "^3.0.2"
}
},
+ "lucide-react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
+ "requires": {}
+ },
"magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
diff --git a/MtdrSpring/backend/src/main/frontend/package.json b/MtdrSpring/backend/src/main/frontend/package.json
index 39c31ab63..9157d88f9 100644
--- a/MtdrSpring/backend/src/main/frontend/package.json
+++ b/MtdrSpring/backend/src/main/frontend/package.json
@@ -10,6 +10,7 @@
"@mui/material": "^5.8.0",
"@mui/styles": "^5.7.0",
"@tailwindcss/vite": "^4.2.2",
+ "lucide-react": "^1.8.0",
"moment": "^2.29.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
diff --git a/MtdrSpring/backend/src/main/frontend/src/App.js b/MtdrSpring/backend/src/main/frontend/src/App.js
deleted file mode 100644
index 18897dfb2..000000000
--- a/MtdrSpring/backend/src/main/frontend/src/App.js
+++ /dev/null
@@ -1,252 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import NewItem from './NewItem';
-import Dashboard from './Dashboard';
-import API_LIST from './API';
-import DeleteIcon from '@mui/icons-material/Delete';
-import TaskAltIcon from '@mui/icons-material/TaskAlt';
-import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
-import BarChartIcon from '@mui/icons-material/BarChart';
-import { CircularProgress } from '@mui/material';
-import Moment from 'react-moment';
-
-const CARD_COLORS = ['#7C3AED', '#F59E0B', '#14B8A6', '#EC4899', '#3B82F6', '#EF4444'];
-
-function App() {
- const [activeTab, setActiveTab] = useState('tasks');
- const [isLoading] = useState(false);
- const [isInserting, setInserting] = useState(false);
- const [items, setItems] = useState([]);
- const [, setError] = useState();
-
- function deleteItem(deleteId) {
- fetch(API_LIST + "/" + deleteId, { method: 'DELETE' })
- .then(response => {
- if (response.ok) return response;
- throw new Error('Something went wrong ...');
- })
- .then(
- () => { setItems(prev => prev.filter(item => item.id !== deleteId)); },
- (err) => { setError(err); }
- );
- }
-
- function toggleDone(event, id, description, done) {
- event.preventDefault();
- modifyItem(id, description, done).then(
- () => { reloadOneItem(id); },
- (err) => { setError(err); }
- );
- }
-
- function reloadOneItem(id) {
- fetch(API_LIST + "/" + id)
- .then(response => {
- if (response.ok) return response.json();
- throw new Error('Something went wrong ...');
- })
- .then(
- (result) => {
- setItems(prev => prev.map(x =>
- x.id === id ? { ...x, description: result.description, done: result.done } : x
- ));
- },
- (err) => { setError(err); }
- );
- }
-
- function modifyItem(id, description, done) {
- return fetch(API_LIST + "/" + id, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ description, done }),
- }).then(response => {
- if (response.ok) return response;
- throw new Error('Something went wrong ...');
- });
- }
-
- useEffect(() => {
- setItems([
- { id: 1, description: 'Design new dashboard layout', createdAt: '2026-04-14T09:00:00', done: false },
- { id: 2, description: 'Fix login bug on mobile', createdAt: '2026-04-14T10:30:00', done: false },
- { id: 3, description: 'Write unit tests for API', createdAt: '2026-04-13T15:00:00', done: false },
- { id: 4, description: 'Deploy to staging environment', createdAt: '2026-04-13T11:00:00', done: true },
- { id: 5, description: 'Review pull request #42', createdAt: '2026-04-12T08:00:00', done: true },
- ]);
- }, []);
-
- function addItem(text) {
- setInserting(true);
- fetch(API_LIST, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ description: text }),
- })
- .then(response => {
- if (response.ok) return response;
- throw new Error('Something went wrong ...');
- })
- .then(
- (result) => {
- const id = result.headers.get('location');
- setItems(prev => [{ id, description: text }, ...prev]);
- setInserting(false);
- },
- (err) => { setInserting(false); setError(err); }
- );
- }
-
- const todoItems = items.filter(item => !item.done);
- const doneItems = items.filter(item => item.done);
- const donePercent = items.length > 0 ? Math.round((doneItems.length / items.length) * 100) : 0;
-
- return (
-
-
-
- {/* Left panel — header + stats */}
-
-
-
-
-
- My Tasks
- Is this orange?
- Is this orange?
- Stay organized, stay focused
-
-
-
- {todoItems.length} pending
-
-
-
- {doneItems.length} completed
-
-
-
-
- {items.length > 0 && (
-
-
- {doneItems.length} of {items.length} tasks completed
- {donePercent}%
-
-
-
- )}
-
-
- {/* Right panel — tasks */}
-
-
- setActiveTab('tasks')}
- >
-
- Tasks
-
- setActiveTab('analytics')}
- >
-
- Analytics
-
-
-
- {activeTab === 'analytics' ? (
-
- ) : (
-
-
-
- {isLoading ? (
-
-
-
- ) : (
- <>
-
-
-
To Do
-
- {todoItems.length}
-
-
- {todoItems.length === 0 ? (
- All caught up — nothing left to do!
- ) : (
- todoItems.map((item, i) => (
-
-
toggleDone(e, item.id, item.description, true)}
- title="Mark as done"
- />
-
- {item.description}
- {item.createdAt && (
-
- {item.createdAt}
-
- )}
-
-
- ))
- )}
-
-
- {doneItems.length > 0 && (
-
-
-
Completed
- {doneItems.length}
-
- {doneItems.map((item) => (
-
-
toggleDone(e, item.id, item.description, false)}
- title="Mark as to do"
- />
-
- {item.description}
- {item.createdAt && (
-
- {item.createdAt}
-
- )}
-
-
- deleteItem(item.id)}
- title="Delete task"
- >
-
-
-
-
- ))}
-
- )}
- >
- )}
-
- )}
-
-
-
-
- );
-}
-
-export default App;
diff --git a/MtdrSpring/backend/src/main/frontend/src/App.tsx b/MtdrSpring/backend/src/main/frontend/src/App.tsx
new file mode 100644
index 000000000..8b0638f1d
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/App.tsx
@@ -0,0 +1,45 @@
+import { WorkItemContextCard } from '@/features/work-items/components/work-item-context-card';
+import { WorkItemDetailHeader } from '@/features/work-items/components/work-item-detail-header';
+import type { WorkItemDetail } from '@/features/work-items/types/work-item-ui.types';
+
+const mockWorkItem: WorkItemDetail = {
+ id: 'WI-102',
+ title: 'Create work item detail experience for managers and developers',
+ type: 'feature',
+ status: 'in_progress',
+ priority: 'high',
+ sprintName: 'Sprint 04 · Frontend Foundations',
+ estimatedHours: 12,
+ loggedHours: 7.5,
+ dueDate: 'Apr 22, 2026',
+ description:
+ 'Design and implement a polished work item detail screen that consolidates task context, assignees, discussion, related items, and activity history.',
+ acceptanceCriteria: [
+ 'Header shows title, type, status, priority, sprint, and main actions.',
+ 'The layout supports comments, related links, and activity in distinct reusable panels.',
+ 'The visual hierarchy is strong enough for manager visibility and quick developer execution.',
+ ],
+ tags: ['frontend', 'design-system', 'mvp'],
+ assignees: [
+ { id: 'u1', name: 'Bernardo', role: 'Manager' },
+ { id: 'u2', name: 'Ana Torres', role: 'Frontend Dev' },
+ { id: 'u3', name: 'Luis Vega', role: 'Backend Dev' },
+ ],
+ reporter: { id: 'u4', name: 'Sofia Ruiz', role: 'Product Owner' },
+ externalLink: 'https://example.com/spec/work-item-detail',
+ commentsCount: 6,
+ linkedItemsCount: 3,
+};
+
+function App() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-item.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-item.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-thread.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/comment-thread.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/metric-card.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/metric-card.tsx
new file mode 100644
index 000000000..edcaabc14
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/metric-card.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+interface MetricCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string;
+ hint: string;
+}
+
+export function MetricCard({ icon, label, value, hint }: MetricCardProps) {
+ return (
+
+
+
+ {icon}
+
+
+
+
+ {label}
+
+
{value}
+
{hint}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-avatar.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-avatar.tsx
new file mode 100644
index 000000000..529018f02
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-avatar.tsx
@@ -0,0 +1,24 @@
+import type { Person } from '../../types/work-item-ui.types';
+import { getPersonInitials, joinClasses } from '../../lib/work-item-ui';
+
+interface PersonAvatarProps {
+ person: Person;
+ className?: string;
+}
+
+export function PersonAvatar({ person, className }: PersonAvatarProps) {
+ return (
+
+
+ {getPersonInitials(person)}
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-stack.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-stack.tsx
new file mode 100644
index 000000000..69736afdc
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/person-stack.tsx
@@ -0,0 +1,29 @@
+import type { Person } from '../../types/work-item-ui.types';
+import { PersonAvatar } from './person-avatar';
+
+interface PersonStackProps {
+ people: Person[];
+}
+
+export function PersonStack({ people }: PersonStackProps) {
+ return (
+
+
+ {people.map((person) => (
+
+ ))}
+
+
+
+
+ {people.length} collaborators
+
+
Cross-functional ownership
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/work-item-badge-row.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/work-item-badge-row.tsx
new file mode 100644
index 000000000..e497d2468
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/shared/work-item-badge-row.tsx
@@ -0,0 +1,44 @@
+import type {
+ WorkItemPriority,
+ WorkItemStatus,
+ WorkItemType,
+} from '../../types/work-item-ui.types';
+import {
+ formatStatus,
+ getPriorityClasses,
+ getStatusClasses,
+ getTypeClasses,
+ joinClasses,
+} from '../../lib/work-item-ui';
+import React from "react";
+
+interface WorkItemBadgeRowProps {
+ id: string;
+ type: WorkItemType;
+ status: WorkItemStatus;
+ priority: WorkItemPriority;
+}
+
+function Pill({children, className}: { children: React.ReactNode; className?: string; }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function WorkItemBadgeRow({id, type, status, priority,}: WorkItemBadgeRowProps) {
+ return (
+
+
{type}
+
{formatStatus(status)}
+
{priority} priority
+
{id}
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-activity-panel.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-activity-panel.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-comments-panel.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-comments-panel.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-context-card.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-context-card.tsx
new file mode 100644
index 000000000..c0bbd1228
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-context-card.tsx
@@ -0,0 +1,45 @@
+import { CheckCircle2 } from 'lucide-react';
+import type { WorkItemDetail } from '../types/work-item-ui.types';
+
+interface WorkItemContextCardProps {
+ item: WorkItemDetail;
+}
+
+export function WorkItemContextCard({ item }: WorkItemContextCardProps) {
+ return (
+
+
+
Work context
+
+
+
+
+
Description
+
+ {item.description}
+
+
+
+
+
+
+
+ Acceptance criteria
+
+
+
+ {item.acceptanceCriteria?.map((criterion) => (
+
+ ))}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-header.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-header.tsx
new file mode 100644
index 000000000..d8bd121da
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-header.tsx
@@ -0,0 +1,93 @@
+import { CalendarDays, ExternalLink, Flag, UserRound } from 'lucide-react';
+import type { WorkItemDetail } from '../types/work-item-ui.types';
+import { WorkItemBadgeRow } from './shared/work-item-badge-row';
+import { WorkItemMetaCard } from './work-item-meta-card';
+import { WorkItemMetrics } from './work-item-metrics';
+import { WorkItemProgressCard } from './work-item-progress-card';
+
+interface WorkItemDetailHeaderProps {
+ item: WorkItemDetail;
+ onMarkDone?: () => void;
+ onLogTime?: () => void;
+ onOpenExternal?: () => void;
+}
+
+export function WorkItemDetailHeader({ item, onMarkDone, onLogTime, onOpenExternal,}: WorkItemDetailHeaderProps) {
+ return (
+
+
+
+
+
+
+
+
+ {item.title}
+
+
+
+ {item.description}
+
+
+
+
+
+
+ {item.dueDate}
+
+
+
+
+ {item.sprintName}
+
+
+
+
+ Reporter: {item.reporter.name}
+
+
+
+
+
+
+ Mark as Done
+
+
+
+ Log Time
+
+
+
+
+ Open Spec
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-summary.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-detail-summary.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-links-panel.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-links-panel.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-meta-card.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-meta-card.tsx
new file mode 100644
index 000000000..f4a36aee9
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-meta-card.tsx
@@ -0,0 +1,25 @@
+import type { WorkItemDetail } from '../types/work-item-ui.types';
+import { PersonStack } from './shared/person-stack';
+
+interface WorkItemMetaCardProps {
+ item: WorkItemDetail;
+}
+
+export function WorkItemMetaCard({ item }: WorkItemMetaCardProps) {
+ return (
+
+
+
+
+ {item.tags.map((tag) => (
+
+ #{tag}
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-metrics.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-metrics.tsx
new file mode 100644
index 000000000..4a0ad2fd6
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-metrics.tsx
@@ -0,0 +1,41 @@
+import { Clock3, Link2, MessageSquare, Timer } from 'lucide-react';
+import type { WorkItemDetail } from '../types/work-item-ui.types';
+import { MetricCard } from './shared/metric-card';
+
+interface WorkItemMetricsProps {
+ item: WorkItemDetail;
+}
+
+export function WorkItemMetrics({ item }: WorkItemMetricsProps) {
+ return (
+
+ }
+ label="Estimate"
+ value={`${item.estimatedHours}h`}
+ hint="Original planning effort"
+ />
+
+ }
+ label="Logged"
+ value={`${item.loggedHours}h`}
+ hint="Actual work captured"
+ />
+
+ }
+ label="Discussion"
+ value={`${item.commentsCount}`}
+ hint="Active collaboration"
+ />
+
+ }
+ label="Linked items"
+ value={`${item.linkedItemsCount}`}
+ hint="Dependencies and blockers"
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-progress-card.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-progress-card.tsx
new file mode 100644
index 000000000..5b4d38393
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/work-item-progress-card.tsx
@@ -0,0 +1,34 @@
+import type { WorkItemDetail } from '../types/work-item-ui.types';
+
+interface WorkItemProgressCardProps {
+ item: WorkItemDetail;
+}
+
+export function WorkItemProgressCard({ item }: WorkItemProgressCardProps) {
+ const progress = Math.min(
+ 100,
+ Math.round((item.loggedHours / item.estimatedHours) * 100),
+ );
+
+ return (
+
+
+
+
Execution progress
+
+ Logged effort versus original estimate
+
+
+
+
{progress}%
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.impl.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.impl.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/facades/work-item-detail.facade.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/work-item-ui.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/work-item-ui.ts
new file mode 100644
index 000000000..6c0e7bd64
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/work-item-ui.ts
@@ -0,0 +1,65 @@
+import type {
+ Person,
+ WorkItemPriority,
+ WorkItemStatus,
+ WorkItemType,
+} from '../types/work-item-ui.types';
+
+export function joinClasses(...values: Array): string {
+ return values.filter(Boolean).join(' ');
+}
+
+export function getPersonInitials(person: Person): string {
+ return person.name
+ .split(' ')
+ .map((part) => part[0])
+ .join('')
+ .slice(0, 2)
+ .toUpperCase();
+}
+
+export function formatStatus(status: WorkItemStatus): string {
+ return status.replace('_', ' ');
+}
+
+export function getTypeClasses(type: WorkItemType): string {
+ switch (type) {
+ case 'feature':
+ return 'border-cyan-400/30 bg-cyan-500/15 text-cyan-300';
+ case 'bug':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'issue':
+ return 'border-orange-400/30 bg-orange-500/15 text-orange-300';
+ case 'task':
+ default:
+ return 'border-indigo-400/30 bg-indigo-500/15 text-indigo-300';
+ }
+}
+
+export function getStatusClasses(status: WorkItemStatus): string {
+ switch (status) {
+ case 'done':
+ return 'border-emerald-400/30 bg-emerald-500/15 text-emerald-300';
+ case 'blocked':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'in_progress':
+ return 'border-sky-400/30 bg-sky-500/15 text-sky-300';
+ case 'todo':
+ default:
+ return 'border-zinc-400/30 bg-zinc-500/15 text-zinc-300';
+ }
+}
+
+export function getPriorityClasses(priority: WorkItemPriority): string {
+ switch (priority) {
+ case 'critical':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'high':
+ return 'border-amber-400/30 bg-amber-500/15 text-amber-300';
+ case 'medium':
+ return 'border-violet-400/30 bg-violet-500/15 text-violet-300';
+ case 'low':
+ default:
+ return 'border-zinc-400/30 bg-zinc-500/15 text-zinc-300';
+ }
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-screen-data.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-screen-data.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-state.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/model/work-item-detail-state.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-page.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-page.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-ui-prototype.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-ui-prototype.tsx
new file mode 100644
index 000000000..65ce3f326
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-detail-ui-prototype.tsx
@@ -0,0 +1,43 @@
+import { WorkItemContextCard } from '@/features/work-items/components/work-item-context-card';
+import { WorkItemDetailHeader } from '@/features/work-items/components/work-item-detail-header';
+import type { WorkItemDetail } from '@/features/work-items/types/work-item-ui.types';
+
+const mockWorkItem: WorkItemDetail = {
+ id: 'WI-102',
+ title: 'Create work item detail experience for managers and developers',
+ type: 'feature',
+ status: 'in_progress',
+ priority: 'high',
+ sprintName: 'Sprint 04 · Frontend Foundations',
+ estimatedHours: 12,
+ loggedHours: 7.5,
+ dueDate: 'Apr 22, 2026',
+ description:
+ 'Design and implement a polished work item detail screen that consolidates task context, assignees, discussion, related items, and activity history.',
+ acceptanceCriteria: [
+ 'Header shows title, type, status, priority, sprint, and main actions.',
+ 'The layout supports comments, related links, and activity in distinct reusable panels.',
+ 'The visual hierarchy is strong enough for manager visibility and quick developer execution.',
+ ],
+ tags: ['frontend', 'design-system', 'mvp'],
+ assignees: [
+ { id: 'u1', name: 'Bernardo', role: 'Manager' },
+ { id: 'u2', name: 'Ana Torres', role: 'Frontend Dev' },
+ { id: 'u3', name: 'Luis Vega', role: 'Backend Dev' },
+ ],
+ reporter: { id: 'u4', name: 'Sofia Ruiz', role: 'Product Owner' },
+ externalLink: 'https://example.com/spec/work-item-detail',
+ commentsCount: 6,
+ linkedItemsCount: 3,
+};
+
+export function WorkItemPrototypePage() {
+ return (
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/types/work-item-ui.types.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/types/work-item-ui.types.ts
new file mode 100644
index 000000000..48c8285dc
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/types/work-item-ui.types.ts
@@ -0,0 +1,31 @@
+export type WorkItemType = 'feature' | 'issue' | 'bug' | 'task';
+
+export type WorkItemStatus = 'todo' | 'in_progress' | 'blocked' | 'done';
+
+export type WorkItemPriority = 'low' | 'medium' | 'high' | 'critical';
+
+export interface Person {
+ id: string;
+ name: string;
+ role: string;
+}
+
+export interface WorkItemDetail {
+ id: string;
+ title: string;
+ type: WorkItemType;
+ status: WorkItemStatus;
+ priority: WorkItemPriority;
+ sprintName: string;
+ estimatedHours: number;
+ loggedHours: number;
+ dueDate: string;
+ description: string;
+ acceptanceCriteria?: string[];
+ tags: string[];
+ assignees: Person[];
+ reporter: Person;
+ externalLink?: string;
+ commentsCount: number;
+ linkedItemsCount: number;
+}
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/tsconfig.json b/MtdrSpring/backend/src/main/frontend/tsconfig.json
index 84ad7aecf..1d61ef4e1 100644
--- a/MtdrSpring/backend/src/main/frontend/tsconfig.json
+++ b/MtdrSpring/backend/src/main/frontend/tsconfig.json
@@ -28,9 +28,9 @@
"module": "ESNext", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
- "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["src/*"]
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
From 4412948195b826fdabf35af3b0cdeb9eeea26fef Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 14:01:46 -0600
Subject: [PATCH 12/23] Enhance Frontend UI Developer agent documentation
Updated the Frontend UI Developer agent with detailed mission, scope, working rules, UI expectations, data integration, and done criteria.
---
.github/agents/frontend-ui-developer.md | 74 +++++++++++++++++++++++++
1 file changed, 74 insertions(+)
create mode 100644 .github/agents/frontend-ui-developer.md
diff --git a/.github/agents/frontend-ui-developer.md b/.github/agents/frontend-ui-developer.md
new file mode 100644
index 000000000..5d82107ad
--- /dev/null
+++ b/.github/agents/frontend-ui-developer.md
@@ -0,0 +1,74 @@
+---
+name: Frontend UI Developer
+description: Designs and implements frontend UI interfaces for the React + TypeScript application only. Focuses on reusable components, pages, layouts, and mock-driven frontend flows. Does not modify backend, database, infrastructure, or API contracts unless explicitly instructed by a human.
+---
+
+# Frontend UI Developer
+
+## Mission
+Build and refine the frontend user interface for the project using React + TypeScript.
+
+Prioritize:
+- clear and reusable UI components;
+- clean page composition;
+- frontend-first development with mock data/services when needed;
+- consistency with existing project styles and structure.
+
+## Read first
+Before making changes, check:
+
+1. `.github/copilot-instructions.md`
+2. `.github/context/project-overview.md`
+3. `.github/context/frontend-boundaries.md`
+4. `.github/context/ui-conventions.md`
+
+## Scope
+You may:
+- create and update React components, pages, layouts, hooks, mappers, view models, mock data, and frontend services;
+- improve visual hierarchy, spacing, responsiveness, and usability;
+- connect UI to existing frontend DTOs and mock service layers;
+- prepare the UI so real backend integration can be wired later with minimal refactoring.
+- Work in the `/MtdrSpring/backend/src/main/frontend/` React subdirectory.
+- Read the `/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/` java package for guidance about the response types of the backend.
+
+You must not:
+- modify backend code, database scripts, infrastructure, CI/CD, Telegram bot code, or API contracts on your own;
+- invent backend behavior without clearly isolating it behind mock services or typed interfaces;
+- couple UI components directly to backend implementation details;
+- introduce large dependencies unless already used in the repo or clearly justified.
+
+## Working rules
+- Stay inside the frontend area of the repository.
+- Prefer small, composable, reusable components over large page-specific ones.
+- Follow existing folder and naming conventions.
+- Use TypeScript interfaces/types for domain-facing data models and component props.
+- Use semicolons.
+- Strongly type where it improves readability and safety; avoid unnecessary noise.
+- Reuse shared UI patterns before creating new ones.
+- Keep components presentational when possible; place mapping/transformation logic outside UI components.
+- Support loading, empty, error, and populated states where relevant.
+- Keep accessibility in mind: semantic HTML, labels, keyboard navigation, and sensible contrast.
+- Keep styling consistent with the project’s Tailwind and design patterns.
+
+## UI expectations
+- Design for clarity first, then polish.
+- Prefer simple layouts with strong hierarchy and consistent spacing.
+- Build interfaces that are easy to extend later.
+- When creating new screens, think in terms of:
+ - page shell;
+ - section blocks;
+ - reusable cards/lists/forms/dialogs;
+ - typed mock data flow.
+
+## Data and integration
+- Assume the frontend may use mock services before real backend integration.
+- Keep DTOs, view models, and mappers explicit when the transformation adds clarity.
+- If backend data is missing or unclear, do not change backend assumptions; isolate the uncertainty in mock data or adapters.
+
+## Done criteria
+A task is complete when:
+- the UI works locally and is coherent with surrounding screens;
+- the code is readable and reusable;
+- the component/page handles its main visual states;
+- changes stay within frontend scope only;
+- the implementation is ready to be connected to real backend data later without major rewrites.
From 66699ddffbe372eb13153f6294482bb563e8f0c8 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 14:06:39 -0600
Subject: [PATCH 13/23] Add project overview documentation
Added a comprehensive project overview detailing the platform's objectives, user groups, architecture, and MVP focus.
---
.github/context/project-overview.md | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 .github/context/project-overview.md
diff --git a/.github/context/project-overview.md b/.github/context/project-overview.md
new file mode 100644
index 000000000..4fe6794a7
--- /dev/null
+++ b/.github/context/project-overview.md
@@ -0,0 +1,23 @@
+# Project Overview
+
+This repository contains a project management platform designed to improve productivity and activity visibility for remote and
+hybrid software development teams. The system’s stated objective is to increase team productivity and visibility by 20% through
+task automation, structured work tracking, and KPI reporting. The platform serves two main user groups: developers and managers.
+Developers interact primarily through Telegram to review and manage their personal work, while managers need broader visibility
+across the team, including progress, blockers, and estimated-versus-actual effort.
+
+The solution is composed of two delivery channels: a web portal and a Telegram chatbot service. The overall system follows a
+cloud-native approach and is intended to run on Oracle Cloud Infrastructure with Oracle Autonomous Database, Docker, Kubernetes,
+CI/CD pipelines, and infrastructure as code. The backend is planned around Java, Spring Boot, microservices, and REST-based
+integrations. For frontend work, this context matters only to understand the product and data flow; frontend changes should remain
+isolated from backend, infrastructure, and database implementation details.
+
+At the domain level, the core workflow revolves around users, teams, sprints, and work items. A work item may represent a feature,
+issue, or bug, and can include assignments, tags, links to other work items, comments, time entries, and activity logs. The data model
+also includes sprint baselines and KPI definitions/snapshots so the system can track productivity and reporting over time. This means
+the frontend should be designed around a project/work management experience rather than a generic dashboard shell.
+
+From a product perspective, the MVP focuses on work item management, sprint tracking, manager visibility, Telegram-based developer
+interaction, and basic KPI reporting. The frontend should therefore prioritize interfaces such as work item lists, sprint views,
+detail panels, assignments, comments, time tracking, and lightweight KPI summaries. The UI should be structured so mock services
+can be used first and later replaced by real backend integrations with minimal refactoring.
From 1700c9f21a141deda5d34925499b89a77b6e77f1 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 14:07:17 -0600
Subject: [PATCH 14/23] Simplify read first section in frontend-ui-developer.md
Removed redundant instructions for checking files before making changes.
---
.github/agents/frontend-ui-developer.md | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/.github/agents/frontend-ui-developer.md b/.github/agents/frontend-ui-developer.md
index 5d82107ad..e25dd9d11 100644
--- a/.github/agents/frontend-ui-developer.md
+++ b/.github/agents/frontend-ui-developer.md
@@ -17,10 +17,7 @@ Prioritize:
## Read first
Before making changes, check:
-1. `.github/copilot-instructions.md`
-2. `.github/context/project-overview.md`
-3. `.github/context/frontend-boundaries.md`
-4. `.github/context/ui-conventions.md`
+1. `.github/context/project-overview.md`
## Scope
You may:
From e57dd34991f6c340f19ee1e98f3a876b6f2bd7e7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:29:24 +0000
Subject: [PATCH 15/23] Initial plan
From 0cad99714854291ab19c66a56696cefbe1bdddd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:50:02 +0000
Subject: [PATCH 16/23] feat: add work item dashboard with list/kanban views,
create/edit/detail modals, and filters
Agent-Logs-Url: https://github.com/bernardosantiago44/talos_oci_devops_project/sessions/e230285c-35dc-4860-b9c9-a10c2761acb6
Co-authored-by: bernardosantiago44 <63428964+bernardosantiago44@users.noreply.github.com>
---
.../backend/src/main/frontend/src/App.tsx | 42 +-
.../dashboard/dashboard-summary-cards.tsx | 94 +++++
.../dashboard/dashboard-toolbar.tsx | 114 ++++++
.../components/dashboard/kanban-view.tsx | 197 +++++++++
.../dashboard/work-item-detail-modal.tsx | 241 +++++++++++
.../dashboard/work-item-form-modal.tsx | 384 ++++++++++++++++++
.../dashboard/work-item-list-view.tsx | 202 +++++++++
.../features/work-items/lib/dashboard-ui.ts | 124 ++++++
.../work-items/mock/work-items.mock.ts | 215 ++++++++++
.../pages/work-item-dashboard-page.tsx | 234 +++++++++++
.../work-items/services/work-item.service.ts | 32 +-
.../src/main/frontend/tailwind.config.js | 11 +
12 files changed, 1847 insertions(+), 43 deletions(-)
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-summary-cards.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-toolbar.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-detail-modal.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-form-modal.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-list-view.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
create mode 100644 MtdrSpring/backend/src/main/frontend/tailwind.config.js
diff --git a/MtdrSpring/backend/src/main/frontend/src/App.tsx b/MtdrSpring/backend/src/main/frontend/src/App.tsx
index 8b0638f1d..d31527d77 100644
--- a/MtdrSpring/backend/src/main/frontend/src/App.tsx
+++ b/MtdrSpring/backend/src/main/frontend/src/App.tsx
@@ -1,45 +1,7 @@
-import { WorkItemContextCard } from '@/features/work-items/components/work-item-context-card';
-import { WorkItemDetailHeader } from '@/features/work-items/components/work-item-detail-header';
-import type { WorkItemDetail } from '@/features/work-items/types/work-item-ui.types';
-
-const mockWorkItem: WorkItemDetail = {
- id: 'WI-102',
- title: 'Create work item detail experience for managers and developers',
- type: 'feature',
- status: 'in_progress',
- priority: 'high',
- sprintName: 'Sprint 04 · Frontend Foundations',
- estimatedHours: 12,
- loggedHours: 7.5,
- dueDate: 'Apr 22, 2026',
- description:
- 'Design and implement a polished work item detail screen that consolidates task context, assignees, discussion, related items, and activity history.',
- acceptanceCriteria: [
- 'Header shows title, type, status, priority, sprint, and main actions.',
- 'The layout supports comments, related links, and activity in distinct reusable panels.',
- 'The visual hierarchy is strong enough for manager visibility and quick developer execution.',
- ],
- tags: ['frontend', 'design-system', 'mvp'],
- assignees: [
- { id: 'u1', name: 'Bernardo', role: 'Manager' },
- { id: 'u2', name: 'Ana Torres', role: 'Frontend Dev' },
- { id: 'u3', name: 'Luis Vega', role: 'Backend Dev' },
- ],
- reporter: { id: 'u4', name: 'Sofia Ruiz', role: 'Product Owner' },
- externalLink: 'https://example.com/spec/work-item-detail',
- commentsCount: 6,
- linkedItemsCount: 3,
-};
+import { WorkItemDashboardPage } from '@/features/work-items/pages/work-item-dashboard-page';
function App() {
- return (
-
-
-
-
-
-
- );
+ return ;
}
export default App;
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-summary-cards.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-summary-cards.tsx
new file mode 100644
index 000000000..b34c6b4be
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-summary-cards.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { CheckSquare, Clock, AlertCircle, CheckCircle2, AlertTriangle } from 'lucide-react';
+import type { WorkItemDetailDto } from '../../dtos/work-item-detail.dto';
+import { isOverdue } from '../../lib/dashboard-ui';
+
+interface SummaryCardsProps {
+ items: WorkItemDetailDto[];
+}
+
+interface StatCard {
+ label: string;
+ value: number;
+ icon: React.ReactNode;
+ color: string;
+ bg: string;
+ border: string;
+}
+
+export function DashboardSummaryCards({ items }: SummaryCardsProps) {
+ const total = items.length;
+ const todo = items.filter((i) => i.status === 'TODO').length;
+ const inProgress = items.filter((i) => i.status === 'IN_PROGRESS').length;
+ const blocked = items.filter((i) => i.status === 'BLOCKED').length;
+ const done = items.filter((i) => i.status === 'DONE').length;
+ const overdue = items.filter((i) => isOverdue(i.dueDate, i.status)).length;
+
+ const cards: StatCard[] = [
+ {
+ label: 'Total Tasks',
+ value: total,
+ icon: ,
+ color: 'text-zinc-300',
+ bg: 'bg-zinc-800/60',
+ border: 'border-zinc-700/50',
+ },
+ {
+ label: 'Todo',
+ value: todo,
+ icon: ,
+ color: 'text-zinc-400',
+ bg: 'bg-zinc-800/60',
+ border: 'border-zinc-700/50',
+ },
+ {
+ label: 'In Progress',
+ value: inProgress,
+ icon: ,
+ color: 'text-sky-300',
+ bg: 'bg-sky-500/10',
+ border: 'border-sky-500/20',
+ },
+ {
+ label: 'Blocked',
+ value: blocked,
+ icon: ,
+ color: 'text-rose-300',
+ bg: 'bg-rose-500/10',
+ border: 'border-rose-500/20',
+ },
+ {
+ label: 'Done',
+ value: done,
+ icon: ,
+ color: 'text-emerald-300',
+ bg: 'bg-emerald-500/10',
+ border: 'border-emerald-500/20',
+ },
+ {
+ label: 'Overdue',
+ value: overdue,
+ icon: ,
+ color: 'text-amber-300',
+ bg: 'bg-amber-500/10',
+ border: 'border-amber-500/20',
+ },
+ ];
+
+ return (
+
+ {cards.map((card) => (
+
+
+ {card.icon}
+ {card.value}
+
+
{card.label}
+
+ ))}
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-toolbar.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-toolbar.tsx
new file mode 100644
index 000000000..dec751696
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/dashboard-toolbar.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import { Search, ListIcon, LayoutGrid, Plus } from 'lucide-react';
+import type { WorkItemStatus } from '../../enums/work-item-status.enum';
+import type { UserSummaryDto } from '@/shared/dtos/user-summary.dto';
+import { WORK_ITEM_STATUSES } from '../../enums/work-item-status.enum';
+import { formatStatusLabel } from '../../lib/dashboard-ui';
+
+export type ViewMode = 'list' | 'kanban';
+
+interface DashboardToolbarProps {
+ search: string;
+ onSearchChange: (v: string) => void;
+ statusFilter: WorkItemStatus | '';
+ onStatusFilterChange: (v: WorkItemStatus | '') => void;
+ assigneeFilter: string;
+ onAssigneeFilterChange: (v: string) => void;
+ viewMode: ViewMode;
+ onViewModeChange: (v: ViewMode) => void;
+ onCreateClick: () => void;
+ users: UserSummaryDto[];
+}
+
+export function DashboardToolbar({
+ search,
+ onSearchChange,
+ statusFilter,
+ onStatusFilterChange,
+ assigneeFilter,
+ onAssigneeFilterChange,
+ viewMode,
+ onViewModeChange,
+ onCreateClick,
+ users,
+}: DashboardToolbarProps) {
+ return (
+
+ {/* Search */}
+
+
+ onSearchChange(e.target.value)}
+ className="w-full rounded-xl border border-zinc-700/60 bg-zinc-800/60 py-2 pl-9 pr-4 text-sm text-zinc-200 placeholder-zinc-500 outline-none focus:border-sky-500/60 focus:ring-1 focus:ring-sky-500/30"
+ />
+
+
+ {/* Status filter */}
+
onStatusFilterChange(e.target.value as WorkItemStatus | '')}
+ className="rounded-xl border border-zinc-700/60 bg-zinc-800/60 px-3 py-2 text-sm text-zinc-300 outline-none focus:border-sky-500/60 focus:ring-1 focus:ring-sky-500/30"
+ >
+ All statuses
+ {WORK_ITEM_STATUSES.map((s) => (
+ {formatStatusLabel(s)}
+ ))}
+
+
+ {/* Assignee filter */}
+
onAssigneeFilterChange(e.target.value)}
+ className="rounded-xl border border-zinc-700/60 bg-zinc-800/60 px-3 py-2 text-sm text-zinc-300 outline-none focus:border-sky-500/60 focus:ring-1 focus:ring-sky-500/30"
+ >
+ All assignees
+ {users.map((u) => (
+ {u.name}
+ ))}
+
+
+ {/* View toggle */}
+
+ onViewModeChange('list')}
+ className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
+ viewMode === 'list'
+ ? 'bg-zinc-700 text-white'
+ : 'text-zinc-400 hover:text-zinc-200'
+ }`}
+ title="List view"
+ >
+
+ List
+
+ onViewModeChange('kanban')}
+ className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
+ viewMode === 'kanban'
+ ? 'bg-zinc-700 text-white'
+ : 'text-zinc-400 hover:text-zinc-200'
+ }`}
+ title="Kanban view"
+ >
+
+ Kanban
+
+
+
+ {/* Create button */}
+
+
+ New Task
+
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
new file mode 100644
index 000000000..571696b0d
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
@@ -0,0 +1,197 @@
+import React from 'react';
+import { CheckCircle2, Pencil, Eye } from 'lucide-react';
+import type { WorkItemDetailDto } from '../../dtos/work-item-detail.dto';
+import type { WorkItemStatus } from '../../enums/work-item-status.enum';
+import {
+ formatStatusLabel,
+ formatTypeLabel,
+ formatPriorityLabel,
+ getStatusBadgeClasses,
+ getPriorityBadgeClasses,
+ getTypeBadgeClasses,
+ getStatusDotColor,
+ calcProgress,
+ isOverdue,
+ formatDate,
+ getInitials,
+ cx,
+} from '../../lib/dashboard-ui';
+
+interface KanbanViewProps {
+ items: WorkItemDetailDto[];
+ onEdit: (item: WorkItemDetailDto) => void;
+ onComplete: (item: WorkItemDetailDto) => void;
+ onViewDetail: (item: WorkItemDetailDto) => void;
+}
+
+const COLUMNS: { status: WorkItemStatus; label: string }[] = [
+ { status: 'TODO', label: 'Todo' },
+ { status: 'IN_PROGRESS', label: 'In Progress' },
+ { status: 'BLOCKED', label: 'Blocked' },
+ { status: 'DONE', label: 'Done' },
+];
+
+function KanbanCard({
+ item,
+ onEdit,
+ onComplete,
+ onViewDetail,
+}: {
+ item: WorkItemDetailDto;
+ onEdit: (item: WorkItemDetailDto) => void;
+ onComplete: (item: WorkItemDetailDto) => void;
+ onViewDetail: (item: WorkItemDetailDto) => void;
+}) {
+ const progress = calcProgress(item.totalLoggedMinutes, item.estimatedMinutes);
+ const overdue = isOverdue(item.dueDate, item.status);
+ const isDone = item.status === 'DONE';
+
+ return (
+
+ {/* Type + Priority badges */}
+
+
+ {formatTypeLabel(item.type)}
+
+
+ {formatPriorityLabel(item.priority)}
+
+
+
+ {/* Title */}
+
onViewDetail(item)}
+ className={cx(
+ 'mt-2 block w-full text-left text-sm font-medium leading-snug transition-colors hover:text-sky-300',
+ isDone ? 'text-zinc-500 line-through' : 'text-zinc-100',
+ )}
+ >
+ {item.title}
+
+
+ {/* Due date */}
+ {item.dueDate && (
+
+ Due {formatDate(item.dueDate)}
+
+ )}
+
+ {/* Progress bar */}
+ {item.estimatedMinutes && item.estimatedMinutes > 0 && (
+
+ )}
+
+ {/* Footer: assignees + actions */}
+
+
+ {item.assignees.length === 0 && (
+
Unassigned
+ )}
+ {item.assignees.slice(0, 3).map((a, i) => (
+
+ {getInitials(a.user.name)}
+
+ ))}
+
+
+
+
onViewDetail(item)}
+ title="View detail"
+ className="rounded-md p-1 text-zinc-500 hover:bg-zinc-700 hover:text-zinc-200"
+ >
+
+
+
onEdit(item)}
+ title="Edit"
+ className="rounded-md p-1 text-zinc-500 hover:bg-zinc-700 hover:text-zinc-200"
+ >
+
+
+ {!isDone && (
+
onComplete(item)}
+ title="Mark done"
+ className="rounded-md p-1 text-zinc-500 hover:bg-emerald-500/20 hover:text-emerald-400"
+ >
+
+
+ )}
+
+
+
+ );
+}
+
+export function KanbanView({ items, onEdit, onComplete, onViewDetail }: KanbanViewProps) {
+ return (
+
+ {COLUMNS.map(({ status, label }) => {
+ const colItems = items.filter((i) => i.status === status);
+ return (
+
+ {/* Column header */}
+
+
+ c.startsWith('text-')) ?? 'text-zinc-300',
+ )}
+ >
+ {formatStatusLabel(status)}
+
+
+ {colItems.length}
+
+
+
+ {/* Cards */}
+
+ {colItems.length === 0 && (
+
+ )}
+ {colItems.map((item) => (
+
+ ))}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-detail-modal.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-detail-modal.tsx
new file mode 100644
index 000000000..7edd961b5
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-detail-modal.tsx
@@ -0,0 +1,241 @@
+import React from 'react';
+import { X, Calendar, Flag, Clock, Tag, Users, CheckCircle2 } from 'lucide-react';
+import type { WorkItemDetailDto } from '../../dtos/work-item-detail.dto';
+import {
+ formatStatusLabel,
+ formatTypeLabel,
+ formatPriorityLabel,
+ getStatusBadgeClasses,
+ getPriorityBadgeClasses,
+ getTypeBadgeClasses,
+ calcProgress,
+ isOverdue,
+ formatDate,
+ getSprintLabel,
+ getInitials,
+ cx,
+} from '../../lib/dashboard-ui';
+
+interface WorkItemDetailModalProps {
+ isOpen: boolean;
+ item: WorkItemDetailDto | null;
+ onClose: () => void;
+ onEdit: (item: WorkItemDetailDto) => void;
+ onComplete: (item: WorkItemDetailDto) => void;
+}
+
+function DetailRow({ icon, label, children }: {
+ icon: React.ReactNode;
+ label: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ );
+}
+
+export function WorkItemDetailModal({
+ isOpen,
+ item,
+ onClose,
+ onEdit,
+ onComplete,
+}: WorkItemDetailModalProps) {
+ if (!isOpen || !item) return null;
+
+ const progress = calcProgress(item.totalLoggedMinutes, item.estimatedMinutes);
+ const overdue = isOverdue(item.dueDate, item.status);
+ const isDone = item.status === 'DONE';
+
+ return (
+
+ {/* Overlay */}
+
+
+ {/* Panel */}
+
+ {/* Header */}
+
+
+
+
+
+ {formatTypeLabel(item.type)}
+
+
+ {formatStatusLabel(item.status)}
+
+
+ {formatPriorityLabel(item.priority)}
+
+
+
+ {item.title}
+
+
{item.id}
+
+
+
+
+
+
+
+ {/* Body */}
+
+
+ {/* Description */}
+ {item.description && (
+
+
Description
+
+ {item.description}
+
+
+ )}
+
+ {/* Meta grid */}
+
+
} label="Due Date">
+
+ {formatDate(item.dueDate)}
+ {overdue && ' · Overdue'}
+
+
+
+
} label="Sprint">
+
{getSprintLabel(item.sprintId)}
+
+
+
} label="Time">
+
+ {item.totalLoggedMinutes}
+ {item.estimatedMinutes ? `/${item.estimatedMinutes}` : ''} min
+
+
+
+
} label="Progress">
+
+
+
+
+ {/* Assignees */}
+
} label="Assignees">
+ {item.assignees.length === 0 ? (
+
Unassigned
+ ) : (
+
+ {item.assignees.map((a) => (
+
+
+ {getInitials(a.user.name)}
+
+
+
{a.user.name}
+
{a.role}
+
+
+ ))}
+
+ )}
+
+
+ {/* Tags */}
+ {item.tags.length > 0 && (
+
} label="Tags">
+
+ {item.tags.map((tag) => (
+
+ #{tag.name}
+
+ ))}
+
+
+ )}
+
+ {/* Comments placeholder */}
+
+
Activity
+
+ Comments and activity history will appear here once connected to the backend.
+
+
+
+
+
+ {/* Footer */}
+
+ onEdit(item)}
+ className="rounded-xl border border-zinc-700/60 bg-zinc-800/60 px-4 py-2 text-sm font-medium text-zinc-300 transition-colors hover:bg-zinc-700 hover:text-zinc-100"
+ >
+ Edit
+
+ {!isDone && (
+ onComplete(item)}
+ className="rounded-xl bg-emerald-500 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-emerald-400"
+ >
+ Mark as Done
+
+ )}
+
+
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-form-modal.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-form-modal.tsx
new file mode 100644
index 000000000..0d3002619
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-form-modal.tsx
@@ -0,0 +1,384 @@
+import React, { useEffect, useState } from 'react';
+import { X } from 'lucide-react';
+import type { WorkItemDetailDto } from '../../dtos/work-item-detail.dto';
+import type { CreateWorkItemDto } from '../../dtos/create-work-item.dto';
+import type { UpdateWorkItemDto } from '../../dtos/update-work-item.dto';
+import type { WorkItemType } from '../../enums/work-item-type.enum';
+import type { WorkItemStatus } from '../../enums/work-item-status.enum';
+import type { WorkItemPriority } from '../../enums/work-item-priority.enum';
+import type { UserSummaryDto } from '@/shared/dtos/user-summary.dto';
+import type { TagDto } from '@/shared/dtos/tag.dto';
+import { WORK_ITEM_TYPES } from '../../enums/work-item-type.enum';
+import { WORK_ITEM_STATUSES } from '../../enums/work-item-status.enum';
+import { WORK_ITEM_PRIORITIES } from '../../enums/work-item-priority.enum';
+import {
+ formatTypeLabel,
+ formatStatusLabel,
+ formatPriorityLabel,
+} from '../../lib/dashboard-ui';
+
+interface WorkItemFormModalProps {
+ isOpen: boolean;
+ item?: WorkItemDetailDto | null;
+ users: UserSummaryDto[];
+ tags: TagDto[];
+ onClose: () => void;
+ onCreate: (dto: CreateWorkItemDto) => Promise;
+ onUpdate: (id: string, dto: UpdateWorkItemDto) => Promise;
+}
+
+interface FormState {
+ title: string;
+ description: string;
+ type: WorkItemType;
+ status: WorkItemStatus;
+ priority: WorkItemPriority;
+ dueDate: string;
+ estimatedMinutes: string;
+ sprintId: string;
+ assigneeUserIds: string[];
+ tagIds: string[];
+}
+
+const DEFAULT_FORM: FormState = {
+ title: '',
+ description: '',
+ type: 'TASK',
+ status: 'TODO',
+ priority: 'MEDIUM',
+ dueDate: '',
+ estimatedMinutes: '',
+ sprintId: '',
+ assigneeUserIds: [],
+ tagIds: [],
+};
+
+const SPRINT_OPTIONS = [
+ { id: '', label: 'No Sprint' },
+ { id: 'spr-001', label: 'Sprint 1' },
+ { id: 'spr-002', label: 'Sprint 2' },
+ { id: 'spr-003', label: 'Sprint 3' },
+];
+
+function Label({ children }: { children: React.ReactNode }) {
+ return {children} ;
+}
+
+function Input({ value, onChange, placeholder, type = 'text' }: {
+ value: string;
+ onChange: (v: string) => void;
+ placeholder?: string;
+ type?: string;
+}) {
+ return (
+ onChange(e.target.value)}
+ placeholder={placeholder}
+ className="w-full rounded-xl border border-zinc-700/60 bg-zinc-800/60 px-3 py-2 text-sm text-zinc-200 placeholder-zinc-600 outline-none focus:border-sky-500/60 focus:ring-1 focus:ring-sky-500/30"
+ />
+ );
+}
+
+function Select({ value, onChange, children }: {
+ value: string;
+ onChange: (v: string) => void;
+ children: React.ReactNode;
+}) {
+ return (
+ onChange(e.target.value)}
+ className="w-full rounded-xl border border-zinc-700/60 bg-zinc-800/60 px-3 py-2 text-sm text-zinc-200 outline-none focus:border-sky-500/60 focus:ring-1 focus:ring-sky-500/30"
+ >
+ {children}
+
+ );
+}
+
+export function WorkItemFormModal({
+ isOpen,
+ item,
+ users,
+ tags,
+ onClose,
+ onCreate,
+ onUpdate,
+}: WorkItemFormModalProps) {
+ const isEditing = !!item;
+ const [form, setForm] = useState(DEFAULT_FORM);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ if (!isOpen) return;
+ if (item) {
+ setForm({
+ title: item.title,
+ description: item.description ?? '',
+ type: item.type,
+ status: item.status,
+ priority: item.priority,
+ dueDate: item.dueDate ?? '',
+ estimatedMinutes: item.estimatedMinutes?.toString() ?? '',
+ sprintId: item.sprintId ?? '',
+ assigneeUserIds: item.assignees.map((a) => a.user.id),
+ tagIds: item.tags.map((t) => t.id),
+ });
+ } else {
+ setForm(DEFAULT_FORM);
+ }
+ setError('');
+ }, [isOpen, item]);
+
+ function set(key: K, value: FormState[K]) {
+ setForm((prev) => ({ ...prev, [key]: value }));
+ }
+
+ function toggleArrayItem(key: 'assigneeUserIds' | 'tagIds', id: string) {
+ setForm((prev) => {
+ const arr = prev[key] as string[];
+ return {
+ ...prev,
+ [key]: arr.includes(id) ? arr.filter((x) => x !== id) : [...arr, id],
+ };
+ });
+ }
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!form.title.trim()) {
+ setError('Title is required.');
+ return;
+ }
+ setSaving(true);
+ setError('');
+ try {
+ const minutes = form.estimatedMinutes ? parseInt(form.estimatedMinutes, 10) : undefined;
+ if (isEditing && item) {
+ const dto: UpdateWorkItemDto = {
+ title: form.title.trim(),
+ description: form.description.trim() || undefined,
+ status: form.status,
+ priority: form.priority,
+ dueDate: form.dueDate || undefined,
+ estimatedMinutes: minutes,
+ assigneeUserIds: form.assigneeUserIds,
+ tagIds: form.tagIds,
+ };
+ await onUpdate(item.id, dto);
+ } else {
+ const dto: CreateWorkItemDto = {
+ title: form.title.trim(),
+ description: form.description.trim() || undefined,
+ type: form.type,
+ status: form.status,
+ priority: form.priority,
+ dueDate: form.dueDate || undefined,
+ estimatedMinutes: minutes,
+ sprintId: form.sprintId || undefined,
+ assigneeUserIds: form.assigneeUserIds,
+ tagIds: form.tagIds,
+ };
+ await onCreate(dto);
+ }
+ onClose();
+ } catch {
+ setError('Something went wrong. Please try again.');
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Overlay */}
+
+
+ {/* Panel */}
+
+ {/* Header */}
+
+
+ {isEditing ? 'Edit Task' : 'New Task'}
+
+
+
+
+
+
+ {/* Form */}
+
+
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-list-view.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-list-view.tsx
new file mode 100644
index 000000000..0cea0b7c7
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/work-item-list-view.tsx
@@ -0,0 +1,202 @@
+import React from 'react';
+import { CheckCircle2, Pencil, Eye } from 'lucide-react';
+import type { WorkItemDetailDto } from '../../dtos/work-item-detail.dto';
+import {
+ formatStatusLabel,
+ formatTypeLabel,
+ formatPriorityLabel,
+ getStatusBadgeClasses,
+ getPriorityBadgeClasses,
+ getTypeBadgeClasses,
+ calcProgress,
+ isOverdue,
+ formatDate,
+ getSprintLabel,
+ getInitials,
+ cx,
+} from '../../lib/dashboard-ui';
+
+interface WorkItemListViewProps {
+ items: WorkItemDetailDto[];
+ onEdit: (item: WorkItemDetailDto) => void;
+ onComplete: (item: WorkItemDetailDto) => void;
+ onViewDetail: (item: WorkItemDetailDto) => void;
+}
+
+function Pill({ children, className }: { children: React.ReactNode; className?: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function AvatarStack({ names }: { names: string[] }) {
+ if (names.length === 0) {
+ return Unassigned ;
+ }
+ return (
+
+ {names.slice(0, 3).map((name, i) => (
+
+ {getInitials(name)}
+
+ ))}
+ {names.length > 3 && (
+
+ +{names.length - 3}
+
+ )}
+
+ );
+}
+
+function ProgressBar({ value }: { value: number }) {
+ return (
+
+ );
+}
+
+export function WorkItemListView({ items, onEdit, onComplete, onViewDetail }: WorkItemListViewProps) {
+ if (items.length === 0) {
+ return (
+
+
+
No tasks found
+
Try adjusting your filters or create a new task.
+
+ );
+ }
+
+ return (
+
+ {/* Table header */}
+
+ Title
+ Type
+ Status
+ Priority
+ Assignees
+ Due Date
+ Sprint
+ Progress
+ Actions
+
+
+
+ {items.map((item) => {
+ const progress = calcProgress(item.totalLoggedMinutes, item.estimatedMinutes);
+ const overdue = isOverdue(item.dueDate, item.status);
+ const assigneeNames = item.assignees.map((a) => a.user.name);
+ const isDone = item.status === 'DONE';
+
+ return (
+
+ {/* Title */}
+
+ onViewDetail(item)}
+ className={cx(
+ 'truncate text-left text-sm font-medium hover:text-sky-300 transition-colors',
+ isDone ? 'text-zinc-500 line-through' : 'text-zinc-100',
+ )}
+ title={item.title}
+ >
+ {item.title}
+
+ {item.id}
+
+
+ {/* Type */}
+
+ {formatTypeLabel(item.type)}
+
+
+ {/* Status */}
+
+ {formatStatusLabel(item.status)}
+
+
+ {/* Priority */}
+
+ {formatPriorityLabel(item.priority)}
+
+
+ {/* Assignees */}
+
+
+ {/* Due Date */}
+
+ {formatDate(item.dueDate)}
+
+
+ {/* Sprint */}
+
+ {getSprintLabel(item.sprintId)}
+
+
+ {/* Progress */}
+
+
+ {/* Actions */}
+
+
onViewDetail(item)}
+ title="View detail"
+ className="rounded-lg p-1.5 text-zinc-500 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
+ >
+
+
+
onEdit(item)}
+ title="Edit task"
+ className="rounded-lg p-1.5 text-zinc-500 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
+ >
+
+
+ {!isDone && (
+
onComplete(item)}
+ title="Mark as done"
+ className="rounded-lg p-1.5 text-zinc-500 transition-colors hover:bg-emerald-500/20 hover:text-emerald-400"
+ >
+
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
new file mode 100644
index 000000000..2a7bc145b
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
@@ -0,0 +1,124 @@
+import type { WorkItemPriority } from '../enums/work-item-priority.enum';
+import type { WorkItemStatus } from '../enums/work-item-status.enum';
+import type { WorkItemType } from '../enums/work-item-type.enum';
+
+export function cx(...classes: Array): string {
+ return classes.filter(Boolean).join(' ');
+}
+
+export function getInitials(name: string): string {
+ return name
+ .split(' ')
+ .map((part) => part[0])
+ .join('')
+ .slice(0, 2)
+ .toUpperCase();
+}
+
+export function formatStatusLabel(status: WorkItemStatus): string {
+ switch (status) {
+ case 'TODO': return 'Todo';
+ case 'IN_PROGRESS': return 'In Progress';
+ case 'BLOCKED': return 'Blocked';
+ case 'DONE': return 'Done';
+ default: return status;
+ }
+}
+
+export function formatTypeLabel(type: WorkItemType): string {
+ switch (type) {
+ case 'FEATURE': return 'Feature';
+ case 'BUG': return 'Bug';
+ case 'ISSUE': return 'Issue';
+ case 'TASK': return 'Task';
+ default: return type;
+ }
+}
+
+export function formatPriorityLabel(priority: WorkItemPriority): string {
+ switch (priority) {
+ case 'LOW': return 'Low';
+ case 'MEDIUM': return 'Medium';
+ case 'HIGH': return 'High';
+ case 'CRITICAL': return 'Critical';
+ default: return priority;
+ }
+}
+
+export function getStatusBadgeClasses(status: WorkItemStatus): string {
+ switch (status) {
+ case 'DONE':
+ return 'border-emerald-400/30 bg-emerald-500/15 text-emerald-300';
+ case 'BLOCKED':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'IN_PROGRESS':
+ return 'border-sky-400/30 bg-sky-500/15 text-sky-300';
+ case 'TODO':
+ default:
+ return 'border-zinc-400/30 bg-zinc-500/15 text-zinc-400';
+ }
+}
+
+export function getPriorityBadgeClasses(priority: WorkItemPriority): string {
+ switch (priority) {
+ case 'CRITICAL':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'HIGH':
+ return 'border-amber-400/30 bg-amber-500/15 text-amber-300';
+ case 'MEDIUM':
+ return 'border-violet-400/30 bg-violet-500/15 text-violet-300';
+ case 'LOW':
+ default:
+ return 'border-zinc-400/30 bg-zinc-500/15 text-zinc-400';
+ }
+}
+
+export function getTypeBadgeClasses(type: WorkItemType): string {
+ switch (type) {
+ case 'FEATURE':
+ return 'border-cyan-400/30 bg-cyan-500/15 text-cyan-300';
+ case 'BUG':
+ return 'border-rose-400/30 bg-rose-500/15 text-rose-300';
+ case 'ISSUE':
+ return 'border-orange-400/30 bg-orange-500/15 text-orange-300';
+ case 'TASK':
+ default:
+ return 'border-indigo-400/30 bg-indigo-500/15 text-indigo-300';
+ }
+}
+
+export function getStatusDotColor(status: WorkItemStatus): string {
+ switch (status) {
+ case 'DONE': return 'bg-emerald-400';
+ case 'BLOCKED': return 'bg-rose-400';
+ case 'IN_PROGRESS': return 'bg-sky-400';
+ case 'TODO': return 'bg-zinc-500';
+ default: return 'bg-zinc-500';
+ }
+}
+
+export function calcProgress(logged: number, estimated?: number): number {
+ if (!estimated || estimated === 0) return 0;
+ return Math.min(100, Math.round((logged / estimated) * 100));
+}
+
+export function isOverdue(dueDate?: string, status?: WorkItemStatus): boolean {
+ if (!dueDate || status === 'DONE') return false;
+ return new Date(dueDate) < new Date(new Date().toDateString());
+}
+
+export function formatDate(dateStr?: string): string {
+ if (!dateStr) return '—';
+ const d = new Date(dateStr);
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+}
+
+export function getSprintLabel(sprintId?: string): string {
+ if (!sprintId) return '—';
+ const map: Record = {
+ 'spr-001': 'Sprint 1',
+ 'spr-002': 'Sprint 2',
+ 'spr-003': 'Sprint 3',
+ };
+ return map[sprintId] ?? sprintId;
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/mock/work-items.mock.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/mock/work-items.mock.ts
index 1a5f5eb05..4f3d5bc1c 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/mock/work-items.mock.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/mock/work-items.mock.ts
@@ -143,5 +143,220 @@ export const mockWorkItems: WorkItemDetailDto[] = [
environment: 'Web Portal / Requirements',
reproductionSteps: 'Compare board filtering and list filtering expected behavior.'
}
+ },
+ {
+ id: 'wrk-004',
+ sprintId: 'spr-001',
+ title: 'Set up CI pipeline for frontend builds',
+ description: 'Configure GitHub Actions workflow to lint, test, and build the React app on each push.',
+ type: 'TASK',
+ status: 'DONE',
+ priority: 'HIGH',
+ estimatedMinutes: 240,
+ totalLoggedMinutes: 210,
+ dueDate: '2026-04-12',
+ createdAt: '2026-04-08T10:00:00Z',
+ updatedAt: '2026-04-12T15:00:00Z',
+ completedAt: '2026-04-12T15:00:00Z',
+ createdBy: {
+ id: 'usr-001',
+ name: 'Bernardo Manager',
+ email: 'bernardo.manager@demo.com',
+ telegramUserId: 'tg_bernardo_manager'
+ },
+ assignees: [
+ {
+ id: 'asg-004',
+ user: {
+ id: 'usr-003',
+ name: 'Luis Developer',
+ email: 'luis.dev@demo.com',
+ telegramUserId: 'tg_luis_dev'
+ },
+ role: 'OWNER',
+ assignedAt: '2026-04-08T11:00:00Z'
+ }
+ ],
+ tags: [
+ {
+ id: 'tag-001',
+ name: 'Frontend',
+ color: '#3B82F6',
+ description: 'UI and client-side work'
+ }
+ ]
+ },
+ {
+ id: 'wrk-005',
+ sprintId: 'spr-001',
+ title: 'Design shared component library tokens',
+ description: 'Define color, spacing, and typography tokens used across the design system.',
+ type: 'FEATURE',
+ status: 'DONE',
+ priority: 'MEDIUM',
+ estimatedMinutes: 300,
+ totalLoggedMinutes: 300,
+ dueDate: '2026-04-14',
+ createdAt: '2026-04-09T08:00:00Z',
+ updatedAt: '2026-04-14T12:00:00Z',
+ completedAt: '2026-04-14T12:00:00Z',
+ createdBy: {
+ id: 'usr-002',
+ name: 'Ana Developer',
+ email: 'ana.dev@demo.com',
+ telegramUserId: 'tg_ana_dev'
+ },
+ assignees: [
+ {
+ id: 'asg-005',
+ user: {
+ id: 'usr-002',
+ name: 'Ana Developer',
+ email: 'ana.dev@demo.com',
+ telegramUserId: 'tg_ana_dev'
+ },
+ role: 'OWNER',
+ assignedAt: '2026-04-09T08:30:00Z'
+ }
+ ],
+ tags: [
+ {
+ id: 'tag-001',
+ name: 'Frontend',
+ color: '#3B82F6',
+ description: 'UI and client-side work'
+ }
+ ],
+ featureDetails: {
+ businessValue: 'Ensures visual consistency across all UI components.',
+ acceptanceCriteria: 'Tokens documented and applied in at least 3 shared components.'
+ }
+ },
+ {
+ id: 'wrk-006',
+ sprintId: 'spr-002',
+ title: 'Implement sprint progress API endpoint',
+ description: 'Expose a REST endpoint returning current sprint completion percentage and item breakdown.',
+ type: 'FEATURE',
+ status: 'TODO',
+ priority: 'HIGH',
+ estimatedMinutes: 480,
+ totalLoggedMinutes: 0,
+ dueDate: '2026-04-30',
+ createdAt: '2026-04-15T09:00:00Z',
+ updatedAt: '2026-04-15T09:00:00Z',
+ createdBy: {
+ id: 'usr-001',
+ name: 'Bernardo Manager',
+ email: 'bernardo.manager@demo.com',
+ telegramUserId: 'tg_bernardo_manager'
+ },
+ assignees: [
+ {
+ id: 'asg-006',
+ user: {
+ id: 'usr-003',
+ name: 'Luis Developer',
+ email: 'luis.dev@demo.com',
+ telegramUserId: 'tg_luis_dev'
+ },
+ role: 'ASSIGNEE',
+ assignedAt: '2026-04-15T09:30:00Z'
+ }
+ ],
+ tags: [
+ {
+ id: 'tag-002',
+ name: 'Backend',
+ color: '#10B981',
+ description: 'API and service work'
+ }
+ ],
+ featureDetails: {
+ businessValue: 'Enables real-time sprint dashboards for managers.',
+ acceptanceCriteria: 'Endpoint returns 200 with correct data shape. Validated with integration tests.'
+ }
+ },
+ {
+ id: 'wrk-007',
+ sprintId: 'spr-002',
+ title: 'Fix date timezone offset in due date display',
+ description: 'Due dates appear one day off when the user is in UTC-5 or earlier timezones.',
+ type: 'BUG',
+ status: 'IN_PROGRESS',
+ priority: 'MEDIUM',
+ estimatedMinutes: 120,
+ totalLoggedMinutes: 60,
+ dueDate: '2026-04-17',
+ createdAt: '2026-04-13T14:00:00Z',
+ updatedAt: '2026-04-15T11:00:00Z',
+ createdBy: {
+ id: 'usr-003',
+ name: 'Luis Developer',
+ email: 'luis.dev@demo.com',
+ telegramUserId: 'tg_luis_dev'
+ },
+ assignees: [
+ {
+ id: 'asg-007',
+ user: {
+ id: 'usr-002',
+ name: 'Ana Developer',
+ email: 'ana.dev@demo.com',
+ telegramUserId: 'tg_ana_dev'
+ },
+ role: 'OWNER',
+ assignedAt: '2026-04-13T15:00:00Z'
+ }
+ ],
+ tags: [
+ {
+ id: 'tag-003',
+ name: 'Bug',
+ color: '#EF4444',
+ description: 'Defect or error'
+ },
+ {
+ id: 'tag-001',
+ name: 'Frontend',
+ color: '#3B82F6',
+ description: 'UI and client-side work'
+ }
+ ],
+ bugDetails: {
+ severity: 'MEDIUM',
+ environment: 'Web Portal / Production',
+ isReproducible: true,
+ steps: 'Set browser timezone to UTC-5. Open any task with a due date. Observe offset.'
+ }
+ },
+ {
+ id: 'wrk-008',
+ sprintId: 'spr-001',
+ title: 'Write onboarding documentation for new developers',
+ description: 'Create a concise getting-started guide covering setup, conventions, and key workflows.',
+ type: 'TASK',
+ status: 'TODO',
+ priority: 'LOW',
+ estimatedMinutes: 150,
+ totalLoggedMinutes: 0,
+ dueDate: '2026-05-01',
+ createdAt: '2026-04-15T10:00:00Z',
+ updatedAt: '2026-04-15T10:00:00Z',
+ createdBy: {
+ id: 'usr-001',
+ name: 'Bernardo Manager',
+ email: 'bernardo.manager@demo.com',
+ telegramUserId: 'tg_bernardo_manager'
+ },
+ assignees: [],
+ tags: [
+ {
+ id: 'tag-002',
+ name: 'Backend',
+ color: '#10B981',
+ description: 'API and service work'
+ }
+ ]
}
];
\ No newline at end of file
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
new file mode 100644
index 000000000..1eb6abc65
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
@@ -0,0 +1,234 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { Layers } from 'lucide-react';
+import type { WorkItemDetailDto } from '../dtos/work-item-detail.dto';
+import type { CreateWorkItemDto } from '../dtos/create-work-item.dto';
+import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';
+import type { WorkItemStatus } from '../enums/work-item-status.enum';
+import { workItemService } from '../services/work-item.service';
+import { mockUsers } from '@/shared/mock/users.mock';
+import { mockTags } from '@/shared/mock/tags.mock';
+import { DashboardSummaryCards } from '../components/dashboard/dashboard-summary-cards';
+import { DashboardToolbar } from '../components/dashboard/dashboard-toolbar';
+import type { ViewMode } from '../components/dashboard/dashboard-toolbar';
+import { WorkItemListView } from '../components/dashboard/work-item-list-view';
+import { KanbanView } from '../components/dashboard/kanban-view';
+import { WorkItemFormModal } from '../components/dashboard/work-item-form-modal';
+import { WorkItemDetailModal } from '../components/dashboard/work-item-detail-modal';
+
+export function WorkItemDashboardPage() {
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // Toolbar state
+ const [search, setSearch] = useState('');
+ const [statusFilter, setStatusFilter] = useState('');
+ const [assigneeFilter, setAssigneeFilter] = useState('');
+ const [viewMode, setViewMode] = useState('list');
+
+ // Modal state
+ const [formOpen, setFormOpen] = useState(false);
+ const [editingItem, setEditingItem] = useState(null);
+ const [detailItem, setDetailItem] = useState(null);
+ const [detailOpen, setDetailOpen] = useState(false);
+
+ // Load all items on mount (using the service so we stay in the service-layer contract)
+ const loadItems = useCallback(async () => {
+ setLoading(true);
+ try {
+ // Load a large page to get all items for the dashboard
+ const result = await workItemService.getWorkItems({ page: 1, pageSize: 100 });
+ if (result.success) {
+ // getWorkItems returns WorkItemListItemDto[] but we need full detail for mutations.
+ // Use the mock store directly by fetching each id — the service exposes getWorkItemById.
+ const ids = result.data.items.map((i) => i.id);
+ const details = await Promise.all(ids.map((id) => workItemService.getWorkItemById(id)));
+ const fullItems = details
+ .filter((r) => r.success && r.data !== null)
+ .map((r) => r.data as WorkItemDetailDto);
+ setItems(fullItems);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadItems();
+ }, [loadItems]);
+
+ // In-memory filtered items
+ const filteredItems = useMemo(() => {
+ return items.filter((item) => {
+ const matchesSearch =
+ !search ||
+ item.title.toLowerCase().includes(search.toLowerCase()) ||
+ (item.description ?? '').toLowerCase().includes(search.toLowerCase());
+
+ const matchesStatus = !statusFilter || item.status === statusFilter;
+
+ const matchesAssignee =
+ !assigneeFilter ||
+ item.assignees.some((a) => a.user.id === assigneeFilter);
+
+ return matchesSearch && matchesStatus && matchesAssignee;
+ });
+ }, [items, search, statusFilter, assigneeFilter]);
+
+ // Create handler
+ const handleCreate = useCallback(async (dto: CreateWorkItemDto) => {
+ const result = await workItemService.createWorkItem(dto);
+ if (result.success) {
+ setItems((prev) => [result.data, ...prev]);
+ }
+ }, []);
+
+ // Update handler
+ const handleUpdate = useCallback(async (id: string, dto: UpdateWorkItemDto) => {
+ const result = await workItemService.updateWorkItem(id, dto);
+ if (result.success && result.data) {
+ const updated = result.data;
+ setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
+ // Refresh detail modal if open for this item
+ if (detailItem?.id === id) {
+ setDetailItem(updated);
+ }
+ }
+ }, [detailItem]);
+
+ // Complete handler
+ const handleComplete = useCallback(async (item: WorkItemDetailDto) => {
+ await handleUpdate(item.id, {
+ status: 'DONE',
+ completedAt: new Date().toISOString(),
+ });
+ }, [handleUpdate]);
+
+ // Open edit
+ const handleEdit = useCallback((item: WorkItemDetailDto) => {
+ setEditingItem(item);
+ setDetailOpen(false);
+ setFormOpen(true);
+ }, []);
+
+ // Open detail
+ const handleViewDetail = useCallback((item: WorkItemDetailDto) => {
+ setDetailItem(item);
+ setDetailOpen(true);
+ }, []);
+
+ // Close form
+ const handleCloseForm = useCallback(() => {
+ setFormOpen(false);
+ setEditingItem(null);
+ }, []);
+
+ // Close detail
+ const handleCloseDetail = useCallback(() => {
+ setDetailOpen(false);
+ setDetailItem(null);
+ }, []);
+
+ // Edit from detail modal
+ const handleEditFromDetail = useCallback((item: WorkItemDetailDto) => {
+ handleCloseDetail();
+ handleEdit(item);
+ }, [handleCloseDetail, handleEdit]);
+
+ // Complete from detail modal
+ const handleCompleteFromDetail = useCallback(async (item: WorkItemDetailDto) => {
+ await handleComplete(item);
+ handleCloseDetail();
+ }, [handleComplete, handleCloseDetail]);
+
+ return (
+
+
+
+ {/* Page header */}
+
+
+
+
+
+
+
Work Items
+
Sprint 1 · Talos OCI DevOps Project
+
+
+
+
+ {/* Summary cards */}
+
+
+
+
+ {/* Toolbar */}
+
+ {
+ setEditingItem(null);
+ setFormOpen(true);
+ }}
+ users={mockUsers}
+ />
+
+
+ {/* Results count */}
+
+ {loading
+ ? 'Loading…'
+ : `${filteredItems.length} of ${items.length} task${items.length !== 1 ? 's' : ''}`}
+
+
+ {/* View area */}
+ {loading ? (
+
+ ) : viewMode === 'list' ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Create / Edit modal */}
+
+
+ {/* Detail preview modal */}
+
+
+ );
+}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
index 2a51c73ea..2577faa37 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
@@ -2,6 +2,8 @@ import type { ApiResult } from '@/shared/dtos/api-result.dto';
import type { PagedResult } from '@/shared/dtos/paged-result.dto';
import { mockApi } from '@/shared/services/mock-api';
import { mockWorkItems } from '../mock/work-items.mock';
+import { mockUsers } from '@/shared/mock/users.mock';
+import { mockTags } from '@/shared/mock/tags.mock';
import type { CreateWorkItemDto } from '../dtos/create-work-item.dto';
import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';
import type { Assignee, WorkItemDetailDto } from '../dtos/work-item-detail.dto';
@@ -9,6 +11,26 @@ import type { WorkItemFiltersDto } from '../dtos/work-item-filters.dto';
import type { WorkItemListItemDto } from '../dtos/work-item-list-item.dto';
import { UserSummaryDto } from "@/shared/dtos/user-summary.dto";
+function resolveAssignees(userIds?: string[]): Assignee[] {
+ if (!userIds?.length) return [];
+ return userIds
+ .map((uid) => mockUsers.find((u) => u.id === uid))
+ .filter((u): u is UserSummaryDto => !!u)
+ .map((user, i) => ({
+ id: `asg-${crypto.randomUUID()}`,
+ user,
+ role: i === 0 ? 'OWNER' : 'ASSIGNEE',
+ assignedAt: new Date().toISOString(),
+ } as Assignee));
+}
+
+function resolveTags(tagIds?: string[]) {
+ if (!tagIds?.length) return [];
+ return tagIds
+ .map((tid) => mockTags.find((t) => t.id === tid))
+ .filter(Boolean) as typeof mockTags;
+}
+
function toListItemDto(item: WorkItemDetailDto): WorkItemListItemDto {
return {
id: item.id,
@@ -121,8 +143,8 @@ export const workItemService = {
email: 'bernardo.manager@demo.com',
telegramUserId: 'tg_bernardo_manager'
},
- assignees: [],
- tags: [],
+ assignees: resolveAssignees(input.assigneeUserIds),
+ tags: resolveTags(input.tagIds),
featureDetails: input.featureDetails,
issueDetails: input.issueDetails,
bugDetails: input.bugDetails
@@ -145,9 +167,13 @@ export const workItemService = {
const current: WorkItemDetailDto = mockWorkItems[index];
+ const { assigneeUserIds, tagIds, ...rest } = input;
+
const updated: WorkItemDetailDto = {
...current,
- ...input,
+ ...rest,
+ assignees: assigneeUserIds !== undefined ? resolveAssignees(assigneeUserIds) : current.assignees,
+ tags: tagIds !== undefined ? resolveTags(tagIds) : current.tags,
updatedAt: new Date().toISOString(),
featureDetails: input.featureDetails ?? current.featureDetails,
issueDetails: input.issueDetails ?? current.issueDetails,
diff --git a/MtdrSpring/backend/src/main/frontend/tailwind.config.js b/MtdrSpring/backend/src/main/frontend/tailwind.config.js
new file mode 100644
index 000000000..84e50559c
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ './src/**/*.{js,jsx,ts,tsx}',
+ './public/index.html',
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
From 2fae25306084c38ec83bf9c075cfc2e42314c41d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:53:04 +0000
Subject: [PATCH 17/23] fix: address code review - cleaner date comparison,
dedicated status text color helper, deterministic assignee IDs
Agent-Logs-Url: https://github.com/bernardosantiago44/talos_oci_devops_project/sessions/e230285c-35dc-4860-b9c9-a10c2761acb6
Co-authored-by: bernardosantiago44 <63428964+bernardosantiago44@users.noreply.github.com>
---
.../work-items/components/dashboard/kanban-view.tsx | 9 ++-------
.../src/features/work-items/lib/dashboard-ui.ts | 13 ++++++++++++-
.../work-items/services/work-item.service.ts | 2 +-
3 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
index 571696b0d..e5c7c0c2b 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/components/dashboard/kanban-view.tsx
@@ -6,10 +6,10 @@ import {
formatStatusLabel,
formatTypeLabel,
formatPriorityLabel,
- getStatusBadgeClasses,
getPriorityBadgeClasses,
getTypeBadgeClasses,
getStatusDotColor,
+ getStatusTextColor,
calcProgress,
isOverdue,
formatDate,
@@ -159,12 +159,7 @@ export function KanbanView({ items, onEdit, onComplete, onViewDetail }: KanbanVi
{/* Column header */}
-
c.startsWith('text-')) ?? 'text-zinc-300',
- )}
- >
+
{formatStatusLabel(status)}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
index 2a7bc145b..c7b732483 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/lib/dashboard-ui.ts
@@ -87,6 +87,16 @@ export function getTypeBadgeClasses(type: WorkItemType): string {
}
}
+export function getStatusTextColor(status: WorkItemStatus): string {
+ switch (status) {
+ case 'DONE': return 'text-emerald-300';
+ case 'BLOCKED': return 'text-rose-300';
+ case 'IN_PROGRESS': return 'text-sky-300';
+ case 'TODO': return 'text-zinc-400';
+ default: return 'text-zinc-300';
+ }
+}
+
export function getStatusDotColor(status: WorkItemStatus): string {
switch (status) {
case 'DONE': return 'bg-emerald-400';
@@ -104,7 +114,8 @@ export function calcProgress(logged: number, estimated?: number): number {
export function isOverdue(dueDate?: string, status?: WorkItemStatus): boolean {
if (!dueDate || status === 'DONE') return false;
- return new Date(dueDate) < new Date(new Date().toDateString());
+ const today = new Date().toISOString().slice(0, 10);
+ return dueDate < today;
}
export function formatDate(dateStr?: string): string {
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
index 2577faa37..d06f18e94 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
@@ -17,7 +17,7 @@ function resolveAssignees(userIds?: string[]): Assignee[] {
.map((uid) => mockUsers.find((u) => u.id === uid))
.filter((u): u is UserSummaryDto => !!u)
.map((user, i) => ({
- id: `asg-${crypto.randomUUID()}`,
+ id: `asg-${user.id}-${i}`,
user,
role: i === 0 ? 'OWNER' : 'ASSIGNEE',
assignedAt: new Date().toISOString(),
From 61b50bc20e89f8d7d87b002d92496ff1360495d5 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago
Date: Wed, 15 Apr 2026 20:28:02 -0600
Subject: [PATCH 18/23] Migrated page state to a viewModel
---
.../src/main/frontend/package-lock.json | 367 ------------------
.../pages/work-item-dashboard-page.tsx | 301 ++++----------
.../viewModels/useWorkItemsViewModel.ts | 159 ++++++++
3 files changed, 244 insertions(+), 583 deletions(-)
create mode 100644 MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
diff --git a/MtdrSpring/backend/src/main/frontend/package-lock.json b/MtdrSpring/backend/src/main/frontend/package-lock.json
index 8a45b839d..8ca352f2d 100644
--- a/MtdrSpring/backend/src/main/frontend/package-lock.json
+++ b/MtdrSpring/backend/src/main/frontend/package-lock.json
@@ -3793,263 +3793,6 @@
"url": "https://opencollective.com/popperjs"
}
},
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
- "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
- "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
- "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
- "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
- "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
- "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
- "cpu": [
- "wasm32"
- ],
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@emnapi/core": "1.9.2",
- "@emnapi/runtime": "1.9.2",
- "@napi-rs/wasm-runtime": "^1.1.3"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
- "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
- "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
@@ -21564,116 +21307,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
},
- "@rolldown/binding-android-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-darwin-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-darwin-x64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
- "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-freebsd-x64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
- "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
- "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
- "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
- "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-linux-x64-musl": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
- "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-openharmony-arm64": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
- "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-wasm32-wasi": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
- "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
- "optional": true,
- "peer": true,
- "requires": {
- "@emnapi/core": "1.9.2",
- "@emnapi/runtime": "1.9.2",
- "@napi-rs/wasm-runtime": "^1.1.3"
- }
- },
- "@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
- "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
- "optional": true,
- "peer": true
- },
- "@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.0-rc.15",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
- "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
- "optional": true,
- "peer": true
- },
"@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
index 1eb6abc65..5497bc7da 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
@@ -1,234 +1,103 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Layers } from 'lucide-react';
-import type { WorkItemDetailDto } from '../dtos/work-item-detail.dto';
-import type { CreateWorkItemDto } from '../dtos/create-work-item.dto';
-import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';
-import type { WorkItemStatus } from '../enums/work-item-status.enum';
-import { workItemService } from '../services/work-item.service';
import { mockUsers } from '@/shared/mock/users.mock';
import { mockTags } from '@/shared/mock/tags.mock';
import { DashboardSummaryCards } from '../components/dashboard/dashboard-summary-cards';
import { DashboardToolbar } from '../components/dashboard/dashboard-toolbar';
-import type { ViewMode } from '../components/dashboard/dashboard-toolbar';
import { WorkItemListView } from '../components/dashboard/work-item-list-view';
import { KanbanView } from '../components/dashboard/kanban-view';
import { WorkItemFormModal } from '../components/dashboard/work-item-form-modal';
import { WorkItemDetailModal } from '../components/dashboard/work-item-detail-modal';
+import { useWorkItemsViewModel, IWorkItemsViewModel } from "@/features/work-items/viewModels/useWorkItemsViewModel";
export function WorkItemDashboardPage() {
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(true);
+ const viewModel: IWorkItemsViewModel = useWorkItemsViewModel();
- // Toolbar state
- const [search, setSearch] = useState('');
- const [statusFilter, setStatusFilter] = useState('');
- const [assigneeFilter, setAssigneeFilter] = useState('');
- const [viewMode, setViewMode] = useState('list');
+ return (
+
+
- // Modal state
- const [formOpen, setFormOpen] = useState(false);
- const [editingItem, setEditingItem] = useState
(null);
- const [detailItem, setDetailItem] = useState(null);
- const [detailOpen, setDetailOpen] = useState(false);
-
- // Load all items on mount (using the service so we stay in the service-layer contract)
- const loadItems = useCallback(async () => {
- setLoading(true);
- try {
- // Load a large page to get all items for the dashboard
- const result = await workItemService.getWorkItems({ page: 1, pageSize: 100 });
- if (result.success) {
- // getWorkItems returns WorkItemListItemDto[] but we need full detail for mutations.
- // Use the mock store directly by fetching each id — the service exposes getWorkItemById.
- const ids = result.data.items.map((i) => i.id);
- const details = await Promise.all(ids.map((id) => workItemService.getWorkItemById(id)));
- const fullItems = details
- .filter((r) => r.success && r.data !== null)
- .map((r) => r.data as WorkItemDetailDto);
- setItems(fullItems);
- }
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- loadItems();
- }, [loadItems]);
-
- // In-memory filtered items
- const filteredItems = useMemo(() => {
- return items.filter((item) => {
- const matchesSearch =
- !search ||
- item.title.toLowerCase().includes(search.toLowerCase()) ||
- (item.description ?? '').toLowerCase().includes(search.toLowerCase());
-
- const matchesStatus = !statusFilter || item.status === statusFilter;
-
- const matchesAssignee =
- !assigneeFilter ||
- item.assignees.some((a) => a.user.id === assigneeFilter);
-
- return matchesSearch && matchesStatus && matchesAssignee;
- });
- }, [items, search, statusFilter, assigneeFilter]);
-
- // Create handler
- const handleCreate = useCallback(async (dto: CreateWorkItemDto) => {
- const result = await workItemService.createWorkItem(dto);
- if (result.success) {
- setItems((prev) => [result.data, ...prev]);
- }
- }, []);
-
- // Update handler
- const handleUpdate = useCallback(async (id: string, dto: UpdateWorkItemDto) => {
- const result = await workItemService.updateWorkItem(id, dto);
- if (result.success && result.data) {
- const updated = result.data;
- setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
- // Refresh detail modal if open for this item
- if (detailItem?.id === id) {
- setDetailItem(updated);
- }
- }
- }, [detailItem]);
-
- // Complete handler
- const handleComplete = useCallback(async (item: WorkItemDetailDto) => {
- await handleUpdate(item.id, {
- status: 'DONE',
- completedAt: new Date().toISOString(),
- });
- }, [handleUpdate]);
-
- // Open edit
- const handleEdit = useCallback((item: WorkItemDetailDto) => {
- setEditingItem(item);
- setDetailOpen(false);
- setFormOpen(true);
- }, []);
-
- // Open detail
- const handleViewDetail = useCallback((item: WorkItemDetailDto) => {
- setDetailItem(item);
- setDetailOpen(true);
- }, []);
-
- // Close form
- const handleCloseForm = useCallback(() => {
- setFormOpen(false);
- setEditingItem(null);
- }, []);
-
- // Close detail
- const handleCloseDetail = useCallback(() => {
- setDetailOpen(false);
- setDetailItem(null);
- }, []);
-
- // Edit from detail modal
- const handleEditFromDetail = useCallback((item: WorkItemDetailDto) => {
- handleCloseDetail();
- handleEdit(item);
- }, [handleCloseDetail, handleEdit]);
-
- // Complete from detail modal
- const handleCompleteFromDetail = useCallback(async (item: WorkItemDetailDto) => {
- await handleComplete(item);
- handleCloseDetail();
- }, [handleComplete, handleCloseDetail]);
-
- return (
-
-
-
- {/* Page header */}
-
-
-
-
-
-
-
Work Items
-
Sprint 1 · Talos OCI DevOps Project
-
-
-
-
- {/* Summary cards */}
-
-
-
-
- {/* Toolbar */}
-
- {
- setEditingItem(null);
- setFormOpen(true);
- }}
- users={mockUsers}
- />
-
-
- {/* Results count */}
-
- {loading
- ? 'Loading…'
- : `${filteredItems.length} of ${items.length} task${items.length !== 1 ? 's' : ''}`}
-
-
- {/* View area */}
- {loading ? (
-
- ) : viewMode === 'list' ? (
-
- ) : (
-
- )}
+ {/* Page header */}
+
+
+
+
+
+
+
Work Items
+
Sprint 1 · Talos OCI DevOps Project
+
+
- {/* Create / Edit modal */}
-
+ {/* Summary cards */}
+
+
+
- {/* Detail preview modal */}
-
+ {/* Toolbar */}
+
+
- );
+
+ {/* Results count */}
+
+ {viewModel.loading
+ ? 'Loading…'
+ : `${viewModel.items.length} of ${viewModel.totalItemCount()} task${viewModel.totalItemCount() !== 1 ? 's' : ''}`}
+
+
+ {/* View area */}
+ {viewModel.loading ? (
+
+ ) : viewModel.viewMode === 'list' ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Create / Edit modal */}
+
+
+ {/* Detail preview modal */}
+
+
+ );
}
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
new file mode 100644
index 000000000..66aa87540
--- /dev/null
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
@@ -0,0 +1,159 @@
+import { useState, useCallback, useEffect, useMemo } from 'react';
+import { workItemService } from '../services/work-item.service';
+import { ViewMode } from "@/features/work-items/components/dashboard/dashboard-toolbar";
+import type { WorkItemDetailDto } from '../dtos/work-item-detail.dto';
+import type { CreateWorkItemDto } from '../dtos/create-work-item.dto';
+import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';
+import type { WorkItemStatus } from '../enums/work-item-status.enum';
+
+export const useWorkItemsViewModel = () => {
+ // 1. Data State
+ const [items, setItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // 2. UI State (Grouped logically)
+ const [filters, setFilters] = useState({
+ search: '',
+ status: '' as WorkItemStatus | '',
+ assignee: '',
+ });
+ const [viewMode, setViewMode] = useState('list');
+
+ // 3. Modal/Overlay State
+ const [modals, setModals] = useState({
+ formOpen: false,
+ detailOpen: false,
+ editingItem: null as WorkItemDetailDto | null,
+ detailItem: null as WorkItemDetailDto | null,
+ });
+
+ // --- Actions ---
+
+ const loadItems = useCallback(async () => {
+ setLoading(true);
+ try {
+ const result = await workItemService.getWorkItems({ page: 1, pageSize: 100 });
+ if (result.success) {
+ const ids = result.data.items.map((i) => i.id);
+ const details = await Promise.all(ids.map((id) => workItemService.getWorkItemById(id)));
+ const fullItems = details
+ .filter((r) => r.success && r.data !== null)
+ .map((r) => r.data as WorkItemDetailDto);
+ setItems(fullItems);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { loadItems().then(); }, [loadItems]);
+
+ // Derived State (SwiftUI "Computed Properties")
+ const filteredItems = useMemo(() => {
+ return items.filter((item) => {
+ const matchesSearch = !filters.search ||
+ item.title.toLowerCase().includes(filters.search.toLowerCase()) ||
+ (item.description ?? '').toLowerCase().includes(filters.search.toLowerCase());
+ const matchesStatus = !filters.status || item.status === filters.status;
+ const matchesAssignee = !filters.assignee ||
+ item.assignees.some((a) => a.user.id === filters.assignee);
+
+ return matchesSearch && matchesStatus && matchesAssignee;
+ });
+ }, [items, filters]);
+
+ // --- Handlers ---
+ const handleCreate = async (dto: CreateWorkItemDto) => {
+ const result = await workItemService.createWorkItem(dto);
+ if (result.success) setItems((prev) => [result.data, ...prev]);
+ };
+
+ const handleUpdate = async (id: string, dto: UpdateWorkItemDto) => {
+ const result = await workItemService.updateWorkItem(id, dto);
+ if (result.success && result.data) {
+ const updated = result.data;
+ setItems((prev) => prev.map((item) => (item.id === id ? updated : item)));
+ if (modals.detailItem?.id === id) {
+ setModals(m => ({ ...m, detailItem: updated }));
+ }
+ }
+ };
+
+ const handleEdit = useCallback((item: WorkItemDetailDto) => {
+ setModals({
+ formOpen: true,
+ detailOpen: true,
+ editingItem: item,
+ detailItem: null // Perhaps this needs to be the item?
+ })
+ }, []);
+
+ const handleComplete = async (item: WorkItemDetailDto) => {
+ await handleUpdate(item.id, {
+ status: 'DONE',
+ completedAt: new Date().toISOString()
+ });
+ };
+
+ // UI Navigation / Modal Handlers
+ const openNew = () =>
+ setModals({ ...modals, editingItem: null, formOpen: true});
+
+ const openEdit = (item: WorkItemDetailDto) =>
+ setModals({ ...modals, editingItem: item, formOpen: true, detailOpen: false });
+
+ const openDetail = (item: WorkItemDetailDto) =>
+ setModals({ ...modals, detailItem: item, detailOpen: true });
+
+ const closeAll = () =>
+ setModals({ ...modals, formOpen: false, detailOpen: false, editingItem: null, detailItem: null });
+
+ const handleEditFromDetail = useCallback((item: WorkItemDetailDto) => {
+ closeAll();
+ handleEdit(item);
+ }, [handleEdit, closeAll]);
+
+ const handleCompleteFromDetail = useCallback(async (item: WorkItemDetailDto) => {
+ await handleComplete(item);
+ closeAll();
+ }, [handleEdit, closeAll]);
+
+ // --- Final API ---
+ return {
+ // Data
+ items: filteredItems,
+ totalItemCount: () => { return items.length },
+ loading,
+ viewMode,
+ setViewMode,
+
+ // UI State
+ search: filters.search,
+ statusFilter: filters.status,
+ assigneeFilter: filters.assignee,
+ setSearch: (search: string) => setFilters(f => ({ ...f, search })),
+ setStatusFilter: (status: WorkItemStatus | '') => setFilters(f => ({ ...f, status })),
+ setAssigneeFilter: (assignee: string) => setFilters(f => ({ ...f, assignee })),
+
+ // Modal state
+ ...modals,
+ editingItem: modals.editingItem,
+
+ // Actions
+ actions: {
+ loadItems,
+ openNew,
+ handleCreate,
+ handleUpdate,
+ handleComplete,
+ handleEdit,
+ handleEditFromDetail,
+ handleCompleteFromDetail,
+ openEdit,
+ openDetail,
+ closeAll
+ }
+ };
+};
+
+export type IWorkItemsViewModel = ReturnType;
\ No newline at end of file
From 1ac9f80cef8ceb7b6dce09fb5d016d418bb6e926 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:30:49 -0600
Subject: [PATCH 19/23] Add guideline for using viewModel in complex UIs
Emphasize using viewModel for complex pages to manage state effectively.
---
.github/agents/frontend-ui-developer.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/agents/frontend-ui-developer.md b/.github/agents/frontend-ui-developer.md
index e25dd9d11..3b954526d 100644
--- a/.github/agents/frontend-ui-developer.md
+++ b/.github/agents/frontend-ui-developer.md
@@ -46,6 +46,7 @@ You must not:
- Support loading, empty, error, and populated states where relevant.
- Keep accessibility in mind: semantic HTML, labels, keyboard navigation, and sensible contrast.
- Keep styling consistent with the project’s Tailwind and design patterns.
+- For complex pages or heavy state-based interfaces, prefer to use a viewModel in a separate file to avoid big useState soups.
## UI expectations
- Design for clarity first, then polish.
From e875ddd4d873a5217a104ff89d0c20f96d7334d7 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:40:11 -0600
Subject: [PATCH 20/23] Update
MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../work-items/viewModels/useWorkItemsViewModel.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
index 66aa87540..e774689c0 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
@@ -81,11 +81,11 @@ export const useWorkItemsViewModel = () => {
const handleEdit = useCallback((item: WorkItemDetailDto) => {
setModals({
- formOpen: true,
- detailOpen: true,
- editingItem: item,
- detailItem: null // Perhaps this needs to be the item?
- })
+ formOpen: true,
+ detailOpen: false,
+ editingItem: item,
+ detailItem: null,
+ });
}, []);
const handleComplete = async (item: WorkItemDetailDto) => {
From 9c655d211750d639d70cf671c11ea17378d80c72 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:40:39 -0600
Subject: [PATCH 21/23] Update
MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../src/features/work-items/viewModels/useWorkItemsViewModel.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
index e774689c0..3fd872df0 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
@@ -116,7 +116,7 @@ export const useWorkItemsViewModel = () => {
const handleCompleteFromDetail = useCallback(async (item: WorkItemDetailDto) => {
await handleComplete(item);
closeAll();
- }, [handleEdit, closeAll]);
+ }, [handleComplete, closeAll]);
// --- Final API ---
return {
From 3fefe276b86d02f6badaf8df70441dc0b93860b6 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago <63428964+bernardosantiago44@users.noreply.github.com>
Date: Wed, 15 Apr 2026 20:41:47 -0600
Subject: [PATCH 22/23] Update
MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../src/features/work-items/services/work-item.service.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
index d06f18e94..04eb8524c 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
@@ -24,11 +24,11 @@ function resolveAssignees(userIds?: string[]): Assignee[] {
} as Assignee));
}
-function resolveTags(tagIds?: string[]) {
+function resolveTags(tagIds?: string[]): Array<(typeof mockTags)[number]> {
if (!tagIds?.length) return [];
return tagIds
.map((tid) => mockTags.find((t) => t.id === tid))
- .filter(Boolean) as typeof mockTags;
+ .filter((tag): tag is (typeof mockTags)[number] => !!tag);
}
function toListItemDto(item: WorkItemDetailDto): WorkItemListItemDto {
From 882c9833d9f98614e0d96da4b4830f0f91278d82 Mon Sep 17 00:00:00 2001
From: Bernardo Santiago
Date: Wed, 15 Apr 2026 20:47:25 -0600
Subject: [PATCH 23/23] Changed import type bugs
---
.../src/features/work-items/pages/work-item-dashboard-page.tsx | 3 ++-
.../src/features/work-items/services/work-item.service.ts | 2 +-
.../features/work-items/viewModels/useWorkItemsViewModel.ts | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
index 5497bc7da..dad939c78 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/pages/work-item-dashboard-page.tsx
@@ -7,7 +7,8 @@ import { WorkItemListView } from '../components/dashboard/work-item-list-view';
import { KanbanView } from '../components/dashboard/kanban-view';
import { WorkItemFormModal } from '../components/dashboard/work-item-form-modal';
import { WorkItemDetailModal } from '../components/dashboard/work-item-detail-modal';
-import { useWorkItemsViewModel, IWorkItemsViewModel } from "@/features/work-items/viewModels/useWorkItemsViewModel";
+import { useWorkItemsViewModel } from "@/features/work-items/viewModels/useWorkItemsViewModel";
+import type { IWorkItemsViewModel } from "@/features/work-items/viewModels/useWorkItemsViewModel";
export function WorkItemDashboardPage() {
const viewModel: IWorkItemsViewModel = useWorkItemsViewModel();
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
index 04eb8524c..2159f9876 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/services/work-item.service.ts
@@ -9,7 +9,7 @@ import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';
import type { Assignee, WorkItemDetailDto } from '../dtos/work-item-detail.dto';
import type { WorkItemFiltersDto } from '../dtos/work-item-filters.dto';
import type { WorkItemListItemDto } from '../dtos/work-item-list-item.dto';
-import { UserSummaryDto } from "@/shared/dtos/user-summary.dto";
+import type { UserSummaryDto } from "@/shared/dtos/user-summary.dto";
function resolveAssignees(userIds?: string[]): Assignee[] {
if (!userIds?.length) return [];
diff --git a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
index 3fd872df0..334a566f3 100644
--- a/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
+++ b/MtdrSpring/backend/src/main/frontend/src/features/work-items/viewModels/useWorkItemsViewModel.ts
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { workItemService } from '../services/work-item.service';
-import { ViewMode } from "@/features/work-items/components/dashboard/dashboard-toolbar";
+import type { ViewMode } from "@/features/work-items/components/dashboard/dashboard-toolbar";
import type { WorkItemDetailDto } from '../dtos/work-item-detail.dto';
import type { CreateWorkItemDto } from '../dtos/create-work-item.dto';
import type { UpdateWorkItemDto } from '../dtos/update-work-item.dto';