From d32d956b0e100a6d658b8483c750fd881ea65cbc Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 17 Feb 2026 22:17:51 +0100 Subject: [PATCH] feat(dashboard-admin): connect admin dashboard to real API data (Ticket #92) - Frontend: - Create UserService to handle user-related API calls (gestionnaires, parents, AMs, admins) - Update AdminDashboardScreen to use dynamic widgets - Implement dynamic management widgets: - GestionnaireManagementWidget - ParentManagementWidget - AssistanteMaternelleManagementWidget - AdminManagementWidget - Add data models: ParentModel, AssistanteMaternelleModel - Update AppUser model - Update ApiConfig - Backend: - Update controllers (Parents, AMs, Gestionnaires, Users) to allow ADMINISTRATEUR role to list users - Fix: Activate endpoint GET /gestionnaires (import GestionnairesModule in UserModule) - Docs: - Add note about backend fix for Gestionnaires module - Update .cursorrules to forbid worktrees - Seed: - Add test data seed script (reset-and-seed-db.sh) Co-authored-by: Cursor --- .../assistantes_maternelles.controller.ts | 2 +- .../src/routes/parents/parents.controller.ts | 2 +- .../gestionnaires/gestionnaires.controller.ts | 2 +- .../gestionnaires/gestionnaires.module.ts | 6 +- backend/src/routes/user/user.controller.ts | 2 +- backend/src/routes/user/user.module.ts | 2 + database/README.md | 10 + database/seed/03_seed_test_data.sql | 73 ++++++ docker-compose.yml | 2 + docs/23_LISTE-TICKETS.md | 52 ++-- docs/92_NOTE-BACKEND-GESTIONNAIRES.md | 63 +++++ docs/PROCEDURE-API-GITEA.md | 176 ++++++++++++++ docs/STATUS-APPLICATION.md | 115 +++++++++ .../models/assistante_maternelle_model.dart | 30 +++ frontend/lib/models/parent_model.dart | 18 ++ frontend/lib/models/user.dart | 55 ++++- .../admin_dashboardScreen.dart | 3 +- frontend/lib/services/api/api_config.dart | 3 + frontend/lib/services/user_service.dart | 94 ++++++++ .../admin/admin_management_widget.dart | 141 +++++++++++ ...sistante_maternelle_management_widget.dart | 191 ++++++++++----- .../admin/gestionnaire_management_widget.dart | 100 ++++++-- .../admin/parent_managmant_widget.dart | 222 ++++++++++++------ scripts/reset-and-seed-db.sh | 61 +++++ 24 files changed, 1242 insertions(+), 183 deletions(-) create mode 100644 database/seed/03_seed_test_data.sql create mode 100644 docs/92_NOTE-BACKEND-GESTIONNAIRES.md create mode 100644 docs/PROCEDURE-API-GITEA.md create mode 100644 docs/STATUS-APPLICATION.md create mode 100644 frontend/lib/models/assistante_maternelle_model.dart create mode 100644 frontend/lib/models/parent_model.dart create mode 100644 frontend/lib/services/user_service.dart create mode 100644 frontend/lib/widgets/admin/admin_management_widget.dart create mode 100755 scripts/reset-and-seed-db.sh diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts index d803c20..47d51eb 100644 --- a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts @@ -35,7 +35,7 @@ export class AssistantesMaternellesController { return this.assistantesMaternellesService.create(dto); } - @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @Get() @ApiOperation({ summary: 'Récupérer la liste des nounous' }) @ApiResponse({ status: 200, description: 'Liste des nounous' }) diff --git a/backend/src/routes/parents/parents.controller.ts b/backend/src/routes/parents/parents.controller.ts index aa02313..e4a9ee2 100644 --- a/backend/src/routes/parents/parents.controller.ts +++ b/backend/src/routes/parents/parents.controller.ts @@ -20,7 +20,7 @@ import { UpdateParentsDto } from '../user/dto/update_parent.dto'; export class ParentsController { constructor(private readonly parentsService: ParentsService) {} - @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @Get() @ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' }) @ApiResponse({ status: 403, description: 'Accès refusé !' }) diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts b/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts index 7a1489f..8fd23c6 100644 --- a/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts +++ b/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts @@ -35,7 +35,7 @@ export class GestionnairesController { return this.gestionnairesService.create(dto); } - @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) @ApiOperation({ summary: 'Liste des gestionnaires' }) @ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] }) @Get() diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.module.ts b/backend/src/routes/user/gestionnaires/gestionnaires.module.ts index bfd32f8..9cea564 100644 --- a/backend/src/routes/user/gestionnaires/gestionnaires.module.ts +++ b/backend/src/routes/user/gestionnaires/gestionnaires.module.ts @@ -3,9 +3,13 @@ import { GestionnairesService } from './gestionnaires.service'; import { GestionnairesController } from './gestionnaires.controller'; import { Users } from 'src/entities/users.entity'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthModule } from 'src/routes/auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([Users])], + imports: [ + TypeOrmModule.forFeature([Users]), + AuthModule, + ], controllers: [GestionnairesController], providers: [GestionnairesService], }) diff --git a/backend/src/routes/user/user.controller.ts b/backend/src/routes/user/user.controller.ts index 3fe3187..54caa8a 100644 --- a/backend/src/routes/user/user.controller.ts +++ b/backend/src/routes/user/user.controller.ts @@ -28,7 +28,7 @@ export class UserController { // Lister tous les utilisateurs (super_admin uniquement) @Get() - @Roles(RoleType.SUPER_ADMIN) + @Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR) @ApiOperation({ summary: 'Lister tous les utilisateurs' }) findAll() { return this.userService.findAll(); diff --git a/backend/src/routes/user/user.module.ts b/backend/src/routes/user/user.module.ts index 484f85d..4d5d7cc 100644 --- a/backend/src/routes/user/user.module.ts +++ b/backend/src/routes/user/user.module.ts @@ -9,6 +9,7 @@ import { ParentsModule } from '../parents/parents.module'; import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module'; import { Parents } from 'src/entities/parents.entity'; +import { GestionnairesModule } from './gestionnaires/gestionnaires.module'; @Module({ imports: [TypeOrmModule.forFeature( @@ -20,6 +21,7 @@ import { Parents } from 'src/entities/parents.entity'; ]), forwardRef(() => AuthModule), ParentsModule, AssistantesMaternellesModule, + GestionnairesModule, ], controllers: [UserController], providers: [UserService], diff --git a/database/README.md b/database/README.md index 4ebcd90..98200b5 100644 --- a/database/README.md +++ b/database/README.md @@ -41,6 +41,16 @@ docker compose -f docker-compose.dev.yml down -v --- +## Réinitialiser la BDD et charger les données de test (dashboard admin) + +Depuis la **racine du projet** (ptitspas-app, où se trouve `docker-compose.yml`) : + +```bash +./scripts/reset-and-seed-db.sh +``` + +Ce script : arrête les conteneurs, supprime le volume Postgres, redémarre la base (le schéma est recréé via `BDD.sql`), puis exécute `database/seed/03_seed_test_data.sql`. Tu obtiens un super_admin (`admin@ptits-pas.fr`) plus 9 comptes de test (1 admin, 1 gestionnaire, 2 AM, 5 parents) avec **mot de passe : `password`**. Idéal pour développer le ticket #92 (dashboard admin). + ## Importation automatique des données de test Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`. diff --git a/database/seed/03_seed_test_data.sql b/database/seed/03_seed_test_data.sql new file mode 100644 index 0000000..1aa805f --- /dev/null +++ b/database/seed/03_seed_test_data.sql @@ -0,0 +1,73 @@ +-- ============================================================ +-- 03_seed_test_data.sql : Données de test complètes (dashboard admin) +-- Aligné sur utilisateurs-test-complet.json +-- Mot de passe universel : password (bcrypt) +-- À exécuter après BDD.sql (init DB) +-- ============================================================ + +BEGIN; + +-- Hash bcrypt pour "password" (10 rounds) + +-- ========== UTILISATEURS (1 admin + 1 gestionnaire + 2 AM + 5 parents) ========== +-- On garde admin@ptits-pas.fr (super_admin) déjà créé par BDD.sql + +INSERT INTO utilisateurs (id, email, password, prenom, nom, role, statut, telephone, adresse, ville, code_postal, profession, situation_familiale, date_naissance, consentement_photo) +VALUES + ('a0000001-0001-0001-0001-000000000001', 'sophie.bernard@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Sophie', 'BERNARD', 'administrateur', 'actif', '0678123456', '12 Avenue Gabriel Péri', 'Bezons', '95870', 'Responsable administrative', 'marie', '1978-03-15', false), + ('a0000002-0002-0002-0002-000000000002', 'lucas.moreau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Lucas', 'MOREAU', 'gestionnaire', 'actif', '0687234567', '8 Rue Jean Jaurès', 'Bezons', '95870', 'Gestionnaire des placements', 'celibataire', '1985-09-22', false), + ('a0000003-0003-0003-0003-000000000003', 'marie.dubois@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Marie', 'DUBOIS', 'assistante_maternelle', 'actif', '0696345678', '25 Rue de la République', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1980-06-08', true), + ('a0000004-0004-0004-0004-000000000004', 'fatima.elmansouri@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Fatima', 'EL MANSOURI', 'assistante_maternelle', 'actif', '0675456789', '17 Boulevard Aristide Briand', 'Bezons', '95870', 'Assistante maternelle', 'marie', '1975-11-12', true), + ('a0000005-0005-0005-0005-000000000005', 'claire.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Claire', 'MARTIN', 'parent', 'actif', '0689567890', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Infirmière', 'marie', '1990-04-03', false), + ('a0000006-0006-0006-0006-000000000006', 'thomas.martin@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Thomas', 'MARTIN', 'parent', 'actif', '0678456789', '5 Avenue du Général de Gaulle', 'Bezons', '95870', 'Ingénieur', 'marie', '1988-07-18', false), + ('a0000007-0007-0007-0007-000000000007', 'amelie.durand@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Amélie', 'DURAND', 'parent', 'actif', '0667788990', '23 Rue Victor Hugo', 'Bezons', '95870', 'Comptable', 'divorce', '1987-12-14', false), + ('a0000008-0008-0008-0008-000000000008', 'julien.rousseau@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'Julien', 'ROUSSEAU', 'parent', 'actif', '0656677889', '14 Rue Pasteur', 'Bezons', '95870', 'Commercial', 'divorce', '1985-08-29', false), + ('a0000009-0009-0009-0009-000000000009', 'david.lecomte@ptits-pas.fr', '$2b$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW', 'David', 'LECOMTE', 'parent', 'actif', '0645566778', '31 Rue Émile Zola', 'Bezons', '95870', 'Développeur web', 'parent_isole', '1992-10-07', false) +ON CONFLICT (email) DO NOTHING; + +-- ========== PARENTS (avec co-parent pour le couple Martin) ========== +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES + ('a0000005-0005-0005-0005-000000000005', 'a0000006-0006-0006-0006-000000000006'), + ('a0000006-0006-0006-0006-000000000006', 'a0000005-0005-0005-0005-000000000005'), + ('a0000007-0007-0007-0007-000000000007', NULL), + ('a0000008-0008-0008-0008-000000000008', NULL), + ('a0000009-0009-0009-0009-000000000009', NULL) +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- ========== ASSISTANTES MATERNELLES ========== +INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nir_chiffre, nb_max_enfants, biographie, date_agrement, ville_residence, disponible, place_disponible) +VALUES + ('a0000003-0003-0003-0003-000000000003', 'AGR-2019-095001', '280069512345671', 4, 'Assistante maternelle agréée depuis 2019. Spécialité bébés 0-18 mois. Accueil bienveillant et cadre sécurisant. 2 places disponibles.', '2019-09-01', 'Bezons', true, 2), + ('a0000004-0004-0004-0004-000000000004', 'AGR-2017-095002', '275119512345672', 3, 'Assistante maternelle expérimentée. Spécialité 1-3 ans. Accueil à la journée. 1 place disponible.', '2017-06-15', 'Bezons', true, 1) +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- ========== ENFANTS ========== +INSERT INTO enfants (id, prenom, nom, genre, date_naissance, statut, est_multiple) +VALUES + ('e0000001-0001-0001-0001-000000000001', 'Emma', 'MARTIN', 'F', '2023-02-15', 'actif', true), + ('e0000002-0002-0002-0002-000000000002', 'Noah', 'MARTIN', 'H', '2023-02-15', 'actif', true), + ('e0000003-0003-0003-0003-000000000003', 'Léa', 'MARTIN', 'F', '2023-02-15', 'actif', true), + ('e0000004-0004-0004-0004-000000000004', 'Chloé', 'ROUSSEAU', 'F', '2022-04-20', 'actif', false), + ('e0000005-0005-0005-0005-000000000005', 'Hugo', 'ROUSSEAU', 'H', '2024-03-10', 'actif', false), + ('e0000006-0006-0006-0006-000000000006', 'Maxime', 'LECOMTE', 'H', '2023-04-15', 'actif', false) +ON CONFLICT (id) DO NOTHING; + +-- ========== ENFANTS_PARENTS (liaison N:N) ========== +-- Martin (Claire + Thomas) -> Emma, Noah, Léa +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES + ('a0000005-0005-0005-0005-000000000005', 'e0000001-0001-0001-0001-000000000001'), + ('a0000005-0005-0005-0005-000000000005', 'e0000002-0002-0002-0002-000000000002'), + ('a0000005-0005-0005-0005-000000000005', 'e0000003-0003-0003-0003-000000000003'), + ('a0000006-0006-0006-0006-000000000006', 'e0000001-0001-0001-0001-000000000001'), + ('a0000006-0006-0006-0006-000000000006', 'e0000002-0002-0002-0002-000000000002'), + ('a0000006-0006-0006-0006-000000000006', 'e0000003-0003-0003-0003-000000000003'), + ('a0000007-0007-0007-0007-000000000007', 'e0000004-0004-0004-0004-000000000004'), + ('a0000007-0007-0007-0007-000000000007', 'e0000005-0005-0005-0005-000000000005'), + ('a0000008-0008-0008-0008-000000000008', 'e0000004-0004-0004-0004-000000000004'), + ('a0000008-0008-0008-0008-000000000008', 'e0000005-0005-0005-0005-000000000005'), + ('a0000009-0009-0009-0009-000000000009', 'e0000006-0006-0006-0006-000000000006') +ON CONFLICT DO NOTHING; + +COMMIT; diff --git a/docker-compose.yml b/docker-compose.yml index 1e97b62..a4fad4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,8 @@ services: JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES} NODE_ENV: ${NODE_ENV} + LOG_API_REQUESTS: ${LOG_API_REQUESTS:-false} + CONFIG_ENCRYPTION_KEY: ${CONFIG_ENCRYPTION_KEY} depends_on: - database labels: diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 8e0b98d..0d39b55 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -23,11 +23,12 @@ | 10 | [Backend] Service Configuration | ✅ Fermé | | 11 | [Backend] API Configuration | ✅ Fermé | | 12 | [Backend] Guard Configuration Initiale | ✅ Fermé | -| 13 | [Backend] Adaptation MailService pour config dynamique | Ouvert | +| 13 | [Backend] Adaptation MailService pour config dynamique | ✅ Fermé | | 14 | [Frontend] Panneau Paramètres / Configuration (première config + accès permanent) | Ouvert | | 15 | [Frontend] Écran Paramètres (accès permanent) | Ouvert | | 16 | [Doc] Documentation configuration on-premise | Ouvert | -| 17–88 | (voir sections ci‑dessous ; #78, #79, #81, #83, #82, #86, #87, #88, etc.) | — | +| 17–88 | (voir sections ci‑dessous ; #82, #78, #79, #81, #83 ; #86, #87, #88 fermés en doublon) | — | +| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | Ouvert | *Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues* @@ -229,6 +230,8 @@ Créer un Guard/Middleware qui détecte si la configuration initiale est incompl **Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#workflow-setup-initial) +*Issue Gitea #86 fermée en doublon ; ce ticket (#12) est la référence.* + --- ### Ticket #13 : [Backend] Adaptation MailService pour config dynamique @@ -267,6 +270,8 @@ Un seul panneau **Paramètres / Configuration** dans le dashboard admin, avec ** **Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#interface-admin) +*Issue Gitea #87 fermée en doublon de #14.* + --- ### Ticket #15 : [Frontend] Écran Paramètres (accès permanent) / Intégration panneau @@ -281,6 +286,8 @@ S’assurer que le panneau Paramètres (décrit en #14) est accessible en perman - [ ] Chargement des valeurs actuelles (GET `/configuration` ou par catégorie) - [ ] Modification et sauvegarde (PATCH bulk) sans appel à `setup/complete` +*Issue Gitea #88 fermée en doublon ; ce ticket (#15) est la référence.* + --- ### Ticket #16 : [Doc] Documentation configuration on-premise @@ -302,19 +309,8 @@ Rédiger la documentation pour aider les collectivités à configurer l'applicat --- -### Ticket #86 : [Backend] Guard Configuration Initiale (concept v1.3) -**Estimation** : 2h -**Labels** : `backend`, `p1-bloquant`, `on-premise` - -Issue Gitea ouverte pour le Guard aligné avec le concept v1.3 (pas de redirection vers `/admin/setup`, le frontend affiche le panneau Configuration et bloque la navigation). Voir aussi Ticket #12 (version fermée). - ---- - -### Ticket #88 : [Frontend] Intégration panneau Paramètres au dashboard -**Estimation** : 1h -**Labels** : `frontend`, `p1-bloquant`, `on-premise` - -Complément de #14 et #15 : s’assurer que le panneau Paramètres est accessible en permanence (onglet Configuration, chargement des valeurs, sauvegarde PATCH bulk sans `setup/complete`). +### Ticket #86 / #88 : Doublons fermés +*#86* fermé en doublon de **#12** (Guard). *#88* fermé en doublon de **#15** (Intégration panneau). Voir les tickets #12, #14 et #15 pour le travail à faire. --- @@ -898,6 +894,30 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin. --- +### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API +**Estimation** : 8h +**Labels** : `frontend`, `p3`, `admin` + +**Description** : +Le dashboard admin (onglets Gestionnaires | Parents | Assistantes maternelles | Administrateurs) affiche actuellement des données en dur (mock). Remplacer par des appels API pour afficher les vrais utilisateurs et permettre les actions de gestion (voir, modifier, valider/refuser). Référence : [90_AUDIT.md](./90_AUDIT.md). + +**Fichiers concernés** : +- `gestionnaire_management_widget.dart` — liste actuellement 5 cartes "Dupont" en dur +- `parent_managmant_widget.dart` — 2 parents simulés +- `assistante_maternelle_management_widget.dart` — 2 AM simulées + +**Tâches** : +- [ ] S'assurer que les endpoints backend existent (liste users par rôle) +- [ ] Onglet Gestionnaires : appel API, affichage dynamique, recherche, lien "Créer gestionnaire" +- [ ] Onglet Parents : appel API, affichage dynamique, recherche/filtres, actions Voir/Modifier/Valider/Refuser +- [ ] Onglet Assistantes maternelles : appel API, affichage dynamique, filtres, actions +- [ ] Onglet Administrateurs : liste ou placeholder documenté +- [ ] Gestion états (chargement, erreur, liste vide) et rafraîchissement après actions + +**Références** : #44, #45, #46 (dashboard Gestionnaire), #25, #26 (API liste/validation), #17, #35 (création gestionnaire) + +--- + ### Ticket #50 : [Frontend] Affichage dynamique CGU lors inscription **Estimation** : 2h **Labels** : `frontend`, `p3`, `juridique` @@ -1237,7 +1257,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit - **Juridique** : 1 ticket ### Modifications par rapport à la version initiale -- ✅ **v1.4** : Numéros de section du doc = numéros Gitea (Ticket #n = issue #n). Tableau et sections renumérotés en conséquence ; #87 fermé (doublon de #14). +- ✅ **v1.4** : Numéros de section du doc = numéros Gitea (Ticket #n = issue #n). Tableau et sections renumérotés. Doublons #86, #87, #88 fermés sur Gitea (#86→#12, #87→#14, #88→#15) ; tickets sources #12, #14, #15 mis à jour (doc + body Gitea). - ✅ **Concept v1.3** : Configuration initiale = un seul panneau Paramètres (3 sections) dans le dashboard ; plus de page dédiée « Setup Wizard » ; navigation bloquée jusqu’à sauvegarde au premier déploiement. Tickets #10, #12, #13 alignés. - ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire - ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel diff --git a/docs/92_NOTE-BACKEND-GESTIONNAIRES.md b/docs/92_NOTE-BACKEND-GESTIONNAIRES.md new file mode 100644 index 0000000..d8bd595 --- /dev/null +++ b/docs/92_NOTE-BACKEND-GESTIONNAIRES.md @@ -0,0 +1,63 @@ +# Note Backend - Activation du module Gestionnaires (Ticket #92) + +## Problème +L'endpoint `GET /api/v1/gestionnaires` renvoie une erreur **404 Not Found**. +Cela est dû au fait que le `GestionnairesModule` n'est pas importé dans l'arbre des modules de l'application (via `UserModule` ou `AppModule`). + +## Solution de contournement actuelle (Frontend) +Le frontend utilise actuellement l'endpoint générique `/api/v1/users` et filtre les résultats côté client pour ne garder que les utilisateurs ayant le rôle `gestionnaire`. +*Fichier concerné : `frontend/lib/services/user_service.dart`* + +## Correctif Backend à appliquer +Pour activer proprement l'endpoint dédié, il faut effectuer les modifications suivantes dans le backend : + +### 1. Importer le module dans `UserModule` +Fichier : `backend/src/routes/user/user.module.ts` + +Ajouter `GestionnairesModule` dans les imports. + +```typescript +import { GestionnairesModule } from './gestionnaires/gestionnaires.module'; + +@Module({ + imports: [ + // ... autres imports + GestionnairesModule, // <--- AJOUTER ICI + ], + // ... +}) +export class UserModule { } +``` + +### 2. Ajouter AuthModule dans `GestionnairesModule` +Fichier : `backend/src/routes/user/gestionnaires/gestionnaires.module.ts` + +Le contrôleur utilise `AuthGuard`, qui dépend de `JwtService` fourni par `AuthModule`. + +```typescript +import { AuthModule } from 'src/routes/auth/auth.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Users]), + AuthModule // <--- AJOUTER ICI + ], + controllers: [GestionnairesController], + providers: [GestionnairesService], +}) +export class GestionnairesModule { } +``` + +## Après application du correctif +Une fois ces modifications backend effectuées : +1. Redémarrer le serveur backend. +2. Modifier le frontend (`frontend/lib/services/user_service.dart`) pour utiliser à nouveau l'endpoint dédié : + ```dart + static Future> getGestionnaires() async { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'), + headers: await _headers(), + ); + // ... + } + ``` diff --git a/docs/PROCEDURE-API-GITEA.md b/docs/PROCEDURE-API-GITEA.md new file mode 100644 index 0000000..6a41046 --- /dev/null +++ b/docs/PROCEDURE-API-GITEA.md @@ -0,0 +1,176 @@ +# Procédure – Utilisation de l’API Gitea + +## 1. Contexte + +- **Instance** : https://git.ptits-pas.fr +- **API de base** : `https://git.ptits-pas.fr/api/v1` +- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`) + +## 2. Authentification + +### 2.1 Token + +Le token est défini dans l’environnement (ex. `~/.bashrc`) : + +```bash +export GITEA_TOKEN="" +``` + +Pour l’utiliser dans les commandes : + +```bash +source ~/.bashrc # ou : . ~/.bashrc +# Puis utiliser $GITEA_TOKEN dans les curl +``` + +### 2.2 En-tête HTTP + +Toutes les requêtes API doivent envoyer le token : + +```bash +-H "Authorization: token $GITEA_TOKEN" +``` + +Exemple : + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" +``` + +## 3. Endpoints utiles + +### 3.1 Dépôt (repository) + +| Action | Méthode | URL | +|---------------|---------|-----| +| Infos dépôt | GET | `/repos/{owner}/{repo}` | +| Liste dépôts | GET | `/repos/search?q=petitspas` | + +Exemple – infos du dépôt : + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq . +``` + +### 3.2 Issues (tickets) + +| Action | Méthode | URL | +|------------------|---------|-----| +| Liste des issues | GET | `/repos/{owner}/{repo}/issues` | +| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` | +| Créer une issue | POST | `/repos/{owner}/{repo}/issues` | +| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` | +| Fermer une issue | PATCH | (même URL, `state: "closed"`) | + +**Paramètres GET utiles pour la liste :** + +- `state` : `open` ou `closed` +- `labels` : filtre par label (ex. `frontend`) +- `page`, `limit` : pagination + +Exemples : + +```bash +# Toutes les issues ouvertes +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq . + +# Issues ouvertes avec label "frontend" +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \ + jq '.[] | select(.labels[].name == "frontend") | {number, title, state}' + +# Détail de l’issue #47 +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq . + +# Fermer l’issue #31 +curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"state":"closed"}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31" + +# Créer une issue +curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues" +``` + +### 3.3 Pull requests + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` | +| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` | +| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` | +| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` | + +Exemples : + +```bash +# Liste des PR ouvertes +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq . + +# Créer une PR (head = branche source, base = branche cible) +curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"head":"develop","base":"master","title":"Titre de la PR"}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls" +``` + +### 3.4 Branches + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des branches | GET | `/repos/{owner}/{repo}/branches` | + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name' +``` + +### 3.5 Webhooks + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` | +| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` | + +### 3.6 Labels + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des labels | GET | `/repos/{owner}/{repo}/labels` | + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}' +``` + +## 4. Résumé des URLs pour P'titsPas + +Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` : + +| Ressource | URL | +|------------------|-----| +| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` | +| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` | +| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` | +| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` | +| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` | +| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` | + +## 5. Documentation officielle + +- Swagger / OpenAPI : https://docs.gitea.com/api +- Référence selon la version de Gitea installée (ex. 1.21, 1.25). + +## 6. Dépannage + +- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token `. +- **404** : vérifier owner/repo et l’URL (sensible à la casse). +- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide. diff --git a/docs/STATUS-APPLICATION.md b/docs/STATUS-APPLICATION.md new file mode 100644 index 0000000..83461d0 --- /dev/null +++ b/docs/STATUS-APPLICATION.md @@ -0,0 +1,115 @@ +# Statut de l'application P'titsPas + +**Date du point** : 8 février 2026 + +--- + +## 1. Environnement de production + +| Élément | Statut | Détail | +|--------|--------|--------| +| **URL** | OK | https://app.ptits-pas.fr | +| **Frontend** | 200 | Flutter Web, Nginx | +| **API** | 200 | NestJS, préfixe `/api/v1` | +| **Base de données** | OK | PostgreSQL 17 | +| **PgAdmin** | OK | https://app.ptits-pas.fr/pgadmin | + +### Conteneurs Docker + +| Service | Image | État | +|---------|--------|------| +| ptitspas-frontend | ptitspas-app-frontend | Up (recréé récemment) | +| ptitspas-backend | ptitspas-app-backend | Up ~26h | +| ptitspas-postgres | postgres:17 | Up ~28h | +| ptitspas-pgadmin | dpage/pgadmin4 | Up ~28h | + +--- + +## 2. Dépôt Git + +- **Branche déployée** : `master` +- **Derniers commits** : + - `10bf255` – fix(ui): renforcer ombre boutons Parents/AM sur mobile + - `678f421` – docs: ticket #82 fermé (écran Login mobile) + - `5295e8e` – Merge develop: login mobile, formulaire sous slogan par ratio + - `6bf0932` – docs: Index, doc API Gitea, script fermeture issue + - `2f1740b` – docs: ticket #83 RegisterChoiceScreen Mobile (terminé) + +- **Branches actives** : `master`, `develop`, diverses `feature/*` (inscription, config, documents légaux, etc.) + +--- + +## 3. Déploiement (hook Gitea) + +| Élément | Statut | +|--------|--------| +| **Webhook** | Opérationnel (`hooks.ptits-pas.fr/hooks/petitspas-deploy`) | +| **Déclencheur** | Push sur `master`, dépôt `petitspas` | +| **Script** | Monté depuis l’hôte (verrou + sans Prisma) | +| **Dernier déploiement** | 08/02/2026 18:18:26 – Succès | + +Un seul déploiement à la fois (verrou) ; plus d’étape Prisma dans le script. + +--- + +## 4. Fonctionnalités livrées + +### Backend (API) + +- Auth : login, refresh, profil, **changement MDP obligatoire** (first login) +- Configuration : setup status, bulk, test SMTP, catégories +- Documents légaux : actifs, versions, upload, activation, téléchargement +- Inscription : parents (workflow complet), enfants (CRUD) +- Compte super_admin par défaut (seed BDD) : `admin@ptits-pas.fr` / `4dm1n1strateur` + +### Frontend + +- **Formulaires d’inscription** : compatibles **desktop et mobile** + - Choix d’inscription (Parents / Assistante maternelle) – responsive + - Inscription Parent : étapes 1 à 5 (infos parent 1 & 2, enfants, présentation, CGU, récap) + - Inscription AM : étapes 1 à 4 (identité, pro, présentation, récap) +- **Login** : écran adapté mobile (formulaire sous slogan selon ratio) +- Modale **changement de mot de passe obligatoire** après première connexion si `changement_mdp_obligatoire` +- CORS configuré (localhost + prod) + +### Base de données + +- Schéma database-first (BDD.sql) +- Tables : utilisateurs, configuration, documents_legaux, acceptations_documents, enfants, etc. +- Champs tokens création MDP, genre enfants, configuration système + +--- + +## 5. Tickets / Priorités (résumé) + +- **Liste détaillée** : `docs/23_LISTE-TICKETS.md` +- **Récent fermé** : #82 (Login mobile), #83 (RegisterChoiceScreen mobile), #73, #78, #79, #81 +- **P0 (BDD)** : quelques amendements ouverts (champs CDC, présentation dossier, etc.) +- **P1** : configuration système (panneau Paramètres, 3 sections, première config + accès permanent) +- **P2/P3** : backend métier et frontend (dashboards, écrans création MDP, etc.) + +--- + +## 6. Documentation utile + +| Fichier | Usage | +|---------|--------| +| `00_INDEX.md` | Index de la doc | +| `01_CAHIER-DES-CHARGES.md` | CDC v1.3 | +| `11_API.md` | Endpoints API | +| `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte | +| `23_LISTE-TICKETS.md` | Liste des tickets | +| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires | +| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) | + +--- + +## 7. Synthèse + +L’application est **en production** sur https://app.ptits-pas.fr avec : + +- Frontend et API accessibles et répondant en 200. +- Déploiement automatique sur push `master` avec script à jour (verrou, sans Prisma). +- Formulaires d’inscription (Parents et AM) **responsive desktop et mobile**. +- Login et changement de mot de passe obligatoire opérationnels. +- Prochaines priorités : P0 BDD si besoin, P1 panneau Paramètres / Configuration (tickets #12, #13), puis dashboards et workflows métier (P2/P3). diff --git a/frontend/lib/models/assistante_maternelle_model.dart b/frontend/lib/models/assistante_maternelle_model.dart new file mode 100644 index 0000000..2d53fb0 --- /dev/null +++ b/frontend/lib/models/assistante_maternelle_model.dart @@ -0,0 +1,30 @@ +import 'package:p_tits_pas/models/user.dart'; + +class AssistanteMaternelleModel { + final AppUser user; + final String? approvalNumber; + final String? residenceCity; + final int? maxChildren; + final int? placesAvailable; + + AssistanteMaternelleModel({ + required this.user, + this.approvalNumber, + this.residenceCity, + this.maxChildren, + this.placesAvailable, + }); + + factory AssistanteMaternelleModel.fromJson(Map json) { + final userJson = json['user'] ?? json; + final user = AppUser.fromJson(userJson); + + return AssistanteMaternelleModel( + user: user, + approvalNumber: json['numero_agrement'] as String?, + residenceCity: json['ville_residence'] as String?, + maxChildren: json['nb_max_enfants'] as int?, + placesAvailable: json['place_disponible'] as int?, + ); + } +} diff --git a/frontend/lib/models/parent_model.dart b/frontend/lib/models/parent_model.dart new file mode 100644 index 0000000..5b893fd --- /dev/null +++ b/frontend/lib/models/parent_model.dart @@ -0,0 +1,18 @@ +import 'package:p_tits_pas/models/user.dart'; + +class ParentModel { + final AppUser user; + final int childrenCount; + + ParentModel({required this.user, this.childrenCount = 0}); + + factory ParentModel.fromJson(Map json) { + final userJson = json['user'] ?? json; + final user = AppUser.fromJson(userJson); + final children = json['parentChildren'] as List?; + return ParentModel( + user: user, + childrenCount: children?.length ?? 0, + ); + } +} diff --git a/frontend/lib/models/user.dart b/frontend/lib/models/user.dart index 29712ac..7ecbe79 100644 --- a/frontend/lib/models/user.dart +++ b/frontend/lib/models/user.dart @@ -5,6 +5,14 @@ class AppUser { final DateTime createdAt; final DateTime updatedAt; final bool changementMdpObligatoire; + final String? nom; + final String? prenom; + final String? statut; + final String? telephone; + final String? photoUrl; + final String? adresse; + final String? ville; + final String? codePostal; AppUser({ required this.id, @@ -13,6 +21,14 @@ class AppUser { required this.createdAt, required this.updatedAt, this.changementMdpObligatoire = false, + this.nom, + this.prenom, + this.statut, + this.telephone, + this.photoUrl, + this.adresse, + this.ville, + this.codePostal, }); factory AppUser.fromJson(Map json) { @@ -20,13 +36,26 @@ class AppUser { id: json['id'] as String, email: json['email'] as String, role: json['role'] as String, - createdAt: json['createdAt'] != null - ? DateTime.parse(json['createdAt'] as String) - : DateTime.now(), - updatedAt: json['updatedAt'] != null - ? DateTime.parse(json['updatedAt'] as String) - : DateTime.now(), - changementMdpObligatoire: json['changement_mdp_obligatoire'] as bool? ?? false, + createdAt: json['cree_le'] != null + ? DateTime.parse(json['cree_le'] as String) + : (json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : DateTime.now()), + updatedAt: json['modifie_le'] != null + ? DateTime.parse(json['modifie_le'] as String) + : (json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : DateTime.now()), + changementMdpObligatoire: + json['changement_mdp_obligatoire'] as bool? ?? false, + nom: json['nom'] as String?, + prenom: json['prenom'] as String?, + statut: json['statut'] as String?, + telephone: json['telephone'] as String?, + photoUrl: json['photo_url'] as String?, + adresse: json['adresse'] as String?, + ville: json['ville'] as String?, + codePostal: json['code_postal'] as String?, ); } @@ -38,6 +67,16 @@ class AppUser { 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), 'changement_mdp_obligatoire': changementMdpObligatoire, + 'nom': nom, + 'prenom': prenom, + 'statut': statut, + 'telephone': telephone, + 'photo_url': photoUrl, + 'adresse': adresse, + 'ville': ville, + 'code_postal': codePostal, }; } -} \ No newline at end of file + + String get fullName => '${prenom ?? ''} ${nom ?? ''}'.trim(); +} diff --git a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart index 1868c9d..4707621 100644 --- a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -3,6 +3,7 @@ import 'package:p_tits_pas/services/configuration_service.dart'; import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; +import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart'; import 'package:p_tits_pas/widgets/admin/parametres_panel.dart'; import 'package:p_tits_pas/widgets/app_footer.dart'; import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; @@ -104,7 +105,7 @@ class _AdminDashboardScreenState extends State { case 2: return const AssistanteMaternelleManagementWidget(); case 3: - return const Center(child: Text('👨‍💼 Administrateurs')); + return const AdminManagementWidget(); default: return const Center(child: Text('Page non trouvée')); } diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart index 774b1d2..a9b48fe 100644 --- a/frontend/lib/services/api/api_config.dart +++ b/frontend/lib/services/api/api_config.dart @@ -15,6 +15,9 @@ class ApiConfig { static const String users = '/users'; static const String userProfile = '/users/profile'; static const String userChildren = '/users/children'; + static const String gestionnaires = '/gestionnaires'; + static const String parents = '/parents'; + static const String assistantesMaternelles = '/assistantes-maternelles'; // Configuration (admin) static const String configuration = '/configuration'; diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart new file mode 100644 index 0000000..80c9cbd --- /dev/null +++ b/frontend/lib/services/user_service.dart @@ -0,0 +1,94 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/models/parent_model.dart'; +import 'package:p_tits_pas/models/assistante_maternelle_model.dart'; +import 'package:p_tits_pas/services/api/api_config.dart'; +import 'package:p_tits_pas/services/api/tokenService.dart'; + +class UserService { + static Future> _headers() async { + final token = await TokenService.getToken(); + return token != null + ? ApiConfig.authHeaders(token) + : Map.from(ApiConfig.headers); + } + + static String? _toStr(dynamic v) { + if (v == null) return null; + if (v is String) return v; + return v.toString(); + } + + // Récupérer la liste des gestionnaires (endpoint dédié) + static Future> getGestionnaires() async { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'), + headers: await _headers(), + ); + + if (response.statusCode != 200) { + final err = jsonDecode(response.body) as Map?; + throw Exception(_toStr(err?['message']) ?? 'Erreur chargement gestionnaires'); + } + + final List data = jsonDecode(response.body); + return data.map((e) => AppUser.fromJson(e)).toList(); + } + + // Récupérer la liste des parents + static Future> getParents() async { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.parents}'), + headers: await _headers(), + ); + + if (response.statusCode != 200) { + final err = jsonDecode(response.body) as Map?; + throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parents'); + } + + final List data = jsonDecode(response.body); + return data.map((e) => ParentModel.fromJson(e)).toList(); + } + + // Récupérer la liste des assistantes maternelles + static Future> getAssistantesMaternelles() async { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternelles}'), + headers: await _headers(), + ); + + if (response.statusCode != 200) { + final err = jsonDecode(response.body) as Map?; + throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM'); + } + + final List data = jsonDecode(response.body); + return data.map((e) => AssistanteMaternelleModel.fromJson(e)).toList(); + } + + // Récupérer la liste des administrateurs (via /users filtré ou autre) + // Pour l'instant on va utiliser /users et filtrer côté client si on est super admin + static Future> getAdministrateurs() async { + // TODO: Endpoint dédié ou filtrage + // En attendant, on retourne une liste vide ou on tente /users + try { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.users}'), + headers: await _headers(), + ); + + if (response.statusCode == 200) { + final List data = jsonDecode(response.body); + return data + .map((e) => AppUser.fromJson(e)) + .where((u) => u.role == 'administrateur' || u.role == 'super_admin') + .toList(); + } + } catch (e) { + print('Erreur chargement admins: $e'); + } + return []; + } +} diff --git a/frontend/lib/widgets/admin/admin_management_widget.dart b/frontend/lib/widgets/admin/admin_management_widget.dart new file mode 100644 index 0000000..58e5eeb --- /dev/null +++ b/frontend/lib/widgets/admin/admin_management_widget.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/services/user_service.dart'; + +class AdminManagementWidget extends StatefulWidget { + const AdminManagementWidget({super.key}); + + @override + State createState() => _AdminManagementWidgetState(); +} + +class _AdminManagementWidgetState extends State { + bool _isLoading = false; + String? _error; + List _admins = []; + List _filteredAdmins = []; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadAdmins(); + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadAdmins() async { + setState(() { + _isLoading = true; + _error = null; + }); + try { + final list = await UserService.getAdministrateurs(); + if (!mounted) return; + setState(() { + _admins = list; + _filteredAdmins = list; + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredAdmins = _admins.where((u) { + final name = u.fullName.toLowerCase(); + final email = u.email.toLowerCase(); + return name.contains(query) || email.contains(query); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: const InputDecoration( + hintText: "Rechercher un administrateur...", + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 16), + ElevatedButton.icon( + onPressed: () { + // TODO: Créer admin + }, + icon: const Icon(Icons.add), + label: const Text("Créer un admin"), + ), + ], + ), + const SizedBox(height: 24), + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) + else if (_filteredAdmins.isEmpty) + const Center(child: Text("Aucun administrateur trouvé.")) + else + Expanded( + child: ListView.builder( + itemCount: _filteredAdmins.length, + itemBuilder: (context, index) { + final user = _filteredAdmins[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: CircleAvatar( + child: Text(user.fullName.isNotEmpty + ? user.fullName[0].toUpperCase() + : 'A'), + ), + title: Text(user.fullName.isNotEmpty + ? user.fullName + : 'Sans nom'), + subtitle: Text(user.email), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () {}, + ), + ], + ), + ), + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart index 220f946..c8e1a19 100644 --- a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -1,72 +1,142 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/assistante_maternelle_model.dart'; +import 'package:p_tits_pas/services/user_service.dart'; -class AssistanteMaternelleManagementWidget extends StatelessWidget { +class AssistanteMaternelleManagementWidget extends StatefulWidget { const AssistanteMaternelleManagementWidget({super.key}); @override - Widget build(BuildContext context) { - final assistantes = [ - { - "nom": "Marie Dupont", - "numeroAgrement": "AG123456", - "zone": "Paris 14", - "capacite": 3, - }, - { - "nom": "Claire Martin", - "numeroAgrement": "AG654321", - "zone": "Lyon 7", - "capacite": 2, - }, - ]; + State createState() => + _AssistanteMaternelleManagementWidgetState(); +} +class _AssistanteMaternelleManagementWidgetState + extends State { + bool _isLoading = false; + String? _error; + List _assistantes = []; + List _filteredAssistantes = []; + + final TextEditingController _zoneController = TextEditingController(); + final TextEditingController _capacityController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadAssistantes(); + _zoneController.addListener(_filter); + _capacityController.addListener(_filter); + } + + @override + void dispose() { + _zoneController.dispose(); + _capacityController.dispose(); + super.dispose(); + } + + Future _loadAssistantes() async { + setState(() { + _isLoading = true; + _error = null; + }); + try { + final list = await UserService.getAssistantesMaternelles(); + if (!mounted) return; + setState(() { + _assistantes = list; + _filter(); + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _filter() { + final zoneQuery = _zoneController.text.toLowerCase(); + final capacityQuery = int.tryParse(_capacityController.text); + + setState(() { + _filteredAssistantes = _assistantes.where((am) { + final matchesZone = zoneQuery.isEmpty || + (am.residenceCity?.toLowerCase().contains(zoneQuery) ?? false); + final matchesCapacity = capacityQuery == null || + (am.maxChildren != null && am.maxChildren! >= capacityQuery); + return matchesZone && matchesCapacity; + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 🔎 Zone de filtre - _buildFilterSection(), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🔎 Zone de filtre + _buildFilterSection(), - const SizedBox(height: 16), + const SizedBox(height: 16), - // 📋 Liste des assistantes - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: assistantes.length, - itemBuilder: (context, index) { - final assistante = assistantes[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: const Icon(Icons.face), - title: Text(assistante['nom'].toString()), - subtitle: Text( - "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.edit), - onPressed: () { - // TODO: Ajouter modification - }, + // 📋 Liste des assistantes + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) + else if (_filteredAssistantes.isEmpty) + const Center(child: Text("Aucune assistante maternelle trouvée.")) + else + Expanded( + child: ListView.builder( + itemCount: _filteredAssistantes.length, + itemBuilder: (context, index) { + final assistante = _filteredAssistantes[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: CircleAvatar( + backgroundImage: assistante.user.photoUrl != null + ? NetworkImage(assistante.user.photoUrl!) + : null, + child: assistante.user.photoUrl == null + ? const Icon(Icons.face) + : null, + ), + title: Text(assistante.user.fullName.isNotEmpty + ? assistante.user.fullName + : 'Sans nom'), + subtitle: Text( + "N° Agrément : ${assistante.approvalNumber ?? 'N/A'}\nZone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // TODO: Ajouter modification + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + // TODO: Ajouter suppression + }, + ), + ], + ), ), - IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - // TODO: Ajouter suppression - }, - ), - ], - ), + ); + }, ), - ); - }, - ), - ], - ), + ), + ], + ), ); } @@ -78,26 +148,23 @@ class AssistanteMaternelleManagementWidget extends StatelessWidget { SizedBox( width: 200, child: TextField( + controller: _zoneController, decoration: const InputDecoration( labelText: "Zone géographique", border: OutlineInputBorder(), + prefixIcon: Icon(Icons.location_on), ), - onChanged: (value) { - // TODO: Ajouter logique de filtrage par zone - }, ), ), SizedBox( width: 200, child: TextField( + controller: _capacityController, decoration: const InputDecoration( labelText: "Capacité minimum", border: OutlineInputBorder(), ), keyboardType: TextInputType.number, - onChanged: (value) { - // TODO: Ajouter logique de filtrage par capacité - }, ), ), ], diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart index 3f5d6c2..82fa52f 100644 --- a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -1,9 +1,70 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/user.dart'; +import 'package:p_tits_pas/services/user_service.dart'; import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart'; -class GestionnaireManagementWidget extends StatelessWidget { +class GestionnaireManagementWidget extends StatefulWidget { const GestionnaireManagementWidget({Key? key}) : super(key: key); + @override + State createState() => + _GestionnaireManagementWidgetState(); +} + +class _GestionnaireManagementWidgetState + extends State { + bool _isLoading = false; + String? _error; + List _gestionnaires = []; + List _filteredGestionnaires = []; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _loadGestionnaires(); + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadGestionnaires() async { + setState(() { + _isLoading = true; + _error = null; + }); + try { + final list = await UserService.getGestionnaires(); + if (!mounted) return; + setState(() { + _gestionnaires = list; + _filteredGestionnaires = list; + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredGestionnaires = _gestionnaires.where((u) { + final name = u.fullName.toLowerCase(); + final email = u.email.toLowerCase(); + return name.contains(query) || email.contains(query); + }).toList(); + }); + } + @override Widget build(BuildContext context) { return Padding( @@ -14,9 +75,10 @@ class GestionnaireManagementWidget extends StatelessWidget { // 🔹 Barre du haut avec bouton Row( children: [ - const Expanded( + Expanded( child: TextField( - decoration: InputDecoration( + controller: _searchController, + decoration: const InputDecoration( hintText: "Rechercher un gestionnaire...", prefixIcon: Icon(Icons.search), border: OutlineInputBorder(), @@ -26,7 +88,7 @@ class GestionnaireManagementWidget extends StatelessWidget { const SizedBox(width: 16), ElevatedButton.icon( onPressed: () { - // Rediriger vers la page de création + // TODO: Rediriger vers la page de création }, icon: const Icon(Icons.add), label: const Text("Créer un gestionnaire"), @@ -36,17 +98,25 @@ class GestionnaireManagementWidget extends StatelessWidget { const SizedBox(height: 24), // 🔹 Liste des gestionnaires - Expanded( - child: ListView.builder( - itemCount: 5, // À remplacer par liste dynamique - itemBuilder: (context, index) { - return GestionnaireCard( - name: "Dupont $index", - email: "dupont$index@mail.com", - ); - }, - ), - ) + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) + else if (_filteredGestionnaires.isEmpty) + const Center(child: Text("Aucun gestionnaire trouvé.")) + else + Expanded( + child: ListView.builder( + itemCount: _filteredGestionnaires.length, + itemBuilder: (context, index) { + final user = _filteredGestionnaires[index]; + return GestionnaireCard( + name: user.fullName.isNotEmpty ? user.fullName : "Sans nom", + email: user.email, + ); + }, + ), + ) ], ), ); diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart index 1bf78a5..1764056 100644 --- a/frontend/lib/widgets/admin/parent_managmant_widget.dart +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -1,83 +1,149 @@ import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/parent_model.dart'; +import 'package:p_tits_pas/services/user_service.dart'; -class ParentManagementWidget extends StatelessWidget { +class ParentManagementWidget extends StatefulWidget { const ParentManagementWidget({super.key}); @override - Widget build(BuildContext context) { - // 🔁 Simulation de données parents - final parents = [ - { - "nom": "Jean Dupuis", - "email": "jean.dupuis@email.com", - "statut": "Actif", - "enfants": 2, - }, - { - "nom": "Lucie Morel", - "email": "lucie.morel@email.com", - "statut": "En attente", - "enfants": 1, - }, - ]; + State createState() => _ParentManagementWidgetState(); +} +class _ParentManagementWidgetState extends State { + bool _isLoading = false; + String? _error; + List _parents = []; + List _filteredParents = []; + + final TextEditingController _searchController = TextEditingController(); + String? _selectedStatus; + + @override + void initState() { + super.initState(); + _loadParents(); + _searchController.addListener(_filter); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadParents() async { + setState(() { + _isLoading = true; + _error = null; + }); + try { + final list = await UserService.getParents(); + if (!mounted) return; + setState(() { + _parents = list; + _filter(); // Apply initial filter (if any) + _isLoading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + void _filter() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredParents = _parents.where((p) { + final matchesName = p.user.fullName.toLowerCase().contains(query) || + p.user.email.toLowerCase().contains(query); + final matchesStatus = _selectedStatus == null || + _selectedStatus == 'Tous' || + (p.user.statut?.toLowerCase() == _selectedStatus?.toLowerCase()); + + // Mapping simple pour le statut affiché vs backend + // Backend: en_attente, actif, suspendu + // Dropdown: En attente, Actif, Suspendu + + return matchesName && matchesStatus; + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - - _buildSearchSection(), - - const SizedBox(height: 16), - - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: parents.length, - itemBuilder: (context, index) { - final parent = parents[index]; - return Card( - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - leading: const Icon(Icons.person_outline), - title: Text(parent['nom'].toString()), - subtitle: Text( - "${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}", - ), - isThreeLine: true, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.visibility), - tooltip: "Voir dossier", - onPressed: () { - // TODO: Voir le statut du dossier - }, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSearchSection(), + const SizedBox(height: 16), + if (_isLoading) + const Center(child: CircularProgressIndicator()) + else if (_error != null) + Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red))) + else if (_filteredParents.isEmpty) + const Center(child: Text("Aucun parent trouvé.")) + else + Expanded( + child: ListView.builder( + itemCount: _filteredParents.length, + itemBuilder: (context, index) { + final parent = _filteredParents[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: CircleAvatar( + backgroundImage: parent.user.photoUrl != null + ? NetworkImage(parent.user.photoUrl!) + : null, + child: parent.user.photoUrl == null + ? const Icon(Icons.person) + : null, + ), + title: Text(parent.user.fullName.isNotEmpty + ? parent.user.fullName + : 'Sans nom'), + subtitle: Text( + "${parent.user.email}\nStatut : ${parent.user.statut ?? 'Inconnu'} | Enfants : ${parent.childrenCount}", + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: "Voir dossier", + onPressed: () { + // TODO: Voir le statut du dossier + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: "Modifier", + onPressed: () { + // TODO: Modifier parent + }, + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: "Supprimer", + onPressed: () { + // TODO: Supprimer compte + }, + ), + ], + ), ), - IconButton( - icon: const Icon(Icons.edit), - tooltip: "Modifier", - onPressed: () { - // TODO: Modifier parent - }, - ), - IconButton( - icon: const Icon(Icons.delete), - tooltip: "Supprimer", - onPressed: () { - // TODO: Supprimer compte - }, - ), - ], - ), + ); + }, ), - ); - }, - ), - ], - ) + ), + ], + ), ); } @@ -89,13 +155,12 @@ class ParentManagementWidget extends StatelessWidget { SizedBox( width: 220, child: TextField( + controller: _searchController, decoration: const InputDecoration( labelText: "Nom du parent", border: OutlineInputBorder(), + prefixIcon: Icon(Icons.search), ), - onChanged: (value) { - // TODO: Ajouter logique de recherche - }, ), ), SizedBox( @@ -105,13 +170,18 @@ class ParentManagementWidget extends StatelessWidget { labelText: "Statut", border: OutlineInputBorder(), ), + value: _selectedStatus, items: const [ - DropdownMenuItem(value: "Actif", child: Text("Actif")), - DropdownMenuItem(value: "En attente", child: Text("En attente")), - DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")), + DropdownMenuItem(value: null, child: Text("Tous")), + DropdownMenuItem(value: "actif", child: Text("Actif")), + DropdownMenuItem(value: "en_attente", child: Text("En attente")), + DropdownMenuItem(value: "suspendu", child: Text("Suspendu")), ], onChanged: (value) { - // TODO: Ajouter logique de filtrage + setState(() { + _selectedStatus = value; + _filter(); + }); }, ), ), diff --git a/scripts/reset-and-seed-db.sh b/scripts/reset-and-seed-db.sh new file mode 100755 index 0000000..1bbea91 --- /dev/null +++ b/scripts/reset-and-seed-db.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# ============================================================ +# reset-and-seed-db.sh : Réinitialise la BDD et injecte les données de test +# Usage : depuis la racine du projet ptitspas-app +# ./scripts/reset-and-seed-db.sh +# ============================================================ + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +echo "=== Réinitialisation BDD + seed données de test ===" +echo "Projet : $PROJECT_ROOT" +echo "" + +# 1) Arrêter les conteneurs et supprimer le volume Postgres +echo "[1/4] Arrêt des conteneurs et suppression du volume Postgres..." +docker compose down -v 2>/dev/null || docker-compose down -v 2>/dev/null || true + +# 2) Démarrer uniquement la base +echo "[2/4] Démarrage du conteneur database..." +docker compose up -d database 2>/dev/null || docker-compose up -d database 2>/dev/null + +# 3) Attendre que Postgres soit prêt +echo "[3/4] Attente du démarrage de Postgres..." +for i in {1..30}; do + if docker exec ptitspas-postgres pg_isready -U admin -d ptitpas_db 2>/dev/null; then + echo " Postgres prêt." + break + fi + if [ "$i" -eq 30 ]; then + echo "Erreur : Postgres ne répond pas après 30 tentatives." + exit 1 + fi + sleep 1 +done + +# Petit délai supplémentaire pour la fin de l'init (BDD.sql) +sleep 2 + +# 4) Exécuter le seed des données de test +echo "[4/4] Exécution du seed (03_seed_test_data.sql)..." +docker exec -i ptitspas-postgres psql -U admin -d ptitpas_db < database/seed/03_seed_test_data.sql + +echo "" +echo "=== Terminé ===" +echo "Comptes de test (mot de passe : password) :" +echo " - admin@ptits-pas.fr (super_admin, créé par BDD.sql)" +echo " - sophie.bernard@ptits-pas.fr (administrateur)" +echo " - lucas.moreau@ptits-pas.fr (gestionnaire)" +echo " - marie.dubois@ptits-pas.fr (assistante maternelle)" +echo " - fatima.elmansouri@ptits-pas.fr (assistante maternelle)" +echo " - claire.martin@ptits-pas.fr (parent)" +echo " - thomas.martin@ptits-pas.fr (parent)" +echo " - amelie.durand@ptits-pas.fr (parent)" +echo " - julien.rousseau@ptits-pas.fr (parent)" +echo " - david.lecomte@ptits-pas.fr (parent)" +echo "" +echo "Tu peux redémarrer le backend/frontend si besoin : docker compose up -d"