235 Commits

Author SHA1 Message Date
trochas
3920016f3b Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-12 14:06:51 +01:00
trochas
3fce71b893 gestion des groupe, todo : rejoindre une session 2026-01-12 14:06:38 +01:00
Alexis Leboeuf
4d1d8d70df Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-12 14:06:36 +01:00
Alexis Leboeuf
bbd33a7ea2 Added class diagram 2026-01-12 14:05:10 +01:00
Alexis Leboeuf
e4dd334832 Changed permission management to a safer one 2026-01-12 14:04:57 +01:00
trochas
4bd0e0a299 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-12 09:57:17 +01:00
trochas
c205b1b396 front groupe activation + fix Modal qui dépasse de l'écran (petit bug visuel mais ça marche) 2026-01-12 09:57:10 +01:00
tuanvu
796f973f7f Merge remote-tracking branch 'origin/main' 2026-01-12 09:47:58 +01:00
tuanvu
fdca74fef1 update README 2026-01-12 09:47:52 +01:00
Alexis Leboeuf
158ee781c6 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-12 09:29:16 +01:00
Alexis Leboeuf
6a104edebf Removed debug logs 2026-01-12 09:28:43 +01:00
Amaël Kesteman
eadd180422 Feat: correction StatsAthletes 2026-01-12 09:18:49 +01:00
tuanvu
8c56880964 add getGroupe dans session et athlete 2026-01-12 08:48:07 +01:00
Amaël Kesteman
70d6f9b01b merge 2026-01-11 21:45:11 +01:00
Amaël Kesteman
903da8e11a Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-11 21:44:54 +01:00
Amaël Kesteman
cbd53ba471 Feat: Stats athlètes fonctionnel 2026-01-11 21:27:32 +01:00
trochas
55ac436e5b fix gap dans gestion 2026-01-11 21:08:44 +01:00
trochas
0186370adc fix back ground bug 2026-01-11 20:56:39 +01:00
trochas
6ed94c17b5 merge + clean 2026-01-11 20:39:36 +01:00
trochas
c8c98cadeb pages + correction coach dans detail Session 2026-01-11 19:29:59 +01:00
tuanvu
40917f52d4 commit 2026-01-11 19:23:49 +01:00
trochas
ddb2b93489 correction bug admin + lecture de toute les session dans l'edt pour l'admin 2026-01-11 16:56:10 +01:00
trochas
ecbddd3a58 barre menu en haut + ajustement style 2026-01-11 01:36:32 +01:00
trochas
b9e67589ed merge 2026-01-10 19:55:50 +01:00
trochas
62b9231d38 fix style 2026-01-10 19:50:41 +01:00
Amaël Kesteman
3a74f6b52d Feat: Ajout de la liste des athlètes + correction visuelle detailSession 2026-01-10 18:56:45 +01:00
trochas
e85f76c810 ressourcePanel getAll 2026-01-10 17:38:40 +01:00
trochas
9aeef08e65 subscribe + unsubscribe + edt 2026-01-10 16:50:53 +01:00
Amaël Kesteman
c720bc93ff Feat: restructuration du CSS 2026-01-10 15:28:07 +01:00
trochas
0cdce29e40 correction quelques variable css 2026-01-10 13:29:15 +01:00
trochas
1feda556ff Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-10 12:47:11 +01:00
trochas
8d844d2b2f get session de l'user dans edt 2026-01-10 12:47:08 +01:00
Amaël Kesteman
5af249e0ca Feat: ajout du CSS 2026-01-10 11:33:03 +01:00
trochas
fca7362bb7 fix add activite 2026-01-10 11:31:24 +01:00
trochas
864ea784b1 merge 2026-01-09 19:19:18 +01:00
trochas
f8866efc13 add & delet activite 2026-01-09 19:16:59 +01:00
Alexis Leboeuf
9340145200 Add activity should be fixed
Changed type to POST in BE
Changed param type in FE API
2026-01-09 19:16:10 +01:00
Alexis Leboeuf
8ea47c5ca1 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 18:54:28 +01:00
tuanvu
c234dd3f48 session/id/activites 2026-01-09 18:54:07 +01:00
tuanvu
29d483a2a6 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	back_end/src/main/java/hackathon/FrisbYEE/rest/SessionResource.java
2026-01-09 18:48:54 +01:00
Alexis Leboeuf
8a078410b1 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 18:47:10 +01:00
tuanvu
aa877339fa session/id/activites 2026-01-09 18:46:39 +01:00
Alexis Leboeuf
8e4d1cce57 Edited doc + removed useless imports 2026-01-09 18:46:29 +01:00
trochas
536f2a7162 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-09 18:45:22 +01:00
trochas
8638e962c9 fix creation session avec activite 2026-01-09 18:45:15 +01:00
Alexis Leboeuf
a4963aed6a Modifying front API to accept new endpoint 2026-01-09 18:20:42 +01:00
Alexis Leboeuf
f79433d0fb Added endpoint to add an existing activity to session 2026-01-09 18:20:04 +01:00
Alexis Leboeuf
f477d94e55 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 17:55:12 +01:00
Alexis Leboeuf
7067153072 🐛 Fixed Activite creation not sending created object 2026-01-09 17:55:03 +01:00
trochas
aa21d046cf front correction id number|null 2026-01-09 17:49:35 +01:00
Alexis Leboeuf
7a2b72e0b1 🍻 Removed setId in toEntity methods
I need to sleep
2026-01-09 17:26:16 +01:00
Alexis Leboeuf
0036dfa3df Reverted preivous item 2026-01-09 17:21:38 +01:00
Alexis Leboeuf
316bc247c4 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 17:13:00 +01:00
Alexis Leboeuf
1d99d5f097 Setting null to ids before creation to avoir problems
Postgre was using the given id instead of generating one
2026-01-09 17:12:51 +01:00
trochas
1cbc8f91d8 todo fix session id 2026-01-09 16:33:48 +01:00
Alexis Leboeuf
970844c33e Added ID to Sessions (i forgor again) 2026-01-09 16:31:17 +01:00
Alexis Leboeuf
aa82614227 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 16:21:58 +01:00
Alexis Leboeuf
247a05f70e Added ID 2026-01-09 16:21:46 +01:00
trochas
58e89779a5 fix loading du coach des sessions 2026-01-09 15:33:48 +01:00
trochas
c2c5c3ff6a Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-09 15:25:46 +01:00
tuanvu
b6cffcdb44 j'oublie 2026-01-09 15:25:41 +01:00
trochas
27438f50fb Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-09 15:24:30 +01:00
trochas
fce2c22fa7 getSessions 2026-01-09 15:24:27 +01:00
tuanvu
9269989130 getByID 2026-01-09 15:23:21 +01:00
tuanvu
6156df6fef Merge remote-tracking branch 'origin/main' 2026-01-09 14:50:30 +01:00
trochas
5ce2fe71a5 correction id coach bug 2026-01-09 14:50:13 +01:00
Alexis Leboeuf
1cf7f85eea Un jour ca marchera 2026-01-09 14:34:15 +01:00
Alexis Leboeuf
2240487915 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 14:29:46 +01:00
Alexis Leboeuf
142e51f3e6 Prints de débug
Un jour ca marchera
2026-01-09 14:29:39 +01:00
tuanvu
b806446763 Merge remote-tracking branch 'origin/main' 2026-01-09 14:01:47 +01:00
trochas
38c15dc408 id de l'user courant dans idCoach 2026-01-09 14:01:34 +01:00
trochas
fd2412e7ef fix nom en trop dans coach 2026-01-09 13:54:30 +01:00
tuanvu
3ec62df156 Merge remote-tracking branch 'origin/main' 2026-01-09 13:51:13 +01:00
Alexis Leboeuf
1ec1f318f6 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 13:50:39 +01:00
Alexis Leboeuf
56071f0448 Ajout prénom (et allez zywoo) 2026-01-09 13:48:17 +01:00
tuanvu
95933b62df Merge remote-tracking branch 'origin/main' 2026-01-09 13:39:02 +01:00
tuanvu
c368462ccb petit commit 2026-01-09 13:38:54 +01:00
trochas
e52ba2fe3b login presque fix 2026-01-09 13:38:50 +01:00
trochas
2651c34df5 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-09 12:58:37 +01:00
trochas
39e8be7427 création de session fix en cours 2026-01-09 12:58:20 +01:00
Amaël Kesteman
42134e0db6 Feat: ajout car en fait je suis débile 2026-01-09 12:55:25 +01:00
Alexis Leboeuf
b707167627 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-09 12:54:46 +01:00
Alexis Leboeuf
29bb452d19 Re-done session coach on creation 2026-01-09 12:54:37 +01:00
Amaël Kesteman
9ac25f9ab6 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-09 12:17:21 +01:00
Amaël Kesteman
7e41266d80 Feat: Ajout de activite.tsx 2026-01-09 12:16:51 +01:00
tuanvu
4e8587d445 ptet revoir les requetes.tsx 2026-01-09 11:23:20 +01:00
tuanvu
d23ae32379 fix 2026-01-09 10:49:09 +01:00
tuanvu
e40c648536 Merge remote-tracking branch 'origin/main' 2026-01-09 10:46:49 +01:00
trochas
b4200cc029 correction login 2026-01-09 10:46:41 +01:00
tuanvu
908dd8090f Merge remote-tracking branch 'origin/main' 2026-01-09 10:46:16 +01:00
tuanvu
46396d035b change endpoints 2026-01-09 10:46:12 +01:00
Alexis Leboeuf
34f37b99cc Inverted creation HTTP status because I am stupid 2026-01-09 10:36:10 +01:00
Alexis Leboeuf
3d00b0ad2d Changed HTTP codes on Coach and Athlete creation 2026-01-09 10:31:14 +01:00
Alexis Leboeuf
3eadabfa4c Modified HTTP status on create when user already exists
Changed from 409 CONFLICT to 302 FOUND
2026-01-09 08:40:40 +01:00
Alexis Leboeuf
dc814d4a7b Addd user retrieval if exists on creation for Coach and Athlete 2026-01-09 08:27:52 +01:00
trochas
cf509d1a7c correction /api en trop 2026-01-08 18:04:33 +01:00
trochas
dddbd6afd3 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-08 18:04:00 +01:00
trochas
748bb97139 pas de setId dans mapToEntity, c'est la BDD qui gère les id 2026-01-08 18:03:01 +01:00
Alexis Leboeuf
787eafbfae Removed unused comment 2026-01-08 18:02:50 +01:00
Alexis Leboeuf
b4779f6007 Modified API base URL in frontend 2026-01-08 18:02:19 +01:00
trochas
4eed2e2954 fix en cours 2026-01-08 17:53:42 +01:00
Alexis Leboeuf
43488884ff Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 16:53:46 +01:00
Alexis Leboeuf
e3043b021d Fixed role typo in athelete creation 2026-01-08 16:53:39 +01:00
tuanvu
8a2cfbd2f7 login fix 2026-01-08 16:36:52 +01:00
trochas
ba45985394 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-08 16:30:35 +01:00
trochas
9bb487c369 toDTO pour les user 2026-01-08 16:30:29 +01:00
Alexis Leboeuf
f20d20dd40 Added Admin endpoints
Fixed typos in Users Endpoints
2026-01-08 16:26:32 +01:00
trochas
d68662e91c getUser 2026-01-08 16:00:43 +01:00
Alexis Leboeuf
0bd93ac824 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 15:47:53 +01:00
Alexis Leboeuf
95ce13181f Added Keycloak related endpoints and other ones 2026-01-08 15:47:46 +01:00
trochas
ca7444315e merge 2026-01-08 15:46:39 +01:00
trochas
f2a0f8ca86 contructeur classes, type DTO dans TS, clean hard code 2026-01-08 15:42:11 +01:00
tuanvu
286fa78eb0 clean edt athlete 2026-01-08 15:31:07 +01:00
Alexis Leboeuf
e72243d355 Added ids to DTOs as they're needed 2026-01-08 15:01:38 +01:00
Alexis Leboeuf
6bc8b165ff Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 14:57:50 +01:00
Alexis Leboeuf
2818bdae8a Fixed front API conf broken in previous commit 2026-01-08 14:57:41 +01:00
Amaël Kesteman
365b7f5bdd Feat: Déplacement stats athlètes 2026-01-08 14:53:12 +01:00
Alexis Leboeuf
b82a32d0eb Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 14:51:56 +01:00
Alexis Leboeuf
0e8ba63be5 Updated API connection 2026-01-08 14:51:50 +01:00
trochas
78b82fcfee clean hard code 2026-01-08 14:14:56 +01:00
Alexis Leboeuf
1b44116936 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 13:40:14 +01:00
Alexis Leboeuf
42a6c57369 Coaches and Athletes by keycloak id 2026-01-08 13:39:54 +01:00
tuanvu
57762f4340 Merge remote-tracking branch 'origin/main' 2026-01-08 13:33:11 +01:00
tuanvu
85daf4647a create account marche 2026-01-08 13:32:53 +01:00
trochas
3daff2511e merge 2026-01-08 13:17:57 +01:00
trochas
be4ab7d7cf correction ressource list, clean composant 2026-01-08 12:41:25 +01:00
tuanvu
a3cf9b821a Merge remote-tracking branch 'origin/main' 2026-01-08 12:34:35 +01:00
tuanvu
919149e012 //TODO WebSecurityConfig 2026-01-08 12:34:24 +01:00
Amaël Kesteman
13e49a6229 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-08 12:04:17 +01:00
Amaël Kesteman
40c9d091b8 Feat: Ajout des types de sessions en map 2026-01-08 12:04:05 +01:00
Alexis Leboeuf
988a7c16b3 P U S H 2026-01-08 12:01:56 +01:00
Alexis Leboeuf
ff5750ffae Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 11:43:07 +01:00
tuanvu
1eef3c4944 edt_athlete 2026-01-08 11:42:42 +01:00
Alexis Leboeuf
90ecf2767c Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 11:36:59 +01:00
Alexis Leboeuf
25bae7652e Changed roles in Activite and Session endpoints 2026-01-08 11:36:54 +01:00
tuanvu
f77b401628 athlete test 2026-01-08 11:34:13 +01:00
tuanvu
6c2ae936ac Merge remote-tracking branch 'origin/main' 2026-01-08 11:27:25 +01:00
tuanvu
e28b126838 change in athlete and getkeycloak 2026-01-08 11:27:18 +01:00
Alexis Leboeuf
824d5f4388 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 11:26:27 +01:00
Alexis Leboeuf
9a2d1ae5e6 Lot of things
Refactored Role enum to be the same as Keycloak roles
Managed CORS errors in backend
Edited Keycloak config to avoid CORS error
Edited frontend API to avoid CORS errors
Changed Activite creation management
Added debug print in Login (should be removed);
2026-01-08 11:26:16 +01:00
Amaël Kesteman
5d99d8325b Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-08 11:22:22 +01:00
Amaël Kesteman
fa70da8f25 Feat: Ajout stats athlètes 2026-01-08 11:22:04 +01:00
tuanvu
41f574bc94 Merge remote-tracking branch 'origin/main' 2026-01-08 10:03:01 +01:00
Alexis Leboeuf
fc98b7aef9 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-08 09:15:53 +01:00
Alexis Leboeuf
b6793f0a0e Added overriden getter/setters because of error 2026-01-08 09:15:42 +01:00
tuanvu
068eb7f611 remove admin previlige in creation 2026-01-08 08:59:52 +01:00
tuanvu
ea0e8eceb9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	front_end/src/components/ressourcePanel.tsx
2026-01-08 08:59:19 +01:00
Amaël Kesteman
0a5d7bccd5 Feat: Calcul de temps de jeu total pour les lignes 2026-01-08 08:58:26 +01:00
tuanvu
94cba5e60c add registration 2026-01-08 08:57:58 +01:00
trochas
7c6ee6b65f clean ressourcePanel 2026-01-07 18:31:25 +01:00
Alexis Leboeuf
f03629ba33 Changed Athlete entity properties, was missing Getter Setter 2026-01-07 18:20:51 +01:00
Alexis Leboeuf
8b94f4ca74 Removing unused imports 2026-01-07 18:18:42 +01:00
Alexis Leboeuf
6787495bc1 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 18:07:17 +01:00
Alexis Leboeuf
a695f5deb1 Removing useless classes, it wasn't the right implem 2026-01-07 18:06:54 +01:00
Amaël Kesteman
9802acb80a Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 18:03:59 +01:00
trochas
8a108a3c08 merge 2026-01-07 18:02:38 +01:00
trochas
153035cefa ajout d'activité 2026-01-07 18:00:39 +01:00
Amaël Kesteman
cd4fb99429 Feat: Ajout de la classe ligne pour la partie 2. 2026-01-07 17:57:18 +01:00
Alexis Leboeuf
c760510ffb Activity creation connection between front and API 2026-01-07 17:44:22 +01:00
Alexis Leboeuf
203d0fe157 Finished refactoring classes, DTOs, endpoints I guess ? 2026-01-07 17:40:41 +01:00
Alexis Leboeuf
c8d1407bcc Continuing refactoring 2026-01-07 17:09:37 +01:00
Amaël Kesteman
816b1e3965 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 16:19:46 +01:00
Amaël Kesteman
9878357c71 Feat: Ajout liste des sessions pour admin et coach 2026-01-07 16:19:31 +01:00
Alexis Leboeuf
99d6aabe12 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 16:06:33 +01:00
Alexis Leboeuf
1a0fc33167 Re-wrote part of object classes 2026-01-07 16:06:25 +01:00
Amaël Kesteman
30b27e7420 Feat: Ajout de la liste pour les coachs visible que par les admins et la classe de test avec 2026-01-07 14:23:15 +01:00
Alexis Leboeuf
57cd52ca3d Changed API path for get button 2026-01-07 13:52:31 +01:00
trochas
7fdd9681dc Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 13:52:20 +01:00
trochas
677f6cdc17 merge 2026-01-07 13:52:13 +01:00
Alexis Leboeuf
2e06c53b62 CORS Management 2026-01-07 13:51:13 +01:00
trochas
abbf4cb726 debug api 2026-01-07 13:50:50 +01:00
tuanvu
3423042646 Merge remote-tracking branch 'origin/main' 2026-01-07 13:16:48 +01:00
tuanvu
928fe11842 add security 2026-01-07 13:16:43 +01:00
Amaël Kesteman
b5cdf4e699 Feat: ajout de la recurence des sessions 2026-01-07 13:12:28 +01:00
Alexis Leboeuf
435dc6171a Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 13:10:56 +01:00
Alexis Leboeuf
68d4373d05 Different updates
Changed API behaviour to manage Session objects
Added route in frontend to link to API
2026-01-07 13:10:49 +01:00
tuanvu
a4536d85a4 update curl api 2026-01-07 13:10:18 +01:00
trochas
bb4c0f67ad Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 13:02:53 +01:00
trochas
070a719054 ajout bouton d'édition dans session de l'edt + bouton debug pour api 2026-01-07 13:02:48 +01:00
Amaël Kesteman
4574994bc8 Feat: remplacement liste groupe par listes classique. 2026-01-07 12:47:25 +01:00
Alexis Leboeuf
50230ea682 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 12:42:42 +01:00
Alexis Leboeuf
4ed877a258 Added first route in front 2026-01-07 12:41:58 +01:00
Amaël Kesteman
b5050f489b Feat: mise a jour du bouton ( mis en HTML select) 2026-01-07 12:40:52 +01:00
trochas
c9ecc4c808 Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 12:28:35 +01:00
trochas
e775024545 Dropdown todo 2026-01-07 12:28:05 +01:00
Alexis Leboeuf
54e0f86f87 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 12:19:16 +01:00
Alexis Leboeuf
ac0d0b9328 Commented Keycloak elem to avoid useless error message 2026-01-07 12:17:50 +01:00
Amaël Kesteman
8b1d65040a Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 12:02:27 +01:00
Amaël Kesteman
7a0bbb410f Feat: ajout de la liste pour voir les activités et les athlètes en tant que coach/ admin 2026-01-07 12:00:07 +01:00
trochas
d7abc913de Merge branch 'main' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon 2026-01-07 11:51:13 +01:00
trochas
5f62ba2a54 edt fix 2026-01-07 11:51:09 +01:00
tuanvu
f5b582bab4 Merge remote-tracking branch 'origin/main' 2026-01-07 10:16:41 +01:00
tuanvu
f6634338a7 test on server 8081 2026-01-07 10:16:17 +01:00
Alexis Leboeuf
8ccc23696b Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-07 10:02:45 +01:00
Alexis Leboeuf
9f4f1a7b92 Corrected error duplicate container in docker compose 2026-01-07 10:01:24 +01:00
Alexis Leboeuf
56402455e6 Frontend API interface 2026-01-07 10:00:35 +01:00
tuanvu
20712412a8 add security to frontend 2026-01-07 09:55:12 +01:00
Alexis Leboeuf
d35405842d Merge remote-tracking branch 'refs/remotes/origin/main' 2026-01-06 18:33:24 +01:00
Alexis Leboeuf
fb9515cc3c Documentation 2026-01-06 18:33:15 +01:00
trochas
7094373ca2 détail d'une session de l'edt en cours 2026-01-06 18:27:46 +01:00
tuanvu
3ec0110aab Merge remote-tracking branch 'origin/main' 2026-01-06 16:15:30 +01:00
tuanvu
ad5ca2189b tested spring boot 2026-01-06 16:15:23 +01:00
trochas
7b2b864d86 edt (et pas ent) toujours en cours 2026-01-06 16:05:41 +01:00
tuanvu
39d603e7e9 Merge branch 'jpa' 2026-01-06 15:54:27 +01:00
trochas
bf2c6a5874 merge 2026-01-06 15:13:41 +01:00
Alexis Leboeuf
f90387ba45 mapToDTO and mapToEntity 2026-01-06 15:09:52 +01:00
trochas
d5ea854dcb ent en cours 2026-01-06 15:06:48 +01:00
tuanvu
7558343761 Merge branch 'jpa' 2026-01-06 15:04:27 +01:00
tuanvu
934991caac add endpoint to frontend 2026-01-06 15:04:14 +01:00
Alexis Leboeuf
d38b88e68c Removed unused interface 2026-01-06 11:07:21 +01:00
Alexis Leboeuf
90b55bea38 Removed unused imports 2026-01-06 11:04:06 +01:00
Alexis Leboeuf
0afb619d40 Resolved errors in AthleteResource 2026-01-06 11:02:24 +01:00
Alexis Leboeuf
8b240b8b60 Merge remote-tracking branch 'refs/remotes/origin/jpa' into jpa 2026-01-06 10:50:37 +01:00
Alexis Leboeuf
c9891ae7e8 Still a few errors to correct 2026-01-06 10:49:24 +01:00
Amaël Kesteman
5e5661635e Feat: Ajout des imports manquants pour Athlete DTO + Athlete 2026-01-06 10:17:04 +01:00
Alexis Leboeuf
b3399f377b Merge remote-tracking branch 'refs/remotes/origin/jpa' into jpa 2026-01-06 10:14:01 +01:00
Amaël Kesteman
5085320a1f Merge branch 'jpa' of https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon into jpa 2026-01-06 10:12:27 +01:00
Amaël Kesteman
741d01bcd2 Feat: Ajout méthode sur ActiviteRessource 2026-01-06 10:07:34 +01:00
trochas
defefd2c79 ent en cours 2026-01-06 10:05:57 +01:00
Alexis Leboeuf
0c82691a40 Athlete endpoints, not finished 2026-01-06 10:02:10 +01:00
tuanvu
20197a69ce Merge branch 'jpa' 2026-01-06 09:47:26 +01:00
tuanvu
9494bb3458 push activite athlete coach 2026-01-06 09:39:09 +01:00
Alexis Leboeuf
4de8e2da22 Added little doc to compile/install dependencies 2026-01-06 08:41:18 +01:00
trochas
d13572347f authentification keyloack + début front 2026-01-06 08:27:09 +01:00
Alexis Leboeuf
5c191bcff0 Fixed compilation errors in AthleteResource 2026-01-06 00:03:27 +01:00
Amaël Kesteman
fad05e8bb1 Feat: Fin de ActiviteResource + ajout id dans les DTO (erreur de ma part) 2026-01-05 22:20:29 +01:00
tuanvu
98bd9c636b add reponseentity 2026-01-05 17:04:40 +01:00
Amaël Kesteman
d124f2bda6 Feat: Ajout des DTO manquantes 2026-01-05 16:03:54 +01:00
tuanvu
f4b6698090 sessionDTO 2026-01-05 15:51:03 +01:00
tuanvu
c6f8e552eb DTO and Resource test 2026-01-05 15:22:22 +01:00
Amaël Kesteman
609cc1b66f Merge remote-tracking branch 'origin/endpoints' into jpa 2026-01-05 14:10:51 +01:00
tuanvu
59117be5bb Merge remote-tracking branch 'origin/jpa' into jpa 2026-01-05 14:05:46 +01:00
tuanvu
d17fee3ee9 test controller 2026-01-05 14:05:41 +01:00
Amaël Kesteman
fddfe32984 Feat : Ajout de certaines DAO + Correction du type des ID dans les classes métiers 2026-01-05 13:53:52 +01:00
Amaël Kesteman
94dbc95437 Feat: fin de JPA 2026-01-05 13:25:21 +01:00
91 changed files with 7829 additions and 639 deletions

