99 Commits

Author SHA1 Message Date
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
144 changed files with 7536 additions and 455 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>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version>
<version>1.5-SNAPSHOT</version>
</parent>
<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>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version>
<version>1.5-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -61,5 +61,11 @@
<version>1.77</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -23,22 +23,49 @@ public class Adherent {
@org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE)
private LocalDate dateNaissance;
@Column(length = 20)
private String telephone;
@Column(name = "lieu_naissance_ville", nullable = false, length = 100)
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;
@Column(columnDefinition = "TEXT")
private String adresse;
@Column(nullable = false, length = 20)
private String telephone;
@Column(name = "representant_legal", length = 150)
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;
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
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 void setId(Long id) { this.id = id; }
@@ -51,18 +78,33 @@ public class Adherent {
public LocalDate getDateNaissance() { return dateNaissance; }
public void setDateNaissance(LocalDate dateNaissance) { this.dateNaissance = dateNaissance; }
public String getTelephone() { return telephone; }
public void setTelephone(String telephone) { this.telephone = telephone; }
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 getAdresse() { return adresse; }
public void setAdresse(String adresse) { this.adresse = adresse; }
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 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 boolean isResidentTalange() { return residentTalange; }
public void setResidentTalange(boolean residentTalange) { this.residentTalange = residentTalange; }
@@ -26,6 +26,9 @@ public class AppUser {
@Column(nullable = false)
private boolean enabled = true;
@Column(name = "must_change_password", nullable = false)
private boolean mustChangePassword = false;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
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 java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "categorie")
@Table(name = "categorie", uniqueConstraints = {
@UniqueConstraint(columnNames = {"nom", "saison_id"})
})
public class Categorie {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 20)
@Column(nullable = false, length = 20)
private String nom;
@Column(nullable = false, length = 10)
private String sexe = "MASCULIN";
@Column(name = "annee_min")
private Integer anneeMin;
@@ -26,6 +33,13 @@ public class Categorie {
@Column(name = "tarif_exterieur", nullable = false, precision = 10, scale = 2)
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
public Long getId() { return id; }
@@ -34,6 +48,9 @@ public class Categorie {
public String getNom() { return 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 void setAnneeMin(Integer anneeMin) { this.anneeMin = anneeMin; }
@@ -45,4 +62,61 @@ public class Categorie {
public BigDecimal getTarifExterieur() { return 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)
private Equipement equipement;
@Column(length = 10)
@Column(length = 50)
private String taille;
@Column(length = 50)
private String couleur;
@Column(length = 50)
private String flocage;
@Column(nullable = false)
private Boolean fourni = false;
@Column(length = 50)
private String numero;
@Column(nullable = false)
private Boolean choisi = false;
// Getters and Setters
public Long getId() { return id; }
@@ -41,9 +50,18 @@ public class Dotation {
public String getTaille() { return 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 void setFlocage(String flocage) { this.flocage = flocage; }
public Boolean getFourni() { return 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;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "equipement")
@@ -13,11 +14,28 @@ public class Equipement {
@Column(nullable = false, length = 100)
private String nom;
@Column(length = 100)
private String reference;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false)
private Boolean obligatoire = false;
@Column(name = "gestion_sexe", nullable = 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;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "saison_id", nullable = false)
private Saison saison;
// Getters and Setters
@@ -27,9 +45,24 @@ public class Equipement {
public String getNom() { return 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 void setDescription(String description) { this.description = description; }
public Boolean getObligatoire() { return obligatoire; }
public void setObligatoire(Boolean obligatoire) { this.obligatoire = obligatoire; }
public boolean isGestionSexe() { return gestionSexe; }
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 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)
private Categorie categorie;
@Column(nullable = false, length = 20)
private String saison;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@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)
private String etat;
@@ -30,15 +35,21 @@ public class Licence {
@Column(name = "numero_licence", length = 50)
private String numeroLicence;
@Column(name = "type_demande", length = 50)
private String typeDemande;
@Column(name = "type_licence", length = 50)
private String typeLicence;
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Paiement> paiements = new ArrayList<>();
@OneToMany(mappedBy = "licence", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Dotation> dotations = new ArrayList<>();
// Logic for Reste A Payer
// Logic for Global Total Price
@Transient
public BigDecimal getResteAPayer() {
public BigDecimal getPrixTotal() {
if (categorie == null) return BigDecimal.ZERO;
BigDecimal prixTotal = (adherent != null && adherent.isResidentTalange())
@@ -47,6 +58,25 @@ public class Licence {
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()) {
return prixTotal;
}
@@ -59,6 +89,16 @@ public class Licence {
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
public Long getId() { return id; }
@@ -70,20 +110,29 @@ public class Licence {
public Categorie getCategorie() { return categorie; }
public void setCategorie(Categorie categorie) { this.categorie = categorie; }
public String getSaison() { return saison; }
public void setSaison(String saison) { this.saison = saison; }
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 String getEtat() { return 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 List<Paiement> getPaiements() { return paiements; }
public void setPaiements(List<Paiement> paiements) { this.paiements = paiements; }
public List<Dotation> getDotations() { return 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) {
paiements.add(paiement);
@@ -26,6 +26,15 @@ public class Paiement {
@JoinColumn(name = "mode_paiement_id", nullable = false)
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
public Long getId() { return id; }
@@ -42,4 +51,13 @@ public class Paiement {
public ModePaiement getModePaiement() { return 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,121 @@
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";
// 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; }
}
@@ -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
public interface AdherentRepository extends JpaRepository<Adherent, Long> {
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.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
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;
import com.astalange.core.entity.Dotation;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import com.astalange.core.entity.Saison;
@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.stereotype.Repository;
import com.astalange.core.entity.Saison;
import java.util.List;
@Repository
public interface EquipementRepository extends JpaRepository<Equipement, Long> {
List<Equipement> findBySaison(Saison saison);
}
@@ -8,9 +8,13 @@ import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@Repository
public interface LicenceRepository extends JpaRepository<Licence, Long> {
public interface LicenceRepository extends JpaRepository<Licence, Long>, JpaSpecificationExecutor<Licence> {
List<Licence> findByAdherentId(Long adherentId);
List<Licence> findBySaisonAndCategorie(com.astalange.core.entity.Saison saison, com.astalange.core.entity.Categorie categorie);
long countByEtat(String etat);
@@ -11,4 +11,12 @@ public interface PaiementRepository extends JpaRepository<Paiement, Long> {
@Query("SELECT SUM(p.montant) FROM Paiement p")
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)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new User(
return new CustomUserDetails(
appUser.getId(),
appUser.getUsername(),
appUser.getPassword(),
appUser.isEnabled(),
true, true, true,
appUser.isMustChangePassword(),
appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.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,160 @@
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) {
newDotation.setFlocage(licence.getAdherent().getNom().toUpperCase() + " " + licence.getAdherent().getPrenom());
}
currentDotations.add(newDotation);
}
}
licenceRepository.save(licence);
}
}
@@ -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,71 @@
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());
// 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,
name VARCHAR(50) UNIQUE NOT NULL
);
CREATE TABLE app_user (
CREATE TABLE IF NOT EXISTS app_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE user_role (
CREATE TABLE IF NOT EXISTS user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
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
);
CREATE TABLE adherent (
CREATE TABLE IF NOT EXISTS adherent (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
@@ -29,7 +29,7 @@ CREATE TABLE adherent (
representant_legal VARCHAR(150)
);
CREATE TABLE categorie (
CREATE TABLE IF NOT EXISTS categorie (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(20) UNIQUE NOT NULL,
annee_min INT,
@@ -37,7 +37,7 @@ CREATE TABLE categorie (
tarif_base DECIMAL(10,2) NOT NULL
);
CREATE TABLE licence (
CREATE TABLE IF NOT EXISTS licence (
id BIGSERIAL PRIMARY KEY,
adherent_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)
);
CREATE TABLE paiement (
CREATE TABLE IF NOT EXISTS paiement (
id BIGSERIAL PRIMARY KEY,
licence_id BIGINT 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
);
CREATE TABLE equipement (
CREATE TABLE IF NOT EXISTS equipement (
id BIGSERIAL PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
description TEXT,
obligatoire BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE dotation (
CREATE TABLE IF NOT EXISTS dotation (
id BIGSERIAL PRIMARY KEY,
licence_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,
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);
@@ -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
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
('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),
('U12', 2014, 2014, 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
('Maillot', true),
('Short', true),
('Chaussettes', true),
('Survêtement', false);
INSERT INTO equipement (nom, obligatoire)
SELECT 'Maillot', true WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Maillot');
INSERT INTO equipement (nom, obligatoire)
SELECT 'Short', true WHERE NOT EXISTS (SELECT 1 FROM equipement WHERE nom = 'Short');
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)
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
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
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>
<artifactId>as-talange-parent</artifactId>
<groupId>com.astalange</groupId>
<version>1.0-SNAPSHOT</version>
<version>1.5-SNAPSHOT</version>
</parent>
<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.password.PasswordEncoder;
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
@EnableWebSecurity
@@ -25,7 +28,12 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.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()
)
.formLogin(form -> form
@@ -36,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
);
return http.build();
}
@@ -52,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder());
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;
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.CategorieRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,18 +22,120 @@ public class AdherentController {
private final CategorieRepository categorieRepository;
private final com.astalange.core.repository.LicenceRepository licenceRepository;
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.categorieRepository = categorieRepository;
this.licenceRepository = licenceRepository;
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
public String listAdherents(Model model) {
List<Adherent> adherents = adherentRepository.findAll();
model.addAttribute("adherents", adherents);
public String listAdherents(
@org.springframework.web.bind.annotation.RequestParam(required = false) String query,
@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(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. 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";
}
@@ -46,19 +152,94 @@ public class AdherentController {
Model model) {
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";
}
adherentRepository.save(adherent);
return "redirect:/adherents";
if (adherent.getId() != null) {
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")
public String editAdherent(@org.springframework.web.bind.annotation.PathVariable Long id, Model model) {
Adherent adherent = adherentRepository.findById(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("categories", categorieRepository.findAll());
model.addAttribute("licences", licenceRepository.findByAdherentId(id));
model.addAttribute("modesPaiement", modePaiementRepository.findAll());
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;
import com.astalange.core.entity.Categorie;
import com.astalange.core.entity.Saison;
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.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/categories")
public class CategorieController {
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.saisonRepository = saisonRepository;
this.equipementRepository = equipementRepository;
this.categorieService = categorieService;
}
@GetMapping
@@ -25,6 +37,8 @@ public class CategorieController {
@GetMapping("/new")
public String showCreateForm(Model model) {
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";
}
@@ -33,12 +47,42 @@ public class CategorieController {
Categorie categorie = categorieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Invalid category ID: " + id));
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";
}
@PostMapping
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie) {
categorieRepository.save(categorie);
public String saveCategorie(@ModelAttribute("categorie") Categorie categorie,
jakarta.servlet.http.HttpServletRequest request) {
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";
}
@@ -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.repository.DotationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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
public class DotationController {
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.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}")
@@ -22,17 +133,63 @@ public class DotationController {
@RequestParam Long adherentId,
@RequestParam(required = false, defaultValue = "") String taille,
@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)
.orElseThrow(() -> new IllegalArgumentException("Invalid Dotation ID: " + id));
dotation.setTaille(taille);
dotation.setFlocage(flocage);
dotation.setNumero(numero);
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);
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";
}
}
@@ -1,7 +1,9 @@
package com.astalange.web.controller;
import com.astalange.core.entity.Equipement;
import com.astalange.core.entity.Saison;
import com.astalange.core.repository.EquipementRepository;
import com.astalange.core.repository.SaisonRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@@ -11,14 +13,21 @@ import org.springframework.web.bind.annotation.*;
public class EquipementController {
private final EquipementRepository equipementRepository;
private final SaisonRepository saisonRepository;
public EquipementController(EquipementRepository equipementRepository) {
public EquipementController(EquipementRepository equipementRepository, SaisonRepository saisonRepository) {
this.equipementRepository = equipementRepository;
this.saisonRepository = saisonRepository;
}
@GetMapping
public String listEquipements(Model model) {
model.addAttribute("equipements", equipementRepository.findAll());
Saison activeSaison = saisonRepository.findByEstActiveTrue().orElse(null);
if (activeSaison != null) {
model.addAttribute("equipements", equipementRepository.findBySaison(activeSaison));
} else {
model.addAttribute("equipements", java.util.List.of());
}
return "parametrage/equipements_list";
}
@@ -38,8 +47,15 @@ public class EquipementController {
@PostMapping
public String saveEquipement(@ModelAttribute("equipement") Equipement equipement) {
if (equipement.getObligatoire() == null) {
equipement.setObligatoire(false);
if (equipement.getId() == null) {
Saison activeSaison = saisonRepository.findByEstActiveTrue()
.orElseThrow(() -> new IllegalStateException("Aucune saison active trouvée"));
equipement.setSaison(activeSaison);
} else {
Equipement existing = equipementRepository.findById(equipement.getId())
.orElseThrow(() -> new IllegalArgumentException("Invalid equipment ID: " + equipement.getId()));
equipement.setSaison(existing.getSaison());
}
equipementRepository.save(equipement);
return "redirect:/equipements";
@@ -0,0 +1,23 @@
package com.astalange.web.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice
public class GlobalControllerAdvice {
@Value("${app.version}")
private String appVersion;
@ModelAttribute("requestURI")
public String requestURI(final HttpServletRequest request) {
return request.getRequestURI();
}
@ModelAttribute("appVersion")
public String appVersion() {
return appVersion;
}
}
@@ -41,12 +41,7 @@ public class HomeController {
List<com.astalange.core.entity.Licence> allLicences = licenceRepository.findAll();
BigDecimal totalAttendu = allLicences.stream()
.map(lic -> {
if (lic.getCategorie() == null) return BigDecimal.ZERO;
return (lic.getAdherent() != null && lic.getAdherent().isResidentTalange())
? lic.getCategorie().getTarifBase()
: lic.getCategorie().getTarifExterieur();
})
.map(com.astalange.core.entity.Licence::getPrixTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal resteARecouvrer = totalAttendu.subtract(totalEncaisse);

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