diff --git a/Requests/Project/CreateProjectRequest.http b/Requests/Project/CreateProjectRequest.http
index d0dfef0..9988532 100644
--- a/Requests/Project/CreateProjectRequest.http
+++ b/Requests/Project/CreateProjectRequest.http
@@ -2,9 +2,10 @@
POST {{host}}/api/projects
Content-Type: application/json
-Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMGQ5MTMwMy1iNWM5LTQ1MzAtOTkxNC1kMjdjN2EwNTQ0MTUiLCJnaXZlbl9uYW1lIjoiSmFjdcWbIiwiZmFtaWx5X25hbWUiOiJCb3NhayIsImp0aSI6IjQ0NTkyMjQ0LWU0NzgtNDBjNC1hMzY5LTg2NWE5ZWExM2FmOCIsImV4cCI6MTcyNTQ0ODg3NCwiaXNzIjoiUGxhbkl0IiwiYXVkIjoiUGxhbkl0In0.E6iCIp1c3QGt5PAdXVsk9UZkFR9vmRSCMXU4poJl1Dw
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMGQ5MTMwMy1iNWM5LTQ1MzAtOTkxNC1kMjdjN2EwNTQ0MTUiLCJnaXZlbl9uYW1lIjoiSmFjdcWbIiwiZmFtaWx5X25hbWUiOiJCb3NhayIsImp0aSI6ImE2ZmI3YmEwLTZiNjUtNGEyMS1hYmFkLTYxOWZjMzExOTNmYyIsImV4cCI6MTcyNTg5NTA3MiwiaXNzIjoiUGxhbkl0IiwiYXVkIjoiUGxhbkl0In0.pd_wuO_8FZ-AL0_GKE4QaxTxwdL4LKOR1IOi6DdvPtM
{
+ "workspaceId": "e02023fa-6b10-46b8-9315-f53a081bf46d",
"name": "Project 1",
"description": "Project 1 description",
"projectTasks":
diff --git a/Requests/User/GetUserWorkspaces.http b/Requests/User/GetUserWorkspaces.http
new file mode 100644
index 0000000..a25743a
--- /dev/null
+++ b/Requests/User/GetUserWorkspaces.http
@@ -0,0 +1,5 @@
+@host=https://localhost:5234
+@userId=e0d91303-b5c9-4530-9914-d27c7a054415
+
+GET {{host}}/api/users/{{userId}}/workspaces
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMGQ5MTMwMy1iNWM5LTQ1MzAtOTkxNC1kMjdjN2EwNTQ0MTUiLCJnaXZlbl9uYW1lIjoiSmFjdcWbIiwiZmFtaWx5X25hbWUiOiJCb3NhayIsImp0aSI6ImUyM2UzMDU1LTBjYTUtNDRhMy05ZjUzLTNjYjkzOWE3YzJhMiIsImV4cCI6MTcyNTg3NzM3NCwiaXNzIjoiUGxhbkl0IiwiYXVkIjoiUGxhbkl0In0.sKvv3kdMfJ5F28vp6r_6sPNNdG8KWCsX7PFi6reuXes
\ No newline at end of file
diff --git a/Requests/Workspace/UpdateWorkspaceRequest.http b/Requests/Workspace/UpdateWorkspaceRequest.http
index 84bf11c..89a46f9 100644
--- a/Requests/Workspace/UpdateWorkspaceRequest.http
+++ b/Requests/Workspace/UpdateWorkspaceRequest.http
@@ -1,9 +1,9 @@
@host=https://localhost:5234
-@workspaceId=e02023fa-6b10-46b8-9315-f53a081bf46d
+@workspaceId=3458798c-c713-4a59-8b6f-99a48cfd05f4
PUT {{host}}/api/workspaces/{{workspaceId}}
Content-Type: application/json
-Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMGQ5MTMwMy1iNWM5LTQ1MzAtOTkxNC1kMjdjN2EwNTQ0MTUiLCJnaXZlbl9uYW1lIjoiSmFjdcWbIiwiZmFtaWx5X25hbWUiOiJCb3NhayIsImp0aSI6ImIyMGExOTU5LTllYWYtNDU4ZS04NjI3LWQ3NDAzMzNkZmZjNSIsImV4cCI6MTcyNTYzMjgzMCwiaXNzIjoiUGxhbkl0IiwiYXVkIjoiUGxhbkl0In0.ZN8-8TdKJoS2KyxB0tM8yJcweCrdLuu9ucy9Dby45OM
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMGQ5MTMwMy1iNWM5LTQ1MzAtOTkxNC1kMjdjN2EwNTQ0MTUiLCJnaXZlbl9uYW1lIjoiSmFjdcWbIiwiZmFtaWx5X25hbWUiOiJCb3NhayIsImp0aSI6ImViOWIwYTE0LWU4NmItNGMwMi1iNzczLTYxZGYyMTgzYWZmOSIsImV4cCI6MTcyNTk4NTIyMCwiaXNzIjoiUGxhbkl0IiwiYXVkIjoiUGxhbkl0In0.Q61IfAqjyHhT9gL8IDHlM4Of_oDPXaB-l7Gwox-6Mw4
{
"name": "Workspace 1 Edited",
diff --git a/clients/plan-it-web/eslint.config.js b/clients/plan-it-web/eslint.config.js
index b9d0f36..598e63a 100644
--- a/clients/plan-it-web/eslint.config.js
+++ b/clients/plan-it-web/eslint.config.js
@@ -31,7 +31,9 @@ export default tseslint.config(
'warn',
{ allowConstantExport: true },
],
- "@typescript-eslint/no-unsafe-assignment": "warn",
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-misused-promises" : "off",
+ "@typescript-eslint/no-unsafe-call" : "warn",
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
diff --git a/clients/plan-it-web/package-lock.json b/clients/plan-it-web/package-lock.json
index b5a3d5f..f52fe88 100644
--- a/clients/plan-it-web/package-lock.json
+++ b/clients/plan-it-web/package-lock.json
@@ -11,14 +11,18 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@mantine/core": "^7.12.2",
+ "@mantine/form": "^7.12.2",
"@mantine/hooks": "^7.12.2",
+ "@mantine/notifications": "^7.12.2",
"@reduxjs/toolkit": "^2.2.7",
"@tabler/icons-react": "^3.14.0",
"classnames": "^2.5.1",
"eslint-plugin-react": "^7.35.0",
+ "jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-redux": "^9.1.2"
+ "react-redux": "^9.1.2",
+ "react-router-dom": "^6.26.1"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
@@ -659,6 +663,19 @@
"react-dom": "^18.2.0"
}
},
+ "node_modules/@mantine/form": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.2.tgz",
+ "integrity": "sha512-MknzDN5F7u/V24wVrL5VIXNvE7/6NMt40K6w3p7wbKFZiLhdh/tDWdMcRN7PkkWF1j2+eoVCBAOCL74U3BzNag==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "klona": "^2.0.6"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
"node_modules/@mantine/hooks": {
"version": "7.12.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz",
@@ -668,6 +685,31 @@
"react": "^18.2.0"
}
},
+ "node_modules/@mantine/notifications": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.2.tgz",
+ "integrity": "sha512-gTvLHkoAZ42v5bZxibP9A50djp5ndEwumVhHSa7mxQ8oSS23tt3It/6hOqH7M+9kHY0a8s+viMiflUzTByA9qg==",
+ "license": "MIT",
+ "dependencies": {
+ "@mantine/store": "7.12.2",
+ "react-transition-group": "4.4.5"
+ },
+ "peerDependencies": {
+ "@mantine/core": "7.12.2",
+ "@mantine/hooks": "7.12.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+ },
+ "node_modules/@mantine/store": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.2.tgz",
+ "integrity": "sha512-NqL31sO/KcAETEWP/CiXrQOQNoE4168vZsxyXacQHGBueVMJa64WIDQtKLHrCnFRMws3vsXF02/OO4bH4XGcMQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -724,6 +766,15 @@
}
}
},
+ "node_modules/@remix-run/router": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz",
+ "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
@@ -1778,8 +1829,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
@@ -1899,6 +1949,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -3083,6 +3143,15 @@
"node": ">=4.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3091,6 +3160,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3684,6 +3762,38 @@
}
}
},
+ "node_modules/react-router": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz",
+ "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.19.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz",
+ "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.19.1",
+ "react-router": "6.26.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -3724,6 +3834,22 @@
"react": "^16.8.0 || ^17.0.0 || ^18.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",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -4935,12 +5061,36 @@
"type-fest": "^4.12.0"
}
},
+ "@mantine/form": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.12.2.tgz",
+ "integrity": "sha512-MknzDN5F7u/V24wVrL5VIXNvE7/6NMt40K6w3p7wbKFZiLhdh/tDWdMcRN7PkkWF1j2+eoVCBAOCL74U3BzNag==",
+ "requires": {
+ "fast-deep-equal": "^3.1.3",
+ "klona": "^2.0.6"
+ }
+ },
"@mantine/hooks": {
"version": "7.12.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz",
"integrity": "sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg==",
"requires": {}
},
+ "@mantine/notifications": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.12.2.tgz",
+ "integrity": "sha512-gTvLHkoAZ42v5bZxibP9A50djp5ndEwumVhHSa7mxQ8oSS23tt3It/6hOqH7M+9kHY0a8s+viMiflUzTByA9qg==",
+ "requires": {
+ "@mantine/store": "7.12.2",
+ "react-transition-group": "4.4.5"
+ }
+ },
+ "@mantine/store": {
+ "version": "7.12.2",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.12.2.tgz",
+ "integrity": "sha512-NqL31sO/KcAETEWP/CiXrQOQNoE4168vZsxyXacQHGBueVMJa64WIDQtKLHrCnFRMws3vsXF02/OO4bH4XGcMQ==",
+ "requires": {}
+ },
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4975,6 +5125,11 @@
"reselect": "^5.1.0"
}
},
+ "@remix-run/router": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz",
+ "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg=="
+ },
"@rollup/rollup-android-arm-eabi": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz",
@@ -5608,8 +5763,7 @@
"csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"data-view-buffer": {
"version": "1.0.1",
@@ -5687,6 +5841,15 @@
"esutils": "^2.0.2"
}
},
+ "dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -6506,6 +6669,11 @@
"object.values": "^1.1.6"
}
},
+ "jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
+ },
"keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6514,6 +6682,11 @@
"json-buffer": "3.0.1"
}
},
+ "klona": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
+ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="
+ },
"levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -6861,6 +7034,23 @@
"tslib": "^2.0.0"
}
},
+ "react-router": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz",
+ "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==",
+ "requires": {
+ "@remix-run/router": "1.19.1"
+ }
+ },
+ "react-router-dom": {
+ "version": "6.26.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz",
+ "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==",
+ "requires": {
+ "@remix-run/router": "1.19.1",
+ "react-router": "6.26.1"
+ }
+ },
"react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -6881,6 +7071,17 @@
"use-latest": "^1.2.1"
}
},
+ "react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
"redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
diff --git a/clients/plan-it-web/package.json b/clients/plan-it-web/package.json
index 2e678f5..7ed7f0b 100644
--- a/clients/plan-it-web/package.json
+++ b/clients/plan-it-web/package.json
@@ -13,14 +13,18 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@mantine/core": "^7.12.2",
+ "@mantine/form": "^7.12.2",
"@mantine/hooks": "^7.12.2",
+ "@mantine/notifications": "^7.12.2",
"@reduxjs/toolkit": "^2.2.7",
"@tabler/icons-react": "^3.14.0",
"classnames": "^2.5.1",
"eslint-plugin-react": "^7.35.0",
+ "jwt-decode": "^4.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-redux": "^9.1.2"
+ "react-redux": "^9.1.2",
+ "react-router-dom": "^6.26.1"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
diff --git a/clients/plan-it-web/src/App.tsx b/clients/plan-it-web/src/App.tsx
index 4adec60..f88d228 100644
--- a/clients/plan-it-web/src/App.tsx
+++ b/clients/plan-it-web/src/App.tsx
@@ -1,21 +1,55 @@
import '@mantine/core/styles.css';
-import { MantineProvider } from '@mantine/core';
import { Navbar } from './components/Navbar/Navbar';
import './App.css';
import { MainWindow } from './components/MainWindow/MainWindow';
-import { Provider } from 'react-redux';
-import { store } from './redux/store';
+import { useGetUserWorkspacesQuery } from './services/planit-api';
+
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import { useAppDispatch} from './hooks/reduxHooks';
+import { useEffect } from 'react';
+import { setWorkspaces } from './redux/workspacesSlice';
+import { WorkspaceSettings } from './components/WorkspaceSettings/WorkspaceSettings';
+import { Login } from './components/Login/Login';
+import { useJwtAuth } from './hooks/useJwtAuth';
+import { Register } from './components/Register/Register';
+import { ProfilePage } from './components/Profile/ProfilePage';
+import { ProtectedRoute } from './router/ProtectedRoute';
+import { Flex, Loader } from '@mantine/core';
export default function App() {
+ const dispatch = useAppDispatch();
+ const isAuthenticated = useJwtAuth();
+
+ const { data, isLoading, error } = useGetUserWorkspacesQuery('e0d91303-b5c9-4530-9914-d27c7a054415');
+
+ useEffect(() => {
+ if (data) {
+ dispatch(setWorkspaces(data));
+ }
+ },[data, dispatch]);
+
+ if (isLoading)
+ {
+ return
+ }
+
+ if (error) return
Error occurred while fetching workspaces
;
+
return (
-
-
-
-
-
-
+ <>
+
+ {isAuthenticated && }
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ >
);
};
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Common/ExtendedModal.tsx b/clients/plan-it-web/src/components/Common/ExtendedModal.tsx
new file mode 100644
index 0000000..726ac28
--- /dev/null
+++ b/clients/plan-it-web/src/components/Common/ExtendedModal.tsx
@@ -0,0 +1,31 @@
+import { Modal, Title } from "@mantine/core";
+
+interface ExtendedModalProps
+{
+ title: string;
+ opened: boolean;
+ onClose: () => void;
+ children: React.ReactNode;
+}
+
+export function ExtendedModal({
+ title,
+ children,
+ opened,
+ onClose} : ExtendedModalProps)
+{
+ return (
+ <>
+ e.stopPropagation() } opened={opened} onClose={onClose} closeOnClickOutside={true}>
+
+
+
+ {title}
+
+
+ {children}
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Login/Login.module.css b/clients/plan-it-web/src/components/Login/Login.module.css
new file mode 100644
index 0000000..e6d17eb
--- /dev/null
+++ b/clients/plan-it-web/src/components/Login/Login.module.css
@@ -0,0 +1,6 @@
+.title {
+ font-family:
+ Greycliff CF,
+ var(--mantine-font-family);
+ font-weight: 900;
+ }
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Login/Login.tsx b/clients/plan-it-web/src/components/Login/Login.tsx
new file mode 100644
index 0000000..ad8c320
--- /dev/null
+++ b/clients/plan-it-web/src/components/Login/Login.tsx
@@ -0,0 +1,93 @@
+import {
+ TextInput,
+ PasswordInput,
+ Checkbox,
+ Anchor,
+ Paper,
+ Title,
+ Text,
+ Container,
+ Group,
+ Button,
+ } from '@mantine/core';
+ import classes from './Login.module.css';
+
+ import { useState, FormEvent } from 'react';
+ import { useLoginMutation } from '../../services/auth-api';
+ import { setCredentials } from '../../redux/authSlice';
+ import { useAppDispatch } from '../../hooks/reduxHooks';
+ import { AuthResponse} from '../../types/Auth';
+import { useNavigate } from 'react-router-dom';
+import { notifications } from '@mantine/notifications';
+
+ export function Login() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [login, { isLoading }] = useLoginMutation();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ try {
+ const userData: AuthResponse = await login({ email, password }).unwrap();
+ dispatch(setCredentials({user: userData.user, token: userData.token}));
+
+ notifications.show({
+ title: 'Login successful',
+ message: 'You have been successfully logged in',
+ color: 'green'
+ });
+ navigate('/workspace');
+ } catch (error) {
+ const err = error as { data?: { title?: string; errors?: Record } };
+
+ // Display more personalized error message including server validation errors
+ if (err.data?.title)
+ {
+ console.log(err.data)
+ let errorMessage = err.data.title;
+ if (err.data.errors)
+ {
+ for (const [_,value] of Object.entries(err.data.errors)) {
+ errorMessage += '\n' + value;
+ }
+ }
+
+ notifications.show({
+ title: 'Error logging in',
+ message: errorMessage,
+ color: 'red'
+ });
+ }
+ }
+ };
+
+ return (
+
+
+ Welcome back!
+
+
+ Do not have an account yet?{' '}
+
+ Create account
+
+
+
+
+ setEmail(e.target.value) } required />
+ setPassword(e.target.value)} required mt="md" />
+
+
+
+ Forgot password?
+
+
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/MainWindow/MainWindow.tsx b/clients/plan-it-web/src/components/MainWindow/MainWindow.tsx
index ecd8e4b..1963007 100644
--- a/clients/plan-it-web/src/components/MainWindow/MainWindow.tsx
+++ b/clients/plan-it-web/src/components/MainWindow/MainWindow.tsx
@@ -1,34 +1,81 @@
-import { Flex, Title } from "@mantine/core";
+import { Flex, Group, Title } from "@mantine/core";
import { MultipleSortableProjects } from '../SortableItems/MultipleSortableProjects';
import classes from './MainWindow.module.css';
-import { useGetProjectByIdQuery } from "../../services/planit-api";
+import { useCreateProjectMutation, useGetProjectsForWorkspaceQuery, useGetWorkspaceQuery } from "../../services/planit-api";
+import { useParams } from "react-router-dom";
+import { useEffect } from "react";
+import { WorkspaceMenu } from "./WorkspaceMenu";
+import { Project } from "../../types/Project";
export function MainWindow() {
- const { data, error, isLoading } = useGetProjectByIdQuery('d0b41044-c9c0-40d3-9750-b1a72d4acbf4');
+ const { workspaceId } = useParams<{ workspaceId: string }>();
- let projects = {};
+ const { data: workspace, error: workspaceFetchError, isLoading: isLoadingWorkspace } = useGetWorkspaceQuery(workspaceId ?? "");
+ const { data : projects, error: workspaceProjectsFetchError, isLoading, refetch } = useGetProjectsForWorkspaceQuery(workspaceId ?? "", {
+ skip: !workspaceId
+ });
+ const [ createProject ] = useCreateProjectMutation();
- console.log(data);
+ useEffect(() => {
+ if (workspaceId) {
+ refetch().catch(console.error);
+ }
+ }, [workspaceId, refetch]);
- if (data && typeof(data) === "object")
- {
- let inData = [data];
- for (let i = 0; i < inData.length; i++)
- {
- projects[inData[i].id] = inData[i];
+ useEffect(() => {
+ console.log('Refetched projects:', projects);
+ }, [projects]);
+
+ if (workspaceFetchError) {
+ return Incorrect workspace
;
+ }
- }
- console.log(projects);
+ if (workspaceProjectsFetchError)
+ {
+ return Could not retrieve projects for this workspace
;
+ }
+
+ const handleAddNewProject = async () => {
+ const newProject = await createProject({
+ workspaceId: workspaceId,
+ name: "New Project",
+ description: "New Project Description",
+ projectTasks: []
+ });
+
+ refetch().catch(console.error);
+
+ return newProject;
}
+ const projectsToObjects = projects?.projects.reduce((prev, cur) => ({...prev, [cur.id]: cur}), {});
+
return (
- { isLoading ? Loading...
:
+ {isLoading || isLoadingWorkspace ? (
+ Loading...
+ ) : (
<>
- Workspace
-
- >
- }
+
+
+ {workspace!.name}
+
+
+
+
+ {projects?.projects && projects.projects.length > 0 ? (
+
+ ) : (
+ <>
+ No projects available
+
+ >
+ )}
+ >
+ )}
);
}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/MainWindow/WorkspaceMenu.tsx b/clients/plan-it-web/src/components/MainWindow/WorkspaceMenu.tsx
new file mode 100644
index 0000000..23f240c
--- /dev/null
+++ b/clients/plan-it-web/src/components/MainWindow/WorkspaceMenu.tsx
@@ -0,0 +1,61 @@
+import { ActionIcon, Group, Menu } from "@mantine/core";
+import { IconSettings, IconTrash } from "@tabler/icons-react";
+import { useAppDispatch } from "../../hooks/reduxHooks";
+import { Navigate, useNavigate, useParams } from "react-router-dom";
+import { useDeleteWorkspaceMutation } from "../../services/planit-api";
+import { deleteWorkspaceLocal } from "../../redux/workspacesSlice";
+
+export function WorkspaceMenu()
+{
+ const { workspaceId } = useParams<{ workspaceId: string }>();
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+ const [ deleteWorkspace ] = useDeleteWorkspaceMutation();
+
+ const handleDeleteWorkspace = async () => {
+ console.log("calling!");
+ if (!workspaceId) return;
+
+ if (!window.confirm('Are you sure you want to delete this workspace?')) return;
+
+ const result = await deleteWorkspace(workspaceId);
+
+ if (result.error)
+ {
+ console.error('Error deleting workspace:', result.error);
+ }
+
+ dispatch(deleteWorkspaceLocal(workspaceId));
+ navigate('/');
+ }
+
+ const handleWorkspaceSettings = () => {
+ navigate(`/workspaces/${workspaceId}/settings`);
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Navbar/Navbar.tsx b/clients/plan-it-web/src/components/Navbar/Navbar.tsx
index 895fdcc..e023072 100644
--- a/clients/plan-it-web/src/components/Navbar/Navbar.tsx
+++ b/clients/plan-it-web/src/components/Navbar/Navbar.tsx
@@ -10,10 +10,15 @@ import {
Group,
ActionIcon,
Tooltip,
+ Loader,
} from '@mantine/core';
- import { IconBulb, IconUser, IconCheckbox, IconSearch, IconPlus } from '@tabler/icons-react';
+ import { IconBulb, IconUser, IconCheckbox, IconSearch, IconPlus} from '@tabler/icons-react';
import { UserButton } from '../UserButton/UserButton';
import classes from './Navbar.module.css';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { useCreateWorkspaceMutation } from '../../services/planit-api';
+import { useAppDispatch, useAppSelector } from '../../hooks/reduxHooks';
+import { addWorkspace } from '../../redux/workspacesSlice';
const links: { icon: any; label: string; notifications?: number }[] = [
@@ -22,19 +27,30 @@ import {
{ icon: IconUser, label: 'Contacts' },
];
- const collections = [
- { emoji: 'π', label: 'Sales' },
- { emoji: 'π', label: 'Deliveries' },
- { emoji: 'πΈ', label: 'Discounts' },
- { emoji: 'π°', label: 'Profits' },
- { emoji: 'β¨', label: 'Reports' },
- { emoji: 'π', label: 'Orders' },
- { emoji: 'π
', label: 'Events' },
- { emoji: 'π', label: 'Debts' },
- { emoji: 'πββοΈ', label: 'Customers' },
- ];
+
export function Navbar() {
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const workspaces = useAppSelector( state => state.workspaces.workspaces);
+ const [ createWorkspace ]= useCreateWorkspaceMutation();
+
+ const handleClickIconPlus = async () => {
+ const newWorkspace = await createWorkspace({
+ name: "New Workspace",
+ description: "",
+ projectIds: []
+ });
+
+ if (!newWorkspace.data) return;
+
+ dispatch(addWorkspace(newWorkspace.data));
+ }
+
+ const handleUserButtonClick = () => {
+ navigate('/profile');
+ }
+
const mainLinks = links.map((link) => (
@@ -49,22 +65,24 @@ import {
));
- const collectionLinks = collections.map((collection) => (
-
event.preventDefault()}
- key={collection.label}
- className={classes.collectionLink}
- >
- {collection.emoji}{' '}
- {collection.label}
-
- ));
+ const workspacesLinks = workspaces
+ ? workspaces.map((workspace) => (
+
+ `${classes.workspaceLink} ${isActive ? classes.workspaceLinkActive : ''}`
+ }
+ >
+ {workspace.name}
+
+ ))
+ : null;
return (
);
diff --git a/clients/plan-it-web/src/components/Profile/ProfilePage.tsx b/clients/plan-it-web/src/components/Profile/ProfilePage.tsx
new file mode 100644
index 0000000..be07167
--- /dev/null
+++ b/clients/plan-it-web/src/components/Profile/ProfilePage.tsx
@@ -0,0 +1,155 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+import { useState, useEffect } from 'react';
+import {
+ Title,
+ TextInput,
+ PasswordInput,
+ Button,
+ Group,
+ Box,
+ Avatar,
+ FileInput,
+ Flex,
+} from '@mantine/core';
+import { useForm } from '@mantine/form';
+import { useGetUserQuery, useUpdateUserMutation, useUploadAvatarMutation } from '../../services/planit-api';
+import { showNotification } from '@mantine/notifications';
+import { useAppSelector } from '../../hooks/reduxHooks';
+
+interface ProfileFormValues {
+ firstName: string;
+ lastName: string;
+ password: string;
+ confirmPassword: string;
+}
+
+export function ProfilePage() {
+ const userFromToken = useAppSelector(state => state.auth.user);
+ const { data: currentUser } = useGetUserQuery(userFromToken?.id ?? "");
+ const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
+ const [uploadAvatar, { isLoading: isUploading }] = useUploadAvatarMutation();
+ const [avatarFile, setAvatarFile] = useState
(null);
+
+ const form = useForm({
+ initialValues: {
+ firstName: currentUser?.firstName ?? '',
+ lastName: currentUser?.lastName ?? '',
+ password: '',
+ confirmPassword: '',
+ },
+ validate: {
+ firstName: (value : string) => (value.length < 2 ? 'First name must have at least 2 characters' : null),
+ lastName: (value: string) => (value.length < 2 ? 'Last name must have at least 2 characters' : null),
+ password: (value : string) => (
+ value.length > 0 && value.length < 8
+ ? 'Password must be at least 8 characters long'
+ : null
+ ),
+ confirmPassword: (value: string, values: ProfileFormValues) =>
+ value !== values.password ? 'Passwords do not match' : null,
+ },
+ });
+
+ useEffect(() => {
+ if (currentUser) {
+ form.setValues({
+ firstName: currentUser.firstName || '',
+ lastName: currentUser.lastName || '',
+ });
+ }
+ }, [currentUser, form]);
+
+ const handleSubmit = async (values: ProfileFormValues) => {
+ if (!currentUser) {
+ showNotification({
+ title: 'Error',
+ message: 'User not found',
+ color: 'red',
+ });
+ return;
+ }
+
+ try {
+ await updateUser({
+ userId: currentUser.id,
+ firstName: values.firstName,
+ lastName: values.lastName,
+ ...(values.password ? { password: values.password } : {}),
+ }).unwrap();
+
+ if (avatarFile) {
+ const formData = new FormData();
+ formData.append('avatar', avatarFile);
+ await uploadAvatar({ userId: currentUser.id, avatar: formData }).unwrap();
+ }
+
+ showNotification({
+ title: 'Success',
+ message: 'Profile updated successfully',
+ color: 'green',
+ });
+ } catch {
+ showNotification({
+ title: 'Error',
+ message: 'Failed to update profile',
+ color: 'red',
+ });
+ }
+ };
+
+ return (
+
+ Edit Profile
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Project/Project.tsx b/clients/plan-it-web/src/components/Project/Project.tsx
index a7d2753..93cf6ee 100644
--- a/clients/plan-it-web/src/components/Project/Project.tsx
+++ b/clients/plan-it-web/src/components/Project/Project.tsx
@@ -1,6 +1,10 @@
import classes from './Project.module.css';
-import { Avatar, Group, Progress, Stack, Title, Text } from "@mantine/core";
+import { Avatar, Group, Progress, Stack, Title, Text, Loader, Modal, ActionIcon } from "@mantine/core";
import { Handle, Remove } from "../SortableItems/Item";
+import { useDisclosure } from '@mantine/hooks';
+import { IconAdjustments } from '@tabler/icons-react';
+import { ProjectSettings } from './ProjectSettings';
+import { ExtendedModal } from '../Common/ExtendedModal';
const avatars = [
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-2.png',
@@ -9,19 +13,29 @@ const avatars = [
];
interface ProjectProps {
- onRemove : (() => void) | undefined;
handleProps : React.HTMLAttributes | undefined;
+ onUpdate: () => void;
+ onRemove: () => void;
name: string;
+ id: string;
description: string;
}
-export function Project({ onRemove, handleProps, name, description } : ProjectProps ) {
+export function Project({ onUpdate, onRemove, handleProps, name, description, id } : ProjectProps ) {
+ const [modalOpened, { open, close }] = useDisclosure(false);
+
return (
-
+ <>
+
+
+
+
{name}
- {onRemove ? : undefined}
+
+
+
@@ -41,5 +55,6 @@ export function Project({ onRemove, handleProps, name, description } : ProjectPr
+ >
);
}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Project/ProjectSettings.module.css b/clients/plan-it-web/src/components/Project/ProjectSettings.module.css
new file mode 100644
index 0000000..cc1237d
--- /dev/null
+++ b/clients/plan-it-web/src/components/Project/ProjectSettings.module.css
@@ -0,0 +1,3 @@
+.container {
+
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Project/ProjectSettings.tsx b/clients/plan-it-web/src/components/Project/ProjectSettings.tsx
new file mode 100644
index 0000000..aac10b9
--- /dev/null
+++ b/clients/plan-it-web/src/components/Project/ProjectSettings.tsx
@@ -0,0 +1,93 @@
+import { Button, Flex, Group, Loader, Stack, TextInput, Title } from "@mantine/core";
+import classes from "./projectSettings.module.css"
+import { useNavigate } from "react-router-dom";
+import { useState } from "react";
+import { notifications } from '@mantine/notifications';
+import { useGetProjectQuery, useUpdateProjectMutation } from "../../services/planit-api";
+import { Project } from "../../types/Project";
+
+interface ProjectSettingsProps {
+ onUpdate: (project: Project) => void;
+ onRemove: () => void;
+ projectId: string;
+}
+
+export function ProjectSettings({
+ onUpdate,
+ onRemove,
+ projectId} : ProjectSettingsProps)
+{
+ const navigate = useNavigate();
+ // Get the project ID from the URL
+
+ if (!projectId)
+ {
+ console.error('No project ID found');
+
+ notifications.show({
+ title: 'Erorr accessing project',
+ message: 'project was not found, please try again!',
+ color: 'red'
+ })
+
+ navigate('/');
+
+ return;
+ }
+
+ // Get details about the project and project projects
+ const { data: project, error : projectError , isLoading : projectLoading } = useGetProjectQuery(projectId);
+
+ // Local state for the project settings
+ const [ projectName, setProjectName ] = useState(project?.name);
+ const [ projectDescription, setProjectDescription ] =useState(project?.description);
+
+ const [ updateproject, { isLoading : projectUpdating } ] = useUpdateProjectMutation();
+
+
+ const handleSaveproject = async () => {
+ // Save the project settings
+ const updatedProject = {
+ name: projectName,
+ description: projectDescription
+ };
+
+ const result = await updateproject({updatedProject, projectId: projectId});
+
+ if (result.error)
+ {
+ console.error('Error updating project:', result.error);
+ notifications.show({
+ title: 'Erorr updating project',
+ message: `${result.error.data.title}`,
+ color: 'red'
+ })
+ return;
+ }
+
+ onUpdate(result.data);
+
+ notifications.show({
+ title: 'Success',
+ message: 'Project settings saved successfuly',
+ color: 'green'
+ });
+ }
+
+ return (
+
+ {(projectLoading) && }
+
+
+ setProjectName(e.currentTarget.value) } />
+ setProjectDescription(e.currentTarget.value) }/>
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Register/Register.module.css b/clients/plan-it-web/src/components/Register/Register.module.css
new file mode 100644
index 0000000..cc1237d
--- /dev/null
+++ b/clients/plan-it-web/src/components/Register/Register.module.css
@@ -0,0 +1,3 @@
+.container {
+
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Register/Register.tsx b/clients/plan-it-web/src/components/Register/Register.tsx
new file mode 100644
index 0000000..da71195
--- /dev/null
+++ b/clients/plan-it-web/src/components/Register/Register.tsx
@@ -0,0 +1,144 @@
+import {
+ TextInput,
+ PasswordInput,
+ Checkbox,
+ Anchor,
+ Paper,
+ Title,
+ Text,
+ Container,
+ Group,
+ Button,
+ } from '@mantine/core';
+ import classes from './Register.module.css';
+ import { useState, FormEvent } from 'react';
+ import { useRegisterMutation } from '../../services/auth-api';
+ import { setCredentials } from '../../redux/authSlice';
+ import { useAppDispatch } from '../../hooks/reduxHooks';
+ import { AuthResponse } from '../../types/Auth';
+ import { notifications } from '@mantine/notifications';
+import { useNavigate } from 'react-router-dom';
+
+ export function Register() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [register, { isLoading }] = useRegisterMutation();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (password !== confirmPassword) {
+
+ notifications.show({
+ title: 'Error registering new account',
+ message: 'Passwords do not match',
+ color: 'red'
+ });
+ return;
+ }
+
+ try {
+ const userData: AuthResponse = await register({
+ email,
+ password,
+ firstName,
+ lastName
+ }).unwrap();
+ dispatch(setCredentials({user: userData.user, token: userData.token}));
+
+ notifications.show({
+ title: 'Account registered',
+ message: 'Your account has been successfully registered',
+ color: 'green'
+ });
+ navigate('/workspace');
+ } catch (error) {
+ const err = error as { data?: { title?: string; errors?: Record } };
+
+ // Display more personalized error message including server validation errors
+ if (err.data?.title)
+ {
+ console.log(err.data)
+ let errorMessage = err.data.title;
+ if (err.data.errors)
+ {
+ for (const [value] of Object.entries(err.data.errors)) {
+ errorMessage += '\n' + value;
+ }
+ }
+
+ notifications.show({
+ title: 'Error registering new account',
+ message: errorMessage,
+ color: 'red'
+ });
+ }
+ }
+ };
+
+ return (
+
+
+ Create an account
+
+
+ Already have an account?{' '}
+
+ Login
+
+
+
+
+ setFirstName(e.target.value)}
+ required
+ />
+ setLastName(e.target.value)}
+ required
+ mt="md"
+ />
+ setEmail(e.target.value)}
+ required
+ mt="md"
+ />
+ setPassword(e.target.value)}
+ required
+ mt="md"
+ />
+ setConfirmPassword(e.target.value)}
+ required
+ mt="md"
+ />
+
+
+
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/SortableItems/Container/Container.tsx b/clients/plan-it-web/src/components/SortableItems/Container/Container.tsx
index eb9b606..7f98e8c 100644
--- a/clients/plan-it-web/src/components/SortableItems/Container/Container.tsx
+++ b/clients/plan-it-web/src/components/SortableItems/Container/Container.tsx
@@ -30,6 +30,7 @@ export const Container = forwardRef(
horizontal,
hover,
onClick,
+ onUpdate,
onRemove,
label,
content,
@@ -71,8 +72,11 @@ export const Container = forwardRef(
+ handleProps={handleProps}
+ id={content?.id}
+ />
) : null}
{placeholder ? children : }
diff --git a/clients/plan-it-web/src/components/SortableItems/Item/Item.tsx b/clients/plan-it-web/src/components/SortableItems/Item/Item.tsx
index 43210dd..d0a2364 100644
--- a/clients/plan-it-web/src/components/SortableItems/Item/Item.tsx
+++ b/clients/plan-it-web/src/components/SortableItems/Item/Item.tsx
@@ -6,13 +6,12 @@ import classNames from 'classnames';
import type {DraggableSyntheticListeners} from '@dnd-kit/core';
import type {Transform} from '@dnd-kit/utilities';
-import {Handle, Remove} from './components';
-
import styles from './Item.module.css';
import { Task } from '../../Task/Task';
import { ProjectTask } from '../../../types/Project';
export interface ItemProps {
+ onDelete: () => void;
dragOverlay?: boolean;
color?: string;
disabled?: boolean;
@@ -20,6 +19,8 @@ export interface ItemProps {
handle?: boolean;
handleProps?: any;
height?: number;
+ projectId: string;
+ taskId: string;
index?: number;
fadeIn?: boolean;
content: ProjectTask;
@@ -32,6 +33,8 @@ export interface ItemProps {
value: React.ReactNode;
onRemove?(): void;
renderItem?(args: {
+ onUpdate: () => void;
+ onDelete: () => void;
dragOverlay: boolean;
dragging: boolean;
sorting: boolean;
@@ -50,15 +53,17 @@ export const Item = React.memo(
React.forwardRef(
(
{
+ onUpdate,
+ onDelete,
dragOverlay,
dragging,
disabled,
fadeIn,
+ projectId,
+ taskId,
handle,
- handleProps,
index,
listeners,
- onRemove,
sorting,
style,
content,
@@ -69,7 +74,9 @@ export const Item = React.memo(
},
ref
) => {
+
useEffect(() => {
+
if (!dragOverlay) {
return;
}
@@ -117,7 +124,6 @@ export const Item = React.memo(
className={classNames(
styles.Item,
dragging && styles.dragging,
- handle && styles.withHandle,
dragOverlay && styles.dragOverlay,
disabled && styles.disabled,
)}
@@ -126,12 +132,8 @@ export const Item = React.memo(
{...(!handle ? listeners : undefined)}
tabIndex={!handle ? 0 : undefined}
>
-
-
- {onRemove ? (
-
- ) : null}
- {handle ? : null}
+
+
diff --git a/clients/plan-it-web/src/components/SortableItems/Item/SortableItem.tsx b/clients/plan-it-web/src/components/SortableItems/Item/SortableItem.tsx
index dd5f8e2..d8d3ebb 100644
--- a/clients/plan-it-web/src/components/SortableItems/Item/SortableItem.tsx
+++ b/clients/plan-it-web/src/components/SortableItems/Item/SortableItem.tsx
@@ -3,6 +3,8 @@ import { Item } from './Item';
import { ProjectTask } from '../../../types/Project';
interface SortableItemProps {
+ onDeleteTask: () => void;
+ onUpdateTask: () => void;
containerId: string;
id: string;
index: number;
@@ -13,7 +15,10 @@ interface SortableItemProps {
}
export function SortableItem({
+ onUpdateTask,
+ onDeleteTask,
disabled,
+ projectId,
id,
index,
handle,
@@ -33,13 +38,17 @@ export function SortableItem({
return (
- (initialprojects);
+ const [projects, setProjects] = useState(workspaceProjects);
+ const [deleteProject] = useDeleteProjectMutation();
+ const [createProjectTask] = useCreateProjectTaskMutation();
+
+ useEffect(() => {
+ setProjects(structuredClone(workspaceProjects));
+ }, [JSON.stringify(workspaceProjects)])
const {
sensors,
activeId,
- containers,
- setContainers,
onDragStart,
onDragOver,
onDragEnd,
@@ -53,24 +60,109 @@ export function MultipleSortableProjects({
getIndex,
renderSortableItemDragOverlay,
renderContainerDragOverlay,
- } = useMultipleContainers(projects, setprojects);
-
- const handleRemove = (containerID: string) => {
- setContainers((containers) => containers.filter((id) => id !== containerID));
- };
-
- const handleAddColumn = () => {
- const newContainerId = getNextContainerId();
- setContainers((containers) => [...containers, newContainerId]);
- setprojects((projects) => ({
- ...projects,
- [newContainerId]: {
- name: "New project",
- description: "",
+ } = useMultipleContainers(projects, setProjects);
+
+ const containers = useMemo(() => Object.keys(projects), [projects]);
+
+ const handleAddTask = async ( projectId : string ) => {
+ const result = await createProjectTask({
+ projectId: projectId,
+ task: {
+ name: "New Task",
+ description: "New Task"
+ }
+ });
+
+ if (result.error) {
+ console.error('Error adding project task:', result.error);
+ notifications.show({
+ title: 'Error adding project task',
+ message: 'Could not add project task, please try again!',
+ color: 'red'
+ });
+ return;
+ }
+
+ if (projects[projectId].projectTasks.find((task) => task.id === result.data.id) != undefined) return;
+
+ setProjects((prevProjects : object) => {
+ if (prevProjects[projectId].projectTasks.find((task) => task.id === result.data.id) != undefined) return prevProjects;
+
+ const newProjects = {...prevProjects};
+ newProjects[projectId].projectTasks.push(result.data);
+ return newProjects;
+ });
+ }
+
+ const handleAddColumn = async () => {
+ const result = await onAddNewProject();
+
+ if (result.error)
+ {
+ console.error('Error adding new project :', result.error);
+
+ notifications.show({
+ title: 'Error adding new project',
+ message: 'Could not add new project, please try again!',
+ color: 'red'
+ });
+
+ return;
+ }
+
+ const newProject: Project = result.data;
+
+ setProjects((prevProjects : object) => ({
+ ...prevProjects,
+ [newProject.id]: {
+ workspaceId: newProject.workspaceId,
+ id: newProject.id,
+ name: newProject.name,
+ description: newProject.description,
projectTasks: []
},
+ }))};
+
+ const handleUpdate = (updatedProject : Project) => {
+ setProjects((prevProjects : Project) => ({
+ ...prevProjects,
+ [updatedProject.id]: {...updatedProject}
}));
- };
+ }
+
+ const handleRemove = useCallback(async (containerID: string) => {
+ const result = await deleteProject(projects[containerID].id);
+ if (result.error) {
+ console.error(result.error);
+ return;
+ }
+ setProjects((prevProjects) => {
+ const newProjects = {...prevProjects};
+ delete newProjects[containerID];
+ return newProjects;
+ });
+ }, [deleteProject, projects]);
+
+ const onDeleteTask = (projectId, taskId) => {
+ setProjects((prevProjects) => {
+ const newProjects = {...prevProjects};
+ newProjects[projectId].projectTasks = newProjects[projectId].projectTasks.filter((task) => task.id !== taskId);
+ return newProjects;
+ });
+ }
+
+ const onUpdateTask = (projectId, taskId, updatedTask) => {
+ setProjects((prevProjects) => {
+ const newProjects = {...prevProjects};
+ newProjects[projectId].projectTasks = newProjects[projectId].projectTasks.map((task) => {
+ if (task.id === taskId) {
+ return updatedTask;
+ }
+ return task;
+ });
+
+ return newProjects;
+ })};
return (
task.id)}
scrollable={scrollable}
style={containerStyle}
+ onUpdate = {(updatedProject) => handleUpdate(updatedProject) }
onRemove={() => handleRemove(containerId)}
>
task.id)} strategy={strategy}>
{projects[containerId].projectTasks.map((task) => (
onDeleteTask(projects[containerId].id, task.id) }
index={task.id}
key={task.id}
id={task.id}
+ projectId={projects[containerId].id}
content={task}
handle={handle}
containerId={containerId}
@@ -109,26 +205,27 @@ export function MultipleSortableProjects({
))}
console.log("Add task")}
- style={{ width: "45px",
- height: "45px",
- color:theme.colors.blue[7],
- cursor: "pointer"
- }}
- stroke={1.3} />
-
+ onClick={() => handleAddTask(projects[containerId].id) }
+ style={{
+ width: "45px",
+ height: "45px",
+ color: theme.colors.blue[7],
+ cursor: "pointer"
+ }}
+ stroke={1.3} />
+
))}
-
+
+
-
+
+
{createPortal(
@@ -142,4 +239,4 @@ export function MultipleSortableProjects({
)}
);
-}
\ No newline at end of file
+}
diff --git a/clients/plan-it-web/src/components/Task/Task.tsx b/clients/plan-it-web/src/components/Task/Task.tsx
index 682876e..9092618 100644
--- a/clients/plan-it-web/src/components/Task/Task.tsx
+++ b/clients/plan-it-web/src/components/Task/Task.tsx
@@ -1,20 +1,20 @@
-import {useSortable} from '@dnd-kit/sortable';
-import {CSS} from '@dnd-kit/utilities';
import { TaskCard } from './TaskCard';
import classes from './Task.module.css';
interface SortableItemProps {
- id: number;
- key: number;
+ id: string;
+ projectId: string;
name: string;
description: string;
+ onDelete: () => void;
+ onUpdate: () => void;
}
-export function Task( {id, name, description, key} : SortableItemProps) {
-
+export function Task( {id, projectId, name, description, onDelete, onUpdate} : SortableItemProps) {
return (
-
-
+
+
+
);
}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Task/TaskCard.tsx b/clients/plan-it-web/src/components/Task/TaskCard.tsx
index 23fdd86..eb2e865 100644
--- a/clients/plan-it-web/src/components/Task/TaskCard.tsx
+++ b/clients/plan-it-web/src/components/Task/TaskCard.tsx
@@ -1,23 +1,79 @@
-import { Card, Avatar, Text, Progress, Badge, Group, ActionIcon, useMantineTheme, Stack } from '@mantine/core';
-import { IconUpload } from '@tabler/icons-react';
+import { Card, Avatar, Text, Progress, Badge, Group, ActionIcon, useMantineTheme, Stack, Title, Modal, Flex } from '@mantine/core';
import classes from './TaskCard.module.css';
+import { IconSettings, IconX } from '@tabler/icons-react';
+import { useDeleteProjectTaskMutation } from '../../services/planit-api';
+import { notifications } from '@mantine/notifications';
+import { useDisclosure } from '@mantine/hooks';
+import { ExtendedModal } from '../Common/ExtendedModal';
+import { TaskSettings } from './TaskSettings';
interface TaskCardProps
{
- id: number;
+ projectId: string;
+ taskId: string;
name: string;
description: string;
+ onDelete: () => void;
};
-export function TaskCard( {id, name, description} : TaskCardProps) {
+export function TaskCard( {
+ projectId,
+ taskId,
+ name,
+ description,
+ onUpdate,
+ onDelete
+} : TaskCardProps) {
+ const [deleteProjectTask] = useDeleteProjectTaskMutation();
+ const [modalOpened, { open, close }] = useDisclosure(false);
+
const theme = useMantineTheme();
+ const handleDelete = async () => {
+ console.log('Deleting project task:', taskId);
+ const result = await deleteProjectTask({projectId, taskId});
+
+ if (result.error)
+ {
+ console.error('Error deleting project task:', result.error);
+ notifications.show({
+ title: 'Error deleting project task',
+ message: 'Could not delete project task, please try again!',
+ color: 'red'
+ });
+ return;
+ }
+
+ // Callback from top component
+ onDelete();
+ notifications.show({
+ title: 'Project task deleted',
+ message: 'Project task was successfully deleted',
+ color: 'green'
+ });
+
+ }
+
return (
+ <>
+
+
+
-
-
- {name}
-
+
+
+
+ {name}
+
+
+
+
+
+
+
+
+
+
{description}
@@ -29,5 +85,6 @@ export function TaskCard( {id, name, description} : TaskCardProps) {
+ >
);
}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Task/TaskSettings.module.css b/clients/plan-it-web/src/components/Task/TaskSettings.module.css
new file mode 100644
index 0000000..cc1237d
--- /dev/null
+++ b/clients/plan-it-web/src/components/Task/TaskSettings.module.css
@@ -0,0 +1,3 @@
+.container {
+
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/Task/TaskSettings.tsx b/clients/plan-it-web/src/components/Task/TaskSettings.tsx
new file mode 100644
index 0000000..f76f3aa
--- /dev/null
+++ b/clients/plan-it-web/src/components/Task/TaskSettings.tsx
@@ -0,0 +1,81 @@
+import { Button, Flex, Group, Loader, Stack, TextInput, Title } from "@mantine/core";
+import classes from "./TaskSettings.module.css"
+import { useEffect, useState } from "react";
+import { notifications } from '@mantine/notifications';
+import { useGetProjectQuery, useGetProjectTasksQuery, useUpdateProjectTaskMutation, } from "../../services/planit-api";
+import { Task } from "../../types/Task";
+
+interface TaskSettingsProps {
+ onUpdate: (projectId: string, taskId: string, updatedTask: Task) => void;
+ projectId: string;
+ taskId: string;
+}
+
+export function TaskSettings({
+ onUpdate,
+ projectId,
+ taskId} : TaskSettingsProps)
+{
+ // Get details about the Task and Task Tasks
+ const { data: project, error : taskError , isLoading : projectLoading } = useGetProjectQuery(projectId);
+
+ // Local state for the Task settings
+ const [ taskName, setTaskName ] = useState("");
+ const [ taskDescription, setTaskDescription ] =useState("");
+ const [ updateProjectTask, { isLoading : TaskUpdating } ] = useUpdateProjectTaskMutation();
+
+ let task;
+
+ if (!projectLoading)
+ {
+ if (project)
+ {
+ task = project.projectTasks.find(t => t.id === taskId);
+ }
+ }
+
+
+ const handleSaveTask = async () => {
+ // Save the Task settings
+ const updatedTask = {
+ name: taskName,
+ description: taskDescription
+ };
+
+ const result = await updateProjectTask({projectId, taskId, updatedTask});
+
+ if (result.error)
+ {
+ console.error('Error updating Task:', result.error);
+ notifications.show({
+ title: 'Erorr updating Task',
+ message: `${result.error.data.title}`,
+ color: 'red'
+ })
+ return;
+ }
+
+ onUpdate(projectId, taskId, result.data);
+
+ notifications.show({
+ title: 'Success',
+ message: 'Task settings saved successfuly',
+ color: 'green'
+ });
+ }
+
+ return (
+
+ { projectLoading && }
+
+
+ setTaskName(e.currentTarget.value) } />
+ setTaskDescription(e.currentTarget.value) }/>
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/UserButton/UserButton.tsx b/clients/plan-it-web/src/components/UserButton/UserButton.tsx
index f3f89e0..bd31885 100644
--- a/clients/plan-it-web/src/components/UserButton/UserButton.tsx
+++ b/clients/plan-it-web/src/components/UserButton/UserButton.tsx
@@ -2,9 +2,13 @@ import { UnstyledButton, Group, Avatar, Text } from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import classes from './UserButton.module.css';
-export function UserButton() {
+interface UserButton {
+ onClick: () => void;
+}
+
+export function UserButton({onClick} : UserButton) {
return (
-
+
{
+
+ return (
+
+
+
+ {row.name}
+
+
+ {row.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+
+ return (
+
+
+
+
+ Title
+ Description
+ Assigned users
+ Reviews distribution
+ Delete
+
+
+ {rows}
+
+
+ );
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.module.css b/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.module.css
new file mode 100644
index 0000000..ee7d415
--- /dev/null
+++ b/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.module.css
@@ -0,0 +1,7 @@
+.container {
+ width: 100%;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin-left: 15px;
+ margin-top: 15px;
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.tsx b/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.tsx
new file mode 100644
index 0000000..00f927d
--- /dev/null
+++ b/clients/plan-it-web/src/components/WorkspaceSettings/WorkspaceSettings.tsx
@@ -0,0 +1,84 @@
+import { Button, Flex, Paper, Stack, Text, TextInput, Title } from "@mantine/core";
+import classes from "./WorkspaceSettings.module.css"
+import { useNavigate, useParams } from "react-router-dom";
+import { useGetProjectsForWorkspaceQuery, useGetWorkspaceQuery, useUpdateWorkspaceMutation } from "../../services/planit-api";
+import { WorkspaceProjectsTable } from "./WorkspaceProjectsTable";
+import { useState } from "react";
+import { updateWorkspaceLocal } from "../../redux/workspacesSlice";
+import { useAppDispatch } from "../../hooks/reduxHooks";
+import { notifications } from '@mantine/notifications';
+
+export function WorkspaceSettings()
+{
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ // Get the workspace ID from the URL
+ const { workspaceId } = useParams<{ workspaceId: string }>();
+
+ if (!workspaceId)
+ {
+ console.error('No workspace ID found');
+
+ notifications.show({
+ title: 'Erorr accessing workspace',
+ message: 'Workspace was not found, please try again!',
+ color: 'red'
+ })
+
+ navigate('/');
+
+ return;
+ }
+
+ // Get details about the workspace and workspace projects
+ const { data: workspace, error : workspaceError , isLoading : workspaceLoading } = useGetWorkspaceQuery(workspaceId);
+ const { data : projects, error : projectsError , isLoading : projectsLoading, refetch } = useGetProjectsForWorkspaceQuery(workspaceId);
+
+ // Local state for the workspace settings
+ const [ workspaceName, setWorkspaceName ] = useState(workspace?.name);
+ const [ workspaceDescription, setWorkspaceDescription ] =useState(workspace?.description);
+
+ const [ updateWorkspace, { isLoading : workspaceUpdating } ] = useUpdateWorkspaceMutation();
+
+
+ const handleSaveWorkspace = async () => {
+ // Save the workspace settings
+ const updatedWorkspace = {
+ name: workspaceName,
+ description: workspaceDescription
+ };
+
+ const result = await updateWorkspace({updatedWorkspace, workspaceId: workspaceId});
+
+ if (result.error)
+ {
+ console.error('Error updating workspace:', result.error);
+ notifications.show({
+ title: 'Erorr updating workspace',
+ message: 'Could not update workspace, please try again!',
+ color: 'red'
+ })
+ return;
+ }
+
+ dispatch(updateWorkspaceLocal(result.data));
+ }
+
+ return (
+
+ {(workspaceLoading || projectsLoading) && Loading...}
+
+
+
+ Workspace Settings
+ setWorkspaceName(e.currentTarget.value) } />
+ setWorkspaceDescription(e.currentTarget.value) }/>
+
+ Projects
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/hooks/reduxHooks.ts b/clients/plan-it-web/src/hooks/reduxHooks.ts
new file mode 100644
index 0000000..3d2b22f
--- /dev/null
+++ b/clients/plan-it-web/src/hooks/reduxHooks.ts
@@ -0,0 +1,6 @@
+import { useDispatch, useSelector } from 'react-redux'
+import type { RootState, AppDispatch } from '../redux/store'
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = useDispatch.withTypes()
+export const useAppSelector = useSelector.withTypes()
\ No newline at end of file
diff --git a/clients/plan-it-web/src/hooks/useJwtAuth.ts b/clients/plan-it-web/src/hooks/useJwtAuth.ts
new file mode 100644
index 0000000..15520dc
--- /dev/null
+++ b/clients/plan-it-web/src/hooks/useJwtAuth.ts
@@ -0,0 +1,40 @@
+import { useEffect } from 'react';
+import { useAppDispatch, useAppSelector } from '../hooks/reduxHooks';
+import { setCredentials, logOut } from '../redux/authSlice';
+import { JwtInformation } from '../types/Auth';
+import { jwtDecode } from 'jwt-decode';
+
+// Hook that checks if the user is authenticated based on JWT token received from the server
+export const useJwtAuth = () => {
+ const dispatch = useAppDispatch();
+ const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated);
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+
+ if (token) {
+ try {
+ const decodedToken: JwtInformation = jwtDecode(token);
+
+ const userFromToken = {
+ id: decodedToken.jti,
+ firstName: decodedToken.given_name,
+ lastName: decodedToken.family_name,
+ };
+
+ // Sprawdzenie, czy token jest nadal waΕΌny
+ if (decodedToken.exp && decodedToken.exp * 1000 > Date.now()) {
+ dispatch(setCredentials({ token, user: userFromToken }));
+ } else {
+ // Token wygasΕ
+ dispatch(logOut());
+ }
+ } catch (err) {
+ console.error('Invalid token:', err);
+ dispatch(logOut());
+ }
+ }
+ }, [dispatch]);
+
+ return isAuthenticated;
+};
diff --git a/clients/plan-it-web/src/hooks/useMultipleProjectsHook.tsx b/clients/plan-it-web/src/hooks/useMultipleProjectsHook.tsx
index 32958ac..2017474 100644
--- a/clients/plan-it-web/src/hooks/useMultipleProjectsHook.tsx
+++ b/clients/plan-it-web/src/hooks/useMultipleProjectsHook.tsx
@@ -12,12 +12,10 @@ const PLACEHOLDER_ID = 'placeholder';
export type Items = Record;
export const useMultipleContainers = (items: Items, setItems: React.Dispatch>) => {
- const [containers, setContainers] = useState(Object.keys(items));
const [activeId, setActiveId] = useState(null);
const [clonedItems, setClonedItems] = useState(null);
const lastOverId = useRef(null);
const recentlyMovedToNewContainer = useRef(false);
-
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
const findContainer = useCallback((id: string) => {
@@ -33,7 +31,7 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch task.id === id);
- }, [findContainer, items]);
+ }, [items, findContainer]);
const onDragStart = useCallback(({ active }) => {
setActiveId(active.id);
@@ -50,15 +48,15 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch {
- const activeItems = items[activeContainer].projectTasks;
- const overItems = items[overContainer].projectTasks;
- const overIndex = overId in items ? overItems.length + 1 : overItems.findIndex((task) => task.id === overId);
+ setItems((prevItems) => {
+ const activeItems = prevItems[activeContainer].projectTasks;
+ const overItems = prevItems[overContainer].projectTasks;
+ const overIndex = overId in prevItems ? overItems.length + 1 : overItems.findIndex((task) => task.id === overId);
const activeIndex = activeItems.findIndex((task) => task.id === active.id);
let newIndex: number;
- if (overId in items) {
+ if (overId in prevItems) {
newIndex = overItems.length + 1;
} else {
const isBelowOverItem = over && active.rect.current.translated &&
@@ -70,30 +68,32 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch item.id !== active.id)
+ ...prevItems[activeContainer],
+ projectTasks: prevItems[activeContainer].projectTasks.filter((item) => item.id !== active.id)
},
[overContainer]: {
- ...items[overContainer],
+ ...prevItems[overContainer],
projectTasks: [
- ...items[overContainer].projectTasks.slice(0, newIndex),
- items[activeContainer].projectTasks.find((item) => item.id === active.id)!,
- ...items[overContainer].projectTasks.slice(newIndex)
+ ...prevItems[overContainer].projectTasks.slice(0, newIndex),
+ prevItems[activeContainer].projectTasks.find((item) => item.id === active.id)!,
+ ...prevItems[overContainer].projectTasks.slice(newIndex)
],
}
};
});
}
- }, [findContainer, items]);
+ }, [items, findContainer, setItems]);
const onDragEnd = useCallback(({ active, over }) => {
if (active.id in items && over?.id) {
- setContainers((containers) => {
- const activeIndex = containers.indexOf(active.id);
- const overIndex = containers.indexOf(over.id);
- return arrayMove(containers, activeIndex, overIndex);
+ setItems((prevItems) => {
+ const activeIndex = Object.keys(prevItems).indexOf(active.id);
+ const overIndex = Object.keys(prevItems).indexOf(over.id);
+ return Object.fromEntries(
+ arrayMove(Object.entries(prevItems), activeIndex, overIndex)
+ );
});
}
@@ -113,17 +113,16 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch [...containers, newContainerId]);
- setItems((items) => ({
- ...items,
+ setItems((prevItems) => ({
+ ...prevItems,
[activeContainer]: {
- ...items[activeContainer],
- projectTasks: items[activeContainer].projectTasks.filter((task) => task.id !== active.id)
+ ...prevItems[activeContainer],
+ projectTasks: prevItems[activeContainer].projectTasks.filter((task) => task.id !== active.id)
},
[newContainerId]: {
name: "New project",
description: "",
- projectTasks: [items[activeContainer].projectTasks.find((task) => task.id === active.id)!]
+ projectTasks: [prevItems[activeContainer].projectTasks.find((task) => task.id === active.id)!]
},
}));
setActiveId(null);
@@ -137,18 +136,18 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch ({
- ...items,
+ setItems((prevItems) => ({
+ ...prevItems,
[overContainer]: {
- ...items[overContainer],
- projectTasks: arrayMove(items[overContainer].projectTasks, activeIndex, overIndex)
+ ...prevItems[overContainer],
+ projectTasks: arrayMove(prevItems[overContainer].projectTasks, activeIndex, overIndex)
},
}));
}
}
setActiveId(null);
- }, [findContainer, getIndex, items]);
+ }, [items, findContainer, getIndex, setItems]);
const onDragCancel = useCallback(() => {
if (clonedItems) {
@@ -156,11 +155,11 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch {
const containerId = findContainer(id) as UniqueIdentifier;
- const projectTask = items[containerId].projectTasks.find( (task) => task.id === id);
+ const projectTask = items[containerId].projectTasks.find((task) => task.id === id);
return (
);
- }
+ }, [items, findContainer]);
- function renderContainerDragOverlay(containerId: UniqueIdentifier) {
+ const renderContainerDragOverlay = useCallback((containerId: UniqueIdentifier) => {
return (
- {items[containerId].projectTasks.map((item, index) => (
+ {items[containerId].projectTasks.map((item) => (
-
);
- }
+ }, [items]);
return {
sensors,
activeId,
- containers,
- setContainers,
onDragStart,
onDragOver,
onDragEnd,
@@ -211,5 +208,4 @@ export const useMultipleContainers = (items: Items, setItems: React.Dispatch
-
+
+
+
+
+
+
,
)
diff --git a/clients/plan-it-web/src/redux/authSlice.ts b/clients/plan-it-web/src/redux/authSlice.ts
new file mode 100644
index 0000000..94f0fd0
--- /dev/null
+++ b/clients/plan-it-web/src/redux/authSlice.ts
@@ -0,0 +1,38 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { User } from '../types/User';
+
+interface AuthState {
+ user: User | null;
+ token: string | null;
+ isAuthenticated: boolean;
+}
+
+const initialState: AuthState = {
+ user: null,
+ token: localStorage.getItem('token'),
+ isAuthenticated: false,
+};
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState,
+ reducers: {
+ setCredentials: (state, action: PayloadAction<{ user: User; token: string }>) => {
+ const { user, token } = action.payload;
+ state.user = user;
+ state.token = token;
+ state.isAuthenticated = true;
+ localStorage.setItem('token', token);
+ },
+ logOut: (state) => {
+ state.user = null;
+ state.token = null;
+ state.isAuthenticated = false;
+ localStorage.removeItem('token');
+ }
+ }
+});
+
+export const { setCredentials, logOut } = authSlice.actions;
+
+export default authSlice.reducer;
diff --git a/clients/plan-it-web/src/redux/store.ts b/clients/plan-it-web/src/redux/store.ts
index f71caf0..617075f 100644
--- a/clients/plan-it-web/src/redux/store.ts
+++ b/clients/plan-it-web/src/redux/store.ts
@@ -2,18 +2,30 @@ import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { projectApi } from '../services/planit-api'
+import authReducer from './authSlice';
+
+import workspacesReducer from './workspacesSlice'
+import { authApi } from '../services/auth-api'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
+ auth: authReducer,
+ [authApi.reducerPath]: authApi.reducer,
[projectApi.reducerPath]: projectApi.reducer,
+ workspaces: workspacesReducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware().concat(projectApi.middleware),
+ getDefaultMiddleware().concat(projectApi.middleware, authApi.middleware),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
-setupListeners(store.dispatch)
\ No newline at end of file
+setupListeners(store.dispatch)
+
+// Infer the `RootState` and `AppDispatch` types from the store itself
+export type RootState = ReturnType
+// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
+export type AppDispatch = typeof store.dispatch
\ No newline at end of file
diff --git a/clients/plan-it-web/src/redux/workspacesSlice.ts b/clients/plan-it-web/src/redux/workspacesSlice.ts
new file mode 100644
index 0000000..564e479
--- /dev/null
+++ b/clients/plan-it-web/src/redux/workspacesSlice.ts
@@ -0,0 +1,40 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { Workspace } from '../types/Project'
+
+interface WorkspacesState {
+ workspaces: Workspace[]
+}
+
+const initialState: WorkspacesState = {
+ workspaces: []
+}
+
+const workspacesSlice = createSlice({
+ name: 'workspaces',
+ initialState,
+ reducers: {
+ addWorkspace: (state, action: PayloadAction) => {
+ state.workspaces.push(action.payload)
+ },
+ updateWorkspaceLocal: (state, action: PayloadAction) => {
+ const index = state.workspaces.findIndex(workspace => workspace.id === action.payload.id)
+ if (index !== -1) {
+ state.workspaces[index] = action.payload
+ }
+ },
+ deleteWorkspaceLocal: (state, action: PayloadAction) => {
+ state.workspaces = state.workspaces.filter(workspace => workspace.id !== action.payload)
+ },
+ setWorkspaces: (state, action: PayloadAction) => {
+ state.workspaces = action.payload
+ }
+ }
+})
+
+export const {
+ addWorkspace,
+ updateWorkspaceLocal,
+ deleteWorkspaceLocal,
+ setWorkspaces
+ } = workspacesSlice.actions
+export default workspacesSlice.reducer
diff --git a/clients/plan-it-web/src/router/ProtectedRoute.tsx b/clients/plan-it-web/src/router/ProtectedRoute.tsx
new file mode 100644
index 0000000..69eec19
--- /dev/null
+++ b/clients/plan-it-web/src/router/ProtectedRoute.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Navigate, useLocation } from 'react-router-dom';
+import { useAppSelector } from '../hooks/reduxHooks';
+import { showNotification } from '@mantine/notifications';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+}
+
+export const ProtectedRoute: React.FC = ({ children }) => {
+ const isAuthenticated = useAppSelector(state => state.auth.isAuthenticated);
+ const location = useLocation();
+
+ React.useEffect(() => {
+ if (!isAuthenticated) {
+ showNotification({
+ title: 'Error',
+ message: 'You must be logged in to view this page',
+ color: 'red',
+ });
+ }
+ }, [isAuthenticated]);
+
+ if (!isAuthenticated) {
+ // Redirect them to the /login page, but save the current location they were
+ // trying to go to when they were redirected. This allows us to send them
+ // along to that page after they login, which is a nicer user experience
+ // than dropping them off on the home page.
+ return ;
+ }
+
+ return <>{children}>;
+};
diff --git a/clients/plan-it-web/src/services/auth-api.ts b/clients/plan-it-web/src/services/auth-api.ts
new file mode 100644
index 0000000..3545e86
--- /dev/null
+++ b/clients/plan-it-web/src/services/auth-api.ts
@@ -0,0 +1,37 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
+import { RootState } from '../redux/store';
+import { LoginCredentials, RegisterData, AuthResponse } from '../types/Auth';
+
+const HOST = "https://localhost:5234";
+
+export const authApi = createApi({
+ reducerPath: 'authApi',
+ baseQuery: fetchBaseQuery({
+ baseUrl: HOST,
+ prepareHeaders: (headers, { getState }) => {
+ const token = (getState() as RootState).auth.token;
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ }
+ return headers;
+ },
+ }),
+ endpoints: (builder) => ({
+ login: builder.mutation({
+ query: (credentials) => ({
+ url: '/auth/login',
+ method: 'POST',
+ body: credentials,
+ }),
+ }),
+ register: builder.mutation({
+ query: (userData) => ({
+ url: '/auth/register',
+ method: 'POST',
+ body: userData,
+ }),
+ }),
+ }),
+});
+
+export const { useLoginMutation, useRegisterMutation } = authApi;
diff --git a/clients/plan-it-web/src/services/planit-api.ts b/clients/plan-it-web/src/services/planit-api.ts
index c1d3452..7687ce2 100644
--- a/clients/plan-it-web/src/services/planit-api.ts
+++ b/clients/plan-it-web/src/services/planit-api.ts
@@ -1,6 +1,6 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
-
-import type { Project } from '../types/Project'
+import type { Project, ProjectTask, Workspace, WorkspaceProjects } from '../types/Project'
+import { User } from '../types/User';
const HOST = "https://localhost:5234";
@@ -10,14 +10,126 @@ export const projectApi = createApi({
baseQuery: fetchBaseQuery({
baseUrl: `${HOST}/api`,
mode: 'cors',
+ prepareHeaders: (headers, { getState }) => {
+ const token = (getState() as RootState).auth.token;
+ if (token) {
+ headers.set('Authorization', `Bearer ${token}`);
+ }
+ return headers;
+ },
}),
endpoints: (builder) => ({
- getProjectById: builder.query({
+ getProject: builder.query({
query: (id) => `projects/${id}`,
}),
+ getUserWorkspaces: builder.query({
+ query: (userId) => `users/${userId}/workspaces/`,
+ }),
+ getProjectsForWorkspace: builder.query({
+ query: (workspaceId) => `workspaces/${workspaceId}/projects`,
+ }),
+ createProject: builder.mutation>({
+ query: (newProject) => ({
+ url: `projects/`,
+ method: 'POST',
+ body: newProject,
+ }),
+ }),
+ updateProject: builder.mutation, projectId: string }>({
+ query: ({ updatedProject, projectId }) => ({
+ url: `projects/${projectId}`,
+ method: 'PUT',
+ body: updatedProject,
+ })}),
+ deleteProject: builder.mutation({
+ query: (projectId) => ({
+ url: `projects/${projectId}`,
+ method: 'DELETE',
+ }),
+ }),
+ getWorkspace: builder.query({
+ query: (workspaceId) => `workspaces/${workspaceId}`,
+ }),
+ createWorkspace: builder.mutation>({
+ query: (newWorkspace) => ({
+ url: `workspaces/`,
+ method: 'POST',
+ body: newWorkspace,
+ }),
+ }),
+ updateWorkspace: builder.mutation, workspaceId: string }>({
+ query: ({ updatedWorkspace, workspaceId }) => ({
+ url: `workspaces/${workspaceId}`,
+ method: 'PUT',
+ body: updatedWorkspace,
+ })}),
+ deleteWorkspace: builder.mutation({
+ query: (workspaceId) => ({
+ url: `workspaces/${workspaceId}`,
+ method: 'DELETE',
+ }),
+ }),
+ // Project tasks
+ getProjectTasks: builder.query({
+ query: (projectId) => `projects/${projectId}`,
+ }),
+ createProjectTask: builder.mutation }>({
+ query: ({ projectId, task }) => ({
+ url: `projects/${projectId}/tasks`,
+ method: 'POST',
+ body: task,
+ })
+ }),
+ updateProjectTask: builder.mutation }>({
+ query: ({ projectId, taskId, updatedTask }) => ({
+ url: `projects/${projectId}/tasks/${taskId}`,
+ method: 'PUT',
+ body: updatedTask,
+ })}),
+ deleteProjectTask: builder.mutation({
+ query: ({ projectId, taskId }) => ({
+ url: `projects/${projectId}/tasks/${taskId}`,
+ method: 'DELETE',
+ })}),
+ // User
+ getUser: builder.query({
+ query: (userId) => `users/${userId}`,
+ }),
+ updateUser: builder.mutation & { userId: string }>({
+ query: ({ userId, ...patch }) => ({
+ url: `/users/${userId}`,
+ method: 'PATCH',
+ body: patch,
+ }),
+ }),
+ uploadAvatar: builder.mutation<{ avatarUrl: string }, { userId: string, avatar: FormData }>({
+ query: ({ userId, avatar }) => ({
+ url: `/users/${userId}/avatar`,
+ method: 'POST',
+ body: avatar,
+ }),
+ }),
}),
-})
+});
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
-export const { useGetProjectByIdQuery } = projectApi;
\ No newline at end of file
+export const {
+ useGetProjectQuery,
+ useGetUserWorkspacesQuery,
+ useGetProjectsForWorkspaceQuery,
+ useCreateProjectMutation,
+ useUpdateProjectMutation,
+ useDeleteProjectMutation,
+ useGetWorkspaceQuery,
+ useCreateWorkspaceMutation,
+ useUpdateWorkspaceMutation,
+ useDeleteWorkspaceMutation,
+ useCreateProjectTaskMutation,
+ useDeleteProjectTaskMutation,
+ useUpdateProjectTaskMutation,
+ useGetProjectTasksQuery,
+ useGetUserQuery,
+ useUpdateUserMutation,
+ useUploadAvatarMutation
+ } = projectApi;
\ No newline at end of file
diff --git a/clients/plan-it-web/src/types/Auth.ts b/clients/plan-it-web/src/types/Auth.ts
new file mode 100644
index 0000000..b66be90
--- /dev/null
+++ b/clients/plan-it-web/src/types/Auth.ts
@@ -0,0 +1,24 @@
+import { JwtPayload } from "jwt-decode";
+import { User } from "./User";
+
+export interface LoginCredentials {
+ email: string;
+ password: string;
+ }
+
+ export interface RegisterData {
+ firstName: string;
+ lastName: string;
+ email: string;
+ password: string;
+ }
+
+ export interface AuthResponse {
+ user: User;
+ token: string;
+ }
+
+ export interface JwtInformation extends JwtPayload {
+ given_name: string;
+ family_name: string;
+ }
\ No newline at end of file
diff --git a/clients/plan-it-web/src/types/Project.ts b/clients/plan-it-web/src/types/Project.ts
index 83ebbaa..35bdc31 100644
--- a/clients/plan-it-web/src/types/Project.ts
+++ b/clients/plan-it-web/src/types/Project.ts
@@ -1,11 +1,25 @@
export interface Project {
+ workspaceId: string;
id: string;
name: string;
description: string;
+ projectTasks: ProjectTask[];
}
export interface ProjectTask {
id: string,
name: string,
description: string
+}
+
+export interface Workspace {
+ id: string,
+ name: string,
+ description: string,
+ projectIds: string[]
+}
+
+export interface WorkspaceProjects {
+ id: string,
+ projects: Project[]
}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/types/Task.ts b/clients/plan-it-web/src/types/Task.ts
new file mode 100644
index 0000000..4086b84
--- /dev/null
+++ b/clients/plan-it-web/src/types/Task.ts
@@ -0,0 +1,5 @@
+export interface Task {
+ name: string;
+ description: string;
+ id: string;
+}
\ No newline at end of file
diff --git a/clients/plan-it-web/src/types/User.ts b/clients/plan-it-web/src/types/User.ts
new file mode 100644
index 0000000..7bae863
--- /dev/null
+++ b/clients/plan-it-web/src/types/User.ts
@@ -0,0 +1,7 @@
+export interface User {
+ id: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ avatarUrl: string | null;
+}
\ No newline at end of file
diff --git a/src/PlanIt.Application/Common/Interfaces/Persistence/IProjectRepository.cs b/src/PlanIt.Application/Common/Interfaces/Persistence/IProjectRepository.cs
index 4651674..f67cacd 100644
--- a/src/PlanIt.Application/Common/Interfaces/Persistence/IProjectRepository.cs
+++ b/src/PlanIt.Application/Common/Interfaces/Persistence/IProjectRepository.cs
@@ -1,14 +1,17 @@
using PlanIt.Domain.ProjectAggregate;
using PlanIt.Domain.ProjectAggregate.ValueObjects;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
namespace PlanIt.Application.Common.Interfaces.Persistence;
public interface IProjectRepository
{
- void Add(Project project);
+ Task AddAsync(Project project);
public Task GetAsync(ProjectId projectId);
+ public Task
> GetProjectsForWorkspaceAsync(WorkspaceId workspaceId);
+
public Task UpdateAsync();
public Task DeleteAsync(Project project);
diff --git a/src/PlanIt.Application/Common/Interfaces/Persistence/IUserRepository.cs b/src/PlanIt.Application/Common/Interfaces/Persistence/IUserRepository.cs
index 05992e0..e0d12c3 100644
--- a/src/PlanIt.Application/Common/Interfaces/Persistence/IUserRepository.cs
+++ b/src/PlanIt.Application/Common/Interfaces/Persistence/IUserRepository.cs
@@ -5,8 +5,8 @@ namespace PlanIt.Application.Common.Interfaces.Persistence
{
public interface IUserRepository
{
- public Task GetUserByEmail(string email);
public Task> AddAsync(User user, string email, string password);
+ public Task GetUserByEmail(string email);
public Task SaveChangesAsync();
}
}
\ No newline at end of file
diff --git a/src/PlanIt.Application/Common/Interfaces/Persistence/IWorkspaceRepository.cs b/src/PlanIt.Application/Common/Interfaces/Persistence/IWorkspaceRepository.cs
index 3987f79..b3f06e2 100644
--- a/src/PlanIt.Application/Common/Interfaces/Persistence/IWorkspaceRepository.cs
+++ b/src/PlanIt.Application/Common/Interfaces/Persistence/IWorkspaceRepository.cs
@@ -1,3 +1,4 @@
+using PlanIt.Domain.UserAggregate.ValueObjects;
using PlanIt.Domain.WorkspaceAggregate;
using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
@@ -8,5 +9,6 @@ public interface IWorkspaceRepository
public Task AddAsync(Workspace workspace);
public Task DeleteAsync(Workspace workspace);
public Task GetAsync(WorkspaceId workspaceId);
+ public Task> GetUserWorkspacesAsync(WorkspaceOwnerId userId);
public Task SaveChangesAsync();
}
\ No newline at end of file
diff --git a/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommand.cs b/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommand.cs
index 7fdd77a..2ae2917 100644
--- a/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommand.cs
+++ b/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommand.cs
@@ -5,7 +5,7 @@
namespace PlanIt.Application.Projects.Commands.CreateProject;
public record CreateProjectCommand(
- string ProjectOwnerId,
+ string WorkspaceId,
string Name,
string Description,
List ProjectTasks
diff --git a/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommandHandler.cs b/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommandHandler.cs
index b1f753c..f2585fd 100644
--- a/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommandHandler.cs
+++ b/src/PlanIt.Application/Projects/Commands/CreateProject/CreateProjectCommandHandler.cs
@@ -5,36 +5,44 @@
using PlanIt.Domain.ProjectAggregate;
using PlanIt.Domain.ProjectAggregate.Entities;
using PlanIt.Domain.ProjectAggregate.ValueObjects;
+using PlanIt.Domain.UserAggregate.ValueObjects;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
namespace PlanIt.Application.Projects.Commands.CreateProject;
public class CreateProjectCommandHandler : IRequestHandler>
{
public readonly IProjectRepository _projectRepository;
+ public readonly IUserContext _userContext;
- public CreateProjectCommandHandler(IProjectRepository projectRepository)
+ public CreateProjectCommandHandler(IProjectRepository projectRepository, IUserContext userContext)
{
_projectRepository = projectRepository;
+ _userContext = userContext;
}
public async Task> Handle(CreateProjectCommand request, CancellationToken cancellationToken)
{
- await Task.CompletedTask;
+ var loggedInUserId = _userContext.TryGetUserId();
+
+ var projectOwnerId = ProjectOwnerId.FromString(loggedInUserId);
+ var taskOwnerId = TaskOwnerId.FromString(loggedInUserId);
// Create Project
var project = Project.Create(
name: request.Name,
description: request.Description,
- projectOwnerId: ProjectOwnerId.Create(new Guid(request.ProjectOwnerId)),
+ workspaceId: WorkspaceId.FromString(request.WorkspaceId),
+ projectOwnerId: projectOwnerId,
projectTasks: request.ProjectTasks.ConvertAll(projectTask => ProjectTask.Create(
- taskOwnerId: TaskOwnerId.Create(new Guid(request.ProjectOwnerId)),
+ taskOwnerId: taskOwnerId,
name: projectTask.Name,
description: projectTask.Description
))
);
// Persist Project
- _projectRepository.Add(project);
+ await _projectRepository.AddAsync(project);
return project;
}
diff --git a/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQuery.cs b/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQuery.cs
new file mode 100644
index 0000000..4c4ce7a
--- /dev/null
+++ b/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQuery.cs
@@ -0,0 +1,10 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Domain.WorkspaceAggregate;
+
+namespace PlanIt.Application.Users.Queries;
+
+public record GetUserWorkspacesQuery
+(
+ string UserId
+) : IRequest>>;
\ No newline at end of file
diff --git a/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQueryHandler.cs b/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQueryHandler.cs
new file mode 100644
index 0000000..97740a8
--- /dev/null
+++ b/src/PlanIt.Application/Users/Queries/GetUserWorkspacesQueryHandler.cs
@@ -0,0 +1,35 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Application.Common.Interfaces.Persistence;
+using PlanIt.Domain.UserAggregate.ValueObjects;
+using PlanIt.Domain.WorkspaceAggregate;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
+
+namespace PlanIt.Application.Users.Queries;
+public class GetUserWorkspacesQueryHandler : IRequestHandler>>
+{
+ private readonly IWorkspaceRepository _workspaceRepository;
+ private readonly IUserRepository _userRepository;
+
+ public GetUserWorkspacesQueryHandler(IWorkspaceRepository workspaceRepository, IUserRepository userRepository)
+ {
+ _workspaceRepository = workspaceRepository;
+ _userRepository = userRepository;
+ }
+
+ public async Task>> Handle(GetUserWorkspacesQuery query, CancellationToken cancellationToken)
+ {
+ var userId = WorkspaceOwnerId.FromString(query.UserId);
+
+ // Get workspaces
+ var workspaces = await _workspaceRepository.GetUserWorkspacesAsync(userId);
+
+ if (workspaces is null)
+ {
+ return Result.Fail>(new NotFoundError($"Couldn't find workspaces for User with id: ${userId.Value}"));
+ }
+
+ // Return it
+ return workspaces;
+ }
+}
\ No newline at end of file
diff --git a/src/PlanIt.Application/Workspaces/Commands/CreateWorkspace/CreateWorkspaceCommandHandler.cs b/src/PlanIt.Application/Workspaces/Commands/CreateWorkspace/CreateWorkspaceCommandHandler.cs
index f7b61b6..734c619 100644
--- a/src/PlanIt.Application/Workspaces/Commands/CreateWorkspace/CreateWorkspaceCommandHandler.cs
+++ b/src/PlanIt.Application/Workspaces/Commands/CreateWorkspace/CreateWorkspaceCommandHandler.cs
@@ -11,7 +11,7 @@ public class CreateWorkspaceCommandHandler : IRequestHandler> Handle(CreateWorkspaceCommand command, Canc
{
var userId = _userContext.TryGetUserId();
- var workspace = Workspace.Create(command.Name, command.Description, WorkspaceOwnerId.Create(new Guid(userId)));
+ var workspace = Workspace.Create(
+ name: command.Name,
+ description: command.Description,
+ workspaceOwnerId: WorkspaceOwnerId.Create(new Guid(userId)));
await _workspaceRepository.AddAsync(workspace);
diff --git a/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQuery.cs b/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQuery.cs
new file mode 100644
index 0000000..0ccb414
--- /dev/null
+++ b/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQuery.cs
@@ -0,0 +1,10 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Domain.WorkspaceAggregate;
+
+namespace PlanIt.Application.Workspaces.Queries.GetWorkspace;
+
+public record GetWorkspaceQuery
+(
+ string WorkspaceId
+) : IRequest>;
\ No newline at end of file
diff --git a/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQueryHandler.cs b/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQueryHandler.cs
new file mode 100644
index 0000000..b63ac74
--- /dev/null
+++ b/src/PlanIt.Application/Workspaces/Queries/GetWorkspace/GetWorkspaceQueryHandler.cs
@@ -0,0 +1,32 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Application.Common.Interfaces.Persistence;
+using PlanIt.Application.Workspaces.Queries.GetWorkspace;
+using PlanIt.Domain.WorkspaceAggregate;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
+
+public class GetWorkspaceQueryHandler : IRequestHandler>
+{
+ private readonly IWorkspaceRepository _workspaceRepository;
+
+ public GetWorkspaceQueryHandler(IWorkspaceRepository workspaceRepository)
+ {
+ _workspaceRepository = workspaceRepository;
+ }
+
+ public async Task> Handle(GetWorkspaceQuery query, CancellationToken cancellationToken)
+ {
+ var workspaceId = WorkspaceId.FromString(query.WorkspaceId);
+
+ // Get workspace
+ var workspace = await _workspaceRepository.GetAsync(workspaceId);
+
+ if (workspace is null)
+ {
+ return Result.Fail(new NotFoundError($"Couldn't find a workspace with id: ${workspaceId.Value}"));
+ }
+
+ // Return it
+ return workspace;
+ }
+}
\ No newline at end of file
diff --git a/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQuery.cs b/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQuery.cs
new file mode 100644
index 0000000..9b3c263
--- /dev/null
+++ b/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQuery.cs
@@ -0,0 +1,9 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Domain.ProjectAggregate;
+
+namespace PlanIt.Application.Workspaces.Queries.GetWorkspaceProjects;
+
+public record GetWorkspaceProjectsQuery(
+ string WorkspaceId
+) : IRequest>>;
\ No newline at end of file
diff --git a/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQueryHandler.cs b/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQueryHandler.cs
new file mode 100644
index 0000000..228132f
--- /dev/null
+++ b/src/PlanIt.Application/Workspaces/Queries/GetWorkspaceProjects/GetWorkspaceProjectsQueryHandler.cs
@@ -0,0 +1,32 @@
+using FluentResults;
+using MediatR;
+using PlanIt.Application.Common.Interfaces.Persistence;
+using PlanIt.Domain.ProjectAggregate;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
+
+namespace PlanIt.Application.Workspaces.Queries.GetWorkspaceProjects;
+
+public class GetWorkspaceProjectsQueryHandler : IRequestHandler>>
+{
+ private readonly IProjectRepository _projectRepository;
+
+ public GetWorkspaceProjectsQueryHandler(IProjectRepository projectRepository)
+ {
+ _projectRepository = projectRepository;
+ }
+
+ public async Task>> Handle(GetWorkspaceProjectsQuery query, CancellationToken cancellationToken)
+ {
+ var workspaceId = WorkspaceId.FromString(query.WorkspaceId);
+
+ // Get project associated with the workspace
+ var projects = await _projectRepository.GetProjectsForWorkspaceAsync(workspaceId);
+
+ if (projects is null)
+ {
+ return Result.Fail>(new NotFoundError($"Couldn't find project for a workspace with id: ${workspaceId.Value}"));
+ }
+
+ return projects;
+ }
+}
\ No newline at end of file
diff --git a/src/PlanIt.Contracts/Projects/Requests/CreateProjectRequest.cs b/src/PlanIt.Contracts/Projects/Requests/CreateProjectRequest.cs
index e039d61..067c803 100644
--- a/src/PlanIt.Contracts/Projects/Requests/CreateProjectRequest.cs
+++ b/src/PlanIt.Contracts/Projects/Requests/CreateProjectRequest.cs
@@ -1,6 +1,7 @@
namespace PlanIt.Contracts.Projects.Requests;
public record CreateProjectRequest(
+ string WorkspaceId,
string Name,
string Description,
List ProjectTasks
diff --git a/src/PlanIt.Contracts/Workspace/Responses/WorkspaceProjectsResponse.cs b/src/PlanIt.Contracts/Workspace/Responses/WorkspaceProjectsResponse.cs
new file mode 100644
index 0000000..cd61c26
--- /dev/null
+++ b/src/PlanIt.Contracts/Workspace/Responses/WorkspaceProjectsResponse.cs
@@ -0,0 +1,9 @@
+using PlanIt.Contracts.Projects.Responses;
+
+namespace PlanIt.Contracts.Workspace.Responses;
+
+public record WorkspaceProjectsResponse
+(
+ string WorkspaceId,
+ List Projects
+);
\ No newline at end of file
diff --git a/src/PlanIt.Contracts/Workspace/Responses/WorkspaceResponse.cs b/src/PlanIt.Contracts/Workspace/Responses/WorkspaceResponse.cs
index 37d5659..fbd1ef1 100644
--- a/src/PlanIt.Contracts/Workspace/Responses/WorkspaceResponse.cs
+++ b/src/PlanIt.Contracts/Workspace/Responses/WorkspaceResponse.cs
@@ -1,6 +1,7 @@
namespace PlanIt.Contracts.Workspace.Responses;
public record WorkspaceResponse(
+ string Id,
string Name,
string Description
);
\ No newline at end of file
diff --git a/src/PlanIt.Domain/ProjectAggregate/Project.cs b/src/PlanIt.Domain/ProjectAggregate/Project.cs
index 73270d8..fe6d965 100644
--- a/src/PlanIt.Domain/ProjectAggregate/Project.cs
+++ b/src/PlanIt.Domain/ProjectAggregate/Project.cs
@@ -3,6 +3,7 @@
using PlanIt.Domain.ProjectAggregate.Entities;
using PlanIt.Domain.ProjectAggregate.Events;
using PlanIt.Domain.ProjectAggregate.ValueObjects;
+using PlanIt.Domain.WorkspaceAggregate.ValueObjects;
namespace PlanIt.Domain.ProjectAggregate;
@@ -10,6 +11,7 @@ public sealed class Project : AggregateRoot
{
private Project(
ProjectId id,
+ WorkspaceId workspaceId,
string name,
string description,
ProjectOwnerId projectOwnerId,
@@ -20,6 +22,7 @@ private Project(
Name = name;
Description = description;
_projectTasks = projectTasks;
+ WorkspaceId = workspaceId;
ProjectOwnerId = projectOwnerId;
CreatedDateTime = createdDateTime;
UpdatedDateTime = updatedDateTime;
@@ -35,6 +38,7 @@ private Project()
private readonly List _projectTasks;
public string Name { get; private set; }
public string Description { get; private set; }
+ public WorkspaceId WorkspaceId {get; private set; }
public ProjectOwnerId ProjectOwnerId { get; private set; }
public DateTime CreatedDateTime { get; private set; }
public DateTime UpdatedDateTime { get; private set; }
@@ -72,13 +76,16 @@ public void ChangeDescription(string newDescription)
return task;
}
- public static Project Create(string name,
+ public static Project Create(
+ string name,
string description,
+ WorkspaceId workspaceId,
ProjectOwnerId projectOwnerId,
List projectTasks)
{
var project = new Project(
ProjectId.CreateUnique(),
+ workspaceId,
name,
description,
projectOwnerId,
diff --git a/src/PlanIt.Domain/ProjectAggregate/ValueObjects/TaskOwnerId.cs b/src/PlanIt.Domain/ProjectAggregate/ValueObjects/TaskOwnerId.cs
index ea70cdc..b96805e 100644
--- a/src/PlanIt.Domain/ProjectAggregate/ValueObjects/TaskOwnerId.cs
+++ b/src/PlanIt.Domain/ProjectAggregate/ValueObjects/TaskOwnerId.cs
@@ -21,6 +21,11 @@ public static TaskOwnerId Create(Guid id)
return new TaskOwnerId(id);
}
+ public static TaskOwnerId FromString(string id)
+ {
+ return new TaskOwnerId(new Guid(id));
+ }
+
public override IEnumerable