5
.gitignore vendored
View File

@@ -1,7 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.vscode
.idea
# dependencies
/node_modules
front_end/node_modules
/.pnp
.pnp.js

View File

@@ -1,3 +0,0 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

187
README.md
View File

@@ -1,93 +1,152 @@
# hackathon
# FrisbyEE (Projet Hackathon)
Ce projet contient : un backend Spring Boot et un frontend React en TypeScript développé lors du hackathon. L'authentification est gérée par Keycloak et la base de données est PostgreSQL; les deux peuvent être lancés via Docker.
---
## Présentation rapide
- **Backend** : Spring Boot (Java 17) dans `back_end/`
- **Frontend** : React et TypeScript dans `front_end/`
- **Auth** : Keycloak (voir `keycloak/` pour la thème et configuré via `docker-compose.yml`)
- **Base de données** : PostgreSQL (configuré via `docker-compose.yml`)
## Getting started
## Pré-requis
- Java 17
- Maven (utiliez `mvn`)
- Node.js et npm
- Docker et Docker Compose
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
## Installation et démarrage
1. Récupérez le dépôt et placez-vous à la racine du projet.
2. Démarrez Keycloak et PostgreSQL avec Docker:
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
* [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon.git
git branch -M main
git push -uf origin main
```bash
sudo docker compose up -d
```
## Integrate with your tools
3. Backend: construire et lancer (depuis la racine du projet):
* [Set up project integrations](https://gitlab2.istic.univ-rennes1.fr/tuvu/hackathon/-/settings/integrations)
```bash
cd back_end
./mvn clean install
./mvn spring-boot:run
```
## Collaborate with your team
4. Frontend: installer les dépendances et démarrer :
* [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
* [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
* [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
* [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
```bash
cd front_end
npm install
npm start
```
## Test and Deploy
Le serveur de développement du frontend écoute par défaut sur `http://localhost:3000`, le backend sur `http://localhost:8080`, et api sur `http://localhost:8081` sauf configuration différente.
Use the built-in continuous integration in GitLab.
## Commandes utiles
- Arrêter et supprimer tous les conteneurs Docker:
* [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
* [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
```bash
sudo docker stop $(sudo docker ps -a -q)
sudo docker rm $(sudo docker ps -a -q)
```
***
- Ouvrir un shell Postgres dans le conteneur en cours d'exécution (exemple : conteneur `frisbyee-postgres`):
# Editing this README
```bash
sudo docker exec -it frisbyee-postgres psql -U frisbyee_user -d frisbyee
# puis, par exemple : \dt ou SELECT * FROM session;
```
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Keycloak / Thème de connexion
- Au début, il faut crée run realm qui s'appelle `Frisbyee_realm`, ensuite dans ce realm, créez un `Frisbyee_client` avec ce config debug:
```
ROOT URL: http://localhost:3000/
HOME URL: http://localhost:3000/
Valid redirect URIs: http://localhost:3000/*
Web origins: *
```
- Après, ajoutez les rôles : `admin`, `coach`, `athlete`
- Et, mettez chaque groupe `ADMIN`, `COACH`, `ATHLETE` et mapping chaque rôle pour chaque groupe.
- Dans User Registration, mettez le `defaut groupe`: `ATHLETE` et activez le user self registration.
## Suggestions for a good README
Pour appliquer le thème de connexion personnalisé fourni dans `keycloak/themes/frisbyee` :
- Ouvrez la console d'administration Keycloak -> sélectionnez le realm -> `Realm Settings` -> modifiez le `Display name` si vous le souhaitez.
- Dans `Themes`, définissez `Login Theme` sur `frisbyee` puis enregistrez.
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Configuration / Environnement
- Les propriétés du backend se trouvent dans `back_end/src/main/resources/application.properties`.
- Le frontend utilise `public/keycloak.json` pour la configuration du client Keycloak.
- Assurez-vous que le client Keycloak et le realm correspondent aux valeurs utilisées par les deux applications.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Contribution
- Thibaut ROCHAS
- Tuan Minh VU
- Amäel KESTEMAN
- Alexis LEBOEUF
## Domain model class diagram
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
```mermaid
classDiagram
%% Classes and attributes (inferred from metier package)
class User {
+Long id
+String keycloakId
+String email
+String nom
+String prenom
+Role role
}
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
class Athlete {
+String categorie
+String niveau
+List<String> groupe
+List<Session> sessions
}
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
%% Represente en enum
class Role {
+String role
}
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
class Coach {
+List<Session> sessions
}
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
class Admin {
}
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
class Activite {
+Long id
+String nom
+String theme
+String description
+Integer dureeMinutes
+List<Activite> activites
+Session session
}
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
class Session {
+Long id
+String name
+LocalDateTime creneau
+Integer duree
+String group
+bool isRecurrent
+Coach coach
+List<Athlete> athletes
}
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
%% Inheritance (if User is a base class for domain actors)
User <|-- Athlete
User <|-- Coach
User <|-- Admin
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
%% Associations with cardinality
Coach "1" -- "0..*" Session : manages
Role "1" -- "0..*" User : is
Session "0..*" -- "0..*" Activite : contains
Session "0..*" -- "0..*" Athlete : participants
Activite "0..*" -- "0..*" Session : usedIn
```

View File

@@ -1,9 +0,0 @@
services:
postgres:
image: 'postgres:latest'
environment:
- 'POSTGRES_DB=mydatabase'
- 'POSTGRES_PASSWORD=secret'
- 'POSTGRES_USER=myuser'
ports:
- '5432'

6
back_end/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "back_end",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -11,7 +11,7 @@
<groupId>hackathon</groupId>
<artifactId>FrisbYEE</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<packaging>jar</packaging>
<name>FrisbYEE</name>
<description>Demo project for Spring Boot</description>
<url/>
@@ -37,72 +37,31 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-client-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-resource-server-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

View File

@@ -0,0 +1,64 @@
package hackathon.FrisbYEE.config;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
// TODO //TODO // T O D O
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 2. Allow public endpoints BEFORE any authenticated() calls
.requestMatchers("/athlete/create", "/", "/public").permitAll()
.requestMatchers("/coach/**").hasRole("coach")
.requestMatchers("/admin/**").hasRole("admin")
.requestMatchers("/athlete/**").hasRole("athlete")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtToken -> {
Map<String, Collection<String>> realmAccess = jwtToken.getClaim("realm_access");
Collection<String> roles = realmAccess.get("roles");
System.out.println("ROLES FROM TOKEN " + roles);
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
return new JwtAuthenticationToken(jwtToken, authorities);
})));
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

View File

@@ -0,0 +1,15 @@
package hackathon.FrisbYEE.jpa.dto;
import lombok.Data;
import java.util.List;
@Data
public class ActiviteDTO {
private Integer id;
private String name;
private String theme;
private Long duree; // optional, can be null
private List<String> dataActivite;
private Integer sessionId;
}

View File

@@ -0,0 +1,10 @@
package hackathon.FrisbYEE.jpa.dto;
import lombok.Data;
@Data
public class AdminDTO {
private Integer id;
private String id_keycloak;
private String name;
private String prenom;
}

View File

@@ -1,12 +1,19 @@
package hackathon.FrisbYEE.jpa.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
public class AthleteDTO implements java.io.Serializable {
private String nom;
private String niveau;
@Data
public class AthleteDTO {
private Integer id;
private String id_keycloak;
private String name;
private String prenom;
private String categorie;
private List<String> groupes;
private String niveau;
private List<String> groupes = new ArrayList<>();
private List<Integer> sessionIds = new ArrayList<>();
}

View File

@@ -0,0 +1,14 @@
package hackathon.FrisbYEE.jpa.dto;
import java.util.List;
import lombok.Data;
@Data
public class CoachDTO {
private Integer id;
private String id_keycloak;
private String name;
private String prenom;
private List<Integer> sessionIds;
}

View File

@@ -0,0 +1,21 @@
package hackathon.FrisbYEE.jpa.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class SessionDTO {
private Integer id;
private String name;
private Boolean isRecurrent;
private LocalDateTime creneau;
private Long duree;
private String groupe;
private Integer coachId;
private List<Integer> athleteIds;
private List<Integer> activiteIds;
}

View File

@@ -0,0 +1,14 @@
package hackathon.FrisbYEE.jpa.dto;
import hackathon.FrisbYEE.jpa.metier.Role;
import lombok.Data;
@Data
public class UserDTO {
private Integer id;
private String id_keycloak;
private String name;
private String prenom;
private String email;
private Role role;
}

View File

@@ -1,15 +0,0 @@
package hackathon.FrisbYEE.jpa;
public interface IAthlete {
public void run();
int getId();
void setId(int id);
String getNom();
void setNom(String nom);
void setCategorie(String categorie);
String getCategorie();
void setNiveau(String niveau);
String getNiveau();
}

View File

@@ -3,9 +3,8 @@ package hackathon.FrisbYEE.jpa.metier;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@@ -22,7 +21,7 @@ public class Activite implements Serializable {
@Id
@GeneratedValue
private Long id;
private Integer id;
private String name;
private String theme;
private Long duree;

View File

@@ -1,7 +1,5 @@
package hackathon.FrisbYEE.jpa.metier;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -13,18 +11,40 @@ import jakarta.persistence.Entity;
@Getter @Setter @NoArgsConstructor
@Access(AccessType.FIELD)
public class Admin {
public class Admin extends User{
@Id
@GeneratedValue
private Long id;
private String name;
public Admin(String name){
this.name = name;
public Admin(String id_keycloak, String name, String prenom){
super(name, id_keycloak, prenom, Role.admin );
}
@Override
public String toString() {
return "Admin [id=" + id + " , name=" + name + "]";
return "Admin [id=" + super.getId() + " , name=" + super.getName() + "]";
}
@Override
public void setName(String name) {
super.setName(name);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getPrenom() {
return super.getPrenom();
}
@Override
public void setPrenom(String prenom) {
super.setPrenom(prenom);
}
@Override
public Role getRole() {
return super.getRole();
}
}

View File

@@ -1,11 +1,10 @@
package hackathon.FrisbYEE.jpa.metier;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Access;
@@ -17,12 +16,8 @@ import jakarta.persistence.Entity;
@Getter @Setter @NoArgsConstructor
@Access(AccessType.FIELD)
public class Athlete {
public class Athlete extends User{
@Id
@GeneratedValue
private Long id;
private String name;
private String categorie;
private String niveau;
@ElementCollection
@@ -32,19 +27,37 @@ public class Athlete {
@ManyToMany(mappedBy = "athletes")
private List<Session> sessions = new ArrayList<>(); // plusieurs sessions sont possibles
public Athlete(String name){
this.name = name;
}
public Athlete(String name, String categorie, String niveau, List<String> groupe){
this.name = name;
this.categorie = categorie;
this.niveau = niveau;
this.groupe = groupe;
public Athlete(String name, String id_keycloak, String prenom){
super(name, id_keycloak, prenom, Role.athlete);
}
@Override
public String toString() {
return "Athlete [id=" + id + " , name=" + name + "]";
return "Athlete [id=" + super.getId() + " , name=" + super.getName() + "]";
}
@Override
public void setName(String name) {
super.setName(name);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getPrenom() {
return super.getPrenom();
}
@Override
public void setPrenom(String prenom) {
super.setPrenom(prenom);
}
@Override
public Role getRole() {
return super.getRole();
}
}

View File

@@ -1,7 +1,5 @@
package hackathon.FrisbYEE.jpa.metier;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -16,21 +14,43 @@ import jakarta.persistence.Entity;
@Getter @Setter @NoArgsConstructor
@Access(AccessType.FIELD)
public class Coach {
@Id
@GeneratedValue
private Long id;
private String name;
public class Coach extends User{
@OneToMany(mappedBy = "coach")
private List<Session> sessions = new ArrayList<>(); // Un coach peut avoir plusieurs sessions
public Coach(String name){
this.name = name;
public Coach(String name, String id_keycloak, String prenom){
super(name, id_keycloak, prenom, Role.coach );
}
@Override
public String toString() {
return "Coach [id=" + id + " , name=" + name + "]";
return "Coach [id=" + super.getId() + " , name=" + super.getName() + "]";
}
@Override
public void setName(String name) {
super.setName(name);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getPrenom() {
return super.getPrenom();
}
@Override
public void setPrenom(String prenom) {
super.setPrenom(prenom);
}
@Override
public Role getRole() {
return super.getRole();
}
}

View File

@@ -0,0 +1,7 @@
package hackathon.FrisbYEE.jpa.metier;
public enum Role {
admin,
coach,
athlete
}

View File

@@ -24,7 +24,7 @@ public class Session {
@Id
@GeneratedValue
private Long id;
private Integer id;
private String name;
private Boolean isRecurrent;
private LocalDateTime creneau;
@@ -56,4 +56,21 @@ public class Session {
public String toString() {
return "Session [id=" + id + " , name=" + name + "]";
}
public void setCoach(Coach coach) {
if (coach.getRole() != Role.coach) {
throw new IllegalArgumentException("L'utilisateur n'est pas un coach");
}
this.coach = coach;
}
public void setAthletes(List<Athlete> athletes) {
for (Athlete athlete : athletes) {
if (athlete.getRole() != Role.athlete) {
throw new IllegalArgumentException("L'utilisateur n'est pas un athlète");
}
}
this.athletes = athletes;
}
}

View File

@@ -0,0 +1,50 @@
package hackathon.FrisbYEE.jpa.metier;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Access(AccessType.FIELD)
@Table(name = "app_user")
public class User implements Serializable {
@Id
@GeneratedValue
@Column(unique = true, nullable = false)
private Integer id;
@Column(name = "id_keycloak", unique = true, nullable = false)
private String keycloakId;
private String name;
private String prenom;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
public User(String name, String id_keycloak, String prenom, Role role) {
this.name = name;
this.keycloakId = id_keycloak;
this.prenom = prenom;
this.role = role;
}
@Override
public String toString() {
return "User [id=" + id + " , name=" + name + "]";
}
}

View File

@@ -2,7 +2,12 @@ package hackathon.FrisbYEE.jpa.service;
import hackathon.FrisbYEE.jpa.metier.Activite;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ActiviteDAO extends JpaRepository<Activite, Integer> {
Activite findByKeycloakId(String keycloakId);
List<Activite> findByTheme(String theme);
}

View File

@@ -0,0 +1,13 @@
package hackathon.FrisbYEE.jpa.service;
import hackathon.FrisbYEE.jpa.metier.Admin;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminDAO extends JpaRepository<Admin, Integer> {
Optional<Admin> findByKeycloakId(String keycloakId);
}

View File

@@ -1,57 +1,12 @@
package hackathon.FrisbYEE.jpa.service;
import hackathon.FrisbYEE.jpa.metier.Activite;
import hackathon.FrisbYEE.jpa.metier.Athlete;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import jakarta.persistence.EntityManager;
import sample.simple.dao.generic.AbstractJpaDao;
import hackathon.FrisbYEE.jpa.interface.IAthlete;
import java.util.List;
import org.springframework.stereotype.Repository;
@Repository
public interface AthleteDAO extends JpaRepository<Athlete, Integer> {
private EntityManager em = EntityManagerHelper.getEntityManager();
public AthleteDAO() {
}
@Override
public void save(IAthlete entity) {
em.getTransaction().begin();
em.persist(entity);
em.getTransaction().commit();
}
@Override
public IAthlete update(IAthlete entity) {
em.getTransaction().begin();
IJoueur merged = em.merge(entity);
em.getTransaction().commit();
return merged;
}
@Override
public IAthlete findOne(Integer id) {
return em.find(IAthlete.class, id);
}
@Override
public List<IAthlete> findAll() {
return em.createQuery("from IAthlete", IAthlete.class).getResultList();
}
@Override
public void delete(IAthlete entity) {
em.getTransaction().begin();
em.remove(entity);
em.getTransaction().commit();
}
@Override
public void deleteById(Integer entityId) {
IAthlete entity = findOne(entityId);
delete(entity);
}
boolean existsByKeycloakId(String keycloakId);
Optional<Athlete> findByKeycloakId(String keycloakId);
}

View File

@@ -1,4 +1,14 @@
package hackathon.FrisbYEE.jpa.service;
public class CoachDAO {
import hackathon.FrisbYEE.jpa.metier.Coach;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CoachDAO extends JpaRepository<Coach, Integer> {
boolean existsByKeycloakId(String keycloakId);
Optional<Coach> findByKeycloakId(String keycloakId);
}

View File

@@ -0,0 +1,14 @@
package hackathon.FrisbYEE.jpa.service;
import hackathon.FrisbYEE.jpa.metier.Session;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SessionDAO extends JpaRepository<Session, Integer> {
List<Session> findByAthletes_Id(Integer athleteId);
}

View File

@@ -0,0 +1,13 @@
package hackathon.FrisbYEE.jpa.service;
import hackathon.FrisbYEE.jpa.metier.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDAO extends JpaRepository<User, Integer> {
Optional<User> findByKeycloakId(String keycloakId);
}

View File

@@ -1,11 +0,0 @@
package hackathon.FrisbYEE.jpa.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/activite")
public class ActiviteController {
}

View File

@@ -0,0 +1,188 @@
package hackathon.FrisbYEE.rest;
import hackathon.FrisbYEE.jpa.dto.ActiviteDTO;
import hackathon.FrisbYEE.jpa.metier.Activite;
import hackathon.FrisbYEE.jpa.metier.Session;
import hackathon.FrisbYEE.jpa.service.ActiviteDAO;
import hackathon.FrisbYEE.jpa.service.SessionDAO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@CrossOrigin(origins = "http://localhost:3000")
@Controller
@RequestMapping("/activite")
public class ActiviteResource {
@Autowired
private ActiviteDAO activiteDAO;
@Autowired
private SessionDAO sessionDAO;
/*
* POST /activite/create
* DELETE /activite/delete/{id}
*
*/
@Operation(summary = "Créer une activité")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Création effectuée",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ActiviteDTO.class)))
})
@PostMapping("/create")
@ResponseBody
@PreAuthorize("hasRole('coach')")
public ResponseEntity<ActiviteDTO> create(@RequestBody ActiviteDTO dto) {
System.out.println("ROLE TEST " + hackathon.FrisbYEE.jpa.metier.Role.coach);
Session session = sessionDAO.findById(dto.getSessionId()).get();
if(activiteDAO.existsById(dto.getId())){
return ResponseEntity.status(200).body(mapToDTO(activiteDAO.findById(dto.getId()).get()));
}
Activite activite = mapToEntity(dto);
activite.setSession(session);
activiteDAO.save(activite);
return ResponseEntity.status(201).body(mapToDTO(activite));
}
@Operation(summary = "Supprime l'activité ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Suppression effectuée",
content = @Content(mediaType = "application/json"))
})
@DeleteMapping("/delete/{id}")
@ResponseBody
@PreAuthorize("hasRole('coach')")
public ResponseEntity<String> delete(@PathVariable("id") int id) {
try {
Activite activite = activiteDAO.findById(id).get();
activiteDAO.delete(activite);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + ex.getMessage());
}
return ResponseEntity.ok("Activity deleted");
}
@Operation(summary = "Modifie l'activité ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Modification effectuée",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ActiviteDTO.class)))
})
@PostMapping("/update/{id}")
@ResponseBody
@PreAuthorize("hasRole('coach')")
public ResponseEntity<String> modifyById(@PathVariable("id") int id, @RequestBody ActiviteDTO dto) {
try {
Session session = sessionDAO.findById(dto.getSessionId()).get();
Activite activite = activiteDAO.findById(id).get();
activite.setName(dto.getName());
activite.setTheme(dto.getTheme());
activite.setDuree(dto.getDuree() != null ? dto.getDuree() : 0L);
activite.setDataActivite(dto.getDataActivite());
activite.setSession(session);
activiteDAO.save(activite);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + ex.getMessage());
}
return ResponseEntity.ok("Activity modified");
}
@Operation(summary = "Récupère l'activité ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ActiviteDTO.class)))
})
@GetMapping("/{id}")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
@ResponseBody
public ResponseEntity<ActiviteDTO> getActivityById(@PathVariable("id") int id) {
try {
Activite activite = activiteDAO.findById(id).get();
ActiviteDTO dto = mapToDTO(activite);
return ResponseEntity.ok(dto);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@Operation(summary = "Récupère toutes les activités")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ActiviteDTO.class)))
})
@GetMapping("/all")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
@ResponseBody
public ResponseEntity<List<ActiviteDTO>> getAllActivity() {
try {
List<Activite> activites = activiteDAO.findAll();
List<ActiviteDTO> dtos = activites.stream().map(this::mapToDTO).collect(Collectors.toList());
return ResponseEntity.ok(dtos);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
@Operation(summary = "Récupère les activités correspondant au thème donné")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ActiviteDTO.class)))
})
@GetMapping("/theme/{theme}")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
@ResponseBody
public ResponseEntity<List<ActiviteDTO>> getActivityByTheme(@PathVariable("theme") String theme) {
try {
List<Activite> activites = activiteDAO.findByTheme(theme);
List<ActiviteDTO> dtos = activites.stream().map(this::mapToDTO).collect(Collectors.toList());
return ResponseEntity.ok(dtos);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
private Activite mapToEntity(ActiviteDTO dto) {
Activite activite = new Activite();
activite.setName(dto.getName());
activite.setTheme(dto.getTheme());
activite.setDuree(dto.getDuree());
activite.setDataActivite(dto.getDataActivite());
return activite;
}
private ActiviteDTO mapToDTO(Activite activite) {
ActiviteDTO dto = new ActiviteDTO();
dto.setId(activite.getId());
dto.setName(activite.getName());
dto.setTheme(activite.getTheme());
dto.setDuree(activite.getDuree());
dto.setDataActivite(activite.getDataActivite());
dto.setSessionId(activite.getSession() != null ? activite.getSession().getId() : null);
return dto;
}
}

View File

@@ -1,5 +0,0 @@
package hackathon.FrisbYEE.rest;
public class ActiviteResources {
}

View File

@@ -0,0 +1,78 @@
package hackathon.FrisbYEE.rest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import hackathon.FrisbYEE.jpa.dto.AdminDTO;
import hackathon.FrisbYEE.jpa.metier.Admin;
import hackathon.FrisbYEE.jpa.service.AdminDAO;
import hackathon.FrisbYEE.jpa.service.UserDAO;
@RestController
@RequestMapping("/admin")
@CrossOrigin(origins = "http://localhost:3000")
public class AdminResource {
@Autowired
private AdminDAO adminDAO;
@Autowired
private UserDAO userDAO;
@PostMapping("/create")
@PreAuthorize("hasRole('admin')") // Only admin can create
public ResponseEntity<AdminDTO> create(@RequestBody AdminDTO dto) {
userDAO.findByKeycloakId(dto.getId_keycloak())
.ifPresent(existing -> {
if (!(existing instanceof Admin)) {
userDAO.delete(existing);
userDAO.flush();
}
});
Admin admin = mapToEntity(dto);
if(adminDAO.findByKeycloakId(admin.getKeycloakId()).isPresent()) {
return ResponseEntity.status(200).body(mapToDTO(adminDAO.findByKeycloakId(admin.getKeycloakId()).get()));
}
adminDAO.save(admin);
return ResponseEntity.status(201).body(mapToDTO(admin));
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<AdminDTO> getAdmin(@PathVariable Integer id) {
Admin admin = adminDAO.findById(id).get();
return ResponseEntity.ok(mapToDTO(admin));
}
@GetMapping("/keycloak/{keycloak_id}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<AdminDTO> getByKeycloakId(@PathVariable String keycloak_id) {
Admin admin = adminDAO.findByKeycloakId(keycloak_id).get();
return ResponseEntity.ok(mapToDTO(admin));
}
private AdminDTO mapToDTO(Admin admin) {
AdminDTO dto = new AdminDTO();
dto.setId(admin.getId());
dto.setId_keycloak(admin.getKeycloakId());
dto.setName(admin.getName());
dto.setPrenom(admin.getPrenom());
return dto;
}
private Admin mapToEntity(AdminDTO dto) {
Admin admin = new Admin();
admin.setKeycloakId(dto.getId_keycloak());
admin.setName(dto.getName());
admin.setPrenom(dto.getPrenom());
admin.setRole(hackathon.FrisbYEE.jpa.metier.Role.admin);
return admin;
}
}

View File

@@ -1,34 +1,355 @@
package hackathon.FrisbYEE.rest;
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDateTime;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import hackathon.FrisbYEE.jpa.dto.ActiviteDTO;
import hackathon.FrisbYEE.jpa.dto.AthleteDTO;
import hackathon.FrisbYEE.jpa.dto.SessionDTO;
import hackathon.FrisbYEE.jpa.metier.Activite;
import hackathon.FrisbYEE.jpa.metier.Athlete;
import hackathon.FrisbYEE.jpa.metier.Session;
import hackathon.FrisbYEE.jpa.service.AthleteDAO;
import hackathon.FrisbYEE.jpa.service.SessionDAO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import hackathon.FrisbYEE.jpa.dto.AthleteDTO;
import hackathon.FrisbYEE.jpa.service.AthleteDAO;
import hackathon.FrisbYEE.jpa.interface.IAthlete;
@RestController
@RequestMapping("/athlete")
@CrossOrigin(origins = "http://localhost:3000")
public class AthleteResource {
@Autowired
private AthleteDAO athleteDAO;
@Operation(summary = "Récupère tous les utilisateurs")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupère le Joueur ayant l'identifiant correspondant",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = AthleteDTO.class)))
})
@GetMapping("/joueur/{id}")
public AthleteDTO getJoueurById(@PathVariable Integer joueurId) {
@Autowired
private SessionDAO sessionDAO;
System.out.println("ID A CHERCHER" + joueurId);
IAthlete j = athleteDAO.findOne(joueurId);
AthleteDTO jDTO = new AthleteDTO();
System.out.println(j);
return jDTO;
@Operation(summary = "Crée un Athlète avec les informations fournies")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Renvoie l'athlète créé", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AthleteDTO.class)))
})
@PostMapping("/create")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<AthleteDTO> create(@RequestBody AthleteDTO dto) {
Athlete athlete = mapToEntity(dto);
if(athleteDAO.existsByKeycloakId(athlete.getKeycloakId())) {
Athlete existing = athleteDAO.findByKeycloakId(athlete.getKeycloakId()).orElse(null);
if (existing != null) {
return ResponseEntity.status(200).body(mapToDTO(existing));
}
return ResponseEntity.status(200).build();
}
athleteDAO.save(athlete);
return ResponseEntity.status(201).body(mapToDTO(athlete));
}
@Operation(summary = "Récupère tous les athlètes")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupère tous les athlètes", content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class)))
})
@GetMapping("/all")
@PreAuthorize("hasRole('admin') or hasRole('coach')")
public ResponseEntity<List<AthleteDTO>> all() {
List<Athlete> athletes = athleteDAO.findAll();
List<AthleteDTO> dtos = new ArrayList<>();
for (Athlete athlete : athletes) {
dtos.add(mapToDTO(athlete));
}
return ResponseEntity.ok(dtos);
}
@Operation(summary = "Récupère tous les athlètes d'un groupe")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupère tous les athlètes", content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class)))
})
@GetMapping("/all/group/{groupe}")
@PreAuthorize("hasRole('admin') or hasRole('coach')")
public ResponseEntity<List<AthleteDTO>> allByGroup(@PathVariable String groupe) {
List<Athlete> athletes = athleteDAO.findAll();
List<AthleteDTO> dtos = new ArrayList<>();
for (Athlete athlete : athletes) {
if(groupe.equals("None")){
if(athlete.getGroupe().size()==0){
dtos.add(mapToDTO(athlete));
}
}
else{
boolean containsGroupe = false;
for (String g : athlete.getGroupe()) {
containsGroupe = containsGroupe || g.equals(groupe);
}
if(containsGroupe){
dtos.add(mapToDTO(athlete));
}
}
}
return ResponseEntity.ok(dtos);
}
@Operation(summary = "Récupère l'athlète ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AthleteDTO.class)))
})
@GetMapping("/{id}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<AthleteDTO> getById(@PathVariable String id) {
Athlete athlete = athleteDAO.findByKeycloakId(id).orElse(null);
if (athlete == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok(mapToDTO(athlete));
}
@GetMapping("/keycloak/{keycloak_id}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<AthleteDTO> getByKeycloakId(@PathVariable String keycloak_id) {
Athlete athlete = athleteDAO.findByKeycloakId(keycloak_id).orElse(null);
if (athlete == null) return ResponseEntity.notFound().build();
return ResponseEntity.ok(mapToDTO(athlete));
}
@Operation(summary = "Met à jour l'athlète ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Mise à jour effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AthleteDTO.class)))
})
@PutMapping("/{id}")
@PreAuthorize("hasRole('admin') or #id == principal.id")
public ResponseEntity<AthleteDTO> update(@PathVariable Integer id, @RequestBody AthleteDTO dto) {
try {
Athlete athlete = athleteDAO.findById(id).orElse(null);
if (athlete == null) return ResponseEntity.notFound().build();
athlete.setName(dto.getName());
athlete.setCategorie(dto.getCategorie());
athlete.setNiveau(dto.getNiveau());
// Relationship: sessionId → session
if (dto.getSessionIds() != null) {
List<Session> sessions = new ArrayList<>();
for (Integer sessionId : dto.getSessionIds()) {
Session session = sessionDAO.findById(sessionId)
.orElseThrow(() -> new RuntimeException("Session not found"));
sessions.add(session);
}
athlete.setSessions(sessions);
}
athleteDAO.save(athlete);
return ResponseEntity.ok(mapToDTO(athlete));
} catch (Exception ex) {
return ResponseEntity.noContent().build();
}
}
@Operation(summary = "Supprime l'athlète ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Suppression effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = AthleteDTO.class)))
})
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('admin')")
public ResponseEntity<Void> delete(@PathVariable Integer id) {
if (!athleteDAO.existsById(id)) {
return ResponseEntity.notFound().build();
}
athleteDAO.deleteById(id);
return ResponseEntity.noContent().build();
}
private AthleteDTO mapToDTO(Athlete athlete) {
AthleteDTO dto = new AthleteDTO();
dto.setId(athlete.getId());
dto.setId_keycloak(athlete.getKeycloakId());
dto.setName(athlete.getName());
dto.setPrenom(athlete.getPrenom());
dto.setCategorie(athlete.getCategorie());
dto.setNiveau(athlete.getNiveau());
dto.setGroupes(athlete.getGroupe());
return dto;
}
private Athlete mapToEntity(AthleteDTO dto) {
Athlete athlete = new Athlete();
athlete.setName(dto.getName());
athlete.setPrenom(dto.getPrenom());
athlete.setKeycloakId(dto.getId_keycloak());
athlete.setCategorie(dto.getCategorie());
athlete.setNiveau(dto.getNiveau());
athlete.setRole(hackathon.FrisbYEE.jpa.metier.Role.athlete);
return athlete;
}
@Operation(summary = "Récupère les sessions correspondant à l'athlète donné")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SessionDTO.class)))
})
@GetMapping("/{athleteId}/session")
public List<SessionDTO> getSessionsAthlete(@PathVariable Integer athleteId) {
Athlete athlete = athleteDAO.findById(athleteId).orElse(null);
if (athlete == null) {
return new ArrayList<>();
}
List<Session> sessions = sessionDAO.findByAthletes_Id(athleteId);
List<SessionDTO> athleteSessions = new ArrayList<>();
for (Session s : sessions) {
if (s.getAthletes().contains(athlete)) {
SessionDTO dto = new SessionDTO();
dto.setId(s.getId());
dto.setName(s.getName());
dto.setCreneau(s.getCreneau());
List<Integer> activiteIDs = new ArrayList<>();
for (Activite activite : s.getActivites()) {
activiteIDs.add(activite.getId());
}
dto.setActiviteIds(activiteIDs);
dto.setCoachId(s.getCoach() != null ? s.getCoach().getId() : null);
dto.setDuree(s.getDuree());
dto.setGroupe(s.getGroupe());
dto.setIsRecurrent(s.getIsRecurrent());
List<Integer> athleteIds = new ArrayList<>();
for (Athlete athlete2 : s.getAthletes()) {
athleteIds.add(athlete2.getId());
}
dto.setAthleteIds(athleteIds);
// Map other fields as necessary
athleteSessions.add(dto);
}
}
System.out.println(athlete);
return athleteSessions;
}
@GetMapping("/{athleteId}/groupes")
public List<String> getGroupesByAthlete(@PathVariable Integer athleteId) {
java.util.Optional<Athlete> athleteOptional = athleteDAO.findById(athleteId);
List<String> groupes = new ArrayList<>();
if (athleteOptional.isPresent()) {
Athlete athlete = athleteOptional.get();
for (Session session : athlete.getSessions()) {
if (!groupes.contains(session.getGroupe())) {
groupes.add(session.getGroupe());
}
}
}
return groupes;
}
@Operation(summary = "Récupère toutes les sessions")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SessionDTO.class)))
})
@GetMapping("/athletes/session")
public List<SessionDTO> getAllSessions() {
List<Session> sessions = sessionDAO.findAll();
System.out.println(sessions);
List<SessionDTO> sessionDTOs = new ArrayList<>();
for (Session session : sessions) {
SessionDTO dto = new SessionDTO();
dto.setName(session.getName());
// Map other fields as necessary
sessionDTOs.add(dto);
}
return sessionDTOs;
}
@Operation(summary = "Récupère les activités correspondant à la session donnée")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ActiviteDTO.class)))
})
@GetMapping("/athletes/session/{id}/activities")
public List<ActiviteDTO> getActivitiesForSession(@PathVariable Integer id) {
// Récupérer la session par ID
Session session = sessionDAO.findById(id).orElse(null);
if (session != null) {
// Retourner les activités de la session
List<ActiviteDTO> activiteDTOs = new ArrayList<>();
for (Activite activite : session.getActivites()) {
ActiviteDTO dto = new ActiviteDTO();
dto.setId(activite.getId());
dto.setName(activite.getName());
// Map other fields as necessary
activiteDTOs.add(dto);
}
return activiteDTOs;
}
return new ArrayList<>();
}
@Operation(summary = "Récupère toutes les sessions après une date donnée")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SessionDTO.class)))
})
@GetMapping("/athletes/{id}/session/after/{date}")
public List<SessionDTO> getSessionsAfterDate(@PathVariable Integer id, @PathVariable String date) {
// Récupérer l'athlète par ID
Athlete athlete = athleteDAO.findById(id).orElse(null);
if (athlete != null) {
// Récupérer les sessions de l'athlète après la date donnée
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> filteredSessions = new ArrayList<>();
for (Session session : sessions) {
if (session.getAthletes().contains(athlete)
&& session.getCreneau().isAfter(ChronoLocalDateTime.from(LocalDate.parse(date)))) { // WTF
// toujours
// sympa les
// dates
SessionDTO dto = new SessionDTO();
dto.setName(session.getName());
// Map other fields as necessary
filteredSessions.add(dto);
}
}
return filteredSessions;
}
return new ArrayList<>();
}
@Operation(summary = "Récupère les sessions entre deux dates")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SessionDTO.class)))
})
@GetMapping("/athletes/{id}/session/between/{startDate}/{endDate}")
public List<SessionDTO> getSessionsBetweenDates(@PathVariable Integer id, @PathVariable String startDate,
@PathVariable String endDate) {
// Récupérer l'athlète par ID
Athlete athlete = athleteDAO.findById(id).orElse(null);
if (athlete != null) {
// Récupérer les sessions de l'athlète entre les deux dates données
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> filteredSessions = new ArrayList<>();
for (Session session : sessions) {
if (session.getAthletes().contains(athlete)
&& session.getCreneau().isAfter(ChronoLocalDateTime.from(LocalDate.parse(startDate)))
&& session.getCreneau().isBefore(ChronoLocalDateTime.from(LocalDate.parse(endDate)))) {
SessionDTO dto = new SessionDTO();
dto.setName(session.getName());
// Map other fields as necessary
filteredSessions.add(dto);
}
}
return filteredSessions;
}
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,158 @@
package hackathon.FrisbYEE.rest;
import hackathon.FrisbYEE.jpa.dto.CoachDTO;
import hackathon.FrisbYEE.jpa.dto.SessionDTO;
import hackathon.FrisbYEE.jpa.metier.Activite;
import hackathon.FrisbYEE.jpa.metier.Admin;
import hackathon.FrisbYEE.jpa.metier.Athlete;
import hackathon.FrisbYEE.jpa.metier.Coach;
import hackathon.FrisbYEE.jpa.metier.Session;
import hackathon.FrisbYEE.jpa.service.CoachDAO;
import hackathon.FrisbYEE.jpa.service.SessionDAO;
import hackathon.FrisbYEE.jpa.service.UserDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/coach")
public class CoachResource {
@Autowired
private CoachDAO coachDAO;
@Autowired
private UserDAO userDAO;
@Autowired
private SessionDAO sessionDAO;
@PostMapping("/create")
@PreAuthorize("hasRole('admin') or hasRole('coach')") // Only admin can create
public ResponseEntity<CoachDTO> create(@RequestBody CoachDTO dto) {
userDAO.findByKeycloakId(dto.getId_keycloak())
.ifPresent(existing -> {
if (!(existing instanceof Coach)) {
userDAO.delete(existing);
userDAO.flush();
}
});
Coach coach = mapToEntity(dto);
if(coachDAO.existsByKeycloakId(coach.getKeycloakId())) {
return ResponseEntity.status(200).body(mapToDTO(coachDAO.findByKeycloakId(coach.getKeycloakId()).get()));
}
coachDAO.save(coach);
return ResponseEntity.status(201).body(mapToDTO(coach));
}
@GetMapping("/all")
public List<CoachDTO> getAll() {
System.out.println("GET /coach/all called");
List<Coach> coaches = coachDAO.findAll();
List<CoachDTO> dtos = new ArrayList<>();
for (Coach coach : coaches) {
dtos.add(mapToDTO(coach));
}
return dtos;
}
@GetMapping("/keycloak/{keycloak_id}")
@PreAuthorize("hasRole('Admin') or hasRole('Coach')")
public CoachDTO getByKeycloakId(@PathVariable String keycloak_id) {
Coach coach = coachDAO.findByKeycloakId(keycloak_id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Coach not found"));
return mapToDTO(coach);
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('Admin') or hasRole('Coach')")
public CoachDTO getById(@PathVariable Integer id) {
Coach coach = coachDAO.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Coach not found"));
return mapToDTO(coach);
}
@PutMapping("/update/{id}")
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<CoachDTO> update(@PathVariable Integer id, @RequestBody CoachDTO dto) {
Coach coach = coachDAO.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Coach not found"));
if (dto.getName() != null)
coach.setName(dto.getName());
coachDAO.save(coach);
CoachDTO updatedDto = mapToDTO(coach);
return ResponseEntity.ok(updatedDto);
}
@GetMapping("/{id}/session")
@PreAuthorize("hasRole('Admin') or hasRole('Coach')")
public ResponseEntity<List<SessionDTO>> getSessionsForCoach(@PathVariable Integer id) {
Coach coach = coachDAO.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Coach not found"));
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> coachSessions = new ArrayList<>();
for (Session s : sessions) {
if (s.getCoach().equals(coach)) {
SessionDTO dto = new SessionDTO();
dto.setId(s.getId());
dto.setName(s.getName());
dto.setCreneau(s.getCreneau());
List<Integer> activiteIDs = new ArrayList<>();
for (Activite activite : s.getActivites()) {
activiteIDs.add(activite.getId());
}
dto.setActiviteIds(activiteIDs);
dto.setCoachId(s.getCoach().getId());
dto.setDuree(s.getDuree());
dto.setGroupe(s.getGroupe());
dto.setIsRecurrent(s.getIsRecurrent());
List<Integer> athleteIds = new ArrayList<>();
for (Athlete athlete : s.getAthletes()) {
athleteIds.add(athlete.getId());
}
dto.setAthleteIds(athleteIds);
// Map other fields as necessary
coachSessions.add(dto);
}
}
return ResponseEntity.ok(coachSessions);
}
@DeleteMapping("/delete/{id}")
@PreAuthorize("hasRole('Admin')")
public ResponseEntity<Void> delete(@PathVariable Integer id) {
Coach coach = coachDAO.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Coach not found"));
coachDAO.delete(coach);
return ResponseEntity.noContent().build();
}
private CoachDTO mapToDTO(Coach coach) {
CoachDTO dto = new CoachDTO();
dto.setId(coach.getId());
dto.setId_keycloak(coach.getKeycloakId());
dto.setName(coach.getName());
dto.setPrenom(coach.getPrenom());
return dto;
}
private Coach mapToEntity(CoachDTO dto) {
Coach coach = new Coach();
coach.setKeycloakId(dto.getId_keycloak());
coach.setName(dto.getName());
coach.setRole(hackathon.FrisbYEE.jpa.metier.Role.coach);
coach.setPrenom(dto.getPrenom());
return coach;
}
}

View File

@@ -0,0 +1,335 @@
package hackathon.FrisbYEE.rest;
import hackathon.FrisbYEE.jpa.dto.ActiviteDTO;
import hackathon.FrisbYEE.jpa.dto.CoachDTO;
import hackathon.FrisbYEE.jpa.dto.SessionDTO;
import hackathon.FrisbYEE.jpa.metier.Activite;
import hackathon.FrisbYEE.jpa.metier.Athlete;
import hackathon.FrisbYEE.jpa.metier.Coach;
import hackathon.FrisbYEE.jpa.metier.Session;
import hackathon.FrisbYEE.jpa.service.ActiviteDAO;
import hackathon.FrisbYEE.jpa.service.AthleteDAO;
import hackathon.FrisbYEE.jpa.service.CoachDAO;
import hackathon.FrisbYEE.jpa.service.SessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@CrossOrigin(origins = "http://localhost:3000")
@Controller
@RequestMapping("/session")
public class SessionResource {
@Autowired
private SessionDAO sessionDAO;
@Autowired
private CoachDAO coachDAO;
@Autowired
private AthleteDAO athleteDAO;
@Autowired
private ActiviteDAO activiteDAO;
@PostMapping("/create")
@ResponseBody
@PreAuthorize("hasRole('coach')")
public ResponseEntity<?> create(@RequestBody SessionDTO dto) {
System.out.println("=== SESSION DTO RECEIVED ===");
System.out.println(dto);
System.out.println("Coach ID: " + dto.getCoachId());
System.out.println("ID null");
try {
if (dto.getId() != null && sessionDAO.findById(dto.getId()).isPresent()) {
return ResponseEntity.status(HttpStatus.OK).body("Session with ID " + dto.getId() + " already exists.");
}
Session session = maptoEntity(dto);
Coach c = coachDAO.findById(dto.getCoachId()).orElse(null);
session.setCoach(c);
sessionDAO.save(session);
return ResponseEntity.status(HttpStatus.CREATED).body(maptoDTO(session));
} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}
}
@GetMapping("/all")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<List<SessionDTO>> getAll() {
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> dtos = new ArrayList<>();
for (Session session : sessions) {
dtos.add(maptoDTO(session));
}
return ResponseEntity.ok(dtos);
}
@GetMapping("/all/group/{groupe}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<List<SessionDTO>> getAllByGroup(@PathVariable String groupe) {
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> dtos = new ArrayList<>();
String groupeStr = groupe;
if(groupe.equals("None")) groupeStr = "";
for (Session session : sessions) {
if(session.getGroupe().equals(groupeStr)){
dtos.add(maptoDTO(session));
}
}
return ResponseEntity.ok(dtos);
}
@GetMapping("/all-between-dates")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<List<SessionDTO>> getAllBetweenDates(
@RequestParam LocalDate startDate,
@RequestParam LocalDate endDate
) {
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> dtos = new ArrayList<>();
System.out.println("date : " + startDate + " " + endDate);
for (Session session : sessions) {
LocalDate sessionDate = session.getCreneau().toLocalDate();
boolean isBetween =
(!sessionDate.isBefore(startDate) || session.getIsRecurrent()) &&
!sessionDate.isAfter(endDate);
if (isBetween) {
dtos.add(maptoDTO(session));
}
}
return ResponseEntity.ok(dtos);
}
@GetMapping("/all-between-dates/group/{groupe}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<List<SessionDTO>> getAllBetweenDates(@RequestParam LocalDate startDate,@RequestParam LocalDate endDate,@PathVariable String groupe) {
List<Session> sessions = sessionDAO.findAll();
List<SessionDTO> dtos = new ArrayList<>();
System.out.println("date : " + startDate + " " + endDate);
String groupeStr = groupe;
if(groupe.equals("None")) groupeStr = "";
for (Session session : sessions) {
if(session.getGroupe().equals(groupeStr)){
LocalDate sessionDate = session.getCreneau().toLocalDate();
boolean isBetween =
(!sessionDate.isBefore(startDate) || session.getIsRecurrent()) &&
!sessionDate.isAfter(endDate);
if (isBetween) {
dtos.add(maptoDTO(session));
}
}
}
return ResponseEntity.ok(dtos);
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
public ResponseEntity<?> getById(@PathVariable Integer id) {
try {
Session session = sessionDAO.findById(id).orElseThrow();
return ResponseEntity.ok(maptoDTO(session));
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
@DeleteMapping("/delete/{id}")
@ResponseBody
@PreAuthorize("hasRole('coach')")
public ResponseEntity<String> delete(@PathVariable("id") int id) {
try {
Session session = sessionDAO.findById(id).get();
sessionDAO.delete(session);
return ResponseEntity.ok("Session deleted successfully");
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
@GetMapping("{id}/groupe")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
public ResponseEntity<?> getGroupeById(@PathVariable Integer id) {
try {
Session session = sessionDAO.findById(id).orElseThrow();
return ResponseEntity.ok(session.getGroupe());
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
@PutMapping("/update/{id}")
@PreAuthorize("hasRole('coach')")
public ResponseEntity<Void> updateSession(@PathVariable Integer id, @RequestBody SessionDTO dto) {
Session session = sessionDAO.findById(id).orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Session not found with id " + id));
if (dto.getDuree() != null) {
session.setDuree(dto.getDuree());
}
if (dto.getAthleteIds() != null) {
List<Athlete> athletes = athleteDAO.findAllById(dto.getAthleteIds());
session.setAthletes(athletes);
}
if (dto.getActiviteIds() != null) {
List<Activite> activites = activiteDAO.findAllById(dto.getActiviteIds());
session.setActivites(activites);
}
sessionDAO.save(session);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}/subscribe/{userId}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<Void> subscribe(@PathVariable Integer id,@PathVariable Integer userId){
Session session = sessionDAO.findById(id).orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Session not found with id " + id));
Athlete athlete = athleteDAO.findById(userId).orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Athlete not found with id " + userId));
session.getAthletes().add(athlete);
sessionDAO.save(session);
return ResponseEntity.noContent().build();
}
@PutMapping("/{id}/unsubscribe/{userId}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<Void> unsubscribe(@PathVariable Integer id,@PathVariable Integer userId){
Session session = sessionDAO.findById(id).orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Session not found with id " + id));
Athlete athlete = athleteDAO.findById(userId).orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Athlete not found with id " + userId));
session.getAthletes().remove(athlete);
sessionDAO.save(session);
return ResponseEntity.noContent().build();
}
@GetMapping("/{id}/coach")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
public ResponseEntity<CoachDTO> getCoachBySessionId(@PathVariable Integer id) {
try {
Session session = sessionDAO.findById(id).orElseThrow();
Coach coach = session.getCoach();
CoachDTO dto = new CoachDTO();
dto.setId(coach.getId());
dto.setName(coach.getName());
dto.setPrenom(coach.getPrenom());
List<Integer> listSession = new ArrayList<Integer>();
for (Session s : coach.getSessions()) {
listSession.add(session.getId());
}
dto.setSessionIds(listSession);
return ResponseEntity.ok(dto);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new CoachDTO());
}
}
@GetMapping("/{id}/activities")
@PreAuthorize("hasRole('coach') or hasRole('athlete')")
public ResponseEntity<?> getActivitiesBySessionId(@PathVariable Integer id) {
try {
Session session = sessionDAO.findById(id).orElseThrow();
List<Activite> activites = session.getActivites();
List<ActiviteDTO> activiteDTOs = new ArrayList<>();
for (Activite activite : activites) {
ActiviteDTO dto = new ActiviteDTO();
dto.setId(activite.getId());
dto.setName(activite.getName());
activiteDTOs.add(dto);
}
return ResponseEntity.ok(activiteDTOs);
} catch (Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
@PostMapping("/{id_session}/activities/add/{id_act}")
@PreAuthorize("hasRole('coach') or hasRole('admin')")
public ResponseEntity<?> addActivity(@PathVariable Integer id_sess, @PathVariable Integer id_act) {
Session s = sessionDAO.findById(id_sess).get();
if (s.equals(null)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Session not found");
}
Activite a = activiteDAO.findById(id_act).get();
if (a.equals(null)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Activite not found");
}
List<Activite> l = s.getActivites();
l.add(a);
s.setActivites(l);
sessionDAO.save(s);
return ResponseEntity.status(200).body(maptoDTO(s));
}
private SessionDTO maptoDTO(Session s) {
SessionDTO dto = new SessionDTO();
dto.setId(s.getId());
dto.setName(s.getName());
dto.setIsRecurrent(s.getIsRecurrent());
dto.setCreneau(s.getCreneau());
dto.setDuree(s.getDuree());
dto.setGroupe(s.getGroupe());
// Coach
if (s.getCoach() != null) {
dto.setCoachId(s.getCoach().getId());
}
// Athletes
if (s.getAthletes() != null) {
List<Integer> athleteIds = new ArrayList<>();
for (Athlete athlete : s.getAthletes()) {
athleteIds.add(athlete.getId());
}
dto.setAthleteIds(athleteIds);
}
// Activites
if (s.getActivites() != null) {
List<Integer> activiteIds = new ArrayList<>();
for (Activite activite : s.getActivites()) {
activiteIds.add(activite.getId());
}
dto.setActiviteIds(activiteIds);
}
return dto;
}
private Session maptoEntity(SessionDTO dto) {
Session session = new Session();
System.out.println("ID " + session.getId());
session.setName(dto.getName());
session.setIsRecurrent(dto.getIsRecurrent());
session.setCreneau(dto.getCreneau());
session.setDuree(dto.getDuree());
session.setGroupe(dto.getGroupe());
return session;
}
}

View File

@@ -0,0 +1,79 @@
package hackathon.FrisbYEE.rest;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import hackathon.FrisbYEE.jpa.dto.AthleteDTO;
import hackathon.FrisbYEE.jpa.dto.UserDTO;
import hackathon.FrisbYEE.jpa.metier.Athlete;
import hackathon.FrisbYEE.jpa.metier.User;
import hackathon.FrisbYEE.jpa.service.UserDAO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@RestController
@RequestMapping("/users")
@CrossOrigin(origins = "http://localhost:3000")
public class UserResource {
@Autowired
private UserDAO userDAO;
@Operation(summary = "Récupère tous les utilisateurs")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupère tous les athlètes", content = @Content(mediaType = "application/json", schema = @Schema(implementation = List.class)))
})
@GetMapping("/all")
@PreAuthorize("hasRole('admin') or hasRole('coach')")
public ResponseEntity<List<UserDTO>> all() {
List<User> users = userDAO.findAll();
List<UserDTO> dtos = new ArrayList<>();
for (User user : users) {
dtos.add(mapToDTO(user));
}
return ResponseEntity.ok(dtos);
}
@Operation(summary = "Récupère l'utilisateur ayant l'identifiant correspondant")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Récupération effectuée", content = @Content(mediaType = "application/json", schema = @Schema(implementation = UserDTO.class)))
})
@GetMapping("/{id}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<UserDTO> getById(@PathVariable Integer id) {
User user = userDAO.findById(id).get();
return ResponseEntity.ok(mapToDTO(user));
}
@GetMapping("/keycloak/{keycloak_id}")
@PreAuthorize("hasRole('admin') or hasRole('coach') or hasRole('athlete')")
public ResponseEntity<UserDTO> getByKeycloakId(@PathVariable String keycloak_id) {
User user = userDAO.findByKeycloakId(keycloak_id).get();
return ResponseEntity.ok(mapToDTO(user));
}
private UserDTO mapToDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setId_keycloak(user.getKeycloakId());
dto.setName(user.getName());
dto.setPrenom(user.getPrenom());
dto.setRole(user.getRole());
return dto;
}
}

View File

@@ -1,11 +1,12 @@
spring.datasource.url=jdbc:postgresql://localhost:5432/frisbyee
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.username=frisbyee_user
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.url=jdbc:postgresql://localhost:5432/frisbyee
spring.datasource.username=frisbyee_user
spring.datasource.password=secret
server.port=8081
server.servlet.context-path=/api
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/Frisbyee_realm
spring.security.oauth2.resourceserver.jwt.jwk-set-uri: http://localhost:8080/realms/Frisbyee_realm/protocol/openid-connect/certs

View File

@@ -1,13 +1,27 @@
package hackathon.FrisbYEE;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.client.RestTestClient;
@SpringBootTest
class FrisbYeeApplicationTests {
import hackathon.FrisbYEE.rest.AthleteResource;
@Test
void contextLoads() {
class FrisbYEEApplicationTests {
//Controller
private AthleteResource athleteResource;
private RestTestClient mockMvc;
@BeforeEach
void setUp() {
athleteResource = new AthleteResource();
mockMvc = RestTestClient.bindToController(athleteResource).build();
}
@Test
void testGetUsers() throws Exception {
//mockMvc.perform(get("/api/users"))
// .andExpect(status().isOk());
}
}

19
backend.log Normal file
View File

@@ -0,0 +1,19 @@
nohup: ignoring input
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/mojo/maven-metadata.xml
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-metadata.xml
Progress (1): 3.4 kB
Progress (1): 7.3 kB
Progress (2): 7.3 kB | 4.6 kB
Progress (2): 13 kB | 4.6 kB
Progress (2): 13 kB | 9.9 kB
Progress (2): 19 kB | 9.9 kB
Progress (2): 20 kB | 9.9 kB
Progress (2): 20 kB | 14 kB
Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/mojo/maven-metadata.xml (20 kB at 8.9 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-metadata.xml (14 kB at 6.3 kB/s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.475 s

View File

@@ -1,14 +1,16 @@
services:
keycloak:
container_name: baeldung-keycloak.openid-provider
container_name: baeldung-keycloak
image: quay.io/keycloak/keycloak:26.4
command:
- start-dev
- --import-realm
- start-dev
- --import-realm
ports:
- 8080:8080
- "8080:8080"
volumes:
- ./keycloak/:/opt/keycloak/data/import/
- ./keycloak/:/opt/keycloak/data/import/
- keycloak_data:/opt/keycloak/data
- ./keycloak/themes:/opt/keycloak/themes
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
@@ -16,12 +18,8 @@ services:
KC_HOSTNAME_URL: http://localhost:8080
KC_HOSTNAME_ADMIN_URL: http://localhost:8080
KC_HOSTNAME_STRICT_BACKCHANNEL: true
KC_HTTP_RELATIVE_PATH: /
KC_HTTP_ENABLED: true
KC_HEALTH_ENABLED: true
KC_METRICS_ENABLED: true
extra_hosts:
- "host.docker.internal:host-gateway"
- "host.docker.internal:host-gateway"
healthcheck:
test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:8080/auth/health/live']
interval: 5s
@@ -41,6 +39,6 @@ services:
volumes:
postgres_data:
keycloak_data:
version: "3.9"

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-keycloak/web": "^3.4.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.1",
@@ -11,8 +12,13 @@
"@types/node": "^16.18.126",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"axios": "^1.13.2",
"bootstrap": "^5.3.8",
"keycloak-js": "^26.2.2",
"react": "18.2.0",
"react-bootstrap": "^2.10.10",
"react-dom": "18.2.0",
"react-router-dom": "^7.12.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,7 @@
{
"realm": "Frisbyee_realm",
"resource": "Frisbyee_client",
"clientId": "Frisbyee_client",
"auth-server-url": "http://localhost:8080",
"public-client": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,38 +1,653 @@
/* Variables de thème */
[data-theme='dark'] {
--tint0: #0a0e27;
--tint1: #1a1f3a;
--tint2: #232d4a;
--tint3: #2e3a59;
--tint4: #3d4a6f;
--tint5: #4a5a85;
--text: #f0f4f8;
--text2: #000000;
--disable: #02291d;
--green-primary: #10b981;
--green-secondary: #059669;
--green-dark: #047857;
--green-A-primary: #10b98130;
--green-A-secondary: #05966930;
--green-A-dark: #04785730;
--themeButtonColor: #00AAFF;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
--blue-accent: #3b82f6;
--purple-accent: #a78bfa;
--cyan-accent: #06b6d4;
}
[data-theme='light'] {
--tint0: #f8fafc;
--tint1: #f1f5f9;
--tint2: #e2e8f0;
--tint3: #cbd5e1;
--tint4: #b0bac4;
--tint5: #94a3b8;
--text: #0f172a;
--text2: #FFFFFF;
--disable: #02291d;
--green-primary: #10b981;
--green-secondary: #059669;
--green-dark: #047857;
--green-A-primary: #10b98125;
--green-A-secondary: #05966925;
--green-A-dark: #04785725;
--themeButtonColor: #f59e0b;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.15);
--blue-accent: #3b82f6;
--purple-accent: #a855f7;
--cyan-accent: #06b6d4;
}
/* Reset et base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,body{
width: 100%;
height: 100%;
}
body {
margin: 0;
font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, var(--tint0) 0%, var(--tint1) 100%);
height: 100%;
color: var(--text);
transition: background 0.4s ease, color 0.4s ease;
font-weight: 400;
line-height: 1.6;
background-attachment: fixed;
}
.padding {
padding: 10px
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
background-color: var(--tint2);
padding: 2px 6px;
border-radius: 4px;
color: var(--green-primary);
}
.App {
text-align: center;
display: grid;
padding: 20px;
gap: 24px;
max-width: 1400px;
margin: 0 auto;
}
.App-logo {
height: 40vmin;
pointer-events: none;
.App h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
background: linear-gradient(135deg, var(--green-primary) 0%, var(--cyan-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
/* Container principal */
.app-container {
min-height: 100vh;
background: linear-gradient(180deg, var(--tint0) 0%, var(--tint1) 100%);
}
.composant-padding {
padding: 10px;
}
.composant-container {
display: grid;
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
border-radius: 24px;
padding: 24px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
gap:15px;
}
/* Header / Navigation */
.app-header {
background-color: var(--tint1);
border-bottom: 2px solid var(--green-primary);
padding: 16px 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.app-nav {
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.app-nav a,
.nav-link {
color: var(--text);
text-decoration: none;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.app-nav a:hover,
.nav-link:hover {
background-color: var(--tint3);
border-color: var(--green-primary);
transform: translateY(-2px);
}
.app-nav a.active,
.nav-link.active {
background-color: var(--green-primary);
color: white;
}
.App-link {
color: #61dafb;
/* Cards et containers */
/* .card {
background: linear-gradient(135deg, var(--tint1) 0%, var(--tint2) 100%);
border-radius: 20px;
padding: 24px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
.card:hover {
border-color: var(--green-primary);
box-shadow: 0 8px 24px var(--green-A-primary);
transform: translateY(-4px);
}
.card-header {
border-bottom: 2px solid var(--green-primary);
padding-bottom: 12px;
margin-bottom: 16px;
color: var(--text);
font-weight: 600;
font-size: 1.2em;
}
.card-body {
color: var(--text);
} */
/* Inputs */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="search"],
input[type="datetime-local"],
textarea {
background-color: var(--tint2);
color: var(--text);
border: 1px solid var(--tint4);
border-radius: 12px;
padding: 12px 14px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 15px;
font-family: inherit;
}
input:focus,
textarea:focus {
outline: none;
border-color: var(--green-primary);
background-color: var(--tint1);
box-shadow: 0 0 0 3px var(--green-A-primary);
}
/* Conteneur de boutons inline */
.button-group,
.form-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
input::placeholder,
textarea::placeholder {
color: var(--tint5);
}
/* Select */
select {
background-color: var(--tint2);
color: var(--text);
border: 1px solid var(--tint4);
border-radius: 8px;
padding: 10px 14px;
cursor: pointer;
transition: all 0.3s ease;
}
select:focus {
outline: none;
border-color: var(--green-primary);
box-shadow: 0 0 0 3px var(--green-A-primary);
}
select option {
background-color: var(--tint2);
color: var(--text);
border-radius: 5px;
}
/* Buttons */
button {
color: var(--text);
background: linear-gradient(135deg, var(--tint3) 0%, var(--tint4) 100%);
border: 1px solid var(--tint4);
border-radius: 12px;
padding: 10px 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 600;
font-size: 14px;
display: inline-block;
width: auto;
box-shadow: var(--shadow-sm);
}
button:hover {
background: linear-gradient(135deg, var(--green-primary) 0%, var(--green-secondary) 100%);
border-color: var(--green-primary);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px var(--green-A-primary);
}
button:active {
transform: translateY(0);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--disable);
}
/* Bouton primaire */
.btn-primary,
button.primary {
background: linear-gradient(135deg, var(--green-primary), var(--green-secondary));
color: white;
border: none;
box-shadow: 0 4px 6px var(--green-A-primary);
padding: 5px 10px;
}
.btn-primary:hover,
button.primary:hover {
background: linear-gradient(135deg, var(--green-secondary), var(--green-dark));
box-shadow: 0 6px 12px var(--green-A-primary);
}
/* Bouton secondaire */
.btn-secondary,
button.secondary {
background-color: transparent;
color: var(--green-primary);
border: 2px solid var(--green-primary);
}
.btn-secondary:hover,
button.secondary:hover {
background-color: var(--green-primary);
color: white;
}
/* Bouton supprimer */
.deleteButton,
button.delete,
.btn-danger {
background: #dc2626;
border: 2px solid #991b1b;
border-radius: 10px;
color: white;
}
.deleteButton:hover,
button.delete:hover,
.btn-danger:hover {
background: #b91c1c;
border-color: #7f1d1d;
}
/* Bouton ajouter */
.addButton,
button.add,
.btn-success {
background: var(--green-primary);
border: none;
border-radius: 10px;
color: white;
}
.addButton:hover,
button.add:hover,
.btn-success:hover {
background-color: var(--green-secondary);
}
/* Modal */
.modalContent {
background-color: var(--tint2);
padding: 20px;
min-width: 300px;
min-height: 150px;
border-radius: 20px;
border: 2px solid var(--green-primary);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
position: relative;
max-height:100%;
overflow-y: auto;
}
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
height:100vh;
width:100vw;
}
/* .modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: grid;
place-items: center;
z-index: 100;
}
.modal-header {
border-bottom: 2px solid var(--green-primary);
padding-bottom: 12px;
margin-bottom: 16px;
font-weight: 600;
font-size: 1.3em;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background-color: var(--tint4);
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 18px;
display: grid;
place-items: center;
}
.modal-close:hover {
background-color: #dc2626;
color: white;
} */
/* Loading */
.loading {
width: 40px;
height: 40px;
border: 3px solid var(--tint5);
border-top-color: var(--green-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.top_left_loading {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
padding: 10px;
}
.center_loading {
position: absolute;
inset: 0;
display: grid;
place-items: center;
pointer-events: none;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
border-radius: 12px;
overflow: hidden;
width: fit-content;
}
thead {
background-color: var(--tint3);
border-bottom: 2px solid var(--green-primary);
}
th {
padding: 12px;
text-align: left;
font-weight: 600;
color: var(--text);
}
td {
padding: 12px;
border-bottom: 1px solid var(--tint4);
color: var(--text);
}
/* Lists */
ul,
ol {
color: var(--text);
padding-left: 20px;
}
li {
margin: 8px 0;
}
/* Links */
a {
color: var(--green-primary);
text-decoration: none;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
}
a:hover {
color: var(--green-secondary);
border-bottom-color: var(--green-primary);
}
/* Badges et tags */
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
background-color: var(--tint3);
color: var(--text);
border: 1px solid var(--tint5);
}
.badge-success {
background-color: rgba(16, 185, 129, 0.2);
color: var(--green-primary);
border-color: var(--green-primary);
}
.badge-danger {
background-color: rgba(220, 38, 38, 0.2);
color: #ef4444;
border-color: #dc2626;
}
.badge-warning {
background-color: rgba(245, 158, 11, 0.2);
color: #f59e0b;
border-color: #d97706;
}
/* Alerts */
.alert {
padding: 12px 16px;
border-radius: 10px;
margin: 12px 0;
border-left: 4px solid;
}
.alert-success {
background-color: rgba(16, 185, 129, 0.15);
border-left-color: var(--green-primary);
color: #6ee7b7;
}
.alert-error {
background-color: rgba(220, 38, 38, 0.15);
border-left-color: #ef4444;
color: #fca5a5;
}
.alert-warning {
background-color: rgba(245, 158, 11, 0.15);
border-left-color: #f59e0b;
color: #fcd34d;
}
.alert-info {
background-color: rgba(59, 130, 246, 0.15);
border-left-color: #3b82f6;
color: #93c5fd;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--tint2);
}
::-webkit-scrollbar-thumb {
background: var(--tint5);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--green-primary);
}
/* Checkbox et radio */
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--green-primary);
width: 18px;
height: 18px;
cursor: pointer;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease;
}
.slide-up {
animation: slideUp 0.4s ease;
}
/* Responsive */
@media (max-width: 768px) {
.card {
padding: 16px;
border-radius: 15px;
}
button {
padding: 8px 16px;
font-size: 14px;
}
.modal {
min-width: 280px;
margin: 10px;
}
}

View File

@@ -1,25 +1,43 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import { ReactKeycloakProvider } from '@react-keycloak/web'
import keycloak from './keycloak'
import Login from './components/login';
import { LocalDataProvider } from './provider/LocalDataProvider';
import EDT from './components/edt';
import SwitchThemeColor from './components/SwitchThemeColor';
import CreateSession from './components/createSession'
import EdtCoach from './components/edt_coach'
import { Coach } from "./classes";
import RessourcePanel from './components/ressourcePanel';
import TopBar from './components/topBar';
import { Routes, Route } from 'react-router-dom'
import Home from './components/pages/pageHome';
import SelectionSession from './components/pages/pageSectionSession';
import Gestion from './components/pages/pageGestion';
import Admin from './components/pages/pageAdmin';
const keycloakInitOptions = {
onLoad: 'login-required',
checkLoginIframe: false
}
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<ReactKeycloakProvider authClient={keycloak} /*initOptions={keycloakInitOptions}*/>
<LocalDataProvider>
<div className="App">
<TopBar/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/sessions" element={<SelectionSession/>}/>
<Route path="/gestion" element={<Gestion/>}/>
<Route path="/admin" element={<Admin/>}/>
</Routes>
</div>
</LocalDataProvider>
</ReactKeycloakProvider>
);
}

115
front_end/src/api.ts Normal file
View File

@@ -0,0 +1,115 @@
import axios from "axios";
import keycloak from "./keycloak";
import { get } from "http";
import { ActiviteDTO, AdminDTO, AthleteDTO, CoachDTO, SessionDTO } from "./classesDTO";
const api = axios.create({
baseURL: "http://localhost:8081/api",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
// Simple interceptor to ensure headers object exists; actual token should be set via setAuthToken()
api.interceptors.request.use((config) => {
if (keycloak?.token) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${keycloak.token}`;
}
return config;
});
// Helpers to set/clear the Authorization header programmatically (call after Keycloak login)
export function setAuthToken(token: string | null | undefined) {
if (token) {
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else {
delete api.defaults.headers.common["Authorization"];
}
}
export function clearAuthToken() {
delete api.defaults.headers.common["Authorization"];
}
export const athleteService = {
// controller is mounted at /athlete
create: (data: any) => api.post<AthleteDTO>("/athlete/create", data),
getAll: () => api.get<AthleteDTO[]>("/athlete/all"),
getAllByGroup: (group:String) => api.get<AthleteDTO[]>(`/athlete/all/group/${group}`),
getById: (id: number | string) => api.get(`/athlete/${id}`),
getByKeycloakId: (keycloakId: string) => api.get(`/athlete/keycloak/${encodeURIComponent(keycloakId)}`),
update: (id: number | string, data: any) => api.put(`/athlete/${id}`, data),
delete: (id: number | string) => api.delete(`/athlete/${id}`),
// session-related endpoints exposed by AthleteResource
getSessionsForAthlete: (athleteId: number | null) => api.get<SessionDTO[]>(`/athlete/${athleteId}/session`),
getAllSessions: () => api.get(`/athletes/session`),
getActivitiesForSession: (sessionId: number | string) => api.get(`/athletes/session/${sessionId}/activities`),
getSessionsAfterDate: (athleteId: number | string, date: string) => api.get(`/athletes/${athleteId}/session/after/${encodeURIComponent(date)}`),
getSessionsBetweenDates: (athleteId: number | string, startDate: string, endDate: string) => api.get(`/athletes/${athleteId}/session/between/${encodeURIComponent(startDate)}/${encodeURIComponent(endDate)}`),
addActivity: (id_sess: number, id_act: number) => api.get(`/${id_sess}/activities/add/${id_act}`),
getGroupes: (athleteId: number | string) => api.get(`/athlete/${athleteId}/groupes`)
};
export const activiteService = {
create: (data: any) => api.post("/activite/create", data),
delete: (id: number | string) => api.delete(`/activite/delete/${id}`),
update: (id: number | string, data: ActiviteDTO) => api.post(`/activite/update/${id}`, data),
getById: (id: number | string) => api.get(`/activite/${id}`),
getAll: () => api.get<ActiviteDTO[]>(`/activite/all`),
getByTheme: (theme: string) => api.get(`/activite/theme/${encodeURIComponent(theme)}`),
getDataActivite: (id: number | string) => api.get(`/activite/${id}`),
};
type DateBetween = {
startDate: string;
endDate: string;
}
export const sessionService = {
// controller uses singular /session/* endpoints
create: (data: SessionDTO) => api.post(`/session/create`, data),
getAll: () => api.get<SessionDTO[]>(`/session/all`),
getAllByGroup: (group: String) => api.get<SessionDTO[]>(`/session/all/group/${group}`),
getAllBetweenDate: (data: any) => api.get<SessionDTO[]>(`/session/all-between-dates`,{params: data,}),
getAllBetweenDateByGroup: (group:String,data: any) => api.get<SessionDTO[]>(`/session/all-between-dates/group/${group}`,{params: data,}),
getById: (id: number | null) => api.get(`/session/${id}`),
delete: (id: number | null) => api.delete(`/session/delete/${id}`),
update: (id: number | null, data: any) => api.put(`/session/update/${id}`, data),
getActivities: (sessionId: number | null) => api.get<ActiviteDTO[]>(`/session/${sessionId}/activities`),
addActivity: (sessionId: number | null, activityId: number) => api.post(`/session/${sessionId}/activities/${activityId}`),
getGroupe: (sessionId: number | null) => api.get(`/session/${sessionId}/groupe`),
getCoach: (sessionId: number | null) => api.get<CoachDTO>(`/session/${sessionId}/coach`),
subscribe: (sessionId: number | null, userId: number) => api.put(`/session/${sessionId}/subscribe/${userId}`),
unsubscribe: (sessionId: number | null, userId: number) => api.put(`/session/${sessionId}/unsubscribe/${userId}`),
};
export const coachService = {
// controller doesn't declare a class-level path consistently; support both common patterns
create: (data: CoachDTO) => api.post<CoachDTO>(`/coach/create`, data),
getAll: () => api.get<CoachDTO[]>(`/coach/all`),
getById: (id: number) => api.get(`/coach/${id}`),
getByKeycloakId: (keycloakId: string) => api.get(`/coach/keycloak/${keycloakId}`),
update: (id: number | string, data: any) => api.put(`/coach/update/${id}`, data),
delete: (id: number | string) => api.delete(`/coach/delete/${id}`),
getSessionsForCoach: (coachId: number | null) => api.get<SessionDTO[]>(`/coach/${coachId}/session`),
};
export const userService = {
getById: (id: number) => api.get(`/users/${id}`),
getByKeycloakId: (keycloak_id: string) => api.get(`/users/keycloak/${keycloak_id}`),
getAll: () => api.get(`/users/all`),
sync: () => api.post(`/users/sync`),
};
export const adminService = {
getByKeycloakId: (keycloak_id: string) => api.get(`/admin/keycloak/${keycloak_id}`),
getById: (id: number | string) => api.get<AdminDTO>(`/admin/${id}`),
create: (data: AdminDTO) => api.post<AdminDTO>("/admin/create", data),
};
export default api;

333
front_end/src/classes.tsx Normal file
View File

@@ -0,0 +1,333 @@
import { ActiviteDTO, AdminDTO, AthleteDTO, CoachDTO, SessionDTO } from "./classesDTO";
export type Groupe = "Entrainement" | "Competition" | "Loisir"| "";
export class Ligne{
id: number|null = null;
nom!: string;
composition!: Athlete[] //les joueurs compososant la ligne
tempsDeJeu!: number; // en minutes
}
export class User{
id: number|null = null;
keycloakId!: string;
nom!: string;
prenom!:string;
email!: string;
}
export class Admin extends User{
constructor(dto:AdminDTO);
constructor();
constructor(dto?:AdminDTO){
super();
this.id = dto?.id ?? null;
this.keycloakId = dto?.id_keycloak ?? "";
this.nom = dto?.name ?? "";
this.prenom = dto?.prenom ?? "";
this.email = ""; //TODO
}
toDTO():AdminDTO{
const dto:AdminDTO = {
id: this.id,
id_keycloak: this.keycloakId,
name: this.nom,
prenom: this.prenom,
};
return dto;
}
}
export class Athlete extends User{
groupes: Groupe[] = [];
sessionsID!: number[];
sessions: Session[] = [];
constructor(dto:AthleteDTO);
constructor();
constructor(dto?:AthleteDTO){
super();
this.id = dto?.id ?? null;
this.keycloakId = dto?.id_keycloak ?? "";
this.nom = dto?.name ?? "";
this.prenom = dto?.prenom ?? "" ;
this.email = "";
this.groupes = dto?.groupes ?? [];
this.sessionsID = dto?.sessionIds ?? [];
this.sessions = [];
}
toDTO():AthleteDTO{
const dto:AthleteDTO = {
id: this.id,
id_keycloak: this.keycloakId,
name: this.nom,
prenom: this.prenom,
categorie: "",
niveau: "",
groupes: this.groupes,
sessionIds: this.sessionsID,
};
return dto;
}
}
export class Coach extends User{
sessionsID!: number[];
sessions: Session[] = [];
constructor(dto:CoachDTO);
constructor();
constructor(dto?:CoachDTO){
super();
this.id = dto?.id ?? null;
this.keycloakId = dto?.id_keycloak ?? "";
this.nom = dto?.name ?? "";
this.prenom = dto?.prenom ?? "";
this.email = ""; //TODO
this.sessionsID = dto?.sessionIds ?? [];
this.sessions = [];
}
toDTO():CoachDTO{
const dto:CoachDTO = {
id: this.id,
id_keycloak: this.keycloakId,
name: this.nom,
prenom: this.prenom,
sessionIds: this.sessionsID ,
};
return dto;
}
}
export class Session{
id: number|null = null;
name!: string;
activitesID: number[] = [];
activites: Activite[] = [];
isRecurrent! : boolean;
creneau!: Date;
coach!: Coach;
athletesID!: number[]
athletes!: Athlete[]
duree! : number;
groupe! : Groupe;
ligne! : Ligne[];
constructor(dto:SessionDTO);
constructor();
constructor(dto?:SessionDTO){
this.id = dto?.id?? 0;
this.name = dto?.name?? "";
this.activitesID = dto?.activiteIds?? [];
this.activites = [];
this.isRecurrent = dto?.isRecurrent?? false;
this.creneau = dto? new Date(dto.creneau) : new Date();
//this.coach = new Coach(); //dto.coachId; //TODO
this.athletesID = dto?.athleteIds ?? [];
this.athletes = [];
this.duree = dto?.duree ?? 0;
this.groupe = dto?.groupe ?? "";
this.ligne = []; //TODO
}
toDTO():SessionDTO{
const dto:SessionDTO = {
id: this.id,
name: this.name,
isRecurrent: this.isRecurrent,
creneau: this.creneau.toISOString(),
duree: this.duree,
groupe: this.groupe,
coachId: this.coach?.id ?? null,
athleteIds: [],
activiteIds: []
};
return dto;
}
}
export class Activite{
id: number|null = null;
nom!: string;
session!: Session;
theme!: string;
data!: Map<string,string>;
duree!: number;
constructor(dto:ActiviteDTO);
constructor();
constructor(dto?:ActiviteDTO){
this.id = dto?.id ?? 0;
this.nom = dto?.name ?? "";
//this.session = dto.sessionId; //TODO
this.theme = dto?.theme ?? "";
//this.data = //TODO
this.duree = dto?.duree ?? 0;
}
toDTO():ActiviteDTO{
const dto:ActiviteDTO = {
id: this.id,
name: this.nom,
duree: this.duree,
dataActivite: [],
sessionId: this.session.id,
theme: this.theme
};
return dto;
}
}
/*
export function getUserTest():User{
const user = new Athlete();
const s1 = new Session();
const s2 = new Session();
const s3 = new Session();
const athlete1 = new Athlete();
athlete1.id = 1;
athlete1.nom = "Alice Dupont";
athlete1.email = "alice@nootnoot.yee";
athlete1.groupes = ["Entrainement"];
const athlete2 = new Athlete();
athlete2.id = 2;
athlete2.nom = "Bob Martin";
athlete2.groupes = ["Competition"];
const athlete3 = new Athlete();
athlete3.id = 3;
athlete3.nom = "Clara Lopez";
athlete3.groupes = ["Loisir"];
const ligne1 = new Ligne();
ligne1.id = 1;
ligne1.nom = "Ligne A";
ligne1.composition = [athlete1, athlete2]; // Alice + Bob
ligne1.tempsDeJeu = 45;
const ligne2 = new Ligne();
ligne2.id = 2;
ligne2.nom = "Ligne B";
ligne2.composition = [athlete2, athlete3]; // Bob + Clara
ligne2.tempsDeJeu = 40;
const ligne3 = new Ligne();
ligne3.id = 3;
ligne3.nom = "Ligne C";
ligne3.composition = [athlete1, athlete3]; // Alice + Clara
ligne3.tempsDeJeu = 50;
user.id = 0;
user.nom = "Emilien-Yee NootNoot";
user.email = "emilien@nootnoot.yee";
s1.creneau = new Date();
s1.id = 1;
s1.name = "Entrainement Frisbee"
s1.isRecurrent = true;
s1.ligne = [ligne1];
s1.duree= 90;
s1.athletes = [athlete1,athlete2];
var date2 = new Date();
date2.setDate(date2.getDate() + 2);
s2.creneau = date2;
s2.id = 2;
s2.isRecurrent = false;
s2.name = "entraintement 2"
s2.ligne = [ligne2];
s2.duree= 120;
s2.athletes = [athlete1,athlete2, athlete3];
s3.creneau = date2;
s3.id = 3;
s3.isRecurrent = false;
s3.name = "entraintement 3"
s3.ligne = [ligne3, ligne1];
s3.duree= 120;
s3.athletes = [athlete2, athlete3];
const act1 = new Activite();
act1.id = 1;
act1.nom = "Échauffement";
act1.theme = "Cardio";
act1.duree = 15;
act1.session = s1;
act1.data = new Map([["objectif", "Préparer le corps"], ["matériel", "Ballon"]]);
const act2 = new Activite();
act2.id = 2;
act2.nom = "Dribbles et passes";
act2.theme = "Technique";
act2.duree = 30;
act2.session = s1;
act2.data = new Map([["objectif", "Améliorer les passes"], ["niveau", "Intermédiaire"]]);
const act3 = new Activite();
act3.id = 3;
act3.nom = "Renforcement musculaire";
act3.theme = "Force";
act3.duree = 25;
act3.session = s2;
act3.data = new Map([["objectif", "Renforcer les jambes"], ["matériel", "Haltères"]]);
const act4 = new Activite();
act4.id = 4;
act4.nom = "Sprint et agilité";
act4.theme = "Vitesse";
act4.duree = 20;
act4.session = s2;
act4.data = new Map([["objectif", "Améliorer les sprints"], ["matériel", "Plots"]]);
const act5 = new Activite();
act5.id = 5;
act5.nom = "Match 5v5";
act5.theme = "Jeu";
act5.duree = 60;
act5.session = s3;
act5.data = new Map([["objectif", "Appliquer les techniques"], ["niveau", "Avancé"]]);
const act6 = new Activite();
act6.id = 6;
act6.nom = "Étirements";
act6.theme = "Récupération";
act6.duree = 10;
act6.session = s3;
act6.data = new Map([["objectif", "Éviter les blessures"], ["matériel", "Tapis"]]);
// attach the concrete activities to their sessions
s1.activites.push(act1);
s1.activites.push(act2);
s2.activites.push(act3);
s2.activites.push(act4);
s2.activites.push(act5);
s2.activites.push(act6);
user.sessions.push(s1);
user.sessions.push(s2);
user.sessions.push(s3);
return user;
}
*/

View File

@@ -0,0 +1,50 @@
import { Groupe } from "./classes";
export type ActiviteDTO = {
id: number|null;
name: string;
theme: string;
duree: number;
dataActivite: string[];
sessionId: number|null;
}
export type AdminDTO = {
id: number|null;
id_keycloak: string;
name: string;
prenom: string;
}
export type AthleteDTO = {
id:number|null;
id_keycloak: string;
name: string;
prenom: string;
categorie: string;
niveau: string;
groupes: Groupe[];
sessionIds: number[];
};
export type CoachDTO = {
id: number|null;
id_keycloak: string;
name: string;
prenom: string;
sessionIds: number[];
};
export type SessionDTO = {
id: number|null;
name: string;
isRecurrent: boolean;
creneau: string; // LocalDateTime → ISO string
duree: number;
groupe: Groupe;
coachId: number|null;
athleteIds: number[];
activiteIds: number[];
};

View File

@@ -0,0 +1,22 @@
import {useEffect } from "react"
type ModalProps = {
isOpen: boolean
onClose: () => void
children: React.ReactNode
}
export function Modal({ isOpen, onClose, children }: ModalProps) {
if (!isOpen) return null
return (
<div className="modal" onClick={onClose}>
<div className="modalContent" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,130 @@
import React, { useEffect, useState } from "react";
import { Athlete, Session } from "../classes";
import { calculStatsAthlete, niveauAlerte, StatsAthlete } from "../utils/athleteUtils"
import { getSessionsByAthleteId, getSessionsOfUserAPI } from "../requetes";
interface AthleteStatsProps {
athlete: Athlete;
}
function StatAthlete({ athlete }: AthleteStatsProps) {
console.log("Athlete:", athlete);
console.log("Sessions:", athlete.sessions);
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [dateDebut, setDateDebut] = useState(new Date());
const [dateFin, setDateFin] = useState(new Date());
const [seuilCritique, setSeuilCritique] = useState(0);
const [seuilMax, setSeuilMax] = useState(0);
const [stats, setStats] = useState<StatsAthlete | null>(null);
const dateToDatetimeLocal = (date: Date) => {
const pad = (n: number) => n.toString().padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
useEffect(() => {
console.log("useEffect déclenché, athlete.id =", athlete.id);
async function loadSessions() {
if (!athlete.id) {
console.error("Athlete ID is null");
setLoading(false);
return;
}
console.log("Début du chargement des sessions");
setLoading(true);
try {
const sessionsData = await getSessionsByAthleteId(athlete.id);
console.log("Sessions reçues de l'API:", sessionsData);
setSessions(sessionsData);
console.log("Sessions chargées:", sessionsData);
console.log("Première session:", sessionsData[0]);
console.log("Athletes de la première session:", sessionsData[0]?.athletes);
} catch (error) {
console.error("Erreur lors du chargement des sessions:", error);
setSessions([]);
} finally {
setLoading(false);
console.log("Fin du chargement");
}
}
loadSessions();
}, [athlete.id]);
const handleCalculerStats = () => {
console.log("Sessions au moment du calcul:", sessions);
console.log("Nombre de sessions:", sessions.length);
let safeDebut = dateDebut;
let safeFin = dateFin;
if (sessions.length > 0) {
const times = sessions
.map(s => s.creneau?.getTime())
.filter(t => typeof t === 'number') as number[];
if (times.length > 0) {
const min = Math.min(...times);
const max = Math.max(...times);
// if user range is zero-length or inverted, default to sessions span
if (safeDebut.getTime() === safeFin.getTime() || safeDebut.getTime() > safeFin.getTime()) {
safeDebut = new Date(min);
safeFin = new Date(max);
}
}
}
const statsCalculees = calculStatsAthlete(sessions, athlete, safeDebut, safeFin);
setStats(statsCalculees);
};
return (
<div className="creneau-stats">
<div>Nb Session : {sessions.length};</div>
<table>
<tr>
<th>Début :</th>
<th><input type="datetime-local" value={dateToDatetimeLocal(dateDebut)} onChange={e => setDateDebut(new Date(e.target.value))}/></th>
</tr>
<tr>
<th>Fin :</th>
<th><input type="datetime-local" value={dateToDatetimeLocal(dateFin)} onChange={e => setDateFin(new Date(e.target.value))}/></th>
</tr>
<tr>
<th>Seuil critique :</th>
<th><input type="number" value={seuilCritique} min={1} onChange={e => setSeuilCritique(Number(e.target.value))}/></th>
</tr>
<tr>
<th>Seuil max :</th>
<th><input type="number" value={seuilMax} min={1} onChange={e => setSeuilMax(Number(e.target.value))}/></th>
</tr>
</table>
<button onClick={handleCalculerStats}>Calculer les statistiques</button>
{stats && (
<div className="stats-display">
<h3>Statistiques de {athlete.prenom} {athlete.nom}</h3>
<p><strong>Nombre total de sessions :</strong> {stats.nbSessions}</p>
<p><strong>Sessions par semaine :</strong> {stats.nbSessionsPerWeek.toFixed(2)}</p>
<p><strong>Statut :</strong> {niveauAlerte(stats, seuilCritique, seuilMax)}</p>
{stats.distributions.size > 0 && (
<>
<h4>Distribution des activités :</h4>
<ul>
{Array.from(stats.distributions.entries()).map(([nomActivite, count]) => (
<li key={String(nomActivite)}>
{nomActivite} : {count} session(s)
</li>
))}
</ul>
</>
)}
</div>
)}
</div>
);
}
export default StatAthlete;

View File

@@ -0,0 +1,37 @@
import { useState,useEffect } from 'react';
const SwitchThemeColor = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
/*
Inverse le thème actuel
*/
function switchTheme(){
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
//const newTheme = window.matchMedia("(prefers-color-scheme: dark)").matches? "dark": "light";
setTheme(newTheme)
document.documentElement.setAttribute('data-theme', newTheme);
}
/*
détecte automatiquement le thème du navigateur au démarrage
*/
useEffect(() => {
const newTheme = window.matchMedia("(prefers-color-scheme: dark)").matches? "dark": "light";
setTheme(newTheme)
document.documentElement.setAttribute('data-theme', newTheme);
}, []);
return (
<div>
<button className="ButtonTheme" onClick={switchTheme}>
{theme === 'light' ? '𖤓' : '☾'}
</button>
</div>
);
};
export default SwitchThemeColor;

View File

@@ -0,0 +1,77 @@
import { useState, useEffect } from "react";
import { Session, User, Coach, Activite, Groupe } from "../classes";
import { Modal } from "./Modal";
type CreateActiciteProps = {
returnActivite: (activite:Activite|null) => void
session: Session;
}
export function CreateActivite({ returnActivite,session }: CreateActiciteProps){
const [activities, setActivities] = useState<Activite[]>([]);
const [activiteNom, setActiviteNom] = useState("");
const [activiteTheme, setActiviteTheme] = useState("");
const [activiteDuree, setActiviteDuree] = useState(0);
function addAcitivte(){
if (!activiteNom) return;
const newActivite = new Activite();
newActivite.nom= activiteNom;
newActivite.theme=activiteTheme;
newActivite.duree= activiteDuree;
newActivite.data= new Map<string,string>();
setActivities([...activities, newActivite]);
setActiviteNom("");
setActiviteTheme("");
setActiviteDuree(0);
returnActivite(newActivite);
}
function cancel(){
returnActivite(null);
}
return (
<Modal isOpen={true} onClose={() => cancel()}>
<div className="create_activite_modal">
<h2>Nouvelle Activité :</h2>
<div>Session : {session.name}</div>
<div>
Nom de l'activité:
</div>
<div>
<input type="text" value={activiteNom} onChange={e => setActiviteNom(e.target.value)} />
</div>
<div>
Theme:
</div>
<div>
<input type="text" value={activiteTheme} onChange={e => setActiviteTheme(e.target.value)} />
</div>
<div>
Duree (minutes):
</div>
<div>
<input type="number" value={activiteDuree} onChange={e => setActiviteDuree(Number(e.target.value))} />
</div>
<button type="button" onClick={()=>addAcitivte()}>Ajouter l'activite</button>
<button type="button" onClick={()=>cancel()}>Annuler</button>
<ul>
{activities.map((act, idx) => (
<li key={idx}>{act.nom} - {act.theme} ({act.duree} min)</li>
))}
</ul>
</div>
</Modal>
);
};
export default CreateActivite;

View File

@@ -0,0 +1,137 @@
import { useState, useEffect } from "react";
import { Session, User, Coach, Activite, Groupe } from "../classes";
import { useLocalData } from "../context/useLocalData";
import { activiteService, sessionService } from "../api";
import { createSessionAPI, postSession } from "../requetes";
import './style/createSession.css';
export const CreateSession = () => {
const {userLocal: user} = useLocalData();
const [session,setSession] = useState<Session>(new Session());
const [activities, setActivities] = useState<Activite[]>([]);
const [name,setName] = useState("");
const [groupe, setGroupe] = useState<Groupe>("");
const [creneau, setCreneau] = useState<Date>(new Date());
const [duree, setDuree] = useState<number>(0);
const [activiteNom, setActiviteNom] = useState("");
const [activiteTheme, setActiviteTheme] = useState("");
const [activiteDuree, setActiviteDuree] = useState(0);
const [isRecurent, setIsRecurent] = useState(false);
async function addAcitivte(){
if (!activiteNom) return;
const newActivite = new Activite();
newActivite.nom= activiteNom;
newActivite.theme=activiteTheme;
newActivite.duree= activiteDuree;
newActivite.data= new Map<string,string>();
setActivities([...activities, newActivite]);
session.activites.push(newActivite);
}
async function handleCreateSession() {
if(user instanceof Coach){
session.name = name;
session.groupe = groupe;
session.creneau = creneau;
session.duree = duree;
session.isRecurrent = isRecurent;
session.activites = activities;
session.coach = user;
const newSession = await createSessionAPI(session);
if(newSession!==null){
console.log("Session créée");
}
else console.error("Erreur lors de la création de la session");
// reset
setName("");
setGroupe("");
setCreneau(new Date());
setDuree(0);
setIsRecurent(false);
setActivities([]);
setSession(new Session());
}
}
return (
<div className="composant-container">
<h2>Créer une session</h2>
<table>
<tr>
<th>Nom:</th>
<th><input type="text" value={name} onChange={e => setName(e.target.value)} /></th>
</tr>
<tr>
<th>Groupe:</th>
<th>
<select
onChange={(e) => {
const v = (e.target as HTMLSelectElement).value;
setGroupe(v as Groupe)
console.log(v);
}}>
<option value=""> Sans groupe</option>
<option value="Loisir">Loisir</option>
<option value="Entrainement">Entrainement</option>
<option value="Competition">Competition</option>
</select>
</th>
</tr>
<tr>
<th>Creneau:</th>
<th><input type="datetime-local" value={creneau.toISOString().slice(0, 16)} onChange={e => setCreneau(new Date(e.target.value))} /></th>
</tr>
<tr>
<th>Duree (minutes):</th>
<th><input type="number" value={duree} onChange={e => setDuree(Number(e.target.value))} /></th>
</tr>
<tr>
<th>Recurrent:</th>
<th><input type="checkbox" checked={isRecurent} onChange={e => setIsRecurent(e.target.checked)} /></th>
</tr>
</table>
<div className="createActivite">
<h3>Ajouter une activité : </h3>
<table>
<tr>
<th>Nom de l'activitée:</th>
<th><input type="text" value={activiteNom} onChange={e => setActiviteNom(e.target.value)} /></th>
</tr>
<tr>
<th>Theme:</th>
<th><input type="text" value={activiteTheme} onChange={e => setActiviteTheme(e.target.value)} /></th>
</tr>
<tr>
<th>Duree (minutes):</th>
<th><input type="number" value={activiteDuree} onChange={e => setActiviteDuree(Number(e.target.value))} /></th>
</tr>
</table>
<button type="button" onClick={addAcitivte}>Ajouter</button>
</div>
<ul>
{activities.map((act, idx) => (
<li key={idx}>{act.nom} - {act.theme} ({act.duree} min)</li>
))}
</ul>
<button type="button" onClick={handleCreateSession}>Create Session</button>
</div>
);
};
export default CreateSession;

View File

@@ -0,0 +1,192 @@
import { useEffect, useState } from "react"
import { Admin, Athlete, Coach, Session} from "../classes"
import { useLocalData } from "../context/useLocalData"
import './style/edt.css';
import {getAllSessionsAPI, getAllSessionsBetweenAPI, getSessionsOfUserAPI } from "../requetes";
import EdtSession from "./edt_session";
import {delay} from "../requetes";
import Loading from "./loading";
export function dateToString(date:Date){
const dd_prefix = date.getDate()<10 ? "0" : "";
const mm_prefix = date.getMonth()+1<10 ? "0" : "";
const dd:String = dd_prefix+date.getDate().toString();
const mm:String = mm_prefix+(date.getMonth()+1).toString();
const yyyy:String = date.getFullYear().toString();
return dd+"/"+mm+"/"+yyyy;
}
export function hoursToString(date:Date){
const hh_prefix = date.getHours()<10 ? "0" : "";
const mm_prefix = date.getMinutes()+1<10 ? "0" : "";
const hh:String = hh_prefix+date.getHours().toString();
const mm:String = mm_prefix+date.getMinutes().toString();
return hh+"h"+mm;
}
export const EDT =() =>{
const {userLocal} = useLocalData()
const {sessionsLocal,setSessionsLocal} = useLocalData()
const [sessions, setSessions] = useState<Session[]>([])
const [week,setWeek] = useState<Date>(getFirstDay(new Date()));
const [loadedWeek,setLoadedWeek] = useState<Date|null>(null);
const [loading,setLoading] = useState<boolean>(false);
const week_days:String[] = ["Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dimanche"];
const week_days_nums:number[] = [1,2,3,4,5,6,0];
function loadSessions(date:Date){
var maxDate = toDateOnly(getNextDay(date,6));
var newWeek: Session[] = []
if(userLocal instanceof Athlete || userLocal instanceof Coach){
userLocal.sessions.forEach(session => {
const creneau = toDateOnly(session.creneau);
if((creneau >= date || session.isRecurrent) && (creneau <= maxDate)){
newWeek.push(session);
}
});
}
else if(userLocal instanceof Admin){
sessionsLocal.forEach(session => {
const creneau = toDateOnly(session.creneau);
if((creneau >= date || session.isRecurrent) && (creneau <= maxDate)){
newWeek.push(session);
}
});
}
newWeek.sort((a, b) =>a.creneau.getHours() * 60 + a.creneau.getMinutes() -(b.creneau.getHours() * 60 + b.creneau.getMinutes()));
setSessions(newWeek);
}
function changeWeek(date:Date){
setWeek(toDateOnly(date));
}
function isSameDay(date1:Date,date2:Date){
return (
date1.getDay()===date2.getDay() &&
date1.getMonth()===date2.getMonth() &&
date1.getFullYear()===date2.getFullYear());
}
useEffect(() => {
setLoadedWeek(null);
updateWeek(week);
loadSessions(week)
setLoading(true);
},[week,userLocal])
useEffect(() => {
if(loadedWeek!==null){
if(isSameDay(week,loadedWeek)){
loadSessions(week)
setLoading(false);
}
else{
setLoadedWeek(null);
}
}
},[loadedWeek])
async function updateWeek(week:Date){
//TODO updateSession
//await delay(2000);
//await updateSessionsOfUser(user,null,null);
if(userLocal instanceof Athlete || userLocal instanceof Coach){
const newSessions:Session[] = await getSessionsOfUserAPI(userLocal);
userLocal.sessions = newSessions;
}
else if(userLocal instanceof Admin){
const newSessions:Session[] = await getAllSessionsBetweenAPI(week,getNextDay(week,6));
const date = toDateOnly(week);
var maxDate = toDateOnly(getNextDay(date,6));
sessionsLocal.forEach(sessionLocal => { //update seulement la semaine
const creneau = toDateOnly(sessionLocal.creneau);
if(!((creneau >= date || sessionLocal.isRecurrent) && (creneau <= maxDate))){
newSessions.push(sessionLocal);
}
});
setSessionsLocal(newSessions);
}
setLoadedWeek(week);
}
function handlePrev(): void {
changeWeek(getNextDay(week,-7));
}
function handleNext(): void {
changeWeek(getNextDay(week,7));
}
function getFirstDay(date:Date):Date{
const numWeek = date.getDay();
var firstDate:Date;
if(numWeek == 0){
firstDate = getNextDay(date,-6);
}
else{
firstDate = getNextDay(date,-numWeek+1);
}
return toDateOnly(firstDate);
}
function toDateOnly(date: Date): Date {
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
}
function getNextDay(date:Date,nb:number):Date{
var newDate = new Date(date);
newDate.setDate(newDate.getDate() + nb);
return newDate;
}
function sameDay(d1:Date,d2:Date):boolean{
return (
d1.getDate() === d2.getDate() &&
d1.getMonth() === d2.getMonth() &&
d1.getFullYear() === d2.getFullYear()
);
}
async function refresh() {
loadSessions(week)
}
return(
<div className="composant-container">
<div className="edt_header">
<button className="edt_button_week_select" onClick={() =>handlePrev()}>Prev</button>
<button onClick={()=>refresh()}>Actualiser</button>
<button className="edt_button_week_select" onClick={() => handleNext()}>Next</button>
</div>
<div className="edt_colonnes">
<div className="top_left_loading">{loading && <Loading/>}</div>
{week_days_nums.map((num,index)=>(
<div className={`edt_colonne ${sameDay(getNextDay(week, index), new Date()) ? "today" : ""}`}>
<div className={`edt_day_header ${sameDay(getNextDay(week, index), new Date()) ? "today" : ""}`}>
<div> {week_days[index]} </div>
<div className="edt_date"> {dateToString(getNextDay(week,index))} </div>
</div>
<div className="edt_day_content">
{sessions.map((session,index2)=>(
session.creneau.getDay()===num &&
<EdtSession session={session}/>
))}
</div>
</div>
))}
</div>
</div>
)
}
export default EDT

View File

@@ -0,0 +1,50 @@
import { useState } from "react";
import { coachService } from "../api";
export default function EdtCoach() {
const [name, setName] = useState("");
const [error, setError] = useState("");
const [statusCode, setStatusCode] = useState<number | null>(null);
const handleCreate = async () => {
try {
const response = await coachService.create({ name });
console.log("Success:", response.status, response.data);
alert(`Coach created! Status: ${response.status}`);
setError("");
setStatusCode(response.status);
} catch (err: any) {
if (err.response) {
// This is the HTTP response from the server
console.error("HTTP status:", err.response.status);
console.error("Response data:", err.response.data);
setError(`Failed to create coach: ${err.response.data}`);
setStatusCode(err.response.status);
} else if (err.request) {
console.error("No response received", err.request);
setError("No response from server!");
setStatusCode(null);
} else {
console.error("Error", err.message);
setError(err.message);
setStatusCode(null);
}
}
};
return (
<div>
<h2>Create Coach</h2>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Coach name"
/>
<button onClick={handleCreate}>Create</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{statusCode && <p>HTTP Status: {statusCode}</p>}
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { useEffect, useState } from 'react';
import { Activite, Session } from '../classes';
import { dateToString, hoursToString } from './edt';
import './style/edt.css';
import { Modal } from './Modal';
import Loading from './loading';
import {delay} from "../requetes";
import CreateActivite from './createActivite';
import DetailSession from './object/detailSession';
type Props = {
session:Session;
}
function EdtSession({session}:Props){
const [open, setOpen] = useState<boolean>(false);
function handleOpen(): void {
setOpen(!open);
}
const sDate = session.creneau;
return(
<div>
<div className="edt_session" onClick={() => handleOpen()}>
<div className="edt_session_header">
<div className="edt_date">{hoursToString(sDate)}</div>
{session.isRecurrent && <div className="edt_date"> recurrent</div>}
</div>
<div>{session.name}</div>
</div>
<DetailSession session={session} open={open} setOpen={setOpen}/>
</div>
)
}
export default EdtSession

View File

@@ -0,0 +1,10 @@
function Loading(){
return(
<div>
<img className="loading" src="/loading.png"></img>
</div>
)
}
export default Loading

View File

@@ -0,0 +1,138 @@
import { useKeycloak } from '@react-keycloak/web'
import { useEffect, useRef, useState } from 'react';
import { Admin, Athlete, Coach, User } from '../classes';
import { useLocalData } from '../context/useLocalData';
import { loginOrRegister, postAthlete } from '../requetes';
import { clearAuthToken, setAuthToken } from '../api';
import { AthleteDTO } from '../classesDTO';
import { Modal } from './Modal';
import './style/topBar.css';
export const Login =() =>{
const ref = useRef<HTMLDivElement | null>(null);
const {userLocal: user,setUserLocal: setUser} = useLocalData()
const { keycloak } = useKeycloak();
const [open,setOpen] = useState<boolean>(false);
const [openGroupe,setOpenGroupe] = useState<boolean>(false);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
setOpenGroupe(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
async function loginUser(){
if(keycloak.authenticated){
setAuthToken(keycloak.token);
const logedUser = await loginOrRegister(keycloak);
console.log(logedUser);
if(logedUser!==null){
setUser(logedUser);
}
else{
alert("Erreur de connexion " + keycloak.tokenParsed?.sub + " " + keycloak.tokenParsed?.realm_access?.roles);
}
}
}
function openGestionGroupe(): void {
setOpenGroupe(true);
}
useEffect(() => {
loginUser()
},[keycloak.authenticated])
async function handleLogin() {
await keycloak.login();
}
function handleLogout(): void {
keycloak.logout()
setUser(new User());
clearAuthToken();
setOpen(false);
setOpenGroupe(false);
}
function handleOpen(): void {
setOpen(!open);
}
if(keycloak.authenticated){
return(
<div ref={ref} className='loginContainer'>
<button className="loginButton" onClick={()=>handleOpen()}>{user.prenom}</button>
{open &&
<div className='login'>
<div>
<div>
Prenom : { user.prenom}
</div>
<div>
Nom : { user.nom}
</div>
{/* <div>Keycloak ID : { keycloak.tokenParsed?.sub}</div> */}
{user instanceof Athlete && <div>Role : Athlete</div>}
{user instanceof Coach && <div>Role : Coach</div>}
{user instanceof Admin && <div>Role : Admin</div>}
{user instanceof Athlete &&
<div>
<div>Groupes : </div>
{(user.groupes.length > 0) ?
<div>
{user.groupes.map((groupe) => (
groupe
))}
</div>
:
<div>aucun groupe</div>}
<button onClick={() => openGestionGroupe()}>gérer les groupes</button>
{/*user instanceof Athlete &&
<div>
<div>
<button>{user.groupes.includes("Loisir")?"Quitter":"Rejoindre"} groupe Loisir</button>
<button>{user.groupes.includes("Entrainement")?"Quitter":"Rejoindre"}Rejoindre groupe Entrainement</button>
<button>{user.groupes.includes("Competition")?"Quitter":"Rejoindre"}Rejoindre groupe Compétition</button>
</div>
</div>
*/}
</div>
}
</div>
<button onClick={() => handleLogout()}>
Se déconnecter
</button>
</div>
}
</div>
)
}
else{
return(
<div>
<button onClick={() => handleLogin()}>
Se connecter
</button>
</div>
)
}
}
export default Login

View File

@@ -0,0 +1,34 @@
import { useState } from 'react';
import { Activite } from '../../classes';
import '../style/objectList.css';
type Props = {
activite: Activite
}
function ObjectActivite({activite}: Props) {
const [open, setOpen] = useState<boolean>(false);
function handleOpen(): void {
setOpen(!open);
}
return (
<div>
<div className="object" onClick={() => handleOpen()}>
<div>{activite.nom}</div>
<div>{activite.theme ? activite.theme : "Pas de thème défini pour l'activité"}</div>
<div>{activite.duree} min</div>
</div>
{open && activite.data && activite.data.size > 0 && (
<div className="object_details">
{Array.from(activite.data.entries()).map(([key, value]) => (
<div key={key}><strong>{key}:</strong> {value}</div>
))}
</div>
)}
</div>
)
}
export default ObjectActivite

View File

@@ -0,0 +1,138 @@
import { useEffect, useState } from "react";
import { Activite, Admin, Athlete, Coach, Session } from "../../classes";
import { dateToString, hoursToString } from "../edt";
import { Modal } from "../Modal";
import CreateActivite from "../createActivite";
import Loading from "../loading";
import { addActiviteToSession, createActivityAPI, delay, deletActiviteFromSession, getCoachOfSession, getSessionOfActivite, subscribeSessionAPI, unsubscribeSessionAPI } from "../../requetes";
import { useLocalData } from "../../context/useLocalData";
type Props = {
session:Session;
open:boolean;
setOpen:(b:boolean)=>void
}
function DetailSession({session,open,setOpen}:Props){
const {userLocal: user,setUserLocal: setUser} = useLocalData()
const [activites,setActivites] = useState<Activite[]>([]);
const [open2, setOpen2] = useState<boolean>(false);
const [loading,setLoading] = useState<boolean>(false);
const [join,setJoin] = useState<boolean>(user instanceof Athlete && user.sessions.includes(session));
//Vérification pour l'ajout et la suppression des activités
const canEdit = user instanceof Admin || user instanceof Coach;
const sDate = session.creneau;
function handleDeleteActivite(activite:Activite): void {
session.activites.splice(session.activites.indexOf(activite), 1);
deletActiviteFromSession(activite);
setActivites([...session.activites])
}
function handleAddActivite(): void {
setOpen2(true)
}
async function updateActivites(){
const newActivites = await getSessionOfActivite(session);
if(newActivites!=null){
session.activites=newActivites;
}
setLoading(false);
}
useEffect(() => {
if(open){
setLoading(true);
updateActivites()
}
},[open])
async function subscribeSession(){
if(user instanceof Athlete){
await subscribeSessionAPI(user,session);
user.sessions.push(session);
setJoin(true);
}
}
async function unsubscribeSession(){
if(user instanceof Athlete){
await unsubscribeSessionAPI(user,session);
user.sessions = user.sessions.filter(s => s !== session);
setJoin(false);
}
}
async function returnActivite(activite: Activite|null){
if(activite!==null){
activite.session = session;
const newActivite = await createActivityAPI(activite);
await addActiviteToSession(session,newActivite);
if(newActivite!=null){
session.activites.push(newActivite);
setActivites([...session.activites])
}
}
setOpen2(false);
}
useEffect(() => {
if(!loading){
setActivites([...session.activites])
}
},[loading])
if(!open2){
return(
<Modal isOpen={open} onClose={() => setOpen(false)}>
<div>
<h2>{session.name}</h2>
<div>{hoursToString(sDate)}</div>
<div>{dateToString(sDate)}</div>
<div>encadré par :</div>
<div>{session.coach?.prenom} {session.coach?.nom}</div>
{user instanceof Athlete &&
<div>
{user.sessions.includes(session) ? <button onClick={()=>unsubscribeSession()}>quitter</button>
:<button onClick={()=>subscribeSession()}>rejoindre</button>}
</div>
}
<div>
Activités :
<div className="session_modal_activite_list">
{activites.map((activite,index)=>(
<div className="activiteList">
- {activite.nom}
{canEdit && (
<button className="deleteButton" onClick={() => handleDeleteActivite(activite)}>x</button>
)}
</div>
))}
{canEdit && (
<button className="addButton" onClick={() => handleAddActivite()}>+</button>
)}
{loading && <div className='top_left_loading'><Loading/></div>}
</div>
</div>
</div>
</Modal>
)
}else{
return(
<CreateActivite returnActivite={(activite) => returnActivite(activite)} session={session}/>
)
}
}
export default DetailSession;

View File

@@ -0,0 +1,23 @@
import { useState } from 'react';
import { Activite, Ligne } from '../../classes';
import '../style/objectList.css';
type Props = {
ligne: Ligne
}
function ObjectLigne({ligne}: Props) {
const [open, setOpen] = useState<boolean>(false);
function handleOpen(): void {
setOpen(!open);
}
return (
<div>
{/* TODO */}
</div>
)
}
export default ObjectLigne

View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from 'react';
import { Activite, Session } from '../../classes';
import { dateToString, hoursToString } from '../edt';
import '../style/objectList.css';
import { Modal } from '../Modal';
import Loading from '../loading';
import {delay} from "../../requetes";
import CreateActivite from '../createActivite';
import DetailSession from './detailSession';
type Props = {
session:Session;
}
function ObjectSession({session}:Props){
const [open, setOpen] = useState<boolean>(false);
function handleOpen(): void {
setOpen(!open);
}
const sDate = session.creneau;
return(
<div>
<div className="object" onClick={() => handleOpen()}>
<div className="object_header">
{session.isRecurrent ?
<div className="object_small"> Recurrent</div> :
<div className="object_small"> {dateToString(session.creneau)}</div>
}
<div className="object_small">{hoursToString(sDate)}</div>
</div>
<div>{session.name}</div>
<div>{session.groupe}</div>
<div>{session.coach ? session.coach.nom : "Pas de coach sur la séance"}</div>
{session.ligne ? session.ligne.map(ligne => ligne.nom).join(", ") : "Pas de ligne sur la séance"}
</div>
<DetailSession session={session} open={open} setOpen={setOpen}/>
</div>
)
}
export default ObjectSession

View File

@@ -0,0 +1,133 @@
import { useEffect, useState } from 'react';
import { Activite, Admin, Athlete, Coach, Session, User } from '../../classes';
import { dateToString, hoursToString } from '../edt';
import '../style/objectList.css';
import { Modal } from '../Modal';
import Loading from '../loading';
import {delay, getSessionsOfUserAPI} from "../../requetes";
import CreateActivite from '../createActivite';
import { useLocalData } from '../../context/useLocalData';
import ObjectSession from './session';
import StatAthlete from '../StatsAthlete';
type Props = {
user:User;
}
function ObjectUser({user}:Props){
//const {user,setUser} = useLocalData());
const [open, setOpen] = useState<boolean>(false);
const [open2, setOpen2] = useState<boolean>(false);
const [loading,setLoading] = useState<boolean>(false);
// Initialisation sécurisée des sessions
function getInitialSessions(): Session[] {
if (user instanceof Athlete || user instanceof Coach) return [...(user.sessions || [])];
return [];
}
const [sessions, setSessions] = useState<Session[]>(getInitialSessions());
function handleOpen(): void {
setOpen(!open);
}
function handleDeleteSession(session:Session): void {
if(user instanceof Coach || user instanceof Athlete){
user.sessions.splice(user.sessions.indexOf(session), 1);
setSessions([...user.sessions])
}
}
function handleAddSession(): void {
if(user instanceof Athlete){
setOpen2(true)
}
}
async function updateSession(){
if(user instanceof Athlete || user instanceof Coach){
user.sessions = await getSessionsOfUserAPI(user);
setLoading(false);
}
}
useEffect(() => {
if(open){
setLoading(true);
updateSession()
}
},[open])
useEffect(() => {
if(!loading){
if(user instanceof Athlete){
setSessions([...user.sessions])
}
if(user instanceof Coach){
setSessions([...user.sessions])
}
}
},[loading])
function returnSession(session: Session|null){
if(session!==null){
if(user instanceof Athlete){
user.sessions.push(session);
setSessions([...user.sessions])
}
if(user instanceof Coach){
user.sessions.push(session);
setSessions([...user.sessions])
}
}
setOpen2(false);
}
return(
<div>
<div className="object" onClick={() => handleOpen()}>
<div>{user.prenom} {user.nom}</div>
{/* <div>{user2.role}</div> */}
</div>
{open &&
<Modal isOpen={open} onClose={() => setOpen(false)}>
<div className="object_modal">
<div>{user.prenom}</div>
<div>{user.nom}</div>
{user instanceof Athlete && <div>Role : Athlete</div>}
{user instanceof Coach && <div>Role : Coach</div>}
{user instanceof Admin && <div>Role : Admin</div>}
{(user instanceof Athlete || user instanceof Coach) &&
<div className='padding'>
<div className='list_object_modal'>
<div>Sessions :</div>
{user.sessions.map((session,index)=>(
<ObjectSession session={session}></ObjectSession>
))}
</div>
</div>
}
{user instanceof Athlete && (
<div className="stats-container">
<StatAthlete athlete={user}/>
</div>
)}
</div>
</Modal>
}
</div>
)
}
export default ObjectUser

View File

@@ -0,0 +1,13 @@
import CreateSession from "../createSession"
import TopBar from "../topBar"
function Admin() {
return (
<div>
<h1>Admin</h1>
rien pour l'instant
</div>
)
}
export default Admin

View File

@@ -0,0 +1,13 @@
import CreateSession from "../createSession"
import TopBar from "../topBar"
function Gestion() {
return (
<div>
<h1>Gestion</h1>
<CreateSession/>
</div>
)
}
export default Gestion

View File

@@ -0,0 +1,13 @@
import EDT from "../edt"
import TopBar from "../topBar"
function Home() {
return (
<div>
<h1>Home</h1>
<EDT/>
</div>
)
}
export default Home

View File

@@ -0,0 +1,13 @@
import RessourcePanel from "../ressourcePanel"
import TopBar from "../topBar"
function SelectionSession() {
return (
<div>
<h1>Selection Session</h1>
<RessourcePanel/>
</div>
)
}
export default SelectionSession

View File

@@ -0,0 +1,151 @@
import React from "react";
import { Athlete, Activite, Coach, Session, Ligne } from "../classes";
import ObjectSession from "./object/session";
import {calculStatsAthlete, niveauAlerte} from "../utils/athleteUtils";
import {calculTempsDeJeuParLigne} from "../utils/ligneUtils";
import ObjectActivite from "./object/activite";
type AthleteListProps = { athletes: Athlete[], sessions: Session[]};
type ActiviteListProps = { activites: Activite[] };
type CoachListProps = { coachs: Coach[] };
type SessionListProps = { sessions: Session[]};
type LigneListProps = { lignes: Ligne[], tempsDeJeuParLigne: Map<number, number> };
function AthleteList({ athletes, sessions }: AthleteListProps) {
const [dateDebut, setDateDebut] = React.useState(new Date());
const [dateFin, setDateFin] = React.useState(new Date());
const [seuilCritique, setSeuilCritique] = React.useState(0);
const [seuilMax, setSeuilMax] = React.useState(0);
const dateToDatetimeLocal = (date: Date) => {
const pad = (n: number) => n.toString().padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
};
return (
<>
<div className="creneau-stats">
<label>
Début :
<input
type="datetime-local"
value={dateToDatetimeLocal(dateDebut)}
onChange={e => setDateDebut(new Date(e.target.value))}
/>
</label>
<label>
Fin :
<input
type="datetime-local"
value={dateToDatetimeLocal(dateFin)}
onChange={e => setDateFin(new Date(e.target.value))}
/>
</label>
<label>
Seuil critique :
<input
type="number"
value={seuilCritique}
min={1}
onChange={e => setSeuilCritique(Number(e.target.value))}
/>
</label>
<label>Seuil max :
<input
type="number"
value={seuilMax}
min={1}
onChange={e => setSeuilMax(Number(e.target.value))}
/>
</label>
</div>
<ul className="AthleteList">
{athletes.map(a => {
const stats = calculStatsAthlete(sessions, a, dateDebut, dateFin);
const alerte = niveauAlerte(stats, seuilCritique, seuilMax);
return (
<li key={a.id}>
<div><strong>Nom:</strong> {a.nom}</div>
<div><strong>Groupe:</strong> {a.groupes}</div>
<div><strong>Nombre de sessions:</strong> {stats.nbSessions}</div>
<div><strong>Sessions/semaine:</strong> {stats.nbSessionsPerWeek.toFixed(2)}</div>
<div><strong>Alerte:</strong> {alerte}</div>
<div><strong>Distribuion des activités:</strong> {stats.distributions}</div>
</li>
);
})}
</ul>
</>
);
}
function ActiviteList({ activites }: ActiviteListProps) {
return (
<div className="list_object">
{activites.map((activite) => (
<ObjectActivite activite={activite}/>
))}
</div>
);
}
function CoachList({ coachs }: CoachListProps) {
return (
<ul className="CoachList">
{coachs.map((coachs) => (
<li key={coachs.id}>
<div>
<strong>Nom:</strong> {coachs.nom}
</div>
</li>
))}
</ul>
);
}
function SessionList({ sessions }: SessionListProps) {
return (
<div className="list_object">
{sessions.map((session) => (
<ObjectSession session={session}/>
))}
</div>
);
}
function LigneList({ lignes, tempsDeJeuParLigne }: LigneListProps) {
return (
<ul className="LigneList">
{lignes.map((lignes) => (
<li key={lignes.id}>
<div>
<strong>Nom:</strong> {lignes.nom}
</div>
<div>
<div>
<strong>Composition :</strong>
<ul>
{lignes.composition.map((athlete) => (
<li key={athlete.id}>
{athlete.nom}
</li>
))}
</ul>
</div>
</div>
<div>
<strong>Temps de jeu total :</strong>{" "}
{/* {tempsDeJeuParLigne.get(lignes.id) ?? 0} min */}
</div>
</li>
))}
</ul>
);
}
export { AthleteList, ActiviteList, CoachList , SessionList, LigneList };

View File

@@ -0,0 +1,143 @@
import { useEffect, useState } from "react";
import { useLocalData } from "../context/useLocalData";
import { Activite, Athlete, Coach , Session, Ligne, Admin, Groupe } from "../classes";
import {calculTempsDeJeuParLigne} from "../utils/ligneUtils";
import { keyboard } from "@testing-library/user-event/dist/keyboard";
import ObjectSession from "./object/session";
import ObjectActivite from "./object/activite";
import ObjectUser from "./object/user";
import { getAllActiviteAPI, getAllAthlete, getAllCoach, getAllSessionsAPI } from "../requetes";
import { useKeycloak } from "@react-keycloak/web";
import ObjectLigne from "./object/lignes";
export type keyWord = "athletes" | "activites" | "coachs" | "sessions"| "lignes";
export default function RessourcePanel() {
const { keycloak } = useKeycloak();
const { userLocal: user } = useLocalData();
//const user = getUserTest(); //TODO
const [value,setValue] = useState<keyWord>("sessions");
const[allAthletes,setAllAthletes] = useState<Athlete[]>([]);
const[allActivites,setAllActivites] = useState<Activite[]>([]);
const[allCoachs,setAllCoachs] = useState<Coach[]>([]);
const[allSessions,setAllSessions] = useState<Session[]>([]);
const[allLignes,setAllLignes] = useState<Ligne[]>([]);
const [groupe, setGroupe] = useState<String>("Tous");
async function updateAthletes() {
const athletes:Athlete[] = await getAllAthlete(groupe);
setAllAthletes(athletes);
}
async function updateCoachs() {
const coachs:Coach[] = await getAllCoach();
setAllCoachs(coachs);
}
async function updateActivites() {
const activites:Activite[] = await getAllActiviteAPI();
setAllActivites(activites);
}
async function updateSessions() {
const sessions:Session[] = await getAllSessionsAPI(groupe);
setAllSessions(sessions);
}
async function updateLignes() {
}
useEffect(() => {
update();
},[user,value,groupe])
function update(){
if(keycloak.authenticated){
if(value=="sessions") updateSessions();
if (user instanceof Admin || user instanceof Coach){
if(value=="athletes") updateAthletes();
if(value=="coachs") updateCoachs();
if(value=="lignes") updateLignes();
if(value=="activites") updateActivites();
}
}
}
return (
<div className="composant-container">
{(user instanceof Admin || user instanceof Coach) &&
<div>
<div>
Sélectionner une ressource:
</div>
<select
onChange={(e) => {
const v = (e.target as HTMLSelectElement).value;
setValue(v as keyWord)
}}>
<option value="sessions"> Sessions</option>
{(user instanceof Admin || user instanceof Coach) &&<option value="athletes">Athlètes</option>}
{(user instanceof Admin || user instanceof Coach) &&<option value="activites">Activités</option>}
{(user instanceof Admin || user instanceof Coach) && <option value="coachs"> Coachs</option>}
{(user instanceof Admin || user instanceof Coach) &&<option value="lignes"> Lignes</option>}
</select>
{(value == "athletes" || value == "sessions") &&
<select
onChange={(e) => {
const v = (e.target as HTMLSelectElement).value;
setGroupe(v as Groupe)
console.log(v);
}}>
<option value="Tous"> Tous</option>
<option value="None"> Sans groupe</option>
<option value="Loisir">Loisir</option>
<option value="Entrainement">Entrainement</option>
<option value="Competition">Competition</option>
</select>
}
</div>
}
<div className="edt_sessions_panel">
<h3>Liste des {value}</h3>
<button onClick={()=>update()}> Actualiser </button>
<div className="list_object">
{value==="athletes" && (
allAthletes.map((athlete) => (
<ObjectUser user={athlete}/>
))
)}
{value==="activites" && (
allActivites.map(activite => (
<ObjectActivite activite={activite}/>
))
)}
{value==="coachs" && (
allCoachs.map((coach) => (
<ObjectUser user={coach}/>
))
)}
{value==="sessions" && (
allSessions.map((session) => (
<ObjectSession session={session}/>
))
)}
{value==="lignes" && (
allLignes.map((ligne) => (
<ObjectLigne ligne={ligne}/>
))
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,8 @@
.createActivite {
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
padding: 20px;
border-radius: 16px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-md);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@@ -0,0 +1,204 @@
.edt {
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
border-radius: 24px;
padding: 20px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.edt:hover {
border-color: var(--green-primary);
box-shadow: 0 12px 40px var(--green-A-primary);
}
.edt_header {
justify-content: center;
display: grid;
grid-template-columns: repeat(3, 0.5fr);
padding-bottom: 16px;
gap: 12px;
margin-bottom: 12px;
}
.edt_colonnes {
position: relative;
display: grid;
align-items: flex-start;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 16px;
color: var(--text);
width: 100%;
overflow: hidden;
}
.edt_loading {
position: absolute;
inset: 0;
display: grid;
pointer-events: none;
}
.edt_colonne {
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint4) 100%);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
border: 1px solid var(--tint4);
padding: 12px;
}
.edt_colonne.today {
background: linear-gradient(135deg, var(--tint1) 25%, var(--tint2) 100%);
border: 1px solid var(--green-primary);
}
.edt_day_header {
font-size: clamp(5px, 1vw, 18px);
padding: 12px;
border-radius: 12px;
height: fit-content;
text-align: center;
font-size: 1em;
background: linear-gradient(135deg, var(--green-primary) 0%, var(--green-secondary) 100%);
font-weight: 700;
font-size: large;
color: white;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.edt_day_header.today {
background: linear-gradient(135deg, var(--green-dark) 0%, var(--cyan-accent) 100%);
color: #FFFFFF;
box-shadow: 0 6px 20px var(--green-A-primary);
}
.edt_day_content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border-radius: 20px;
min-width: 0;
}
.edt_session {
font-size: clamp(1px, 8cqi, 18px);
gap: 8px;
background: linear-gradient(135deg, var(--tint4) 0%, var(--tint5) 100%);
border-radius: 12px;
padding: 12px;
min-width: 0;
border-left: 4px solid var(--green-primary);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.edt_session:hover {
background: linear-gradient(135deg, var(--green-A-primary), var(--green-A-secondary));
border-left-width: 6px;
transform: translateX(4px);
box-shadow: 0 8px 16px var(--green-A-primary);
}
.edt_session:active {
transform: translateX(1px);
}
.edt_session_header {
display: flex;
gap: 5px;
}
.edt_date {
font-size: 0.75em;
font-weight: 500;
}
.edt_button_week_select {
background: linear-gradient(135deg, var(--green-primary), var(--green-secondary));
color: white;
height: 40px;
border-radius: 20px;
border: none;
padding: 0 20px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px var(--green-A-primary);
}
.edt_button_week_select:hover {
background: linear-gradient(135deg, var(--green-secondary), var(--green-dark));
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--green-A-primary);
}
.edt_button_week_select:active {
transform: translateY(0);
box-shadow: 0 1px 2px var(--green-A-primary);
}
.edt_session_modal {
background-color: var(--tint2);
padding: 10px;
border-radius: 20px;
position: relative;
border: 2px solid var(--green-primary);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1), 0 0 0 1px var(--green-A-primary);
}
.ent_activite_list {
padding: 10px;
background-color: var(--tint3);
border-radius: 10px;
border: 1px solid var(--green-A-primary);
}
.edt_activite_modal {
background-color: var(--tint3);
padding: 10px;
border-radius: 20px;
position: relative;
border: 2px solid #10b981;
box-shadow: 0 4px 12px var(--green-A-primary);
}
/* Classes bonus pour d'autres boutons */
.edt_button_secondary {
background-color: var(--tint3);
color: var(--text);
border: 2px solid var(--green-primary);
border-radius: 15px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.edt_button_secondary:hover {
background-color: var(--green-A-primary);
border-color: var(--green-secondary);
}
.edt_button_outline {
background: transparent;
color: var(--green-primary);
border: 2px solid var(--green-primary);
border-radius: 18px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.edt_button_outline:hover {
background-color: var(--green-primary);
color: white;
transform: scale(1.05);
}

View File

@@ -0,0 +1,81 @@
.list_object{
display: grid;
gap: 12px;
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
padding: 16px;
border-radius: 16px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-md);
}
.list_object_modal{
display: grid;
gap: 12px;
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
padding: 16px;
border-radius: 16px;
max-height: 280px;
overflow-y: auto;
border: 1px solid var(--tint4);
}
.object {
font-size: clamp(1px, 8cqi, 18px);
gap: 8px;
background: linear-gradient(135deg, var(--tint3) 0%, var(--tint4) 100%);
border-radius: 12px;
padding: 12px;
min-width: 0;
border: 1px solid var(--tint4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--shadow-sm);
}
.object:hover {
background: linear-gradient(135deg, var(--green-A-primary), var(--green-A-secondary));
border-color: var(--green-primary);
transform: translateY(-2px);
box-shadow: 0 8px 16px var(--green-A-primary);
}
.object:active {
background: linear-gradient(135deg, var(--tint4) 0%, var(--tint5) 100%);
transform: translateY(0);
}
.object_header{
display: flex;
gap: 8px;
align-items: center;
font-weight: 600;
}
.object_small{
font-size: 0.75em;
opacity: 0.8;
}
.object_modal{
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
padding: 14px;
border-radius: 12px;
position: relative;
border: 1px solid var(--tint4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.session_modal_activite_list{
display: grid;
padding: 12px;
background: linear-gradient(135deg, var(--tint3) 0%, var(--tint4) 100%);
border-radius: 12px;
gap: 8px;
border: 1px solid var(--tint4);
}
.activiteList{
display: flex;
gap: 12px;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,132 @@
.topBar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
border-radius: 20px;
height: auto;
min-height: 70px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-md);
backdrop-filter: blur(10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.topBar:hover {
border-color: var(--green-primary);
box-shadow: 0 8px 32px var(--green-A-primary);
}
.toBarLeft{
flex: 1;
display: flex;
gap: 16px;
align-items: center;
}
.toBarMidle{
display: flex;
flex: 1;
justify-content: center;
gap: 10px;
}
.topBarRight{
flex: 1;
font-weight: 600;
}
.toBarLeft h2 {
background: linear-gradient(135deg, var(--green-primary) 0%, var(--cyan-accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
font-size: 1.5rem;
}
.topBarRight {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
z-index: 1;
}
.loginContainer {
position: relative;
z-index: 9999;
isolation: isolate;
}
.login {
position: absolute;
right: 0;
top: calc(100% + 8px);
display: grid;
gap: 12px;
width: fit-content;
white-space: nowrap;
align-items: left;
background: linear-gradient(135deg, var(--tint2) 0%, var(--tint3) 100%);
padding: 16px;
border-radius: 16px;
border: 1px solid var(--tint4);
box-shadow: var(--shadow-lg);
z-index: 1000;
backdrop-filter: blur(10px);
}
.loginButton {
display: flex;
align-items: center;
justify-content: center;
width: 120px;
height: 30px;
border-radius: 15px;
}
.ButtonTheme {
display: flex;
align-items: center;
justify-content: center;
height: 44px;
width: 44px;
color: var(--text);
background: linear-gradient(135deg, var(--tint3) 0%, var(--tint4) 100%);
border: 1px solid var(--tint4);
border-radius: 12px;
font-size: 18px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.ButtonTheme:hover {
border-color: var(--themeButtonColor);
/* color: var(--themeButtonColor); */
box-shadow: 0 4px 12px rgba(0, 170, 255, 0.3);
transform: scale(1.05) rotateZ(-180deg);
}
.logo {
height: 40px;
filter: drop-shadow(0 2px 8px rgba(16, 185, 129, 0.3));
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.logo:hover {
transform: scale(1.1) rotateY(10deg);
}
.logoLink:hover{
border: 0px;
}
.logoLink{
border: 0px;
}

View File

@@ -0,0 +1,33 @@
import { Link } from "react-router-dom"
import { Admin, Athlete, Coach } from "../classes"
import { useLocalData } from "../context/useLocalData"
import Login from "./login"
import SwitchThemeColor from "./SwitchThemeColor"
function TopBar(){
const {userLocal} = useLocalData()
return(
<div className="topBar">
<div className="toBarLeft">
<Link className="logoLink" to="/"><img className="logo" src="/Frisbyee_logo.png"/></Link>
<h2>Frisbyee</h2>
</div>
<div className="toBarMidle">
<Link to="/">Home</Link>
<Link to="/sessions">{(userLocal instanceof Athlete) ? "Sessions" : "List"}</Link>
{userLocal instanceof Coach &&<Link to="/gestion">Gestion</Link>}
{userLocal instanceof Admin &&<Link to="/admin">Admin</Link>}
</div>
<div className="topBarRight">
<SwitchThemeColor/>
<Login/>
</div>
</div>
)
}
export default TopBar

View File

@@ -0,0 +1,12 @@
import { createContext } from 'react'
import { Session, User } from '../classes';
interface LocalDataContextType {
userLocal:User;
setUserLocal: React.Dispatch<React.SetStateAction<User>>
sessionsLocal: Session[];
setSessionsLocal: React.Dispatch<React.SetStateAction<Session[]>>
}
export const LocalDataContext = createContext<LocalDataContextType | undefined>(undefined)

View File

@@ -0,0 +1,10 @@
import { useContext } from 'react'
import { LocalDataContext } from './LocalDataContext'
export const useLocalData = () => {
const context = useContext(LocalDataContext)
if (!context) {
throw new Error('useLocalData must be used within LocalDataProvider')
}
return context
}

View File

@@ -1,3 +1,13 @@
/* Reset global */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Styles de base */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -5,6 +15,9 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--tint0);
color: var(--text);
transition: background-color 0.3s ease, color 0.3s ease;
}
code {

View File

@@ -3,13 +3,17 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

View File

@@ -0,0 +1,5 @@
import Keycloak from 'keycloak-js'
const keycloak = new Keycloak("/keycloak.json")
export default keycloak

View File

@@ -0,0 +1,19 @@
import { useState } from 'react'
import { Session, User } from '../classes'
import { LocalDataContext } from '../context/LocalDataContext'
export const LocalDataProvider = ({ children }: { children: React.ReactNode }) => {
const [userLocal, setUserLocal] = useState<User>(new User())
const [sessionsLocal, setSessionsLocal] = useState<Session[]>([])
return (
<LocalDataContext.Provider
value={{ userLocal, setUserLocal, sessionsLocal,setSessionsLocal}}>
{children}
</LocalDataContext.Provider>
)
}

443
front_end/src/requetes.tsx Normal file
View File

@@ -0,0 +1,443 @@
import api, { activiteService, adminService, athleteService, coachService, sessionService } from "./api";
import { Activite, Admin, Athlete, Coach, Session, User } from "./classes";
import Keycloak from 'keycloak-js'
import { AdminDTO, AthleteDTO, CoachDTO, SessionDTO } from "./classesDTO";
import { useLocalData } from "./context/useLocalData";
//debug:
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
//UPDATE /////////////////////////////////////////////////////////
//COACH / ATHLETE
/*
retourne l'utilisateur lié à l'identifiant keyloack
*/
export async function loginOrRegister(keycloak:Keycloak): Promise<User|null>{
try {
if(keycloak.tokenParsed!=null){
const roles = keycloak.tokenParsed?.realm_access?.roles
if(roles!=null){
if(roles.includes("admin")){
const newAdmin: Admin = new Admin();
newAdmin.keycloakId = keycloak.tokenParsed.sub || "";
newAdmin.email = keycloak.tokenParsed.email || "";
newAdmin.nom = keycloak.tokenParsed.family_name || "";
newAdmin.prenom = keycloak.tokenParsed.given_name || "";
const response = await adminService.create(newAdmin.toDTO());
const admin = new Admin(response.data);
return admin;
}
else if(roles.includes("coach")){
const newCoach: Coach = new Coach();
newCoach.keycloakId = keycloak.tokenParsed.sub || "";
newCoach.email = keycloak.tokenParsed.email || "";
newCoach.nom = keycloak.tokenParsed.family_name || "";
newCoach.prenom = keycloak.tokenParsed.given_name || "";
const response = await coachService.create(newCoach.toDTO());
const coach = new Coach(response.data);
return coach;
}
else if(roles.includes("athlete")){
const newAthlete: Athlete = new Athlete();
newAthlete.keycloakId = keycloak.tokenParsed.sub || "";
newAthlete.email = keycloak.tokenParsed.email || "";
newAthlete.nom = keycloak.tokenParsed.family_name || "";
newAthlete.prenom = keycloak.tokenParsed.given_name || "";
const response = await athleteService.create(newAthlete.toDTO());
const athlete = new Athlete(response.data);
return athlete;
}
else console.error("Error : role inconnu");
}
else console.error("Error : role null");
}
else console.error("Error : token Keylcoak null");
console.error("Error : pendant la récupération de l'User");
return null;
}
catch (error) {
console.error("Error fetching user:", error);
return null;
}
}
export async function getSessionOfActivite(session:Session):Promise<Activite[]|null>{
try {
if(session.id !=null){
const activites:Activite[] = [];
const response = await sessionService.getActivities(session.id!);
response.data.forEach(activiteDTO => {
activites.push(new Activite(activiteDTO));
});
return activites;
}
return null;
} catch (error) {
console.error("Error getting session:", error);
return null;
}
}
export async function deletActiviteFromSession(activite:Activite){
try {
if(activite.id !=null){
activiteService.delete(activite.id);
}
} catch (error) {
console.error("Error getting session:", error);
}
}
export async function addActiviteToSession(session:Session,activite:Activite):Promise<Boolean>{
try {
if(activite.id !=null && session.id !=null){
await sessionService.addActivity(session.id,activite.id)
return true;
}
return false
} catch (error) {
console.error("Error getting session:", error);
return false;
}
}
export async function updateActivitiesOfSessionAPI(session:Session){
try {
session.activites.forEach(activite => {
if(activite.id!=null){
activiteService.update(activite.id, activite.toDTO());
}
});
// To refresh the activities in the session object
//const response = await sessionService.getActivities(session_id!);
//session.activites = response.data;
} catch (error) {
console.error("Error fetching activities for session:", error);
}
}
export async function subscribeSessionAPI(user:User, session:Session):Promise<boolean>{
try {
const session_id =session.id
const user_id = user.id
const response = await sessionService.subscribe(session_id!, user_id!);
return true;
} catch (error) {
console.error("Error subscribing to session:", error);
return false;
}
}
export async function unsubscribeSessionAPI(user:Athlete, session:Session):Promise<boolean>{
try {
const session_id =session.id
const user_id = user.id
const response = await sessionService.unsubscribe(session_id!, user_id!);
return true;
} catch (error) {
console.error("Error unsubscribing from session:", error);
return false;
}
}
// POST /////////////////////////////////////////////////////////
// COACH ADMIN
export async function createSessionAPI(session: Session): Promise<Session> {
async function postActivite(activite:Activite,sessionRes:Session){
activite.session = sessionRes;
const activite2 = await createActivityAPI(activite);
activite2.session = sessionRes;
sessionRes.activites.push(activite2);
}
try {
console.log("GROUPE = " + session.groupe);
const response = await api.post<SessionDTO>("/session/create", session.toDTO());
const sessionRes:Session = new Session(response.data);
await Promise.all(
session.activites.map(activite =>
postActivite(activite, sessionRes)
)
);
console.log(sessionRes.activites);
updateActivitiesOfSessionAPI(sessionRes);
return sessionRes;
} catch (error) {
console.error("Error creating session:", error);
throw error;
}
}
export async function createActivityAPI(activity: Activite):Promise<Activite>{
try {
const response = await activiteService.create(activity.toDTO())
return new Activite(response.data);
} catch (error) {
console.error("Error creating activity:", error);
throw error;
}
}
export async function getAllActiviteAPI(): Promise<Activite[]> {
try {
const response = await activiteService.getAll();
const activites:Activite[] = []
response.data.forEach(dto => {
activites.push(new Activite(dto));
});
return activites;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
}
export async function postAthlete(athlete: Athlete):Promise<Athlete>{
try {
const response = await api.post<Athlete>("/athlete/create/",athlete.toDTO);
console.log(response);
return response.data;
} catch (error) {
console.error("Error fetching coachs:", error);
throw error;
}
}
export async function postSession(session: Session){
try {
const response = await sessionService.create(session.toDTO());
session.id = response.data.id; //TODO ?
session.activites.forEach(activite => {
activiteService.create(activite.toDTO());
// console.log("Session créée");
});
} catch (error) {
console.error("Error post Session:", error);
throw error;
}
}
// SET /////////////////////////////////////////////////////////
export async function createAdminAPI(athlete: Admin):Promise<Admin>{
try {
const response = await api.post<Admin>("/admin/create/",athlete);
console.log(response);
return response.data;
} catch (error) {
console.error("Error fetching coachs:", error);
throw error;
}
}
//GET /////////////////////////////////////////////////////////
//USER
export async function getAllUserAPI(): Promise<User[]> {
try{
const response = await api.get<User[]>("/users/all");
return response.data;
}catch (error) {
console.error("Error fetching users:", error);
throw error;
}
}
export async function getCoachByIdAPI(id:number|null): Promise<Coach|null> {
try{
if(id!==null){
const response = await coachService.getById(id);
return new Coach(response.data);
}
console.error("Error fetching coach by id : id null");
return null;
}catch (error) {
console.error("Error fetching coach by id:", error);
return null;
}
}
//SESSION
export async function getSessionsOfUserAPI(user:User): Promise<Session[]>{
try {
var sessionsDTO:SessionDTO[] = []
if (user instanceof Coach) {
const response = await coachService.getSessionsForCoach(user.id); //TODO
sessionsDTO = response.data;
}else if (user instanceof Athlete) {
const response = await athleteService.getSessionsForAthlete(user.id); //TODO
sessionsDTO = response.data;
}
const sessions:Session[] = [];
for (const sessionDTO of sessionsDTO) {
const session = new Session(sessionDTO);
const coach = await getCoachByIdAPI(sessionDTO.coachId);
if(coach!=null){
session.coach = coach;
}
sessions.push(session);
}
return sessions;
}catch (error) {
console.error("Error fetching sessions for user:", error);
return [];
}
}
export async function getAllSessionsAPI(groupe?:String):Promise<Session[]>{
try {
var response;
if(groupe!=null && groupe!="Tous"){
response = await sessionService.getAllByGroup(groupe);
}
else{
response = await sessionService.getAll();
}
const sessions = await Promise.all(
response.data.map(async sessionDTO => {
const session = new Session(sessionDTO);
const coach = await getCoachByIdAPI(sessionDTO.coachId);
if (coach != null) {
session.coach = coach;
}
return session;
})
);
return sessions;
} catch (error) {
console.error("Error fetching sessions:", error);
throw error;
}
}
function formatDateLocal(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
export async function getAllSessionsBetweenAPI(d1:Date,d2:Date,groupe?:String):Promise<Session[]>{
try {
const data = {
startDate: formatDateLocal(d1),
endDate: formatDateLocal(d2)
}
var response;
if(groupe!=null && groupe!="Tous"){
response = await sessionService.getAllBetweenDateByGroup(groupe,data);
}
else {
response = await sessionService.getAllBetweenDate(data);
}
const sessions = await Promise.all(
response.data.map(async sessionDTO => {
const session = new Session(sessionDTO);
const coach = await getCoachByIdAPI(sessionDTO.coachId);
if (coach != null) {
session.coach = coach;
}
return session;
})
);
return sessions;
} catch (error) {
console.error("Error fetching sessions:", error);
throw error;
}
}
export async function getCoachOfSession(session:Session): Promise<Coach>{
try {
const response = await sessionService.getCoach(session.id);
const coach:Coach = new Coach(response.data);
return coach;
} catch (error) {
console.error("Error fetching coachs:", error);
throw error;
}
}
//COACH
export async function getAllCoach(): Promise<Coach[]> {
try {
const response = await coachService.getAll();
const coachs:Coach[] = []
response.data.forEach(dto => {
coachs.push(new Coach(dto));
});
return coachs;
} catch (error) {
console.error("Error fetching coachs:", error);
throw error;
}
}
//ATHLETE
export async function getAllAthlete(groupe?:String): Promise<Athlete[]> {
try {
var response;
if(groupe!=null && groupe!="Tous"){
response = await athleteService.getAllByGroup(groupe);
}
else{
response = await athleteService.getAll();
}
const athletes:Athlete[] = []
response.data.forEach(dto => {
athletes.push(new Athlete(dto));
});
return athletes;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
}
export async function getSessionsByAthleteId(athleteId: number): Promise<Session[]> {
try {
const response = await api.get(`/athlete/${athleteId}/session`);
const sessions: Session[] = [];
const allAthletes = await getAllAthlete();
response.data.forEach((sessionDTO: SessionDTO) => {
const session = new Session(sessionDTO);
if (sessionDTO.athleteIds && sessionDTO.athleteIds.length > 0) {
session.athletes = allAthletes.filter(a =>
sessionDTO.athleteIds.includes(a.id!)
);
}
sessions.push(session);
});
console.log(`Sessions chargées pour l'athlète ${athleteId}:`, sessions);
return sessions;
} catch (error) {
console.error("Error fetching sessions for athlete:", error);
return [];
}
}

View File

@@ -0,0 +1,51 @@
import { Athlete, Session , Activite} from '../classes';
export interface StatsAthlete {
nbSessions: number;
nbSessionsPerWeek: number;
isAlerte: boolean;
distributions: Map<String, number>; //le nom de l'activité et son nombre
}
export function niveauAlerte(stats: StatsAthlete, seuilCritique = 0, seuilMax = 0) {
if (stats.nbSessionsPerWeek > seuilMax) return "Alerte ! Niveau maximal atteint.";
if (stats.nbSessionsPerWeek > seuilCritique) return "Attention! Niveau critique atteint.";
return "Normal";
}
export function calculStatsAthlete(sessions: Session[], athlete: Athlete, debut: Date, fin: Date): StatsAthlete {
let nb_sessions = 0;
let nb_semaine = 1; //forcément une semaine
const distributions: Map<string, number> = new Map();
const timeDiff = Math.abs(fin.getTime() - debut.getTime());
const msPerWeek = 1000 * 3600 * 24 * 7;
nb_semaine = Math.max(1, Math.ceil(timeDiff / msPerWeek));
sessions.forEach(session => {
// verification session dans l'intervalle
if (session.creneau < debut || session.creneau > fin) return;
// verification athlete dans session
if (!session.athletes.some(a => a.id === athlete.id)) return;
//incrementation (verifie si recurent ou non)
const increment = session.isRecurrent ? nb_semaine : 1;
nb_sessions += increment;
//distribution des activités
session.activites.forEach(activite => {
const currentCount = distributions.get(activite.nom) || 0;
distributions.set(activite.nom, currentCount + increment);
});
});
const nbSessionsPerWeek = nb_sessions / nb_semaine;
const isAlerte = nbSessionsPerWeek > 8;
return {
nbSessions: nb_sessions,
nbSessionsPerWeek: nbSessionsPerWeek,
isAlerte: isAlerte,
distributions: distributions
};
}

View File

@@ -0,0 +1,16 @@
import {Ligne, Session} from '../classes';
//Temps de jeu cumulé par ligne
export function calculTempsDeJeuParLigne(sessions: Session[], ligne : Ligne): number {
let tempsDeJeuTotal = 0;
sessions.forEach(session => {
// Vérifier si la ligne est présente dans la session
if (session.ligne && session.ligne.some(l => l.id === ligne.id)) {
tempsDeJeuTotal += session.duree;
}
});
return tempsDeJeuTotal;
}

View File

@@ -1,11 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -13,14 +12,10 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"]
}

View File

@@ -0,0 +1,258 @@
/* Personnalisation de la page de login Keycloak */
.login-pf body {
background: linear-gradient(135deg, #10b981 0%, #059669 50%, #047857 100%);
min-height: 100vh;
}
#kc-header-wrapper {
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin-bottom: 2rem;
font-size: 32px;
}
#kc-header-wrapper h1,
#kc-header-wrapper .kc-logo-text {
font-weight: 700;
letter-spacing: 0.5px;
font-size: 40px;
}
/* Card principale */
.card-pf {
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2),
0 5px 15px rgba(0, 0, 0, 0.1);
border: 2px solid rgba(16, 185, 129, 0.3);
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.98);
overflow: hidden;
transition: transform 0.3s ease;
width: fit-content;
margin: 0 auto;
}
.card-pf:hover {
transform: translateY(-2px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.25),
0 8px 20px rgba(16, 185, 129, 0.15);
}
/* Header de la card */
#kc-form-login .card-pf h1,
.login-pf-page h1 {
color: #047857;
font-weight: 600;
border-bottom: 3px solid #10b981;
padding-bottom: 10px;
margin-bottom: 20px;
}
/* Champs de formulaire */
.form-group {
margin-bottom: 20px;
}
#kc-form-login input[type="text"],
#kc-form-login input[type="password"],
#kc-form-login input[type="email"],
.pf-c-form-control {
border: 2px solid #d1d5db;
border-radius: 12px;
padding: 12px 16px;
transition: all 0.3s ease;
font-size: 15px;
}
#kc-form-login input[type="text"]:focus,
#kc-form-login input[type="password"]:focus,
#kc-form-login input[type="email"]:focus,
.pf-c-form-control:focus {
border-color: #10b981;
outline: none;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
transform: translateY(-1px);
}
/* Labels */
label,
.pf-c-form__label {
color: #374151;
font-weight: 500;
margin-bottom: 6px;
}
/* Bouton principal */
.btn-primary,
#kc-login,
.pf-c-button.pf-m-primary {
background: linear-gradient(135deg, #10b981, #059669);
border: none;
border-radius: 12px;
padding: 12px 24px;
font-weight: 600;
color: white;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(16, 185, 129, 0.3);
width: 100%;
font-size: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary:hover,
#kc-login:hover,
.pf-c-button.pf-m-primary:hover {
background: linear-gradient(135deg, #059669, #047857);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(16, 185, 129, 0.4);
}
.btn-primary:active,
#kc-login:active,
.pf-c-button.pf-m-primary:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
}
/* Boutons secondaires */
.btn-secondary,
.btn-default,
.pf-c-button.pf-m-secondary {
background-color: transparent;
color: #10b981;
border: 2px solid #10b981;
border-radius: 12px;
padding: 10px 20px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-secondary:hover,
.btn-default:hover,
.pf-c-button.pf-m-secondary:hover {
background-color: #10b981;
color: white;
transform: scale(1.02);
}
/* Liens */
#kc-form-options a,
#kc-registration a,
.login-pf-page a {
color: #10b981;
font-weight: 500;
text-decoration: none;
transition: all 0.2s ease;
border-bottom: 2px solid transparent;
}
#kc-form-options a:hover,
#kc-registration a:hover,
.login-pf-page a:hover {
color: #059669;
border-bottom-color: #10b981;
}
/* Checkbox "Se souvenir de moi" */
.checkbox label {
color: #4b5563;
font-weight: 400;
}
input[type="checkbox"] {
accent-color: #10b981;
width: 18px;
height: 18px;
cursor: pointer;
}
/* Messages d'alerte */
.alert-error,
.pf-c-alert.pf-m-danger {
background-color: #fef2f2;
border-left: 4px solid #ef4444;
border-radius: 10px;
color: #991b1b;
}
.alert-warning,
.pf-c-alert.pf-m-warning {
background-color: #fffbeb;
border-left: 4px solid #f59e0b;
border-radius: 10px;
color: #92400e;
}
.alert-success,
.pf-c-alert.pf-m-success {
background-color: #f0fdf4;
border-left: 4px solid #10b981;
border-radius: 10px;
color: #065f46;
}
.alert-info,
.pf-c-alert.pf-m-info {
background-color: #eff6ff;
border-left: 4px solid #3b82f6;
border-radius: 10px;
color: #1e40af;
}
/* Social login buttons */
#kc-social-providers .btn {
border-radius: 12px;
margin-bottom: 10px;
transition: all 0.3s ease;
border: 2px solid #e5e7eb;
}
#kc-social-providers .btn:hover {
transform: translateX(5px);
border-color: #10b981;
box-shadow: -3px 3px 10px rgba(16, 185, 129, 0.2);
}
/* Footer */
#kc-registration {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
text-align: center;
}
/* Animation au chargement */
.card-pf {
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 768px) {
.card-pf {
border-radius: 15px;
margin: 10px;
}
.btn-primary,
#kc-login {
font-size: 14px;
padding: 10px 20px;
}
}

View File

@@ -0,0 +1,3 @@
parent=keycloak
import=common/keycloak
styles=css/styles.css

View File

@@ -0,0 +1 @@
name=frisbyee

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"react-router-dom": "^7.12.0"
}
}