109 Commits

Author SHA1 Message Date
ucef a19e49392b chore: bump version to 1.6-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m57s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m50s
2026-07-08 02:50:26 +02:00
ucef 8ca20c9a4f chore: release v1.5
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m53s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m54s
2026-07-08 02:49:29 +02:00
ucef 92c9e80bc3 feat: update predefined shoe sizes list in public pre-registration
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m48s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m28s
2026-07-07 22:56:47 +02:00
ucef c94f89997f feat: add equipment duplication and name uniqueness validation 2026-07-07 22:56:29 +02:00
ucef af31766134 feat: use exhaustive standard sizes list in pre-registration and adjust layout
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m52s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m17s
2026-07-07 21:55:01 +02:00
ucef 3fc239683e feat: display and manage adherent clothing and shoe sizes 2026-07-07 21:54:52 +02:00
ucef a7c5b23198 feat: add category duplication functionality 2026-07-07 21:54:38 +02:00
ucef 29f9dc3bc2 feat: add flocage customization options to equipement configuration 2026-07-07 21:54:14 +02:00
ucef 5ee9be467b feat: add new fields in pre-registration and sync with adherent and dotations 2026-07-05 12:47:46 +02:00
ucef 6dd45deb8c feat: add commentaire field to licence modal 2026-07-05 12:38:39 +02:00
ucef 19cee92437 feat: add active user session tracking and visualization in admin dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 4m1s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m34s
2026-07-03 17:18:14 +02:00
ucef 214ec08229 fix: resolve HTMX pagination bug on adherents list 2026-07-03 17:07:42 +02:00
ucef 62209ae659 fix(db): increase taille and numero length in dotation to 50
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m29s
2026-07-01 15:00:50 +02:00
ucef fc3373a1b4 feat(ui): ajout d'un tooltip sur les noms d'équipements tronqués dans la vue dotation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m39s
2026-07-01 10:35:00 +02:00
ucef db3eb3ab9a feat: add sexe to pre-inscription form and entity 2026-07-01 09:32:42 +02:00
ucef d1f38f11f0 feat: add search filters to categories and equipements lists
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m16s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 15:11:16 +02:00
ucef acf9d6d301 fix: apply maillot price and default selection for all new registrations
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m0s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m19s
2026-06-30 14:42:59 +02:00
ucef 3066ff0e86 chore: bump version to 1.5-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m33s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m23s
2026-06-30 13:20:04 +02:00
ucef 64ad4be7dd chore: release v1.4
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m45s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m44s
2026-06-30 13:19:43 +02:00
ucef d30e795325 fix: harmoniser le titre des documents à fournir sur la page de succès
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m18s
2026-06-30 13:16:24 +02:00
ucef e0388c7884 revert: remove viewport meta tag to restore previous mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m21s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 1m40s
2026-06-30 12:45:37 +02:00
ucef 7cf29f6106 Merge remote-tracking branch 'origin/main'
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 10m47s
2026-06-29 16:30:07 +02:00
ucef bef01e3a0e feat: add viewport meta tag to all HTML templates for responsive mobile display 2026-06-29 16:29:58 +02:00
ucef 9cf1d26018 feat: add viewport meta tag to all HTML templates for responsive mobile display
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m6s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m13s
2026-06-29 15:44:48 +02:00
ucef 94d672c0f6 feat: handle goalkeeper vs field player equipment differentiation
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 14:54:36 +02:00
ucef 8156c8f3d0 feat: gestion dynamique des équipements dans la configuration d'une catégorie
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m10s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-29 11:46:15 +02:00
ucef 7249b45533 feat: ajout du numéro de chèque et du commentaire sur les paiements 2026-06-29 11:28:52 +02:00
ucef 7e036c1966 feat: paramétrage dynamique des dotations (sexe, tailles, couleurs) 2026-06-29 11:11:53 +02:00
ucef 194986f880 feat: pré-sélection de la catégorie par âge et sexe lors de la création d'une licence
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m18s
2026-06-29 09:28:38 +02:00
ucef d834b8d0d3 fix: ajout du script de migration Flyway pour les nouveaux champs
- Script V30__add_ancienne_categorie_and_sexe.sql pour ajouter les champs en DB.

- Mise à jour des colonnes telephone, ancienne_categorie et sexe.
2026-06-29 09:14:18 +02:00
ucef e3abe53eb0 feat: amélioration des formulaires d'inscription et pré-inscription
- Rendre le champ téléphone obligatoire (entités et UI).

- Ajout du champ 'Ancienne catégorie' conditionné par la saisie de l'ancien club.

- Synchronisation de l'ancienne catégorie entre la pré-inscription et la fiche adhérent.
2026-06-29 09:03:45 +02:00
ucef 348d6e6ccf chore: bump version to 1.4-SNAPSHOT
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m17s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m28s
2026-06-25 11:05:23 +02:00
ucef e670e38343 feat: harmoniser les documents demandés sur la page de succès de pré-inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m26s
2026-06-25 10:34:41 +02:00
ucef fe3a1ec36c fix: forcer le retour visuel de validation sur mobile (novalidate)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m12s
2026-06-25 10:07:51 +02:00
ucef 44ae524626 ci: ajout d'une vérification de démarrage après le déploiement
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m47s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m22s
2026-06-25 09:47:54 +02:00
ucef 829a69f1a5 fix: corriger l'indentation YAML qui empêchait l'application de démarrer 2026-06-25 09:45:12 +02:00
ucef f8336fb407 feat: affichage du numéro de version de l'application
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 22s
2026-06-25 09:19:59 +02:00
ucef ab1094d29a feat: préciser le justificatif de domicile pour les adhérents nés à l'étranger 2026-06-25 09:12:21 +02:00
ucef 3477461fb2 feat: ajouter un retour visuel sur la validation des champs obligatoires (mobile)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m15s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m14s
2026-06-25 08:57:06 +02:00
ucef bb48f919b0 feat: rendre le message sur les documents à fournir plus générique
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m17s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m26s
2026-06-25 08:21:51 +02:00
ucef 5f2bf61829 feat: gestion type de demande, calcul dynamique du tarif et pre-selection equipement (maillot) pour inscription
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m28s
2026-06-24 23:56:46 +02:00
ucef 2df3e792f1 feat(pre-inscription): augmentation de la durée de validité du token à 15 minutes
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m9s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m31s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m47s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m5s
2026-06-24 15:56:54 +02:00
ucef 8ecdc54028 fix(ci): injection des variables captcha dans la pipeline de test
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m20s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 40s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m59s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m4s
2026-06-24 10:48:55 +02:00
ucef e8baeab0e0 feat(equipement): ajout de la référence et retrait du bouton d'export du dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m54s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m13s
2026-06-24 10:18:53 +02:00
ucef d0fde2f2e8 feat(adherent): réintégration du numéro de téléphone 2026-06-24 10:18:44 +02:00
ucef 754a0c8dcc fix(security): ajout de l'import manquant pour @Value
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m38s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 1m54s
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m26s
2026-06-23 15:30:01 +02:00
ucef bbbb3978b9 fix(ci): retrait de clone --depth 1 pour permettre le checkout des branches secondaires
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 4m27s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Has been skipped
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Failing after 1m38s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Has been skipped
2026-06-23 15:14:53 +02:00
ucef 5e75767f92 fix(security): injection correcte de la clé de site captcha dans la vue
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Failing after 26s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Has been skipped
2026-06-23 02:05:27 +02:00
ucef 2f8c8f0829 fix(docker): ajout de la politique de redémarrage automatique pour la BDD et pgAdmin 2026-06-23 02:01:55 +02:00
ucef bf38e01c49 fix(docker): transmission des variables captcha au conteneur
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m2s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 31s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 1m33s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 11s
2026-06-23 01:47:50 +02:00
ucef 6ea793de87 ci(gitea): séparation des pipelines test et prod en deux fichiers distincts
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m4s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 4m59s
AS Talange CI/CD Pipeline - Production / Build & Run Unit Tests (push) Successful in 2m28s
AS Talange CI/CD Pipeline - Production / Deploy to Prod Environment (push) Successful in 2m41s
2026-06-23 01:16:59 +02:00
ucef 60257f4823 feat(ui): ajout du logo sur la page de connexion 2026-06-23 01:05:55 +02:00
ucef 5c64184c93 feat(ui): ajout d'une bannière de bienvenue avec logo sur le tableau de bord
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m8s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 01:03:03 +02:00
ucef 2dc39521c4 perf: redimensionnement et optimisation du logo pour un chargement rapide 2026-06-23 01:01:53 +02:00
ucef c7d8aaa179 style: mise à jour du logo du club
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m5s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m22s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 00:49:46 +02:00
ucef 62d1158a8a ci(gitea): utilisation du label ubuntu-latest pour l'environnement de test
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m46s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-23 00:40:55 +02:00
ucef 48c3220013 ci(gitea): utilisation de secrets dédiés pour la BDD de production
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m48s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Has been cancelled
AS Talange CI/CD Pipeline / Deploy to Prod Environment (push) Has been cancelled
2026-06-22 23:52:47 +02:00
ucef f1bd5d9e08 ci(gitea): séparation des déploiements test et prod selon les branches 2026-06-22 23:25:01 +02:00
ucef 5737fb6429 ci(docker): injection des variables Captcha dans docker-compose 2026-06-22 23:25:01 +02:00
ucef 96eee4326a feat(security): configuration dynamique du Captcha Cloudflare Turnstile 2026-06-22 23:25:01 +02:00
ucef 14c83be77a feat(paiements): ajout d'un système d'audit log pour les paiements avec filtres et correction d'accès agent
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m46s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 2m48s
2026-06-22 22:44:14 +02:00
ucef 08dc39f61a fix(ui): élargissement des sections informations et licences adhérent 2026-06-22 22:44:14 +02:00
ucef 84265660b9 fix(ui): masquer les encadrés financiers du dashboard pour les agents de saisie 2026-06-22 22:44:14 +02:00
ucef da33ce934d fix(ui): dynamically display connected username in sidebar across all pages
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m15s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m14s
2026-06-15 23:13:06 +02:00
ucef e867acbb70 feat(licence): add licence search page with CSV export and reorganize sidebar menus
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m4s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Failing after 15s
2026-06-15 23:11:18 +02:00
ucef c4bb9803ed feat(equipements): clean equipment search, fix category regression, and sync dotations on category change 2026-06-15 23:11:05 +02:00
ucef e1cd5930fd feat(paiement): track payment manager (gestionnaire) in history 2026-06-15 23:09:46 +02:00
ucef d913579c77 feat(rgpd): add mandatory consent checkbox to public registration form 2026-06-15 23:09:37 +02:00
ucef 2adbdde121 chore: inject HTMX script in all templates to fix badge bugs 2026-06-15 23:09:23 +02:00
ucef bcf92487ab feat: Export CSV des commandes équipements (Dashboard)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m13s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m18s
- Ajout de la génération de CSV des équipements choisis et non fournis pour la saison active.
- Création du DotationService et ajout de la route /admin/equipements/export-commande.
- Ajout du bouton d'export sur le tableau de bord principal (index.html).
2026-06-14 01:53:16 +02:00
ucef bf4c7b36bd fix(ui): Rendre les tableaux principaux responsives (overflow-x-auto)
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 8m8s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 6m31s
2026-06-14 01:08:12 +02:00
ucef 3ed9d0454b feat: Refonte du schéma Adhérent (Lieu de naissance, Nationalité, Type Licence), suppression du téléphone, email obligatoire 2026-06-14 01:07:45 +02:00
ucef 569927c644 ci: secure docker compose deployments using gitea actions secrets
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m35s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 33s
2026-06-13 00:17:15 +02:00
ucef 7d39e561f7 security: decrease pre-registration token expiration from 10 to 5 minutes
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m58s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m26s
2026-06-13 00:00:42 +02:00
ucef 8dfe71ac97 fix: load admin pre-registrations badge immediately on page load 2026-06-12 23:59:12 +02:00
ucef 87c65c6ebd feat: add admin toggle to open/close public pre-registrations with ui messaging
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m52s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m18s
2026-06-12 23:37:41 +02:00
ucef bb24363500 fix: resolve HTMX POST CSRF token and unhandled ViewResolver 500 error 2026-06-12 23:37:37 +02:00
ucef 19139124ca fix: use correct dummy secret for local Cloudflare Turnstile verification 2026-06-12 23:37:07 +02:00
ucef 2e8298b3b6 Feature: Implement secure public pre-registration flow with CAPTCHA and HTMX dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m12s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m4s
- Created V16 Flyway migration for pre_inscription and token_pre_inscription tables.
- Added PreInscription and TokenPreInscription entities with respective repositories.
- Updated SecurityConfig to allow public access to /inscription-public/**.
- Implemented CaptchaValidationService to interact with Cloudflare Turnstile API.
- Created PublicInscriptionController for managing the QR code entry, CAPTCHA validation, ephemeral tokens, and public form submission.
- Added public Thymeleaf views (demarrer, formulaire, succes, lien-expire) with TailwindCSS.
- Developed AdminPreInscriptionController and PreInscriptionService to handle back-office validation and rejection workflows.
- Built the admin pre-inscriptions dashboard list view.
- Added a dynamic, polling notification badge in the sidebar using HTMX, auto-updating on validation/rejection events.
2026-06-11 23:11:34 +02:00
ucef bca84be16a Refactor: Move equipment price and mandatory status to Categorie relationship
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m34s
- Created CategorieEquipement entity to act as a join table between Category and Equipment.
- Moved 'obligatoire' and 'prix' (formerly prixContribution) fields from Equipement to the new CategorieEquipement entity.
- Added Flyway migrations V14 and V15 to update the database schema and migrate existing data.
- Updated Categorie UI to use a select dropdown and price input for managing category-specific equipment configurations.
- Replaced 'equipement.obligatoire' and 'equipement.prixContribution' calls with their new category-level equivalents in logic and templates.
- Updated pricing calculation in Licence entity to source prices dynamically from the Categorie configuration.
- Updated Dotation generation logic to sync with category requirements automatically.
2026-06-11 22:57:16 +02:00
ucef 1a4ff6fdac style: display total due, paid, and remaining balance in adherents list
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m54s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m5s
2026-06-01 22:58:41 +02:00
ucef aba4ed0770 feat: implement educator management and display educator initials on training schedule
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m2s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m20s
2026-06-01 21:44:02 +02:00
ucef e39921f049 feat: modification et suppression des paiements avec validation dynamique
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m7s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 2m53s
2026-05-31 02:44:00 +02:00
ucef 52fbd5fcf8 fix: refactoring de la gestion HTMX des filtres avec triggers precis
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 3m2s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 3m14s
2026-05-31 02:11:08 +02:00
ucef b6651fabb4 fix: resolution du conflit de formulaires imbriques dans la liste des adherents 2026-05-31 01:57:46 +02:00
ucef 9531019aae feat: redirection vers la page de modification lors de la creation d'un adherent 2026-05-31 01:51:11 +02:00
ucef 499be006c0 ci: ajouter un fichier .gitkeep pour forcer le suivi du dossier src de common
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m56s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Successful in 4m50s
2026-05-31 01:13:25 +02:00
ucef f82021f6fc ci: retirer le montage explicite du socket docker pour eviter le doublon
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m57s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Failing after 3m23s
2026-05-31 01:04:29 +02:00
ucef fc81b9e1ac ci: migration vers une architecture de pipeline basée sur des conteneurs dédiés
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 2m57s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Failing after 2m35s
2026-05-31 00:54:07 +02:00
ucef 5b0edcafef ci: correction pipeline
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 15s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been skipped
2026-05-31 00:52:27 +02:00
ucef ecde6a7b68 ci: corriger le protocole double https dans le clone manuel
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 4m6s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been skipped
2026-05-31 00:33:29 +02:00
ucef afabea33be ci: update pipeline
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 0s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been skipped
2026-05-31 00:17:44 +02:00
ucef e895f042ff ci: exécuter les tests maven dans un conteneur docker éphémère
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 42s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been skipped
2026-05-31 00:05:26 +02:00
ucef d1c8883b82 ci: update pipeline
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Failing after 7s
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been skipped
2026-05-30 23:55:51 +02:00
ucef 7743a220ef ci: ajout de la pipeline Gitea Actions et configuration de l'application dans docker-compose
AS Talange CI/CD Pipeline / Build & Run in Docker Container (push) Has been cancelled
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Has been cancelled
2026-05-30 23:24:51 +02:00
ucef 8a15139efb feat: gestion des utilisateurs, rôles et équipes
- Ajout de la section de gestion des utilisateurs avec rôles (Admin, Trésorerie, Agent de Saisie).
- Restriction des accès par rôle (Admin a tout, Trésorerie a adhérents/paiements, Agent de saisie a adhérents/planning).
- Flux de modification obligatoire de mot de passe temporaire au premier login (par défaut AsTalange123).
- Intégration de la configuration des équipes du club.
2026-05-30 23:11:12 +02:00
ucef 7e9504ffe0 feat: implement server-side pagination, eager query loading, and dynamic column filtering via HTMX for Payment History 2026-05-30 00:24:41 +02:00
ucef 1d259831c6 feat: implement server-side pagination, eager query loading, and dynamic column filtering via HTMX 2026-05-30 00:14:07 +02:00
ucef 3ae296aa91 fix: recalculate member category and tariffs dynamically on birthdate change 2026-05-30 00:14:02 +02:00
ucef 0baca4a334 feat: add weekly training schedule planning with Gantt chart and conflict detection 2026-05-30 00:13:55 +02:00
ucef a429e67133 feat: implémentation de la gestion des saisons (UI, service, refactoring sidebar) 2026-05-29 22:35:33 +02:00
ucef dd31d73fed feat: show dynamic labels for all equipments based on gender and playing style 2026-05-20 21:44:34 +02:00
ucef cac229486a feat: recalculate license and remaining prices dynamically on residency checkbox change 2026-05-20 19:12:56 +02:00
ucef 1f803a6ecc feat: add gender and goalkeeper/field player jersey style customization for members 2026-05-20 19:04:34 +02:00
ucef 1af0e177d5 feat: implement global pricing model, optional equipment contribution, and bulk save for dotations 2026-05-20 19:00:10 +02:00
ucef 2a116b1e02 feat: restrict license creation to only one license per adherent 2026-05-19 22:53:05 +02:00
ucef bcd4a94973 fix: rename loop variable eq to equip in equipements_list to avoid clash with reserved keyword 2026-05-19 22:45:54 +02:00
ucef 5cd807f916 feat: make all Flyway SQL migrations fully idempotent 2026-05-19 22:39:19 +02:00
ucef 4f140775a2 fix: change flyway ignore patterns to validate-on-migrate false 2026-05-19 22:30:32 +02:00
147 changed files with 8023 additions and 471 deletions
+77
View File
@@ -0,0 +1,77 @@
name: AS Talange CI/CD Pipeline - Production
on:
push:
branches:
- 'prod/*'
jobs:
test:
name: Build & Run Unit Tests
runs-on: [self-hosted, prod]
container:
image: maven:3.9.6-eclipse-temurin-21
steps:
- name: Clone repository
run: |
if ! command -v git &> /dev/null; then
apt-get update && apt-get install -y git
fi
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Run Maven Tests
run: mvn clean test
deploy_prod:
name: Deploy to Prod Environment
needs: test
if: startsWith(github.ref, 'refs/heads/prod/')
runs-on: [self-hosted, prod]
container:
image: docker:latest
steps:
- name: Clone repository
run: |
apk add --no-cache git
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Deploy with Docker Compose
env:
PROD_DB_USER: ${{ secrets.PROD_DB_USER }}
PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
CAPTCHA_SECRET: ${{ secrets.CAPTCHA_SECRET }}
CAPTCHA_SITEKEY: ${{ secrets.CAPTCHA_SITEKEY }}
run: |
echo "POSTGRES_USER=${PROD_DB_USER:-myuser}" > .env
echo "POSTGRES_PASSWORD=${PROD_DB_PASSWORD:-mypassword}" >> .env
echo "CAPTCHA_SECRET=${CAPTCHA_SECRET:-1x0000000000000000000000000000000AA}" >> .env
echo "CAPTCHA_SITEKEY=${CAPTCHA_SITEKEY:-1x00000000000000000000AA}" >> .env
docker compose down app || true
docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+73
View File
@@ -0,0 +1,73 @@
name: AS Talange CI/CD Pipeline
on:
push:
branches:
- main
jobs:
test:
name: Build & Run Unit Tests
runs-on: [self-hosted, ubuntu-latest]
container:
image: maven:3.9.6-eclipse-temurin-21
steps:
- name: Clone repository
run: |
if ! command -v git &> /dev/null; then
apt-get update && apt-get install -y git
fi
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Run Maven Tests
run: mvn clean test
deploy_test:
name: Deploy to Test Environment
needs: test
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, ubuntu-latest]
container:
image: docker:latest
steps:
- name: Clone repository
run: |
apk add --no-cache git
SERVER_URL="${{ github.server_url }}"
SERVER_NO_PROTO="${SERVER_URL#http://}"
SERVER_NO_PROTO="${SERVER_NO_PROTO#https://}"
git clone "https://oauth2:${{ secrets.GITHUB_TOKEN }}@${SERVER_NO_PROTO}/${{ github.repository }}.git" .
git checkout ${{ github.sha }}
- name: Deploy with Docker Compose
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
echo "POSTGRES_USER=${DB_USER:-myuser}" > .env
echo "POSTGRES_PASSWORD=${DB_PASSWORD:-mypassword}" >> .env
echo "CAPTCHA_SECRET=1x0000000000000000000000000000000AA" >> .env
echo "CAPTCHA_SITEKEY=1x00000000000000000000AA" >> .env
docker compose down app || true
docker compose up -d --build app
- name: Verify application startup
run: |
echo "Waiting for application to start..."
COUNT=0
while [ $COUNT -lt 30 ]; do
if docker exec astalange_app wget -qO- http://localhost:8080/login | grep -q "Connexion"; then
echo "Application is up and login page is accessible!"
exit 0
fi
echo "Waiting... ($COUNT/30)"
sleep 5
COUNT=$((COUNT+1))
done
echo "Application failed to start or login page is not accessible."
docker logs astalange_app
exit 1
+3
View File
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.6-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
+1
View File
@@ -0,0 +1 @@
# Placeholder to ensure git tracks this directory
+7 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.6-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -61,5 +61,11 @@
<version>1.77</version> <version>1.77</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -23,22 +23,61 @@ public class Adherent {
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE) @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
private LocalDate dateNaissance; private LocalDate dateNaissance;
@Column(length = 20) @Column(name = "lieu_naissance_ville", nullable = false, length = 100)
private String telephone; private String lieuNaissanceVille;
@Column(length = 100) @Column(name = "lieu_naissance_pays", nullable = false, length = 100)
private String lieuNaissancePays;
@Column(nullable = false, length = 100)
private String nationalite;
@Column(nullable = false, length = 100)
private String email; private String email;
@Column(columnDefinition = "TEXT") @Column(nullable = false, length = 20)
private String adresse; private String telephone;
@Column(name = "representant_legal", length = 150) @Column(name = "representant_legal", length = 150)
private String representantLegal; private String representantLegal;
@Column(name = "type_demande", length = 50)
private String typeDemande;
@Column(name = "ancien_club", length = 150)
private String ancienClub;
@Column(name = "ancienne_categorie", length = 50)
private String ancienneCategorie;
@Column(name = "raison_changement_club")
private String raisonChangementClub;
@Column(name = "comment_connu_club")
private String commentConnuClub;
@Column(name = "taille_vetement", length = 50)
private String tailleVetement;
@Column(length = 50)
private String pointure;
private boolean residentTalange = true; private boolean residentTalange = true;
@Column(nullable = false, length = 10)
private String sexe = "MASCULIN";
@Column(name = "type_maillot", nullable = false, length = 20)
private String typeMaillot = "JOUEUR";
// Getters and Setters // Getters and Setters
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public String getTypeMaillot() { return typeMaillot; }
public void setTypeMaillot(String typeMaillot) { this.typeMaillot = typeMaillot; }
public Long getId() { return id; } public Long getId() { return id; }
public void setId(Long id) { this.id = id; } public void setId(Long id) { this.id = id; }
@@ -51,18 +90,45 @@ public class Adherent {
public LocalDate getDateNaissance() { return dateNaissance; } public LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; } public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
public String getTelephone() { return telephone; } public String getLieuNaissanceVille() { return lieuNaissanceVille; }
public void setTelephone(String telephone) { this.telephone = telephone; } public void setLieuNaissanceVille(String lieuNaissanceVille) { this.lieuNaissanceVille = lieuNaissanceVille; }
public String getLieuNaissancePays() { return lieuNaissancePays; }
public void setLieuNaissancePays(String lieuNaissancePays) { this.lieuNaissancePays = lieuNaissancePays; }
public String getNationalite() { return nationalite; }
public void setNationalite(String nationalite) { this.nationalite = nationalite; }
public String getEmail() { return email; } public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; } public void setEmail(String email) { this.email = email; }
public String getAdresse() { return adresse; } public String getTelephone() { return telephone; }
public void setAdresse(String adresse) { this.adresse = adresse; } public void setTelephone(String telephone) { this.telephone = telephone; }
public String getRepresentantLegal() { return representantLegal; } public String getRepresentantLegal() { return representantLegal; }
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; } public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
public String getTypeDemande() { return typeDemande; }
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
public String getAncienClub() { return ancienClub; }
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
public String getAncienneCategorie() { return ancienneCategorie; }
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
public String getRaisonChangementClub() { return raisonChangementClub; }
public void setRaisonChangementClub(String raisonChangementClub) { this.raisonChangementClub = raisonChangementClub; }
public String getCommentConnuClub() { return commentConnuClub; }
public void setCommentConnuClub(String commentConnuClub) { this.commentConnuClub = commentConnuClub; }
public String getTailleVetement() { return tailleVetement; }
public void setTailleVetement(String tailleVetement) { this.tailleVetement = tailleVetement; }
public String getPointure() { return pointure; }
public void setPointure(String pointure) { this.pointure = pointure; }
public boolean isResidentTalange() { return residentTalange; } public boolean isResidentTalange() { return residentTalange; }
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; } public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
@@ -26,6 +26,9 @@ public class AppUser {
@Column(nullable = false) @Column(nullable = false)
private boolean enabled = true; private boolean enabled = true;
@Column(name = "must_change_password", nullable = false)
private boolean mustChangePassword = false;
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
@JoinTable( @JoinTable(
name = "user_role", name = "user_role",
@@ -0,0 +1,104 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.math.BigDecimal;
@Entity
@Table(name = "audit_log_paiement")
public class AuditLogPaiement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String action; // CREATE, UPDATE, DELETE
@Column(nullable = false)
private LocalDateTime dateAction;
@Column(nullable = false)
private String utilisateur; // Username or 'Système'
@Column(nullable = true)
private Long paiementId; // Can be null if the payment is completely deleted or we just want to keep track
@Column(nullable = false, length = 1000)
private String details;
@Column(nullable = true)
private BigDecimal montant;
@Column(nullable = true)
private String adherentNomComplet;
@PrePersist
protected void onCreate() {
this.dateAction = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public LocalDateTime getDateAction() {
return dateAction;
}
public void setDateAction(LocalDateTime dateAction) {
this.dateAction = dateAction;
}
public String getUtilisateur() {
return utilisateur;
}
public void setUtilisateur(String utilisateur) {
this.utilisateur = utilisateur;
}
public Long getPaiementId() {
return paiementId;
}
public void setPaiementId(Long paiementId) {
this.paiementId = paiementId;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public BigDecimal getMontant() {
return montant;
}
public void setMontant(BigDecimal montant) {
this.montant = montant;
}
public String getAdherentNomComplet() {
return adherentNomComplet;
}
public void setAdherentNomComplet(String adherentNomComplet) {
this.adherentNomComplet = adherentNomComplet;
}
}
@@ -2,18 +2,25 @@ package com.astalange.core.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity @Entity
@Table(name = "categorie") @Table(name = "categorie", uniqueConstraints = {
@UniqueConstraint(columnNames = {"nom", "saison_id"})
})
public class Categorie { public class Categorie {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false, unique = true, length = 20) @Column(nullable = false, length = 20)
private String nom; private String nom;
@Column(nullable = false, length = 10)
private String sexe = "MASCULIN";
@Column(name = "annee_min") @Column(name = "annee_min")
private Integer anneeMin; private Integer anneeMin;
@@ -26,6 +33,13 @@ public class Categorie {
@Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2) @Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2)
private BigDecimal tarifExterieur; private BigDecimal tarifExterieur;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@OneToMany(mappedBy = "categorie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CategorieEquipement> categorieEquipements = new ArrayList<>();
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -34,6 +48,9 @@ public class Categorie {
public String getNom() { return nom; } public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; } public void setNom(String nom) { this.nom = nom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public Integer getAnneeMin() { return anneeMin; } public Integer getAnneeMin() { return anneeMin; }
public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; } public void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
@@ -45,4 +62,61 @@ public class Categorie {
public BigDecimal getTarifExterieur() { return tarifExterieur; } public BigDecimal getTarifExterieur() { return tarifExterieur; }
public void setTarifExterieur(BigDecimal tarifExterieur) { this.tarifExterieur = tarifExterieur; } public void setTarifExterieur(BigDecimal tarifExterieur) { this.tarifExterieur = tarifExterieur; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
public List<CategorieEquipement> getCategorieEquipements() { return categorieEquipements; }
public void setCategorieEquipements(List<CategorieEquipement> categorieEquipements) { this.categorieEquipements = categorieEquipements; }
@Transient
public boolean hasEquipement(Long equipementId) {
if (categorieEquipements == null) return false;
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId));
}
@Transient
public boolean isEquipementObligatoire(Long equipementId) {
if (categorieEquipements == null) return false;
return categorieEquipements.stream().anyMatch(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId) && Boolean.TRUE.equals(ce.getObligatoire()));
}
@Transient
public BigDecimal getEquipementPrix(Long equipementId) {
if (categorieEquipements == null) return BigDecimal.ZERO;
return categorieEquipements.stream()
.filter(ce -> ce.getEquipement() != null && ce.getEquipement().getId().equals(equipementId))
.findFirst()
.map(CategorieEquipement::getPrix)
.orElse(BigDecimal.ZERO);
}
@Transient
public BigDecimal getEquipementPrixByNom(String nom) {
if (categorieEquipements == null || nom == null) return BigDecimal.ZERO;
return categorieEquipements.stream()
.filter(ce -> ce.getEquipement() != null && nom.equalsIgnoreCase(ce.getEquipement().getNom()))
.findFirst()
.map(CategorieEquipement::getPrix)
.map(p -> p == null ? BigDecimal.ZERO : p)
.orElse(BigDecimal.ZERO);
}
@Transient
public boolean isU11OrBelow() {
if (nom == null) return false;
String nomCat = nom.trim().toUpperCase();
if (nomCat.startsWith("U")) {
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^U(\\d+)").matcher(nomCat);
if (m.find()) {
try {
int age = Integer.parseInt(m.group(1));
return age <= 11;
} catch (NumberFormatException e) {
return false;
}
}
}
return false;
}
} }
@@ -0,0 +1,43 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "categorie_equipement")
public class CategorieEquipement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "equipement_id", nullable = false)
private Equipement equipement;
@Column(nullable = false)
private Boolean obligatoire = false;
@Column(nullable = false, precision = 10, scale = 2)
private java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
// Getters and Setters
public java.math.BigDecimal getPrix() { return prix; }
public void setPrix(java.math.BigDecimal prix) { this.prix = prix; }
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public Equipement getEquipement() { return equipement; }
public void setEquipement(Equipement equipement) { this.equipement = equipement; }
public Boolean getObligatoire() { return obligatoire; }
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
}
@@ -0,0 +1,228 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalTime;
import java.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "creneau_entrainement")
public class CreneauEntrainement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "terrain_id", nullable = false)
private Terrain terrain;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private ZoneTerrain zone;
@Enumerated(EnumType.STRING)
@Column(name = "jour_semaine", nullable = false)
private JourSemaine jourSemaine;
@Column(name = "heure_debut", nullable = false)
private LocalTime heureDebut;
@Column(name = "heure_fin", nullable = false)
private LocalTime heureFin;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipe_id")
private Equipe equipe;
@Transient
private boolean enConflit = false;
@Transient
private Set<String> categoriesEnConflit = new HashSet<>();
@Transient
private String initialesEducateurs = "";
// Logic for Gantt diagram grid placement
public int getStartColumn() {
if (heureDebut == null) return 3;
// Clip to working hours 08:00 to 22:00
LocalTime time = heureDebut;
if (time.isBefore(LocalTime.of(8, 0))) {
time = LocalTime.of(8, 0);
}
long minutes = Duration.between(LocalTime.of(8, 0), time).toMinutes();
return (int) (minutes / 15) + 3; // +3 because col 1 is Field name, col 2 is Zone name
}
public int getEndColumn() {
if (heureFin == null) return 3;
LocalTime time = heureFin;
if (time.isAfter(LocalTime.of(22, 0))) {
time = LocalTime.of(22, 0);
}
long minutes = Duration.between(LocalTime.of(8, 0), time).toMinutes();
return (int) (minutes / 15) + 3;
}
public int getStartRow(int fieldIndex) {
int baseRow = 3 + (fieldIndex * 5); // Row index matching HTML layout (base starts at 3, 5 rows per field)
if (zone == null) return baseRow;
return switch (zone) {
case COMPLET, DEMI_A, QUART_1 -> baseRow;
case QUART_2 -> baseRow + 1;
case DEMI_B, QUART_3 -> baseRow + 2;
case QUART_4 -> baseRow + 3;
};
}
public int getEndRow(int fieldIndex) {
int baseRow = 3 + (fieldIndex * 5);
if (zone == null) return baseRow + 4;
return switch (zone) {
case QUART_1 -> baseRow + 1;
case DEMI_A, QUART_2 -> baseRow + 2;
case QUART_3 -> baseRow + 3;
case DEMI_B, QUART_4, COMPLET -> baseRow + 4;
};
}
// Overlap and conflict checking logic
public boolean overlapsWith(CreneauEntrainement other) {
if (this.id != null && this.id.equals(other.id)) {
return false; // same slot
}
if (this.jourSemaine != other.jourSemaine) {
return false;
}
if (this.terrain == null || other.terrain == null || !Objects.equals(this.terrain.getId(), other.terrain.getId())) {
return false;
}
// Time overlap check: start1 < end2 and start2 < end1
boolean timeOverlap = this.heureDebut.isBefore(other.heureFin) && other.heureDebut.isBefore(this.heureFin);
if (!timeOverlap) {
return false;
}
// Spatial overlap check
if (this.zone == null || other.zone == null) {
return false;
}
Set<Integer> q1 = this.zone.getQuarters();
Set<Integer> q2 = other.zone.getQuarters();
return !Collections.disjoint(q1, q2);
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Categorie getCategorie() {
return categorie;
}
public void setCategorie(Categorie categorie) {
this.categorie = categorie;
}
public Terrain getTerrain() {
return terrain;
}
public void setTerrain(Terrain terrain) {
this.terrain = terrain;
}
public ZoneTerrain getZone() {
return zone;
}
public void setZone(ZoneTerrain zone) {
this.zone = zone;
}
public JourSemaine getJourSemaine() {
return jourSemaine;
}
public void setJourSemaine(JourSemaine jourSemaine) {
this.jourSemaine = jourSemaine;
}
public LocalTime getHeureDebut() {
return heureDebut;
}
public void setHeureDebut(LocalTime heureDebut) {
this.heureDebut = heureDebut;
}
public LocalTime getHeureFin() {
return heureFin;
}
public void setHeureFin(LocalTime heureFin) {
this.heureFin = heureFin;
}
public Saison getSaison() {
return saison;
}
public void setSaison(Saison saison) {
this.saison = saison;
}
public Equipe getEquipe() {
return equipe;
}
public void setEquipe(Equipe equipe) {
this.equipe = equipe;
}
public boolean isEnConflit() {
return enConflit;
}
public void setEnConflit(boolean enConflit) {
this.enConflit = enConflit;
}
public Set<String> getCategoriesEnConflit() {
return categoriesEnConflit;
}
public void setCategoriesEnConflit(Set<String> categoriesEnConflit) {
this.categoriesEnConflit = categoriesEnConflit;
}
public String getInitialesEducateurs() {
return initialesEducateurs;
}
public void setInitialesEducateurs(String initialesEducateurs) {
this.initialesEducateurs = initialesEducateurs;
}
}
@@ -18,15 +18,24 @@ public class Dotation {
@JoinColumn(name = "equipement_id", nullable = false) @JoinColumn(name = "equipement_id", nullable = false)
private Equipement equipement; private Equipement equipement;
@Column(length = 10) @Column(length = 50)
private String taille; private String taille;
@Column(length = 50)
private String couleur;
@Column(length = 50) @Column(length = 50)
private String flocage; private String flocage;
@Column(nullable = false) @Column(nullable = false)
private Boolean fourni = false; private Boolean fourni = false;
@Column(length = 50)
private String numero;
@Column(nullable = false)
private Boolean choisi = false;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -41,9 +50,18 @@ public class Dotation {
public String getTaille() { return taille; } public String getTaille() { return taille; }
public void setTaille(String taille) { this.taille = taille; } public void setTaille(String taille) { this.taille = taille; }
public String getCouleur() { return couleur; }
public void setCouleur(String couleur) { this.couleur = couleur; }
public String getFlocage() { return flocage; } public String getFlocage() { return flocage; }
public void setFlocage(String flocage) { this.flocage = flocage; } public void setFlocage(String flocage) { this.flocage = flocage; }
public Boolean getFourni() { return fourni; } public Boolean getFourni() { return fourni; }
public void setFourni(Boolean fourni) { this.fourni = fourni; } public void setFourni(Boolean fourni) { this.fourni = fourni; }
public String getNumero() { return numero; }
public void setNumero(String numero) { this.numero = numero; }
public Boolean getChoisi() { return choisi; }
public void setChoisi(Boolean choisi) { this.choisi = choisi; }
} }
@@ -0,0 +1,61 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "educateur")
public class Educateur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String nom;
@Column(nullable = false, length = 100)
private String prenom;
@Column(length = 100)
private String email;
@Column(length = 20)
private String telephone;
@ManyToOne
@JoinColumn(name = "categorie_id")
private Categorie categorie;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipe_id")
private Equipe equipe;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public String getPrenom() { return prenom; }
public void setPrenom(String prenom) { this.prenom = prenom; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getTelephone() { return telephone; }
public void setTelephone(String telephone) { this.telephone = telephone; }
public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public Equipe getEquipe() { return equipe; }
public void setEquipe(Equipe equipe) { this.equipe = equipe; }
public String getInitiales() {
String p = (prenom != null && !prenom.isEmpty()) ? prenom.substring(0, 1).toUpperCase() : "";
String n = (nom != null && !nom.isEmpty()) ? nom.substring(0, 1).toUpperCase() : "";
return p + n;
}
}
@@ -0,0 +1,59 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "equipe")
public class Equipe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nom;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
// Constructors
public Equipe() {}
public Equipe(String nom, Categorie categorie, Saison saison) {
this.nom = nom;
this.categorie = categorie;
this.saison = saison;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Equipe equipe = (Equipe) o;
return Objects.equals(id, equipe.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -1,6 +1,7 @@
package com.astalange.core.entity; package com.astalange.core.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity @Entity
@Table(name = "equipement") @Table(name = "equipement")
@@ -13,11 +14,37 @@ public class Equipement {
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String nom; private String nom;
@Column(length = 100)
private String reference;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
private String description; private String description;
@Column(nullable = false) @Column(name = "gestion_sexe", nullable = false)
private Boolean obligatoire = false; private boolean gestionSexe = false;
@Column(name = "type_public", nullable = false, length = 20)
private String typePublic = "TOUS"; // JOUEUR, GARDIEN, TOUS
@Column(name = "tailles_disponibles", length = 500)
private String taillesDisponibles;
@Column(name = "couleurs_disponibles", length = 500)
private String couleursDisponibles;
@Column(name = "has_flocage_prenom", nullable = false)
private boolean hasFlocagePrenom = false;
@Column(name = "has_flocage_initiales", nullable = false)
private boolean hasFlocageInitiales = false;
@Column(name = "has_flocage_numero", nullable = false)
private boolean hasFlocageNumero = false;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
// Getters and Setters // Getters and Setters
@@ -27,9 +54,33 @@ public class Equipement {
public String getNom() { return nom; } public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; } public void setNom(String nom) { this.nom = nom; }
public String getReference() { return reference; }
public void setReference(String reference) { this.reference = reference; }
public String getDescription() { return description; } public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; } public void setDescription(String description) { this.description = description; }
public Boolean getObligatoire() { return obligatoire; } public boolean isGestionSexe() { return gestionSexe; }
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; } public void setGestionSexe(boolean gestionSexe) { this.gestionSexe = gestionSexe; }
public String getTypePublic() { return typePublic; }
public void setTypePublic(String typePublic) { this.typePublic = typePublic; }
public String getTaillesDisponibles() { return taillesDisponibles; }
public void setTaillesDisponibles(String taillesDisponibles) { this.taillesDisponibles = taillesDisponibles; }
public String getCouleursDisponibles() { return couleursDisponibles; }
public void setCouleursDisponibles(String couleursDisponibles) { this.couleursDisponibles = couleursDisponibles; }
public boolean isHasFlocagePrenom() { return hasFlocagePrenom; }
public void setHasFlocagePrenom(boolean hasFlocagePrenom) { this.hasFlocagePrenom = hasFlocagePrenom; }
public boolean isHasFlocageInitiales() { return hasFlocageInitiales; }
public void setHasFlocageInitiales(boolean hasFlocageInitiales) { this.hasFlocageInitiales = hasFlocageInitiales; }
public boolean isHasFlocageNumero() { return hasFlocageNumero; }
public void setHasFlocageNumero(boolean hasFlocageNumero) { this.hasFlocageNumero = hasFlocageNumero; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
} }
@@ -0,0 +1,21 @@
package com.astalange.core.entity;
public enum JourSemaine {
LUNDI("Lundi"),
MARDI("Mardi"),
MERCREDI("Mercredi"),
JEUDI("Jeudi"),
VENDREDI("Vendredi"),
SAMEDI("Samedi"),
DIMANCHE("Dimanche");
private final String libelle;
JourSemaine(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
@@ -21,8 +21,13 @@ public class Licence {
@JoinColumn(name = "categorie_id", nullable = false) @JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie; private Categorie categorie;
@Column(nullable = false, length = 20) @ManyToOne(optional = false, fetch = FetchType.LAZY)
private String saison; @JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "equipe_id")
private Equipe equipe;
@Column(nullable = false, length = 50) @Column(nullable = false, length = 50)
private String etat; private String etat;
@@ -30,15 +35,24 @@ public class Licence {
@Column(name = "numero_licence", length = 50) @Column(name = "numero_licence", length = 50)
private String numeroLicence; private String numeroLicence;
@Column(name = "type_demande", length = 50)
private String typeDemande;
@Column(name = "type_licence", length = 50)
private String typeLicence;
@Column(columnDefinition = "TEXT")
private String commentaire;
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Paiement> paiements = new ArrayList<>(); private List<Paiement> paiements = new ArrayList<>();
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Dotation> dotations = new ArrayList<>(); private List<Dotation> dotations = new ArrayList<>();
// Logic for Reste A Payer // Logic for Global Total Price
@Transient @Transient
public BigDecimal getResteAPayer() { public BigDecimal getPrixTotal() {
if (categorie == null) return BigDecimal.ZERO; if (categorie == null) return BigDecimal.ZERO;
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange()) BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
@@ -47,6 +61,25 @@ public class Licence {
if (prixTotal == null) prixTotal = BigDecimal.ZERO; if (prixTotal == null) prixTotal = BigDecimal.ZERO;
if (dotations != null) {
for (Dotation dot : dotations) {
if (dot.getChoisi() && dot.getEquipement() != null) {
BigDecimal price = categorie.getEquipementPrix(dot.getEquipement().getId());
if (price != null) {
prixTotal = prixTotal.add(price);
}
}
}
}
return prixTotal;
}
// Logic for Reste A Payer
@Transient
public BigDecimal getResteAPayer() {
BigDecimal prixTotal = getPrixTotal();
if (paiements == null || paiements.isEmpty()) { if (paiements == null || paiements.isEmpty()) {
return prixTotal; return prixTotal;
} }
@@ -59,6 +92,16 @@ public class Licence {
return reste.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : reste; return reste.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : reste;
} }
@Transient
public BigDecimal getSommePayee() {
if (paiements == null || paiements.isEmpty()) {
return BigDecimal.ZERO;
}
return paiements.stream()
.map(Paiement::getMontant)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -70,21 +113,33 @@ public class Licence {
public Categorie getCategorie() { return categorie; } public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; } public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public String getSaison() { return saison; } public Saison getSaison() { return saison; }
public void setSaison(String saison) { this.saison = saison; } public void setSaison(Saison saison) { this.saison = saison; }
public Equipe getEquipe() { return equipe; }
public void setEquipe(Equipe equipe) { this.equipe = equipe; }
public String getEtat() { return etat; } public String getEtat() { return etat; }
public void setEtat(String etat) { this.etat = etat; } public void setEtat(String etat) { this.etat = etat; }
public String getNumeroLicence() { return numeroLicence; }
public void setNumeroLicence(String numeroLicence) { this.numeroLicence = numeroLicence; }
public String getTypeDemande() { return typeDemande; }
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
public String getTypeLicence() { return typeLicence; }
public void setTypeLicence(String typeLicence) { this.typeLicence = typeLicence; }
public String getCommentaire() { return commentaire; }
public void setCommentaire(String commentaire) { this.commentaire = commentaire; }
public List<Paiement> getPaiements() { return paiements; } public List<Paiement> getPaiements() { return paiements; }
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; } public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
public List<Dotation> getDotations() { return dotations; } public List<Dotation> getDotations() { return dotations; }
public void setDotations(List<Dotation> dotations) { this.dotations = dotations; } public void setDotations(List<Dotation> dotations) { this.dotations = dotations; }
public String getNumeroLicence() { return numeroLicence; }
public void setNumeroLicence(String numeroLicence) { this.numeroLicence = numeroLicence; }
public void addPaiement(Paiement paiement) { public void addPaiement(Paiement paiement) {
paiements.add(paiement); paiements.add(paiement);
paiement.setLicence(this); paiement.setLicence(this);
@@ -26,6 +26,15 @@ public class Paiement {
@JoinColumn(name = "mode_paiement_id", nullable = false) @JoinColumn(name = "mode_paiement_id", nullable = false)
private ModePaiement modePaiement; private ModePaiement modePaiement;
@Column(name = "gestionnaire")
private String gestionnaire;
@Column(name = "numero_cheque", length = 50)
private String numeroCheque;
@Column(name = "commentaire", columnDefinition = "TEXT")
private String commentaire;
// Getters and Setters // Getters and Setters
public Long getId() { return id; } public Long getId() { return id; }
@@ -42,4 +51,13 @@ public class Paiement {
public ModePaiement getModePaiement() { return modePaiement; } public ModePaiement getModePaiement() { return modePaiement; }
public void setModePaiement(ModePaiement modePaiement) { this.modePaiement = modePaiement; } public void setModePaiement(ModePaiement modePaiement) { this.modePaiement = modePaiement; }
public String getGestionnaire() { return gestionnaire; }
public void setGestionnaire(String gestionnaire) { this.gestionnaire = gestionnaire; }
public String getNumeroCheque() { return numeroCheque; }
public void setNumeroCheque(String numeroCheque) { this.numeroCheque = numeroCheque; }
public String getCommentaire() { return commentaire; }
public void setCommentaire(String commentaire) { this.commentaire = commentaire; }
} }
@@ -0,0 +1,145 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "pre_inscription")
public class PreInscription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String nom;
@Column(nullable = false, length = 100)
private String prenom;
@Column(nullable = false, length = 10)
private String sexe;
@Column(name = "date_naissance", nullable = false)
private LocalDate dateNaissance;
@Column(name = "lieu_naissance_ville", nullable = false, length = 100)
private String lieuNaissanceVille;
@Column(name = "lieu_naissance_pays", nullable = false, length = 100)
private String lieuNaissancePays;
@Column(nullable = false, length = 100)
private String nationalite;
@Column(nullable = false, length = 150)
private String email;
@Column(nullable = false, length = 20)
private String telephone;
@Column(name = "representant_legal", length = 150)
private String representantLegal;
@Column(name = "statut_traite", nullable = false)
private Boolean statutTraite = false;
@Column(name = "date_creation", nullable = false)
private LocalDateTime dateCreation = LocalDateTime.now();
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
@Column(name = "consentement_rgpd", nullable = false)
private Boolean consentementRgpd = false;
@Column(name = "type_demande", nullable = false, length = 50)
private String typeDemande = "NOUVELLE";
@Column(name = "ancien_club", length = 150)
private String ancienClub;
@Column(name = "ancienne_categorie", length = 50)
private String ancienneCategorie = "NA";
@Column(name = "raison_changement_club")
private String raisonChangementClub;
@Column(name = "comment_connu_club")
private String commentConnuClub;
@Column(name = "taille_vetement", length = 50)
private String tailleVetement;
@Column(length = 50)
private String pointure;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public String getPrenom() { return prenom; }
public void setPrenom(String prenom) { this.prenom = prenom; }
public String getSexe() { return sexe; }
public void setSexe(String sexe) { this.sexe = sexe; }
public LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
public String getLieuNaissanceVille() { return lieuNaissanceVille; }
public void setLieuNaissanceVille(String lieuNaissanceVille) { this.lieuNaissanceVille = lieuNaissanceVille; }
public String getLieuNaissancePays() { return lieuNaissancePays; }
public void setLieuNaissancePays(String lieuNaissancePays) { this.lieuNaissancePays = lieuNaissancePays; }
public String getNationalite() { return nationalite; }
public void setNationalite(String nationalite) { this.nationalite = nationalite; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getTelephone() { return telephone; }
public void setTelephone(String telephone) { this.telephone = telephone; }
public String getRepresentantLegal() { return representantLegal; }
public void setRepresentantLegal(String representantLegal) { this.representantLegal = representantLegal; }
public Boolean getStatutTraite() { return statutTraite; }
public void setStatutTraite(Boolean statutTraite) { this.statutTraite = statutTraite; }
public LocalDateTime getDateCreation() { return dateCreation; }
public void setDateCreation(LocalDateTime dateCreation) { this.dateCreation = dateCreation; }
public Saison getSaison() { return saison; }
public void setSaison(Saison saison) { this.saison = saison; }
public Boolean getConsentementRgpd() { return consentementRgpd; }
public void setConsentementRgpd(Boolean consentementRgpd) { this.consentementRgpd = consentementRgpd; }
public String getTypeDemande() { return typeDemande; }
public void setTypeDemande(String typeDemande) { this.typeDemande = typeDemande; }
public String getAncienClub() { return ancienClub; }
public void setAncienClub(String ancienClub) { this.ancienClub = ancienClub; }
public String getAncienneCategorie() { return ancienneCategorie; }
public void setAncienneCategorie(String ancienneCategorie) { this.ancienneCategorie = ancienneCategorie; }
public String getRaisonChangementClub() { return raisonChangementClub; }
public void setRaisonChangementClub(String raisonChangementClub) { this.raisonChangementClub = raisonChangementClub; }
public String getCommentConnuClub() { return commentConnuClub; }
public void setCommentConnuClub(String commentConnuClub) { this.commentConnuClub = commentConnuClub; }
public String getTailleVetement() { return tailleVetement; }
public void setTailleVetement(String tailleVetement) { this.tailleVetement = tailleVetement; }
public String getPointure() { return pointure; }
public void setPointure(String pointure) { this.pointure = pointure; }
}
@@ -0,0 +1,71 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "saison")
public class Saison {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nom;
@Column(name = "est_active", nullable = false)
private Boolean estActive = false;
@Column(name = "inscriptions_ouvertes", nullable = false)
private Boolean inscriptionsOuvertes = false;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public Boolean getEstActive() {
return estActive;
}
public void setEstActive(Boolean estActive) {
this.estActive = estActive;
}
public Boolean getInscriptionsOuvertes() {
return inscriptionsOuvertes != null ? inscriptionsOuvertes : false;
}
public void setInscriptionsOuvertes(Boolean inscriptionsOuvertes) {
this.inscriptionsOuvertes = inscriptionsOuvertes;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Saison saison = (Saison) o;
return Objects.equals(id, saison.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@@ -0,0 +1,31 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "terrain")
public class Terrain {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nom;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
}
@@ -0,0 +1,40 @@
package com.astalange.core.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "token_pre_inscription")
public class TokenPreInscription {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "valeur_uuid", nullable = false, unique = true, length = 36)
private String valeurUuid;
@Column(name = "date_expiration", nullable = false)
private LocalDateTime dateExpiration;
@Column(name = "est_utilise", nullable = false)
private Boolean estUtilise = false;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getValeurUuid() { return valeurUuid; }
public void setValeurUuid(String valeurUuid) { this.valeurUuid = valeurUuid; }
public LocalDateTime getDateExpiration() { return dateExpiration; }
public void setDateExpiration(LocalDateTime dateExpiration) { this.dateExpiration = dateExpiration; }
public Boolean getEstUtilise() { return estUtilise; }
public void setEstUtilise(Boolean estUtilise) { this.estUtilise = estUtilise; }
@Transient
public boolean isValide() {
return !estUtilise && dateExpiration.isAfter(LocalDateTime.now());
}
}
@@ -0,0 +1,29 @@
package com.astalange.core.entity;
import java.util.Set;
public enum ZoneTerrain {
COMPLET("Terrain Complet", Set.of(1, 2, 3, 4)),
DEMI_A("Demi-terrain A", Set.of(1, 2)),
DEMI_B("Demi-terrain B", Set.of(3, 4)),
QUART_1("Quart 1", Set.of(1)),
QUART_2("Quart 2", Set.of(2)),
QUART_3("Quart 3", Set.of(3)),
QUART_4("Quart 4", Set.of(4));
private final String libelle;
private final Set<Integer> quarters;
ZoneTerrain(String libelle, Set<Integer> quarters) {
this.libelle = libelle;
this.quarters = quarters;
}
public String getLibelle() {
return libelle;
}
public Set<Integer> getQuarters() {
return quarters;
}
}
@@ -9,4 +9,10 @@ import java.util.List;
@Repository @Repository
public interface AdherentRepository extends JpaRepository<Adherent, Long> { public interface AdherentRepository extends JpaRepository<Adherent, Long> {
List<Adherent> findTop5ByOrderByIdDesc(); List<Adherent> findTop5ByOrderByIdDesc();
@org.springframework.data.jpa.repository.Query("SELECT DISTINCT a FROM Adherent a LEFT JOIN FETCH a.licences l LEFT JOIN FETCH l.categorie c")
List<Adherent> findAllWithLicencesAndCategories();
@org.springframework.data.jpa.repository.Query("SELECT DISTINCT a FROM Adherent a LEFT JOIN a.licences l WHERE LOWER(a.nom) LIKE LOWER(CONCAT('%', :query, '%')) OR LOWER(a.prenom) LIKE LOWER(CONCAT('%', :query, '%')) OR LOWER(l.numeroLicence) LIKE LOWER(CONCAT('%', :query, '%'))")
List<Adherent> searchByQuery(@org.springframework.data.repository.query.Param("query") String query);
} }
@@ -0,0 +1,26 @@
package com.astalange.core.repository;
import com.astalange.core.entity.AuditLogPaiement;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Repository
public interface AuditLogPaiementRepository extends JpaRepository<AuditLogPaiement, Long> {
List<AuditLogPaiement> findAllByOrderByDateActionDesc();
@Query("SELECT a FROM AuditLogPaiement a WHERE " +
"(:action IS NULL OR :action = '' OR a.action = :action) AND " +
"(:utilisateur IS NULL OR :utilisateur = '' OR LOWER(a.utilisateur) LIKE LOWER(CONCAT('%', :utilisateur, '%'))) AND " +
"(:adherent IS NULL OR :adherent = '' OR LOWER(a.adherentNomComplet) LIKE LOWER(CONCAT('%', :adherent, '%'))) " +
"ORDER BY a.dateAction DESC")
List<AuditLogPaiement> findByFilters(
@Param("action") String action,
@Param("utilisateur") String utilisateur,
@Param("adherent") String adherent
);
}
@@ -4,6 +4,16 @@ import com.astalange.core.entity.Categorie;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.astalange.core.entity.Saison;
import java.util.List;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.Query;
@Repository @Repository
public interface CategorieRepository extends JpaRepository<Categorie, Long> { public interface CategorieRepository extends JpaRepository<Categorie, Long> {
List<Categorie> findBySaison(Saison saison);
@Query("SELECT c FROM Categorie c WHERE c.saison = :saison AND :annee BETWEEN c.anneeMin AND c.anneeMax")
List<Categorie> findBySaisonAndAnnee(@Param("saison") Saison saison, @Param("annee") Integer annee);
} }
@@ -0,0 +1,21 @@
package com.astalange.core.repository;
import com.astalange.core.entity.CreneauEntrainement;
import com.astalange.core.entity.JourSemaine;
import com.astalange.core.entity.Saison;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CreneauEntrainementRepository extends JpaRepository<CreneauEntrainement, Long> {
@Query("SELECT c FROM CreneauEntrainement c JOIN FETCH c.categorie JOIN FETCH c.terrain WHERE c.saison = :saison")
List<CreneauEntrainement> findBySaisonFetch(@Param("saison") Saison saison);
@Query("SELECT c FROM CreneauEntrainement c JOIN FETCH c.categorie JOIN FETCH c.terrain WHERE c.saison = :saison AND c.jourSemaine = :jourSemaine")
List<CreneauEntrainement> findBySaisonAndJourSemaineFetch(@Param("saison") Saison saison, @Param("jourSemaine") JourSemaine jourSemaine);
}
@@ -1,9 +1,14 @@
package com.astalange.core.repository; package com.astalange.core.repository;
import com.astalange.core.entity.Dotation; import com.astalange.core.entity.Dotation;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
import com.astalange.core.entity.Saison;
@Repository @Repository
public interface DotationRepository extends JpaRepository<Dotation, Long> { public interface DotationRepository extends JpaRepository<Dotation, Long>, JpaSpecificationExecutor<Dotation> {
List<Dotation> findByLicence_SaisonAndChoisiTrueAndFourniFalse(Saison saison);
} }
@@ -0,0 +1,15 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Educateur;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
@Repository
public interface EducateurRepository extends JpaRepository<Educateur, Long> {
@Query("SELECT e FROM Educateur e LEFT JOIN FETCH e.categorie LEFT JOIN FETCH e.equipe")
List<Educateur> findAllWithCategorieAndEquipe();
}
@@ -0,0 +1,19 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Equipe;
import com.astalange.core.entity.Saison;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
@Repository
public interface EquipeRepository extends JpaRepository<Equipe, Long> {
List<Equipe> findByCategorieId(Long categorieId);
@Query("SELECT e FROM Equipe e JOIN FETCH e.categorie WHERE e.saison = :saison")
List<Equipe> findBySaison(Saison saison);
@Query("SELECT e FROM Equipe e JOIN FETCH e.categorie")
List<Equipe> findAllWithCategorie();
}
@@ -4,6 +4,10 @@ import com.astalange.core.entity.Equipement;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.astalange.core.entity.Saison;
import java.util.List;
@Repository @Repository
public interface EquipementRepository extends JpaRepository<Equipement, Long> { public interface EquipementRepository extends JpaRepository<Equipement, Long> {
List<Equipement> findBySaison(Saison saison);
} }
@@ -8,10 +8,14 @@ import org.springframework.stereotype.Repository;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository @Repository
public interface LicenceRepository extends JpaRepository<Licence, Long> { public interface LicenceRepository extends JpaRepository<Licence, Long>, JpaSpecificationExecutor<Licence> {
List<Licence> findByAdherentId(Long adherentId); List<Licence> findByAdherentId(Long adherentId);
List<Licence> findBySaisonAndCategorie(com.astalange.core.entity.Saison saison, com.astalange.core.entity.Categorie categorie);
long countByEtat(String etat); long countByEtat(String etat);
@Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c") @Query("SELECT SUM(c.tarifBase) FROM Licence l JOIN l.categorie c")
@@ -11,4 +11,12 @@ public interface PaiementRepository extends JpaRepository<Paiement, Long> {
@Query("SELECT SUM(p.montant) FROM Paiement p") @Query("SELECT SUM(p.montant) FROM Paiement p")
BigDecimal sumMontant(); BigDecimal sumMontant();
@Query("SELECT p FROM Paiement p " +
"LEFT JOIN FETCH p.licence l " +
"LEFT JOIN FETCH l.adherent a " +
"LEFT JOIN FETCH l.categorie c " +
"LEFT JOIN FETCH p.modePaiement m " +
"ORDER BY p.datePaiement DESC, p.id DESC")
java.util.List<Paiement> findAllWithAssociations();
} }
@@ -0,0 +1,13 @@
package com.astalange.core.repository;
import com.astalange.core.entity.PreInscription;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PreInscriptionRepository extends JpaRepository<PreInscription, Long> {
List<PreInscription> findByStatutTraiteFalseOrderByDateCreationDesc();
long countByStatutTraiteFalse();
}
@@ -0,0 +1,9 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
@@ -0,0 +1,12 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Saison;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface SaisonRepository extends JpaRepository<Saison, Long> {
Optional<Saison> findByEstActiveTrue();
}
@@ -0,0 +1,9 @@
package com.astalange.core.repository;
import com.astalange.core.entity.Terrain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TerrainRepository extends JpaRepository<Terrain, Long> {
}
@@ -0,0 +1,12 @@
package com.astalange.core.repository;
import com.astalange.core.entity.TokenPreInscription;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface TokenPreInscriptionRepository extends JpaRepository<TokenPreInscription, Long> {
Optional<TokenPreInscription> findByValeurUuid(String valeurUuid);
}
@@ -0,0 +1,25 @@
package com.astalange.core.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomUserDetails extends User {
private final Long id;
private final boolean mustChangePassword;
public CustomUserDetails(Long id, String username, String password, boolean enabled, boolean mustChangePassword,
Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, true, true, true, authorities);
this.id = id;
this.mustChangePassword = mustChangePassword;
}
public Long getId() {
return id;
}
public boolean isMustChangePassword() {
return mustChangePassword;
}
}
@@ -25,11 +25,12 @@ public class UserDetailsServiceImpl implements UserDetailsService {
AppUser appUser = userRepository.findByUsername(username) AppUser appUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new User( return new CustomUserDetails(
appUser.getId(),
appUser.getUsername(), appUser.getUsername(),
appUser.getPassword(), appUser.getPassword(),
appUser.isEnabled(), appUser.isEnabled(),
true, true, true, appUser.isMustChangePassword(),
appUser.getRoles().stream() appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName())) .map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()) .collect(Collectors.toList())
@@ -0,0 +1,50 @@
package com.astalange.core.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Map;
@Service
public class CaptchaValidationService {
@Value("${captcha.secret:1x0000000000000000000000000000000AA}")
private String captchaSecret;
@Value("${captcha.url:https://challenges.cloudflare.com/turnstile/v0/siteverify}")
private String captchaUrl;
public boolean validateCaptcha(String token) {
if (token == null || token.isEmpty()) {
return false;
}
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("secret", captchaSecret);
map.add("response", token);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
try {
ResponseEntity<Map> response = restTemplate.postForEntity(captchaUrl, request, Map.class);
if (response.getBody() != null && Boolean.TRUE.equals(response.getBody().get("success"))) {
return true;
}
} catch (Exception e) {
// Log error
return false;
}
return false;
}
}
@@ -0,0 +1,204 @@
package com.astalange.core.service;
import com.astalange.core.entity.*;
import com.astalange.core.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
public class CategorieService {
private final CategorieRepository categorieRepository;
private final SaisonRepository saisonRepository;
private final EquipementRepository equipementRepository;
private final LicenceRepository licenceRepository;
public CategorieService(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, LicenceRepository licenceRepository) {
this.categorieRepository = categorieRepository;
this.saisonRepository = saisonRepository;
this.equipementRepository = equipementRepository;
this.licenceRepository = licenceRepository;
}
public record EquipementConfig(boolean obligatoire, java.math.BigDecimal prix) {}
@Transactional
public void saveCategorie(Categorie categorie, java.util.Map<Long, EquipementConfig> equipementConfigs) {
Saison activeSaison;
Categorie existing = null;
if (categorie.getId() == null) {
activeSaison = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
categorie.setSaison(activeSaison);
} else {
final Long catId = categorie.getId();
existing = categorieRepository.findById(catId)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + catId));
categorie.setSaison(existing.getSaison());
activeSaison = existing.getSaison();
}
// Handle CategorieEquipement
if (existing != null) {
categorie.getCategorieEquipements().clear();
categorie.getCategorieEquipements().addAll(existing.getCategorieEquipements());
}
List<CategorieEquipement> nouveauxEquipements = new ArrayList<>();
if (equipementConfigs != null) {
for (java.util.Map.Entry<Long, EquipementConfig> entry : equipementConfigs.entrySet()) {
Long eqId = entry.getKey();
EquipementConfig config = entry.getValue();
Equipement eq = equipementRepository.findById(eqId).orElse(null);
if (eq != null) {
CategorieEquipement ce = new CategorieEquipement();
ce.setCategorie(categorie);
ce.setEquipement(eq);
ce.setObligatoire(config.obligatoire());
ce.setPrix(config.prix() != null ? config.prix() : java.math.BigDecimal.ZERO);
nouveauxEquipements.add(ce);
}
}
}
categorie.getCategorieEquipements().clear();
categorie.getCategorieEquipements().addAll(nouveauxEquipements);
categorie = categorieRepository.save(categorie);
// Mettre à jour les adhérents (via les licences de la saison actuelle)
List<Licence> licences = licenceRepository.findBySaisonAndCategorie(activeSaison, categorie);
for (Licence licence : licences) {
updateDotationsForLicence(licence, nouveauxEquipements);
}
}
public void syncDotationsForLicence(Licence licence) {
if (licence.getCategorie() != null) {
updateDotationsForLicence(licence, licence.getCategorie().getCategorieEquipements());
}
}
public void updateDotationsForLicence(Licence licence, List<CategorieEquipement> categorieEquipements) {
List<Dotation> currentDotations = licence.getDotations();
List<Dotation> toRemove = new ArrayList<>();
// Remove dotations for equipements no longer in the category or not matching public
for (Dotation d : currentDotations) {
boolean found = false;
for (CategorieEquipement ce : categorieEquipements) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (matchPublic) {
found = true;
}
break;
}
}
if (!found) {
toRemove.add(d);
}
}
currentDotations.removeAll(toRemove);
// Add missing dotations
for (CategorieEquipement ce : categorieEquipements) {
String typePublic = ce.getEquipement().getTypePublic();
String typeMaillot = licence.getAdherent() != null ? licence.getAdherent().getTypeMaillot() : "JOUEUR";
boolean matchPublic = "TOUS".equals(typePublic) ||
("GARDIEN".equals(typePublic) && "GARDIEN".equals(typeMaillot)) ||
("JOUEUR".equals(typePublic) && "JOUEUR".equals(typeMaillot));
if (!matchPublic) {
continue;
}
boolean found = false;
for (Dotation d : currentDotations) {
if (ce.getEquipement().getId().equals(d.getEquipement().getId())) {
found = true;
if (Boolean.TRUE.equals(ce.getObligatoire()) && !Boolean.TRUE.equals(d.getChoisi())) {
d.setChoisi(true);
}
break;
}
}
if (!found) {
Dotation newDotation = new Dotation();
newDotation.setLicence(licence);
newDotation.setEquipement(ce.getEquipement());
boolean isMaillot = "Maillot".equalsIgnoreCase(ce.getEquipement().getNom());
boolean isNouvelle = false;
if (licence.getTypeDemande() != null &&
(licence.getTypeDemande().equalsIgnoreCase("Nouvelle demande") || licence.getTypeDemande().equalsIgnoreCase("NOUVELLE")) &&
licence.getCategorie() != null) {
isNouvelle = true;
}
newDotation.setChoisi(ce.getObligatoire() || (isMaillot && isNouvelle)); // Checked if obligatoire or if it's a new registration
newDotation.setFourni(false);
newDotation.setTaille("");
newDotation.setNumero("");
if (licence.getAdherent() != null) {
String defaultFlocage = "";
if (ce.getEquipement().isHasFlocagePrenom() && ce.getEquipement().isHasFlocageInitiales()) {
String firstInitial = licence.getAdherent().getNom() != null && !licence.getAdherent().getNom().isEmpty() ? licence.getAdherent().getNom().substring(0, 1).toUpperCase() + "." : "";
defaultFlocage = (licence.getAdherent().getPrenom() != null ? licence.getAdherent().getPrenom() + " " : "") + firstInitial;
} else if (ce.getEquipement().isHasFlocagePrenom()) {
defaultFlocage = licence.getAdherent().getPrenom() != null ? licence.getAdherent().getPrenom() : "";
} else if (ce.getEquipement().isHasFlocageInitiales()) {
String firstInitP = licence.getAdherent().getPrenom() != null && !licence.getAdherent().getPrenom().isEmpty() ? licence.getAdherent().getPrenom().substring(0, 1).toUpperCase() : "";
String firstInitN = licence.getAdherent().getNom() != null && !licence.getAdherent().getNom().isEmpty() ? licence.getAdherent().getNom().substring(0, 1).toUpperCase() : "";
defaultFlocage = firstInitP + firstInitN;
}
newDotation.setFlocage(defaultFlocage.trim());
}
currentDotations.add(newDotation);
}
}
licenceRepository.save(licence);
}
@Transactional
public Categorie duplicateCategorie(Long id) {
Categorie existing = categorieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
Categorie duplicated = new Categorie();
// Appending ' (dupliqué)' and truncating if necessary since nom is max 20 chars
String newNom = existing.getNom() + " (dupliqué)";
if (newNom.length() > 20) {
newNom = newNom.substring(0, 20);
}
duplicated.setNom(newNom);
duplicated.setSexe(existing.getSexe());
duplicated.setAnneeMin(existing.getAnneeMin());
duplicated.setAnneeMax(existing.getAnneeMax());
duplicated.setTarifBase(existing.getTarifBase());
duplicated.setTarifExterieur(existing.getTarifExterieur());
duplicated.setSaison(existing.getSaison());
List<CategorieEquipement> nouveauxEquipements = new ArrayList<>();
for (CategorieEquipement ce : existing.getCategorieEquipements()) {
CategorieEquipement newCe = new CategorieEquipement();
newCe.setCategorie(duplicated);
newCe.setEquipement(ce.getEquipement());
newCe.setObligatoire(ce.getObligatoire());
newCe.setPrix(ce.getPrix() != null ? ce.getPrix() : java.math.BigDecimal.ZERO);
nouveauxEquipements.add(newCe);
}
duplicated.getCategorieEquipements().addAll(nouveauxEquipements);
return categorieRepository.save(duplicated);
}
}
@@ -0,0 +1,108 @@
package com.astalange.core.service;
import com.astalange.core.entity.Dotation;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.DotationRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DotationService {
private final DotationRepository dotationRepository;
public DotationService(DotationRepository dotationRepository) {
this.dotationRepository = dotationRepository;
}
public String genererCsvCommandeEquipement(Saison saisonActive) {
List<Dotation> dotations = dotationRepository.findByLicence_SaisonAndChoisiTrueAndFourniFalse(saisonActive);
StringBuilder sb = new StringBuilder();
// BOM for Excel to open UTF-8 correctly
sb.append('\ufeff');
// Header
sb.append("Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Référence;Taille;Flocage;Numéro\n");
for (Dotation d : dotations) {
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
String nom = d.getLicence().getAdherent().getNom();
String prenom = d.getLicence().getAdherent().getPrenom();
String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : "";
String reference = d.getEquipement() != null && d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
String taille = d.getTaille() != null ? d.getTaille() : "";
String flocage = d.getFlocage() != null ? d.getFlocage() : "";
String numero = d.getNumero() != null ? d.getNumero() : "";
sb.append(escapeCsv(categorie)).append(";")
.append(escapeCsv(nom)).append(";")
.append(escapeCsv(prenom)).append(";")
.append(escapeCsv(poste)).append(";")
.append(escapeCsv(sexe)).append(";")
.append(escapeCsv(equipement)).append(";")
.append(escapeCsv(reference)).append(";")
.append(escapeCsv(taille)).append(";")
.append(escapeCsv(flocage)).append(";")
.append(escapeCsv(numero)).append("\n");
}
return sb.toString();
}
public String genererCsvSearchEquipement(List<Dotation> dotations) {
StringBuilder sb = new StringBuilder();
// BOM for Excel to open UTF-8 correctly
sb.append('\ufeff');
// Header
sb.append("Saison;Catégorie;Nom;Prénom;Poste;Sexe;Équipement;Référence;Taille;Flocage;Numéro;Fourni\n");
for (Dotation d : dotations) {
String saison = d.getLicence().getSaison() != null ? d.getLicence().getSaison().getNom() : "";
String categorie = d.getLicence().getCategorie() != null ? d.getLicence().getCategorie().getNom() : "";
String nom = d.getLicence().getAdherent().getNom();
String prenom = d.getLicence().getAdherent().getPrenom();
String poste = d.getLicence().getAdherent().getTypeMaillot() != null ? d.getLicence().getAdherent().getTypeMaillot() : "";
String sexe = d.getLicence().getAdherent().getSexe() != null ? d.getLicence().getAdherent().getSexe() : "";
String equipement = d.getEquipement() != null ? d.getEquipement().getNom() : "";
String reference = d.getEquipement() != null && d.getEquipement().getReference() != null ? d.getEquipement().getReference() : "";
String taille = d.getTaille() != null ? d.getTaille() : "";
String flocage = d.getFlocage() != null ? d.getFlocage() : "";
String numero = d.getNumero() != null ? d.getNumero() : "";
String fourni = Boolean.TRUE.equals(d.getFourni()) ? "Oui" : "Non";
sb.append(escapeCsv(saison)).append(";")
.append(escapeCsv(categorie)).append(";")
.append(escapeCsv(nom)).append(";")
.append(escapeCsv(prenom)).append(";")
.append(escapeCsv(poste)).append(";")
.append(escapeCsv(sexe)).append(";")
.append(escapeCsv(equipement)).append(";")
.append(escapeCsv(reference)).append(";")
.append(escapeCsv(taille)).append(";")
.append(escapeCsv(flocage)).append(";")
.append(escapeCsv(numero)).append(";")
.append(escapeCsv(fourni)).append("\n");
}
return sb.toString();
}
private String escapeCsv(String value) {
if (value == null) {
return "";
}
String result = value;
if (result.contains("\"")) {
result = result.replace("\"", "\"\"");
}
if (result.contains(";") || result.contains("\n") || result.contains("\"")) {
return "\"" + result + "\"";
}
return result;
}
}
@@ -0,0 +1,75 @@
package com.astalange.core.service;
import com.astalange.core.entity.Adherent;
import com.astalange.core.entity.Licence;
import com.astalange.core.entity.PreInscription;
import com.astalange.core.repository.AdherentRepository;
import com.astalange.core.repository.LicenceRepository;
import com.astalange.core.repository.PreInscriptionRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PreInscriptionService {
private final PreInscriptionRepository preInscriptionRepository;
private final AdherentRepository adherentRepository;
private final LicenceRepository licenceRepository;
public PreInscriptionService(PreInscriptionRepository preInscriptionRepository,
AdherentRepository adherentRepository,
LicenceRepository licenceRepository) {
this.preInscriptionRepository = preInscriptionRepository;
this.adherentRepository = adherentRepository;
this.licenceRepository = licenceRepository;
}
@Transactional
public Adherent validerPreInscription(Long preInscriptionId, Long categorieId) {
PreInscription pre = preInscriptionRepository.findById(preInscriptionId)
.orElseThrow(() -> new IllegalArgumentException("Pré-inscription introuvable"));
if (pre.getStatutTraite()) {
throw new IllegalStateException("Cette pré-inscription a déjà été traitée");
}
// Créer l'adhérent
Adherent adherent = new Adherent();
adherent.setNom(pre.getNom().toUpperCase());
adherent.setPrenom(pre.getPrenom());
adherent.setDateNaissance(pre.getDateNaissance());
adherent.setLieuNaissanceVille(pre.getLieuNaissanceVille());
adherent.setLieuNaissancePays(pre.getLieuNaissancePays());
adherent.setNationalite(pre.getNationalite());
adherent.setEmail(pre.getEmail());
adherent.setTelephone(pre.getTelephone());
adherent.setRepresentantLegal(pre.getRepresentantLegal());
adherent.setTypeDemande(pre.getTypeDemande());
adherent.setAncienClub(pre.getAncienClub());
adherent.setAncienneCategorie(pre.getAncienneCategorie());
adherent.setSexe(pre.getSexe());
adherent.setRaisonChangementClub(pre.getRaisonChangementClub());
adherent.setCommentConnuClub(pre.getCommentConnuClub());
adherent.setTailleVetement(pre.getTailleVetement());
adherent.setPointure(pre.getPointure());
// Par défaut
adherent.setTypeMaillot("JOUEUR");
adherent.setResidentTalange(false);
adherent = adherentRepository.save(adherent);
pre.setStatutTraite(true);
preInscriptionRepository.save(pre);
return adherent;
}
@Transactional
public void rejeterPreInscription(Long preInscriptionId) {
PreInscription pre = preInscriptionRepository.findById(preInscriptionId)
.orElseThrow(() -> new IllegalArgumentException("Pré-inscription introuvable"));
pre.setStatutTraite(true);
preInscriptionRepository.save(pre);
}
}
@@ -0,0 +1,101 @@
package com.astalange.core.service;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Equipement;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class SaisonService {
private final SaisonRepository saisonRepository;
private final CategorieRepository categorieRepository;
private final EquipementRepository equipementRepository;
public SaisonService(SaisonRepository saisonRepository, CategorieRepository categorieRepository, EquipementRepository equipementRepository) {
this.saisonRepository = saisonRepository;
this.categorieRepository = categorieRepository;
this.equipementRepository = equipementRepository;
}
@Transactional
public Saison creerNouvelleSaison(String nom, boolean importerParametragePrecedent) {
Optional<Saison> ancienneSaisonOpt = saisonRepository.findByEstActiveTrue();
// Désactiver toutes les saisons actuelles (il ne devrait y en avoir qu'une mais on s'assure de tout nettoyer)
List<Saison> toutesLesSaisons = saisonRepository.findAll();
for (Saison s : toutesLesSaisons) {
s.setEstActive(false);
saisonRepository.save(s);
}
// Création de la nouvelle saison
Saison nouvelleSaison = new Saison();
nouvelleSaison.setNom(nom);
nouvelleSaison.setEstActive(true);
nouvelleSaison = saisonRepository.save(nouvelleSaison);
if (importerParametragePrecedent && ancienneSaisonOpt.isPresent()) {
Saison ancienneSaison = ancienneSaisonOpt.get();
dupliquerParametrages(ancienneSaison, nouvelleSaison);
}
return nouvelleSaison;
}
private void dupliquerParametrages(Saison ancienneSaison, Saison nouvelleSaison) {
// 1. Dupliquer les équipements
List<Equipement> anciensEquipements = equipementRepository.findBySaison(ancienneSaison);
java.util.Map<Long, Equipement> oldToNewEquipementMap = new java.util.HashMap<>();
for (Equipement ancien : anciensEquipements) {
Equipement nouvelEquipement = new Equipement();
nouvelEquipement.setNom(ancien.getNom());
nouvelEquipement.setDescription(ancien.getDescription());
nouvelEquipement.setSaison(nouvelleSaison);
nouvelEquipement = equipementRepository.save(nouvelEquipement);
oldToNewEquipementMap.put(ancien.getId(), nouvelEquipement);
}
// 2. Dupliquer les catégories (avec incrémentation de l'année)
List<Categorie> anciennesCategories = categorieRepository.findBySaison(ancienneSaison);
for (Categorie ancienne : anciennesCategories) {
Categorie nouvelleCategorie = new Categorie();
nouvelleCategorie.setNom(ancienne.getNom());
// Incrémentation de +1 sur l'année de naissance pivot
if (ancienne.getAnneeMin() != null) {
nouvelleCategorie.setAnneeMin(ancienne.getAnneeMin() + 1);
}
if (ancienne.getAnneeMax() != null) {
nouvelleCategorie.setAnneeMax(ancienne.getAnneeMax() + 1);
}
nouvelleCategorie.setTarifBase(ancienne.getTarifBase());
nouvelleCategorie.setTarifExterieur(ancienne.getTarifExterieur());
nouvelleCategorie.setSaison(nouvelleSaison);
// Dupliquer les relations avec les équipements
if (ancienne.getCategorieEquipements() != null) {
for (com.astalange.core.entity.CategorieEquipement oldCatEq : ancienne.getCategorieEquipements()) {
Equipement newEq = oldToNewEquipementMap.get(oldCatEq.getEquipement().getId());
if (newEq != null) {
com.astalange.core.entity.CategorieEquipement newCatEq = new com.astalange.core.entity.CategorieEquipement();
newCatEq.setCategorie(nouvelleCategorie);
newCatEq.setEquipement(newEq);
newCatEq.setObligatoire(oldCatEq.getObligatoire());
newCatEq.setPrix(oldCatEq.getPrix());
nouvelleCategorie.getCategorieEquipements().add(newCatEq);
}
}
}
categorieRepository.save(nouvelleCategorie);
}
}
}
@@ -0,0 +1,26 @@
-- V10__add_terrains_and_schedules.sql
-- 1. Table des terrains
CREATE TABLE terrain (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL
);
-- Insertion des deux terrains par défaut
INSERT INTO terrain (nom) VALUES ('Terrain Vert');
INSERT INTO terrain (nom) VALUES ('Terrain Synthétique');
-- 2. Table des créneaux d'entraînement
CREATE TABLE creneau_entrainement (
id BIGSERIAL PRIMARY KEY,
categorie_id BIGINT NOT NULL,
terrain_id BIGINT NOT NULL,
zone VARCHAR(50) NOT NULL,
jour_semaine VARCHAR(50) NOT NULL,
heure_debut TIME NOT NULL,
heure_fin TIME NOT NULL,
saison_id BIGINT NOT NULL,
CONSTRAINT fk_creneau_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
CONSTRAINT fk_creneau_terrain FOREIGN KEY (terrain_id) REFERENCES terrain(id),
CONSTRAINT fk_creneau_saison FOREIGN KEY (saison_id) REFERENCES saison(id) ON DELETE CASCADE
);
@@ -0,0 +1,19 @@
-- V11__add_equipes.sql
-- 1. Table des équipes
CREATE TABLE equipe (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
categorie_id BIGINT NOT NULL,
saison_id BIGINT NOT NULL,
CONSTRAINT fk_equipe_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
CONSTRAINT fk_equipe_saison FOREIGN KEY (saison_id) REFERENCES saison(id) ON DELETE CASCADE
);
-- 2. Ajouter equipe_id à la table licence (nullable, facultative)
ALTER TABLE licence ADD COLUMN equipe_id BIGINT;
ALTER TABLE licence ADD CONSTRAINT fk_licence_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
-- 3. Ajouter equipe_id à la table creneau_entrainement (nullable, facultative)
ALTER TABLE creneau_entrainement ADD COLUMN equipe_id BIGINT;
ALTER TABLE creneau_entrainement ADD CONSTRAINT fk_creneau_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
@@ -0,0 +1,2 @@
ALTER TABLE app_user ADD COLUMN must_change_password BOOLEAN NOT NULL DEFAULT FALSE;
INSERT INTO role (name) VALUES ('ROLE_TRESORERIE'), ('ROLE_AGENT_SAISIE') ON CONFLICT (name) DO NOTHING;
@@ -0,0 +1,3 @@
-- V13__add_equipe_to_educateur.sql
ALTER TABLE educateur ADD COLUMN equipe_id BIGINT;
ALTER TABLE educateur ADD CONSTRAINT fk_educateur_equipe FOREIGN KEY (equipe_id) REFERENCES equipe(id) ON DELETE SET NULL;
@@ -0,0 +1,18 @@
CREATE TABLE categorie_equipement (
id BIGSERIAL PRIMARY KEY,
categorie_id BIGINT NOT NULL,
equipement_id BIGINT NOT NULL,
obligatoire BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT fk_cat_equip_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE CASCADE,
CONSTRAINT fk_cat_equip_equipement FOREIGN KEY (equipement_id) REFERENCES equipement(id) ON DELETE CASCADE
);
-- Migrer les données existantes : associer tous les équipements d'une saison à toutes les catégories de la même saison
-- et transférer la valeur "obligatoire" de l'équipement vers la nouvelle table de liaison
INSERT INTO categorie_equipement (categorie_id, equipement_id, obligatoire)
SELECT c.id, e.id, e.obligatoire
FROM categorie c
JOIN equipement e ON c.saison_id = e.saison_id;
-- Supprimer la colonne "obligatoire" de la table équipement
ALTER TABLE equipement DROP COLUMN obligatoire;
@@ -0,0 +1,9 @@
ALTER TABLE categorie_equipement ADD COLUMN prix NUMERIC(10,2) NOT NULL DEFAULT 0;
-- Migrer les données existantes
UPDATE categorie_equipement ce
SET prix = e.prix_contribution
FROM equipement e
WHERE ce.equipement_id = e.id;
ALTER TABLE equipement DROP COLUMN IF EXISTS prix_contribution;
@@ -0,0 +1,21 @@
CREATE TABLE token_pre_inscription (
id BIGSERIAL PRIMARY KEY,
valeur_uuid VARCHAR(36) NOT NULL UNIQUE,
date_expiration TIMESTAMP NOT NULL,
est_utilise BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE pre_inscription (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
date_naissance DATE NOT NULL,
telephone VARCHAR(20),
email VARCHAR(150),
adresse TEXT,
representant_legal VARCHAR(150),
statut_traite BOOLEAN NOT NULL DEFAULT FALSE,
date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
saison_id BIGINT NOT NULL,
CONSTRAINT fk_pre_inscription_saison FOREIGN KEY (saison_id) REFERENCES saison (id)
);
@@ -0,0 +1 @@
ALTER TABLE saison ADD COLUMN inscriptions_ouvertes BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,5 @@
-- Drop the global unique constraint on 'nom'
ALTER TABLE categorie DROP CONSTRAINT IF EXISTS categorie_nom_key;
-- Add a composite unique constraint for 'nom' and 'saison_id'
ALTER TABLE categorie ADD CONSTRAINT categorie_nom_saison_id_key UNIQUE (nom, saison_id);
@@ -0,0 +1,11 @@
-- adherent modifications
ALTER TABLE adherent DROP COLUMN IF EXISTS adresse;
ALTER TABLE adherent ADD COLUMN lieu_naissance_ville VARCHAR(100);
ALTER TABLE adherent ADD COLUMN lieu_naissance_pays VARCHAR(100);
ALTER TABLE adherent ADD COLUMN nationalite VARCHAR(100);
-- pre_inscription modifications
ALTER TABLE pre_inscription DROP COLUMN IF EXISTS adresse;
ALTER TABLE pre_inscription ADD COLUMN lieu_naissance_ville VARCHAR(100);
ALTER TABLE pre_inscription ADD COLUMN lieu_naissance_pays VARCHAR(100);
ALTER TABLE pre_inscription ADD COLUMN nationalite VARCHAR(100);
@@ -1,16 +1,16 @@
CREATE TABLE role ( CREATE TABLE IF NOT EXISTS role (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL name VARCHAR(50) UNIQUE NOT NULL
); );
CREATE TABLE app_user ( CREATE TABLE IF NOT EXISTS app_user (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL, username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE enabled BOOLEAN NOT NULL DEFAULT TRUE
); );
CREATE TABLE user_role ( CREATE TABLE IF NOT EXISTS user_role (
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL, role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id), PRIMARY KEY (user_id, role_id),
@@ -18,7 +18,7 @@ CREATE TABLE user_role (
CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE CONSTRAINT fk_role FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE
); );
CREATE TABLE adherent ( CREATE TABLE IF NOT EXISTS adherent (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL, nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL, prenom VARCHAR(100) NOT NULL,
@@ -29,7 +29,7 @@ CREATE TABLE adherent (
representant_legal VARCHAR(150) representant_legal VARCHAR(150)
); );
CREATE TABLE categorie ( CREATE TABLE IF NOT EXISTS categorie (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
nom VARCHAR(20) UNIQUE NOT NULL, nom VARCHAR(20) UNIQUE NOT NULL,
annee_min INT, annee_min INT,
@@ -37,7 +37,7 @@ CREATE TABLE categorie (
tarif_base DECIMAL(10,2) NOT NULL tarif_base DECIMAL(10,2) NOT NULL
); );
CREATE TABLE licence ( CREATE TABLE IF NOT EXISTS licence (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
adherent_id BIGINT NOT NULL, adherent_id BIGINT NOT NULL,
categorie_id BIGINT NOT NULL, categorie_id BIGINT NOT NULL,
@@ -47,7 +47,7 @@ CREATE TABLE licence (
CONSTRAINT fk_licence_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) CONSTRAINT fk_licence_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id)
); );
CREATE TABLE paiement ( CREATE TABLE IF NOT EXISTS paiement (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
licence_id BIGINT NOT NULL, licence_id BIGINT NOT NULL,
montant DECIMAL(10,2) NOT NULL, montant DECIMAL(10,2) NOT NULL,
@@ -56,14 +56,14 @@ CREATE TABLE paiement (
CONSTRAINT fk_paiement_licence FOREIGN KEY (licence_id) REFERENCES licence(id) ON DELETE CASCADE CONSTRAINT fk_paiement_licence FOREIGN KEY (licence_id) REFERENCES licence(id) ON DELETE CASCADE
); );
CREATE TABLE equipement ( CREATE TABLE IF NOT EXISTS equipement (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL, nom VARCHAR(100) NOT NULL,
description TEXT, description TEXT,
obligatoire BOOLEAN NOT NULL DEFAULT FALSE obligatoire BOOLEAN NOT NULL DEFAULT FALSE
); );
CREATE TABLE dotation ( CREATE TABLE IF NOT EXISTS dotation (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
licence_id BIGINT NOT NULL, licence_id BIGINT NOT NULL,
equipement_id BIGINT NOT NULL, equipement_id BIGINT NOT NULL,
@@ -0,0 +1,2 @@
ALTER TABLE licence ADD COLUMN type_demande VARCHAR(50);
ALTER TABLE licence ADD COLUMN type_licence VARCHAR(50);
@@ -0,0 +1,18 @@
-- Default values for existing records in adherent
UPDATE adherent SET lieu_naissance_ville = 'Non renseigné' WHERE lieu_naissance_ville IS NULL;
UPDATE adherent SET lieu_naissance_pays = 'France' WHERE lieu_naissance_pays IS NULL;
UPDATE adherent SET nationalite = 'Française' WHERE nationalite IS NULL;
-- Default values for existing records in pre_inscription
UPDATE pre_inscription SET lieu_naissance_ville = 'Non renseigné' WHERE lieu_naissance_ville IS NULL;
UPDATE pre_inscription SET lieu_naissance_pays = 'France' WHERE lieu_naissance_pays IS NULL;
UPDATE pre_inscription SET nationalite = 'Française' WHERE nationalite IS NULL;
-- Now make them NOT NULL
ALTER TABLE adherent ALTER COLUMN lieu_naissance_ville SET NOT NULL;
ALTER TABLE adherent ALTER COLUMN lieu_naissance_pays SET NOT NULL;
ALTER TABLE adherent ALTER COLUMN nationalite SET NOT NULL;
ALTER TABLE pre_inscription ALTER COLUMN lieu_naissance_ville SET NOT NULL;
ALTER TABLE pre_inscription ALTER COLUMN lieu_naissance_pays SET NOT NULL;
ALTER TABLE pre_inscription ALTER COLUMN nationalite SET NOT NULL;
@@ -0,0 +1,11 @@
-- Provide fallback email if null
UPDATE adherent SET email = 'nonrenseigne@example.com' WHERE email IS NULL OR email = '';
UPDATE pre_inscription SET email = 'nonrenseigne@example.com' WHERE email IS NULL OR email = '';
-- Make email NOT NULL
ALTER TABLE adherent ALTER COLUMN email SET NOT NULL;
ALTER TABLE pre_inscription ALTER COLUMN email SET NOT NULL;
-- Remove telephone
ALTER TABLE adherent DROP COLUMN IF EXISTS telephone;
ALTER TABLE pre_inscription DROP COLUMN IF EXISTS telephone;
@@ -0,0 +1 @@
ALTER TABLE paiement ADD COLUMN gestionnaire VARCHAR(255);
@@ -0,0 +1 @@
ALTER TABLE pre_inscription ADD COLUMN consentement_rgpd BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,10 @@
CREATE TABLE audit_log_paiement (
id BIGSERIAL PRIMARY KEY,
action VARCHAR(50) NOT NULL,
date_action TIMESTAMP NOT NULL,
utilisateur VARCHAR(255) NOT NULL,
paiement_id BIGINT,
details VARCHAR(1000) NOT NULL,
montant DECIMAL(10, 2),
adherent_nom_complet VARCHAR(255)
);
@@ -0,0 +1,2 @@
ALTER TABLE adherent ADD COLUMN telephone VARCHAR(20);
ALTER TABLE pre_inscription ADD COLUMN telephone VARCHAR(20);
@@ -0,0 +1 @@
ALTER TABLE equipement ADD COLUMN reference VARCHAR(100);
@@ -0,0 +1,3 @@
ALTER TABLE pre_inscription
ADD COLUMN type_demande VARCHAR(50) DEFAULT 'NOUVELLE' NOT NULL,
ADD COLUMN ancien_club VARCHAR(150);
@@ -0,0 +1,3 @@
ALTER TABLE adherent
ADD COLUMN type_demande VARCHAR(50),
ADD COLUMN ancien_club VARCHAR(150);
@@ -1,12 +1,18 @@
CREATE TABLE mode_paiement ( CREATE TABLE IF NOT EXISTS mode_paiement (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
nom VARCHAR(50) UNIQUE NOT NULL nom VARCHAR(50) UNIQUE NOT NULL
); );
INSERT INTO mode_paiement (nom) VALUES ('Espèces'), ('Chèque'), ('Pass Sport'), ('Virement'); INSERT INTO mode_paiement (nom) VALUES ('Espèces'), ('Chèque'), ('Pass Sport'), ('Virement')
ON CONFLICT (nom) DO NOTHING;
ALTER TABLE paiement ADD COLUMN mode_paiement_id BIGINT; ALTER TABLE paiement ADD COLUMN IF NOT EXISTS mode_paiement_id BIGINT;
ALTER TABLE paiement ADD CONSTRAINT fk_paiement_mode FOREIGN KEY (mode_paiement_id) REFERENCES mode_paiement(id); DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_paiement_mode') THEN
ALTER TABLE paiement ADD CONSTRAINT fk_paiement_mode FOREIGN KEY (mode_paiement_id) REFERENCES mode_paiement(id);
END IF;
END $$;
ALTER TABLE paiement DROP COLUMN mode_paiement; ALTER TABLE paiement DROP COLUMN IF EXISTS mode_paiement;
@@ -0,0 +1,10 @@
ALTER TABLE adherent ADD COLUMN ancienne_categorie VARCHAR(50);
ALTER TABLE pre_inscription ADD COLUMN ancienne_categorie VARCHAR(50) DEFAULT 'NA';
ALTER TABLE categorie ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
UPDATE adherent SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE adherent ALTER COLUMN telephone SET NOT NULL;
UPDATE pre_inscription SET telephone = '0000000000' WHERE telephone IS NULL;
ALTER TABLE pre_inscription ALTER COLUMN telephone SET NOT NULL;
@@ -0,0 +1,5 @@
ALTER TABLE equipement ADD COLUMN gestion_sexe BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN tailles_disponibles VARCHAR(500);
ALTER TABLE equipement ADD COLUMN couleurs_disponibles VARCHAR(500);
ALTER TABLE dotation ADD COLUMN couleur VARCHAR(50);
@@ -0,0 +1,2 @@
ALTER TABLE paiement ADD COLUMN numero_cheque VARCHAR(50);
ALTER TABLE paiement ADD COLUMN commentaire TEXT;
@@ -0,0 +1 @@
ALTER TABLE equipement ADD COLUMN type_public VARCHAR(20) DEFAULT 'TOUS' NOT NULL;
@@ -0,0 +1 @@
ALTER TABLE pre_inscription ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE dotation ALTER COLUMN taille TYPE VARCHAR(50);
ALTER TABLE dotation ALTER COLUMN numero TYPE VARCHAR(50);
@@ -0,0 +1 @@
ALTER TABLE licence ADD COLUMN commentaire TEXT;
@@ -0,0 +1,9 @@
ALTER TABLE pre_inscription ADD COLUMN raison_changement_club VARCHAR(255);
ALTER TABLE pre_inscription ADD COLUMN comment_connu_club VARCHAR(255);
ALTER TABLE pre_inscription ADD COLUMN taille_vetement VARCHAR(50);
ALTER TABLE pre_inscription ADD COLUMN pointure VARCHAR(50);
ALTER TABLE adherent ADD COLUMN raison_changement_club VARCHAR(255);
ALTER TABLE adherent ADD COLUMN comment_connu_club VARCHAR(255);
ALTER TABLE adherent ADD COLUMN taille_vetement VARCHAR(50);
ALTER TABLE adherent ADD COLUMN pointure VARCHAR(50);
@@ -0,0 +1,31 @@
ALTER TABLE equipement ADD COLUMN has_flocage_prenom BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN has_flocage_initiales BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE equipement ADD COLUMN has_flocage_numero BOOLEAN DEFAULT FALSE NOT NULL;
-- Activation automatique de l'option "numéro" pour les équipements ayant déjà des dotations avec un numéro
UPDATE equipement e
SET has_flocage_numero = TRUE
WHERE EXISTS (
SELECT 1 FROM dotation d WHERE d.equipement_id = e.id AND d.numero IS NOT NULL AND d.numero != ''
);
-- Activation automatique de l'option "prénom" (uniquement) pour les équipements ayant déjà des dotations avec un flocage
UPDATE equipement e
SET has_flocage_prenom = TRUE
WHERE EXISTS (
SELECT 1 FROM dotation d WHERE d.equipement_id = e.id AND d.flocage IS NOT NULL AND d.flocage != ''
);
UPDATE dotation
SET flocage = a.prenom
FROM licence l
JOIN adherent a ON l.adherent_id = a.id
WHERE dotation.licence_id = l.id
AND dotation.flocage IS NOT NULL
AND dotation.flocage != ''
AND EXISTS (
SELECT 1 FROM equipement e
WHERE e.id = dotation.equipement_id
AND e.has_flocage_prenom = TRUE
AND e.has_flocage_initiales = FALSE
);
@@ -1,9 +1,11 @@
INSERT INTO role (name) VALUES ('ROLE_ADMIN'), ('ROLE_USER'); INSERT INTO role (name) VALUES ('ROLE_ADMIN'), ('ROLE_USER') ON CONFLICT (name) DO NOTHING;
-- Password is "Talange2026!" hashed with Argon2id -- Password is "Talange2026!" hashed with Argon2id
INSERT INTO app_user (username, password, enabled) VALUES ('Ucef', '$argon2id$v=19$m=65536,t=3,p=4$q29E/VM1esZVxo9HufgILQ$Nzb+rwwsZUMy04BbewYum2A8L8ujs9VZj9VJ/609Am0', true); INSERT INTO app_user (username, password, enabled) VALUES ('Ucef', '$argon2id$v=19$m=65536,t=3,p=4$q29E/VM1esZVxo9HufgILQ$Nzb+rwwsZUMy04BbewYum2A8L8ujs9VZj9VJ/609Am0', true)
ON CONFLICT (username) DO NOTHING;
INSERT INTO user_role (user_id, role_id) VALUES ((SELECT id FROM app_user WHERE username = 'Ucef'), (SELECT id FROM role WHERE name = 'ROLE_ADMIN')); INSERT INTO user_role (user_id, role_id) VALUES ((SELECT id FROM app_user WHERE username = 'Ucef'), (SELECT id FROM role WHERE name = 'ROLE_ADMIN'))
ON CONFLICT (user_id, role_id) DO NOTHING;
INSERT INTO categorie (nom, annee_min, annee_max, tarif_base) VALUES INSERT INTO categorie (nom, annee_min, annee_max, tarif_base) VALUES
('U6', 2020, 2020, 100.00), ('U6', 2020, 2020, 100.00),
@@ -14,10 +16,14 @@ INSERT INTO categorie (nom, annee_min, annee_max, tarif_base) VALUES
('U11', 2015, 2015, 130.00), ('U11', 2015, 2015, 130.00),
('U12', 2014, 2014, 140.00), ('U12', 2014, 2014, 140.00),
('U13', 2013, 2013, 140.00), ('U13', 2013, 2013, 140.00),
('Senior', 1900, 2005, 180.00); ('Senior', 1900, 2005, 180.00)
ON CONFLICT (nom) DO NOTHING;
INSERT INTO equipement (nom, obligatoire) VALUES INSERT INTO equipement (nom, obligatoire)
('Maillot', true), SELECT 'Maillot', true WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Maillot');
('Short', true), INSERT INTO equipement (nom, obligatoire)
('Chaussettes', true), SELECT 'Short', true WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Short');
('Survêtement', false); INSERT INTO equipement (nom, obligatoire)
SELECT 'Chaussettes', true WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Chaussettes');
INSERT INTO equipement (nom, obligatoire)
SELECT 'Survêtement', false WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Survêtement');
@@ -1,8 +1,8 @@
-- Ajouter la colonne pour savoir si l'adhérent est résident de Talange (par défaut oui pour les existants) -- Ajouter la colonne pour savoir si l'adhérent est résident de Talange (par défaut oui pour les existants)
ALTER TABLE adherent ADD COLUMN resident_talange BOOLEAN DEFAULT TRUE; ALTER TABLE adherent ADD COLUMN IF NOT EXISTS resident_talange BOOLEAN DEFAULT TRUE;
-- Ajouter le tarif extérieur aux catégories -- Ajouter le tarif extérieur aux catégories
ALTER TABLE categorie ADD COLUMN tarif_exterieur DECIMAL(10,2); ALTER TABLE categorie ADD COLUMN IF NOT EXISTS tarif_exterieur DECIMAL(10,2);
-- Par défaut, mettre le tarif extérieur égal au tarif de base pour ne pas casser les licences existantes -- Par défaut, mettre le tarif extérieur égal au tarif de base pour ne pas casser les licences existantes
UPDATE categorie SET tarif_exterieur = tarif_base; UPDATE categorie SET tarif_exterieur = tarif_base*1.2 WHERE tarif_exterieur IS NULL;
@@ -1 +1 @@
ALTER TABLE licence ADD COLUMN numero_licence VARCHAR(50); ALTER TABLE licence ADD COLUMN IF NOT EXISTS numero_licence VARCHAR(50);
@@ -0,0 +1,11 @@
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS numero VARCHAR(10);
CREATE TABLE IF NOT EXISTS educateur (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
email VARCHAR(100),
telephone VARCHAR(20),
categorie_id BIGINT,
CONSTRAINT fk_educateur_categorie FOREIGN KEY (categorie_id) REFERENCES categorie(id) ON DELETE SET NULL
);
@@ -0,0 +1,14 @@
-- Ajouter la colonne prix_contribution à la table equipement (avec valeur par défaut 0)
ALTER TABLE equipement ADD COLUMN IF NOT EXISTS prix_contribution DECIMAL(10,2) NOT NULL DEFAULT 0.00;
-- Ajouter la colonne choisi à la table dotation (avec valeur par défaut false)
ALTER TABLE dotation ADD COLUMN IF NOT EXISTS choisi BOOLEAN NOT NULL DEFAULT FALSE;
-- Mettre choisi à true pour tous les équipements obligatoires existants
UPDATE dotation d
SET choisi = TRUE
FROM equipement e
WHERE d.equipement_id = e.id AND e.obligatoire = TRUE;
-- Mettre à jour le prix de contribution du Survêtement à 30.00 € à titre d'exemple
UPDATE equipement SET prix_contribution = 30.00 WHERE nom = 'Survêtement';
@@ -0,0 +1,2 @@
ALTER TABLE adherent ADD COLUMN sexe VARCHAR(10) DEFAULT 'MASCULIN' NOT NULL;
ALTER TABLE adherent ADD COLUMN type_maillot VARCHAR(20) DEFAULT 'JOUEUR' NOT NULL;
@@ -0,0 +1,43 @@
-- V9__introduction_saison.sql
-- 1. Création de la table saison
CREATE TABLE saison (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(255) NOT NULL,
est_active BOOLEAN NOT NULL DEFAULT FALSE
);
-- Création d'une saison par défaut pour les données existantes
INSERT INTO saison (nom, est_active) VALUES ('Saison Actuelle', TRUE);
-- 2. Ajout de saison_id dans les tables de paramétrage
ALTER TABLE categorie ADD COLUMN saison_id BIGINT;
ALTER TABLE equipement ADD COLUMN saison_id BIGINT;
-- Il n'y a pas de table tarif, les tarifs sont dans categorie.
-- Affecter la saison par défaut aux paramètres existants
UPDATE categorie SET saison_id = (SELECT id FROM saison LIMIT 1);
UPDATE equipement SET saison_id = (SELECT id FROM saison LIMIT 1);
-- Rendre saison_id NOT NULL et ajouter les contraintes
ALTER TABLE categorie ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE categorie ADD CONSTRAINT fk_categorie_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
ALTER TABLE equipement ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE equipement ADD CONSTRAINT fk_equipement_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
-- 3. Ajout de saison_id sur licence
-- Licence a déjà une colonne saison (String). On va la remplacer par saison_id
ALTER TABLE licence RENAME COLUMN saison TO ancienne_saison;
ALTER TABLE licence ADD COLUMN saison_id BIGINT;
UPDATE licence SET saison_id = (SELECT id FROM saison LIMIT 1);
ALTER TABLE licence ALTER COLUMN saison_id SET NOT NULL;
ALTER TABLE licence ADD CONSTRAINT fk_licence_saison FOREIGN KEY (saison_id) REFERENCES saison(id);
ALTER TABLE licence DROP COLUMN ancienne_saison;
-- 4. Migration de paiement et dotation pour pointer sur licence_id au lieu de adherent_id
-- Ajout des colonnes licence_id
-- (Paiement et Dotation avaient un adherent_id qui n'est pas dans l'entité actuelle mais peut-être en base ?)
-- D'après les entités, Paiement et Dotation ont DEJA licence_id et n'ont PAS adherent_id !
-- Le plan proposait de modifier ça mais le code a DÉJÀ ces modifications.
-- Je vais juste vérifier s'il faut modifier la base de données.
-- Les entités Paiement.java et Dotation.java ont déjà `private Licence licence;`
@@ -0,0 +1,93 @@
package com.astalange.core;
import com.astalange.core.entity.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AdherentRecalculationTest {
@Test
public void testLicencePriceCalculationBasedOnResidencyAndCategory() {
Adherent adherent = new Adherent();
adherent.setNom("Doe");
adherent.setPrenom("John");
adherent.setDateNaissance(LocalDate.of(2015, 5, 10)); // born 2015
adherent.setResidentTalange(true);
Categorie u11 = new Categorie();
u11.setNom("U11");
u11.setAnneeMin(2015);
u11.setAnneeMax(2015);
u11.setTarifBase(new BigDecimal("130.00"));
u11.setTarifExterieur(new BigDecimal("160.00"));
Licence licence = new Licence();
licence.setAdherent(adherent);
licence.setCategorie(u11);
// Under residentTalange = true, base price should be 130
assertEquals(new BigDecimal("130.00"), licence.getPrixTotal());
// Under residentTalange = false, base price should be 160
adherent.setResidentTalange(false);
assertEquals(new BigDecimal("160.00"), licence.getPrixTotal());
}
@Test
public void testCategoryUpdateOnBirthdateChangeSimulation() {
// Set up adherent
Adherent adherent = new Adherent();
adherent.setDateNaissance(LocalDate.of(2015, 5, 10)); // 2015 birth year
Saison saison = new Saison();
saison.setNom("2025-2026");
saison.setEstActive(true);
// Set up categories
Categorie u11 = new Categorie();
u11.setNom("U11");
u11.setAnneeMin(2015);
u11.setAnneeMax(2015);
u11.setSaison(saison);
u11.setTarifBase(new BigDecimal("130.00"));
Categorie u12 = new Categorie();
u12.setNom("U12");
u12.setAnneeMin(2014);
u12.setAnneeMax(2014);
u12.setSaison(saison);
u12.setTarifBase(new BigDecimal("140.00"));
List<Categorie> categories = List.of(u11, u12);
// Licence initially created for U11
Licence licence = new Licence();
licence.setAdherent(adherent);
licence.setCategorie(u11);
licence.setSaison(saison);
// Simulation of date of birth changing to 2014 (U12)
adherent.setDateNaissance(LocalDate.of(2014, 5, 10));
int newBirthYear = adherent.getDateNaissance().getYear();
// Simulate Controller's update logic
Categorie newCat = categories.stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
assertNotNull(newCat);
assertEquals("U12", newCat.getNom());
licence.setCategorie(newCat);
// Verify the licence's category has updated to U12
assertEquals("U12", licence.getCategorie().getNom());
// Verify price total is updated dynamically
assertEquals(new BigDecimal("140.00"), licence.getPrixTotal());
}
}
@@ -0,0 +1,107 @@
package com.astalange.core;
import com.astalange.core.entity.*;
import org.junit.jupiter.api.Test;
import java.time.LocalTime;
import static org.junit.jupiter.api.Assertions.*;
public class CreneauEntrainementTest {
@Test
public void testStartAndEndColumns() {
CreneauEntrainement c1 = new CreneauEntrainement();
c1.setHeureDebut(LocalTime.of(8, 0));
c1.setHeureFin(LocalTime.of(9, 30));
// Start time 08:00 is (8 * 60) = 480 mins. Offset from 08:00 is 0 mins.
// Column is (0 / 15) + 3 = 3.
assertEquals(3, c1.getStartColumn());
// End time 09:30 is (9*60 + 30) = 570 mins. Offset from 08:00 is 90 mins.
// Column is (90 / 15) + 3 = 9.
assertEquals(9, c1.getEndColumn());
}
@Test
public void testStartAndEndRows() {
CreneauEntrainement c1 = new CreneauEntrainement();
// Test terrain complet (baseRow index 0 is 3, index 1 is 8)
c1.setZone(ZoneTerrain.COMPLET);
assertEquals(3, c1.getStartRow(0));
assertEquals(7, c1.getEndRow(0));
assertEquals(8, c1.getStartRow(1));
assertEquals(12, c1.getEndRow(1));
// Test demi-terrain A (Quarts 1 and 2)
c1.setZone(ZoneTerrain.DEMI_A);
assertEquals(3, c1.getStartRow(0));
assertEquals(5, c1.getEndRow(0));
// Test demi-terrain B (Quarts 3 and 4)
c1.setZone(ZoneTerrain.DEMI_B);
assertEquals(5, c1.getStartRow(0));
assertEquals(7, c1.getEndRow(0));
// Test single Quarts
c1.setZone(ZoneTerrain.QUART_1);
assertEquals(3, c1.getStartRow(0));
assertEquals(4, c1.getEndRow(0));
c1.setZone(ZoneTerrain.QUART_4);
assertEquals(6, c1.getStartRow(0));
assertEquals(7, c1.getEndRow(0));
}
@Test
public void testOverlapsWith() {
Terrain t1 = new Terrain();
t1.setId(1L);
Terrain t2 = new Terrain();
t2.setId(2L);
CreneauEntrainement c1 = new CreneauEntrainement();
c1.setJourSemaine(JourSemaine.LUNDI);
c1.setTerrain(t1);
c1.setZone(ZoneTerrain.DEMI_A);
c1.setHeureDebut(LocalTime.of(18, 0));
c1.setHeureFin(LocalTime.of(19, 30));
// 1. Different day -> No overlap
CreneauEntrainement c2 = new CreneauEntrainement();
c2.setJourSemaine(JourSemaine.MARDI);
c2.setTerrain(t1);
c2.setZone(ZoneTerrain.DEMI_A);
c2.setHeureDebut(LocalTime.of(18, 0));
c2.setHeureFin(LocalTime.of(19, 30));
assertFalse(c1.overlapsWith(c2));
// 2. Different terrain -> No overlap
c2.setJourSemaine(JourSemaine.LUNDI);
c2.setTerrain(t2);
assertFalse(c1.overlapsWith(c2));
// 3. Same day, same terrain, non-overlapping times -> No overlap
c2.setTerrain(t1);
c2.setHeureDebut(LocalTime.of(19, 30));
c2.setHeureFin(LocalTime.of(21, 0));
assertFalse(c1.overlapsWith(c2));
// 4. Same day, same terrain, overlapping times, non-overlapping zones (Demi_A and Demi_B don't overlap)
c2.setHeureDebut(LocalTime.of(18, 30));
c2.setHeureFin(LocalTime.of(20, 0));
c2.setZone(ZoneTerrain.DEMI_B);
assertFalse(c1.overlapsWith(c2));
// 5. Same day, same terrain, overlapping times, overlapping zones (Demi_A and Complet overlap!)
c2.setZone(ZoneTerrain.COMPLET);
assertTrue(c1.overlapsWith(c2));
// 6. Same day, same terrain, overlapping times, overlapping zones (Demi_A and Quart_1 overlap!)
c2.setZone(ZoneTerrain.QUART_1);
assertTrue(c1.overlapsWith(c2));
}
}
@@ -0,0 +1,34 @@
package com.astalange.core;
import com.astalange.core.entity.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EducateurTest {
@Test
public void testEducateurEntityAndAssociations() {
Categorie U15 = new Categorie();
U15.setNom("U15");
Equipe equipeA = new Equipe();
equipeA.setNom("Équipe A");
equipeA.setCategorie(U15);
Educateur educateur = new Educateur();
educateur.setNom("Dupont");
educateur.setPrenom("Jean");
educateur.setTelephone("0612345678");
educateur.setEmail("jean.dupont@example.com");
educateur.setCategorie(U15);
educateur.setEquipe(equipeA);
assertEquals("Dupont", educateur.getNom());
assertEquals("Jean", educateur.getPrenom());
assertEquals("0612345678", educateur.getTelephone());
assertEquals("jean.dupont@example.com", educateur.getEmail());
assertEquals(U15, educateur.getCategorie());
assertEquals(equipeA, educateur.getEquipe());
}
}
@@ -0,0 +1,34 @@
package com.astalange.core;
import com.astalange.core.entity.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class EquipeTest {
@Test
public void testEquipeEntityAndAssociations() {
Categorie U15 = new Categorie();
U15.setNom("U15");
Equipe equipeA = new Equipe();
equipeA.setNom("Équipe 1");
equipeA.setCategorie(U15);
assertEquals("Équipe 1", equipeA.getNom());
assertEquals(U15, equipeA.getCategorie());
Licence licence = new Licence();
licence.setCategorie(U15);
licence.setEquipe(equipeA);
assertEquals(equipeA, licence.getEquipe());
CreneauEntrainement creneau = new CreneauEntrainement();
creneau.setCategorie(U15);
creneau.setEquipe(equipeA);
assertEquals(equipeA, creneau.getEquipe());
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent> <parent>
<artifactId>as-talange-parent</artifactId> <artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId> <groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version> <version>1.6-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@@ -0,0 +1,36 @@
package com.astalange.web.config;
import com.astalange.core.security.CustomUserDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class PasswordChangeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
if (userDetails.isMustChangePassword()) {
String uri = request.getRequestURI();
// Exclude /change-password, /logout, static assets, error pages
if (!uri.equals("/change-password") &&
!uri.equals("/logout") &&
!uri.startsWith("/css/") &&
!uri.startsWith("/js/") &&
!uri.startsWith("/images/") &&
!uri.equals("/error")) {
response.sendRedirect("/change-password");
return false;
}
}
}
return true;
}
}
@@ -10,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -25,7 +28,12 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/inscription-public/**").permitAll()
.requestMatchers("/change-password").authenticated()
.requestMatchers("/utilisateurs/**", "/saisons/**", "/categories/**", "/equipes/**", "/modespaiement/**", "/equipements/**", "/admin/logs/**").hasRole("ADMIN")
.requestMatchers("/paiements/**", "/paiement/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
.requestMatchers("/planning/**").hasAnyRole("ADMIN", "AGENT_SAISIE")
.requestMatchers("/adherents/**", "/dotations/**").hasAnyRole("ADMIN", "TRESORERIE", "AGENT_SAISIE")
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.formLogin(form -> form .formLogin(form -> form
@@ -36,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout .logout(logout -> logout
.logoutSuccessUrl("/login?logout") .logoutSuccessUrl("/login?logout")
.permitAll() .permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
); );
return http.build(); return http.build();
} }
@@ -52,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; return authProvider;
} }
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
} }
@@ -0,0 +1,20 @@
package com.astalange.web.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final PasswordChangeInterceptor passwordChangeInterceptor;
public WebConfig(PasswordChangeInterceptor passwordChangeInterceptor) {
this.passwordChangeInterceptor = passwordChangeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(passwordChangeInterceptor);
}
}
@@ -1,8 +1,12 @@
package com.astalange.web.controller; package com.astalange.web.controller;
import com.astalange.core.entity.Adherent; import com.astalange.core.entity.Adherent;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Licence;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.AdherentRepository; import com.astalange.core.repository.AdherentRepository;
import com.astalange.core.repository.CategorieRepository; import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,18 +22,144 @@ public class AdherentController {
private final CategorieRepository categorieRepository; private final CategorieRepository categorieRepository;
private final com.astalange.core.repository.LicenceRepository licenceRepository; private final com.astalange.core.repository.LicenceRepository licenceRepository;
private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository; private final com.astalange.core.repository.ModePaiementRepository modePaiementRepository;
private final SaisonRepository saisonRepository;
private final com.astalange.core.service.CategorieService categorieService;
public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository) { public AdherentController(AdherentRepository adherentRepository, CategorieRepository categorieRepository, com.astalange.core.repository.LicenceRepository licenceRepository, com.astalange.core.repository.ModePaiementRepository modePaiementRepository, SaisonRepository saisonRepository, com.astalange.core.service.CategorieService categorieService) {
this.adherentRepository = adherentRepository; this.adherentRepository = adherentRepository;
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository; this.licenceRepository = licenceRepository;
this.modePaiementRepository = modePaiementRepository; this.modePaiementRepository = modePaiementRepository;
this.saisonRepository = saisonRepository;
this.categorieService = categorieService;
}
@org.springframework.web.bind.annotation.ModelAttribute("equipes")
public List<com.astalange.core.entity.Equipe> populateEquipes() {
return List.of();
} }
@GetMapping @GetMapping
public String listAdherents(Model model) { public String listAdherents(
List<Adherent> adherents = adherentRepository.findAll(); @org.springframework.web.bind.annotation.RequestParam(required = false) String query,
model.addAttribute("adherents", adherents); @org.springframework.web.bind.annotation.RequestParam(required = false) String nom,
@org.springframework.web.bind.annotation.RequestParam(required = false) String prenom,
@org.springframework.web.bind.annotation.RequestParam(required = false) String categorie,
@org.springframework.web.bind.annotation.RequestParam(required = false) String licence,
@org.springframework.web.bind.annotation.RequestParam(required = false) String email,
@org.springframework.web.bind.annotation.RequestParam(required = false) String paiement,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "nom") String sortField,
@org.springframework.web.bind.annotation.RequestParam(required = false, defaultValue = "asc") String sortDirection,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "0") int page,
@org.springframework.web.bind.annotation.RequestParam(defaultValue = "20") int size,
Model model) {
List<Adherent> adherents = adherentRepository.findAllWithLicencesAndCategories();
// 1. Filter by global query
if (query != null && !query.trim().isEmpty()) {
String q = query.trim().toLowerCase();
adherents = adherents.stream().filter(a ->
(a.getNom() != null && a.getNom().toLowerCase().contains(q)) ||
(a.getPrenom() != null && a.getPrenom().toLowerCase().contains(q)) ||
(a.getLicenceActuelle() != null && a.getLicenceActuelle().getNumeroLicence() != null && a.getLicenceActuelle().getNumeroLicence().toLowerCase().contains(q))
).collect(java.util.stream.Collectors.toList());
model.addAttribute("searchQuery", query.trim());
}
// 2. Filter by specific column filters
if (nom != null && !nom.trim().isEmpty()) {
String n = nom.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getNom() != null && a.getNom().toLowerCase().contains(n)).collect(java.util.stream.Collectors.toList());
model.addAttribute("nomFilter", nom.trim());
}
if (prenom != null && !prenom.trim().isEmpty()) {
String p = prenom.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getPrenom() != null && a.getPrenom().toLowerCase().contains(p)).collect(java.util.stream.Collectors.toList());
model.addAttribute("prenomFilter", prenom.trim());
}
if (categorie != null && !categorie.trim().isEmpty()) {
adherents = adherents.stream().filter(a ->
a.getLicenceActuelle() != null &&
a.getLicenceActuelle().getCategorie() != null &&
a.getLicenceActuelle().getCategorie().getNom().equalsIgnoreCase(categorie.trim())
).collect(java.util.stream.Collectors.toList());
model.addAttribute("categorieFilter", categorie.trim());
}
if (licence != null && !licence.trim().isEmpty()) {
String l = licence.trim().toLowerCase();
adherents = adherents.stream().filter(a ->
a.getLicenceActuelle() != null &&
a.getLicenceActuelle().getNumeroLicence() != null &&
a.getLicenceActuelle().getNumeroLicence().toLowerCase().contains(l)
).collect(java.util.stream.Collectors.toList());
model.addAttribute("licenceFilter", licence.trim());
}
if (email != null && !email.trim().isEmpty()) {
String e = email.trim().toLowerCase();
adherents = adherents.stream().filter(a -> a.getEmail() != null && a.getEmail().toLowerCase().contains(e)).collect(java.util.stream.Collectors.toList());
model.addAttribute("emailFilter", email.trim());
}
if (paiement != null && !paiement.trim().isEmpty()) {
adherents = adherents.stream().filter(a -> {
String statut = a.getStatutPaiement();
if ("A_JOUR".equalsIgnoreCase(paiement)) {
return "À jour".equalsIgnoreCase(statut);
} else if ("RESTE".equalsIgnoreCase(paiement)) {
return statut != null && statut.startsWith("Reste");
} else if ("AUCUNE".equalsIgnoreCase(paiement)) {
return "Aucune licence".equalsIgnoreCase(statut);
}
return true;
}).collect(java.util.stream.Collectors.toList());
model.addAttribute("paiementFilter", paiement.trim());
}
// 3. Sort
java.util.Comparator<Adherent> comparator = (a1, a2) -> 0;
if ("nom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getNom() != null ? a.getNom() : "");
} else if ("prenom".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getPrenom() != null ? a.getPrenom() : "");
} else if ("categorie".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getCategorie() != null && a.getLicenceActuelle().getCategorie().getNom() != null) ? a.getLicenceActuelle().getCategorie().getNom() : "");
} else if ("licence".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> (a.getLicenceActuelle() != null && a.getLicenceActuelle().getNumeroLicence() != null) ? a.getLicenceActuelle().getNumeroLicence() : "");
} else if ("email".equalsIgnoreCase(sortField)) {
comparator = java.util.Comparator.comparing(a -> a.getEmail() != null ? a.getEmail() : "");
}
if ("desc".equalsIgnoreCase(sortDirection)) {
comparator = comparator.reversed();
}
adherents.sort(comparator);
model.addAttribute("sortField", sortField);
model.addAttribute("sortDirection", sortDirection);
// 4. Paginate
int totalElements = adherents.size();
int totalPages = (int) Math.ceil((double) totalElements / size);
if (page < 0) page = 0;
if (page >= totalPages && totalPages > 0) page = totalPages - 1;
int start = page * size;
int end = Math.min(start + size, totalElements);
List<Adherent> pageContent = (start < totalElements) ? adherents.subList(start, end) : List.of();
model.addAttribute("adherents", pageContent);
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", totalPages);
model.addAttribute("totalElements", totalElements);
model.addAttribute("pageSize", size);
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
return "adherents/list"; return "adherents/list";
} }
@@ -46,19 +176,94 @@ public class AdherentController {
Model model) { Model model) {
if (result.hasErrors()) { if (result.hasErrors()) {
if (adherent.getId() != null) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
model.addAttribute("licences", licenceRepository.findByAdherentId(adherent.getId()));
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
} else {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
}
return "adherents/form"; return "adherents/form";
} }
adherentRepository.save(adherent); if (adherent.getId() != null) {
return "redirect:/adherents"; Adherent existing = adherentRepository.findById(adherent.getId()).orElse(null);
if (existing != null) {
boolean dateChanged = !existing.getDateNaissance().equals(adherent.getDateNaissance());
boolean typeMaillotChanged = !existing.getTypeMaillot().equals(adherent.getTypeMaillot());
if (dateChanged || typeMaillotChanged) {
int newBirthYear = adherent.getDateNaissance().getYear();
List<Licence> licences = licenceRepository.findByAdherentId(adherent.getId());
for (Licence licence : licences) {
if (dateChanged) {
Saison saison = licence.getSaison();
Categorie newCat = categorieRepository.findBySaison(saison).stream()
.filter(c -> newBirthYear >= c.getAnneeMin() && newBirthYear <= c.getAnneeMax())
.findFirst()
.orElse(null);
if (newCat != null) {
licence.setCategorie(newCat);
licence = licenceRepository.save(licence);
}
}
// We need to update the entity's state before syncing dotations so it reads the new typeMaillot.
// We haven't updated 'existing' yet, so let's temporarily set it on the licence's adherent or wait.
// Actually, 'adherent' has the new typeMaillot.
licence.getAdherent().setTypeMaillot(adherent.getTypeMaillot());
categorieService.syncDotationsForLicence(licence);
}
}
existing.setNom(adherent.getNom());
existing.setPrenom(adherent.getPrenom());
existing.setDateNaissance(adherent.getDateNaissance());
existing.setLieuNaissanceVille(adherent.getLieuNaissanceVille());
existing.setLieuNaissancePays(adherent.getLieuNaissancePays());
existing.setNationalite(adherent.getNationalite());
existing.setEmail(adherent.getEmail());
existing.setRepresentantLegal(adherent.getRepresentantLegal());
existing.setResidentTalange(adherent.isResidentTalange());
existing.setSexe(adherent.getSexe());
existing.setTypeMaillot(adherent.getTypeMaillot());
adherentRepository.save(existing);
} else {
adherentRepository.save(adherent);
}
return "redirect:/adherents";
} else {
Adherent saved = adherentRepository.save(adherent);
return "redirect:/adherents/" + saved.getId() + "/edit";
}
} }
@GetMapping("/{id}/edit") @GetMapping("/{id}/edit")
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) { public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
Adherent adherent = adherentRepository.findById(id) Adherent adherent = adherentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id)); .orElseThrow(() -> new IllegalArgumentException("Adhérent invalide : " + id));
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
}
model.addAttribute("adherent", adherent); model.addAttribute("adherent", adherent);
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("licences", licenceRepository.findByAdherentId(id)); model.addAttribute("licences", licenceRepository.findByAdherentId(id));
model.addAttribute("modesPaiement", modePaiementRepository.findAll()); model.addAttribute("modesPaiement", modePaiementRepository.findAll());
return "adherents/form"; return "adherents/form";
@@ -0,0 +1,56 @@
package com.astalange.web.controller;
import com.astalange.core.entity.Adherent;
import com.astalange.core.repository.PreInscriptionRepository;
import com.astalange.core.service.PreInscriptionService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/admin/pre-inscriptions")
public class AdminPreInscriptionController {
private final PreInscriptionRepository preInscriptionRepository;
private final PreInscriptionService preInscriptionService;
public AdminPreInscriptionController(PreInscriptionRepository preInscriptionRepository,
PreInscriptionService preInscriptionService) {
this.preInscriptionRepository = preInscriptionRepository;
this.preInscriptionService = preInscriptionService;
}
@GetMapping
public String listPreInscriptions(Model model) {
model.addAttribute("preInscriptions", preInscriptionRepository.findByStatutTraiteFalseOrderByDateCreationDesc());
return "preinscriptions/list";
}
@PostMapping("/{id}/valider")
@ResponseBody
public String valider(@PathVariable Long id, HttpServletResponse response) {
Adherent adherent = preInscriptionService.validerPreInscription(id, null);
response.setHeader("HX-Trigger", "refreshBadge");
// En HTMX, on renvoie une ligne vide pour la faire disparaître, ou on redirige via HX-Redirect
// Mais comme on veut aller sur la page de l'adhérent pour finaliser la licence :
response.setHeader("HX-Redirect", "/adherents/" + adherent.getId() + "/edit");
return "";
}
@PostMapping("/{id}/rejeter")
@ResponseBody
public String rejeter(@PathVariable Long id, HttpServletResponse response) {
preInscriptionService.rejeterPreInscription(id);
response.setHeader("HX-Trigger", "refreshBadge");
return ""; // HTMX target la ligne avec outerHTML -> supprime la ligne
}
@GetMapping("/count")
@ResponseBody
public String countBadge() {
long count = preInscriptionRepository.countByStatutTraiteFalse();
if (count == 0) return "";
return "<span class=\"bg-red-500 text-white rounded-full px-2 py-0.5 text-xs font-bold ml-2\">" + count + "</span>";
}
}
@@ -0,0 +1,34 @@
package com.astalange.web.controller;
import com.astalange.core.repository.AuditLogPaiementRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/admin/logs/paiements")
public class AuditLogPaiementController {
private final AuditLogPaiementRepository auditLogPaiementRepository;
public AuditLogPaiementController(AuditLogPaiementRepository auditLogPaiementRepository) {
this.auditLogPaiementRepository = auditLogPaiementRepository;
}
@GetMapping
public String listLogs(
@RequestParam(required = false) String action,
@RequestParam(required = false) String utilisateur,
@RequestParam(required = false) String adherent,
Model model) {
model.addAttribute("logs", auditLogPaiementRepository.findByFilters(action, utilisateur, adherent));
model.addAttribute("paramAction", action);
model.addAttribute("paramUtilisateur", utilisateur);
model.addAttribute("paramAdherent", adherent);
return "audit/paiements_logs";
}
}
@@ -1,19 +1,31 @@
package com.astalange.web.controller; package com.astalange.web.controller;
import com.astalange.core.entity.Categorie; import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.CategorieRepository; import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.service.CategorieService;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller @Controller
@RequestMapping("/categories") @RequestMapping("/categories")
public class CategorieController { public class CategorieController {
private final CategorieRepository categorieRepository; private final CategorieRepository categorieRepository;
private final SaisonRepository saisonRepository;
private final EquipementRepository equipementRepository;
private final CategorieService categorieService;
public CategorieController(CategorieRepository categorieRepository) { public CategorieController(CategorieRepository categorieRepository, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieService categorieService) {
this.categorieRepository = categorieRepository; this.categorieRepository = categorieRepository;
this.saisonRepository = saisonRepository;
this.equipementRepository = equipementRepository;
this.categorieService = categorieService;
} }
@GetMapping @GetMapping
@@ -24,7 +36,11 @@ public class CategorieController {
@GetMapping("/new") @GetMapping("/new")
public String showCreateForm(Model model) { public String showCreateForm(Model model) {
model.addAttribute("categorie", new Categorie()); if (!model.containsAttribute("categorie")) {
model.addAttribute("categorie", new Categorie());
}
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
model.addAttribute("allEquipements", activeSaison != null ? equipementRepository.findBySaison(activeSaison) : List.of());
return "parametrage/categories_form"; return "parametrage/categories_form";
} }
@@ -33,12 +49,61 @@ public class CategorieController {
Categorie categorie = categorieRepository.findById(id) Categorie categorie = categorieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id)); .orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
model.addAttribute("categorie", categorie); model.addAttribute("categorie", categorie);
Saison saison = categorie.getSaison();
if (saison == null) {
saison = saisonRepository.findByEstActiveTrue().orElse(null);
}
model.addAttribute("allEquipements", saison != null ? equipementRepository.findBySaison(saison) : List.of());
return "parametrage/categories_form"; return "parametrage/categories_form";
} }
@PostMapping @PostMapping
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) { public String saveCategorie(@ModelAttribute("categorie") Categorie categorie,
categorieRepository.save(categorie); jakarta.servlet.http.HttpServletRequest request,
org.springframework.web.servlet.mvc.support.RedirectAttributes redirectAttributes) {
Saison activeSaison;
if (categorie.getId() == null) {
activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
} else {
Categorie existing = categorieRepository.findById(categorie.getId()).orElse(null);
activeSaison = existing != null ? existing.getSaison() : null;
}
if (activeSaison != null) {
for (Categorie c : categorieRepository.findBySaison(activeSaison)) {
if (c.getNom().equalsIgnoreCase(categorie.getNom()) && (categorie.getId() == null || !c.getId().equals(categorie.getId()))) {
redirectAttributes.addFlashAttribute("errorMessage", "Erreur : La catégorie '" + categorie.getNom() + "' existe déjà pour cette saison.");
redirectAttributes.addFlashAttribute("categorie", categorie);
return categorie.getId() == null ? "redirect:/categories/new" : "redirect:/categories/" + categorie.getId() + "/edit";
}
}
}
java.util.Map<Long, com.astalange.core.service.CategorieService.EquipementConfig> configs = new java.util.HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (key.startsWith("etatEquipement_") && values.length > 0) {
String idStr = key.substring("etatEquipement_".length());
try {
Long eqId = Long.parseLong(idStr);
String etat = values[0];
if (!"NON_LIE".equals(etat)) {
boolean obligatoire = "OBLIGATOIRE".equals(etat);
String prixStr = request.getParameter("prixEquipement_" + eqId);
java.math.BigDecimal prix = java.math.BigDecimal.ZERO;
if (prixStr != null && !prixStr.trim().isEmpty()) {
prix = new java.math.BigDecimal(prixStr.trim());
}
configs.put(eqId, new com.astalange.core.service.CategorieService.EquipementConfig(obligatoire, prix));
}
} catch (NumberFormatException ignored) {}
}
});
categorieService.saveCategorie(categorie, configs);
return "redirect:/categories"; return "redirect:/categories";
} }
@@ -49,4 +114,10 @@ public class CategorieController {
categorieRepository.delete(categorie); categorieRepository.delete(categorie);
return "redirect:/categories"; return "redirect:/categories";
} }
@PostMapping("/{id}/duplicate")
public String duplicateCategorie(@PathVariable Long id) {
categorieService.duplicateCategorie(id);
return "redirect:/categories";
}
} }
@@ -0,0 +1,83 @@
package com.astalange.web.controller;
import com.astalange.core.entity.AppUser;
import com.astalange.core.repository.AppUserRepository;
import com.astalange.core.security.CustomUserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class ChangePasswordController {
private final AppUserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public ChangePasswordController(AppUserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping("/change-password")
public String showChangePasswordForm() {
return "change-password";
}
@PostMapping("/change-password")
public String changePassword(
@RequestParam String newPassword,
@RequestParam String confirmPassword,
Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails)) {
return "redirect:/login";
}
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
if (!newPassword.equals(confirmPassword)) {
model.addAttribute("error", "Les mots de passe ne correspondent pas.");
return "change-password";
}
if (newPassword.equals("AsTalange123")) {
model.addAttribute("error", "Le nouveau mot de passe doit être différent de AsTalange123.");
return "change-password";
}
if (newPassword.length() < 6) {
model.addAttribute("error", "Le mot de passe doit contenir au moins 6 caractères.");
return "change-password";
}
AppUser appUser = userRepository.findById(userDetails.getId())
.orElseThrow(() -> new IllegalStateException("Utilisateur non trouvé"));
appUser.setPassword(passwordEncoder.encode(newPassword));
appUser.setMustChangePassword(false);
userRepository.save(appUser);
// Update Authentication in Security Context
CustomUserDetails newPrincipal = new CustomUserDetails(
appUser.getId(),
appUser.getUsername(),
appUser.getPassword(),
appUser.isEnabled(),
false,
auth.getAuthorities()
);
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
newPrincipal, auth.getCredentials(), auth.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(newAuth);
return "redirect:/?passwordChanged=true";
}
}
@@ -3,17 +3,128 @@ package com.astalange.web.controller;
import com.astalange.core.entity.Dotation; import com.astalange.core.entity.Dotation;
import com.astalange.core.repository.DotationRepository; import com.astalange.core.repository.DotationRepository;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.time.LocalDate;
import com.astalange.core.service.DotationService;
import com.astalange.core.repository.SaisonRepository;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.CategorieRepository;
import org.springframework.ui.Model;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
@Controller @Controller
public class DotationController { public class DotationController {
private final DotationRepository dotationRepository; private final DotationRepository dotationRepository;
private final DotationService dotationService;
private final SaisonRepository saisonRepository;
private final EquipementRepository equipementRepository;
private final CategorieRepository categorieRepository;
public DotationController(DotationRepository dotationRepository) { public DotationController(DotationRepository dotationRepository, DotationService dotationService, SaisonRepository saisonRepository, EquipementRepository equipementRepository, CategorieRepository categorieRepository) {
this.dotationRepository = dotationRepository; this.dotationRepository = dotationRepository;
this.dotationService = dotationService;
this.saisonRepository = saisonRepository;
this.equipementRepository = equipementRepository;
this.categorieRepository = categorieRepository;
}
private Specification<Dotation> buildSpecification(Long equipementId, Long categorieId, Boolean fourni, Saison saisonActive) {
return (root, query, cb) -> {
if (Long.class != query.getResultType() && long.class != query.getResultType()) {
root.fetch("equipement", jakarta.persistence.criteria.JoinType.LEFT);
jakarta.persistence.criteria.Fetch<Object, Object> licenceFetch = root.fetch("licence", jakarta.persistence.criteria.JoinType.LEFT);
licenceFetch.fetch("adherent", jakarta.persistence.criteria.JoinType.LEFT);
licenceFetch.fetch("categorie", jakarta.persistence.criteria.JoinType.LEFT);
}
List<Predicate> predicates = new ArrayList<>();
if (saisonActive != null) {
predicates.add(cb.equal(root.get("licence").get("saison"), saisonActive));
}
if (equipementId != null) {
predicates.add(cb.equal(root.get("equipement").get("id"), equipementId));
}
if (categorieId != null) {
predicates.add(cb.equal(root.get("licence").get("categorie").get("id"), categorieId));
}
if (fourni != null) {
predicates.add(cb.equal(root.get("fourni"), fourni));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
@GetMapping("/admin/equipements/recherche")
public String rechercheEquipements(
@RequestParam(required = false) Long equipementId,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) Boolean fourni,
Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
List<Dotation> dotations = dotationRepository.findAll(spec);
model.addAttribute("dotations", dotations);
model.addAttribute("equipements", equipementRepository.findAll());
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("equipementId", equipementId);
model.addAttribute("categorieId", categorieId);
model.addAttribute("fourni", fourni);
return "parametrage/equipements_recherche";
}
@GetMapping("/admin/equipements/recherche/export")
public ResponseEntity<byte[]> exportRechercheEquipements(
@RequestParam(required = false) Long equipementId,
@RequestParam(required = false) Long categorieId,
@RequestParam(required = false) Boolean fourni) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
Specification<Dotation> spec = buildSpecification(equipementId, categorieId, fourni, saisonActive);
List<Dotation> dotations = dotationRepository.findAll(spec);
String csvContent = dotationService.genererCsvSearchEquipement(dotations);
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "recherche_equipements_" + LocalDate.now() + ".csv");
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
}
@GetMapping("/admin/equipements/export-commande")
public ResponseEntity<byte[]> exportCommande() {
Saison saisonActive = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active"));
String csvContent = dotationService.genererCsvCommandeEquipement(saisonActive);
byte[] csvBytes = csvContent.getBytes(java.nio.charset.StandardCharsets.UTF_8);
HttpHeaders headers = new HttpHeaders();
headers.setContentDispositionFormData("attachment", "commande_equipements_" + LocalDate.now() + ".csv");
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
return new ResponseEntity<>(csvBytes, headers, org.springframework.http.HttpStatus.OK);
} }
@PostMapping("/dotations/{id}") @PostMapping("/dotations/{id}")
@@ -22,17 +133,63 @@ public class DotationController {
@RequestParam Long adherentId, @RequestParam Long adherentId,
@RequestParam(required = false, defaultValue = "") String taille, @RequestParam(required = false, defaultValue = "") String taille,
@RequestParam(required = false, defaultValue = "") String flocage, @RequestParam(required = false, defaultValue = "") String flocage,
@RequestParam(required = false) Boolean fourni) { @RequestParam(required = false, defaultValue = "") String numero,
@RequestParam(required = false) Boolean fourni,
@RequestParam(required = false) Boolean choisi) {
Dotation dotation = dotationRepository.findById(id) Dotation dotation = dotationRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id)); .orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
dotation.setTaille(taille); dotation.setTaille(taille);
dotation.setFlocage(flocage); dotation.setFlocage(flocage);
dotation.setNumero(numero);
dotation.setFourni(fourni != null && fourni); dotation.setFourni(fourni != null && fourni);
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
dotation.setChoisi(true);
} else {
dotation.setChoisi(choisi != null && choisi);
}
dotationRepository.save(dotation); dotationRepository.save(dotation);
return "redirect:/adherents/" + adherentId + "/edit"; return "redirect:/adherents/" + adherentId + "/edit";
} }
@Transactional
@PostMapping("/dotations/bulk")
public String bulkUpdateDotations(
@RequestParam Long adherentId,
@RequestParam("dotationIds") List<Long> dotationIds,
HttpServletRequest request) {
for (Long id : dotationIds) {
Dotation dotation = dotationRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
String taille = request.getParameter("taille_" + id);
String couleur = request.getParameter("couleur_" + id);
String numero = request.getParameter("numero_" + id);
String flocage = request.getParameter("flocage_" + id);
String choisiParam = request.getParameter("choisi_" + id);
String fourniParam = request.getParameter("fourni_" + id);
if (taille != null) dotation.setTaille(taille);
if (couleur != null) dotation.setCouleur(couleur);
if (numero != null) dotation.setNumero(numero);
if (flocage != null) dotation.setFlocage(flocage);
if (dotation.getLicence().getCategorie().isEquipementObligatoire(dotation.getEquipement().getId())) {
dotation.setChoisi(true);
} else {
dotation.setChoisi(choisiParam != null);
}
dotation.setFourni(fourniParam != null);
dotationRepository.save(dotation);
}
return "redirect:/adherents/" + adherentId + "/edit";
}
} }
@@ -0,0 +1,88 @@
package com.astalange.web.controller;
import com.astalange.core.entity.Educateur;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Equipe;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.EducateurRepository;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.EquipeRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/educateurs")
public class EducateurController {
private final EducateurRepository educateurRepository;
private final CategorieRepository categorieRepository;
private final EquipeRepository equipeRepository;
private final SaisonRepository saisonRepository;
public EducateurController(EducateurRepository educateurRepository,
CategorieRepository categorieRepository,
EquipeRepository equipeRepository,
SaisonRepository saisonRepository) {
this.educateurRepository = educateurRepository;
this.categorieRepository = categorieRepository;
this.equipeRepository = equipeRepository;
this.saisonRepository = saisonRepository;
}
@GetMapping
public String listEducateurs(Model model) {
model.addAttribute("educateurs", educateurRepository.findAllWithCategorieAndEquipe());
return "parametrage/educateurs_list";
}
@GetMapping("/new")
public String showCreateForm(Model model) {
model.addAttribute("educateur", new Educateur());
populateModelAttributes(model);
return "parametrage/educateurs_form";
}
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) {
Educateur educateur = educateurRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
model.addAttribute("educateur", educateur);
populateModelAttributes(model);
return "parametrage/educateurs_form";
}
@PostMapping
public String saveEducateur(@ModelAttribute("educateur") Educateur educateur) {
if (educateur.getCategorie() != null && educateur.getCategorie().getId() == null) {
educateur.setCategorie(null);
}
if (educateur.getEquipe() != null && educateur.getEquipe().getId() == null) {
educateur.setEquipe(null);
}
educateurRepository.save(educateur);
return "redirect:/educateurs";
}
@PostMapping("/{id}/delete")
public String deleteEducateur(@PathVariable Long id) {
Educateur educateur = educateurRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid educator ID: " + id));
educateurRepository.delete(educateur);
return "redirect:/educateurs";
}
private void populateModelAttributes(Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
} else {
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("equipes", equipeRepository.findAllWithCategorie());
}
}
}
@@ -0,0 +1,73 @@
package com.astalange.web.controller;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Equipe;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.CategorieRepository;
import com.astalange.core.repository.EquipeRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
public class EquipeController {
private final EquipeRepository equipeRepository;
private final CategorieRepository categorieRepository;
private final SaisonRepository saisonRepository;
public EquipeController(EquipeRepository equipeRepository, CategorieRepository categorieRepository, SaisonRepository saisonRepository) {
this.equipeRepository = equipeRepository;
this.categorieRepository = categorieRepository;
this.saisonRepository = saisonRepository;
}
@GetMapping("/categories/equipes")
public String getEquipesByCategorie(@RequestParam(required = false) Long categorieId, Model model) {
if (categorieId != null) {
model.addAttribute("equipes", equipeRepository.findByCategorieId(categorieId));
} else {
model.addAttribute("equipes", List.of());
}
return "adherents/form :: equipe-options";
}
@GetMapping("/equipes")
public String listEquipes(Model model) {
Saison saisonActive = saisonRepository.findByEstActiveTrue().orElse(null);
if (saisonActive != null) {
model.addAttribute("categories", categorieRepository.findBySaison(saisonActive));
model.addAttribute("equipes", equipeRepository.findBySaison(saisonActive));
model.addAttribute("saisonActive", saisonActive);
} else {
model.addAttribute("categories", categorieRepository.findAll());
model.addAttribute("equipes", equipeRepository.findAll());
}
return "parametrage/equipes_list";
}
@PostMapping("/equipes")
public String saveEquipe(@RequestParam String nom, @RequestParam Long categorieId) {
Categorie categorie = categorieRepository.findById(categorieId)
.orElseThrow(() -> new IllegalArgumentException("Catégorie invalide : " + categorieId));
Saison saison = categorie.getSaison();
if (saison == null) {
saison = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active configurée"));
}
Equipe equipe = new Equipe(nom, categorie, saison);
equipeRepository.save(equipe);
return "redirect:/equipes";
}
@PostMapping("/equipes/{id}/delete")
public String deleteEquipe(@PathVariable Long id) {
Equipe equipe = equipeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Équipe invalide : " + id));
equipeRepository.delete(equipe);
return "redirect:/equipes";
}
}

Some files were not shown because too many files have changed in this diff Show More