Compare commits

..
3 Commits
23 changed files with 167 additions and 930 deletions
-18
View File
@@ -1,18 +0,0 @@
# Fins de ligne : toujours LF dans le dépôt (évite les conflits Linux/Windows)
* text=auto eol=lf
# Fichiers binaires : pas de conversion
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.pdf binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
# Scripts shell : toujours LF
*.sh text eol=lf
@@ -35,7 +35,7 @@ export class AssistantesMaternellesController {
return this.assistantesMaternellesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
@ApiResponse({ status: 200, description: 'Liste des nounous' })
@@ -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, RoleType.ADMINISTRATEUR)
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@Get()
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
@ApiResponse({ status: 403, description: 'Accès refusé !' })
@@ -35,7 +35,7 @@ export class GestionnairesController {
return this.gestionnairesService.create(dto);
}
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Liste des gestionnaires' })
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
@Get()
@@ -3,13 +3,9 @@ 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]),
AuthModule,
],
imports: [TypeOrmModule.forFeature([Users])],
controllers: [GestionnairesController],
providers: [GestionnairesService],
})
+1 -1
View File
@@ -28,7 +28,7 @@ export class UserController {
// Lister tous les utilisateurs (super_admin uniquement)
@Get()
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
@Roles(RoleType.SUPER_ADMIN)
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
findAll() {
return this.userService.findAll();
-2
View File
@@ -9,7 +9,6 @@ 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(
@@ -21,7 +20,6 @@ import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
]), forwardRef(() => AuthModule),
ParentsModule,
AssistantesMaternellesModule,
GestionnairesModule,
],
controllers: [UserController],
providers: [UserService],
-63
View File
@@ -1,63 +0,0 @@
# 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<List<AppUser>> getGestionnaires() async {
final response = await http.get(
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
headers: await _headers(),
);
// ...
}
```
@@ -1,30 +0,0 @@
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<String, dynamic> 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?,
);
}
}
-18
View File
@@ -1,18 +0,0 @@
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<String, dynamic> 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,
);
}
}
+5 -44
View File
@@ -5,14 +5,6 @@ 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,
@@ -21,14 +13,6 @@ 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<String, dynamic> json) {
@@ -36,26 +20,13 @@ class AppUser {
id: json['id'] as String,
email: json['email'] as String,
role: json['role'] as String,
createdAt: json['cree_le'] != null
? DateTime.parse(json['cree_le'] as String)
: (json['createdAt'] != null
createdAt: 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.now(),
updatedAt: 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?,
: DateTime.now(),
changementMdpObligatoire: json['changement_mdp_obligatoire'] as bool? ?? false,
);
}
@@ -67,16 +38,6 @@ 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,
};
}
String get fullName => '${prenom ?? ''} ${nom ?? ''}'.trim();
}
@@ -3,7 +3,6 @@ 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';
@@ -105,7 +104,7 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
case 2:
return const AssistanteMaternelleManagementWidget();
case 3:
return const AdminManagementWidget();
return const Center(child: Text('👨‍💼 Administrateurs'));
default:
return const Center(child: Text('Page non trouvée'));
}
@@ -15,9 +15,6 @@ 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';
-94
View File
@@ -1,94 +0,0 @@
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<Map<String, String>> _headers() async {
final token = await TokenService.getToken();
return token != null
? ApiConfig.authHeaders(token)
: Map<String, String>.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<List<AppUser>> 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<String, dynamic>?;
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement gestionnaires');
}
final List<dynamic> data = jsonDecode(response.body);
return data.map((e) => AppUser.fromJson(e)).toList();
}
// Récupérer la liste des parents
static Future<List<ParentModel>> 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<String, dynamic>?;
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement parents');
}
final List<dynamic> data = jsonDecode(response.body);
return data.map((e) => ParentModel.fromJson(e)).toList();
}
// Récupérer la liste des assistantes maternelles
static Future<List<AssistanteMaternelleModel>> 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<String, dynamic>?;
throw Exception(_toStr(err?['message']) ?? 'Erreur chargement AM');
}
final List<dynamic> 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<List<AppUser>> 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<dynamic> 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 [];
}
}
@@ -1,141 +0,0 @@
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<AdminManagementWidget> createState() => _AdminManagementWidgetState();
}
class _AdminManagementWidgetState extends State<AdminManagementWidget> {
bool _isLoading = false;
String? _error;
List<AppUser> _admins = [];
List<AppUser> _filteredAdmins = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadAdmins();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _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: () {},
),
],
),
),
);
},
),
)
],
),
);
}
}
@@ -1,79 +1,25 @@
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 StatefulWidget {
class AssistanteMaternelleManagementWidget extends StatelessWidget {
const AssistanteMaternelleManagementWidget({super.key});
@override
State<AssistanteMaternelleManagementWidget> createState() =>
_AssistanteMaternelleManagementWidgetState();
}
class _AssistanteMaternelleManagementWidgetState
extends State<AssistanteMaternelleManagementWidget> {
bool _isLoading = false;
String? _error;
List<AssistanteMaternelleModel> _assistantes = [];
List<AssistanteMaternelleModel> _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<void> _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) {
final assistantes = [
{
"nom": "Marie Dupont",
"numeroAgrement": "AG123456",
"zone": "Paris 14",
"capacite": 3,
},
{
"nom": "Claire Martin",
"numeroAgrement": "AG654321",
"zone": "Lyon 7",
"capacite": 2,
},
];
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -85,34 +31,19 @@ class _AssistanteMaternelleManagementWidgetState
const SizedBox(height: 16),
// 📋 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,
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: assistantes.length,
itemBuilder: (context, index) {
final assistante = _filteredAssistantes[index];
final assistante = assistantes[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'),
leading: const Icon(Icons.face),
title: Text(assistante['nom'].toString()),
subtitle: Text(
"N° Agrément : ${assistante.approvalNumber ?? 'N/A'}\nZone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}"),
"N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -134,7 +65,6 @@ class _AssistanteMaternelleManagementWidgetState
);
},
),
),
],
),
);
@@ -148,23 +78,26 @@ class _AssistanteMaternelleManagementWidgetState
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é
},
),
),
],
@@ -1,70 +1,9 @@
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 StatefulWidget {
class GestionnaireManagementWidget extends StatelessWidget {
const GestionnaireManagementWidget({Key? key}) : super(key: key);
@override
State<GestionnaireManagementWidget> createState() =>
_GestionnaireManagementWidgetState();
}
class _GestionnaireManagementWidgetState
extends State<GestionnaireManagementWidget> {
bool _isLoading = false;
String? _error;
List<AppUser> _gestionnaires = [];
List<AppUser> _filteredGestionnaires = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadGestionnaires();
_searchController.addListener(_onSearchChanged);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _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(
@@ -75,10 +14,9 @@ class _GestionnaireManagementWidgetState
// 🔹 Barre du haut avec bouton
Row(
children: [
Expanded(
const Expanded(
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: "Rechercher un gestionnaire...",
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
@@ -88,7 +26,7 @@ class _GestionnaireManagementWidgetState
const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () {
// TODO: Rediriger vers la page de création
// Rediriger vers la page de création
},
icon: const Icon(Icons.add),
label: const Text("Créer un gestionnaire"),
@@ -98,21 +36,13 @@ class _GestionnaireManagementWidgetState
const SizedBox(height: 24),
// 🔹 Liste des gestionnaires
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,
itemCount: 5, // À remplacer par liste dynamique
itemBuilder: (context, index) {
final user = _filteredGestionnaires[index];
return GestionnaireCard(
name: user.fullName.isNotEmpty ? user.fullName : "Sans nom",
email: user.email,
name: "Dupont $index",
email: "dupont$index@mail.com",
);
},
),
@@ -1,114 +1,49 @@
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 StatefulWidget {
class ParentManagementWidget extends StatelessWidget {
const ParentManagementWidget({super.key});
@override
State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
}
class _ParentManagementWidgetState extends State<ParentManagementWidget> {
bool _isLoading = false;
String? _error;
List<ParentModel> _parents = [];
List<ParentModel> _filteredParents = [];
final TextEditingController _searchController = TextEditingController();
String? _selectedStatus;
@override
void initState() {
super.initState();
_loadParents();
_searchController.addListener(_filter);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _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) {
// 🔁 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,
},
];
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
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,
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: parents.length,
itemBuilder: (context, index) {
final parent = _filteredParents[index];
final parent = parents[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'),
leading: const Icon(Icons.person_outline),
title: Text(parent['nom'].toString()),
subtitle: Text(
"${parent.user.email}\nStatut : ${parent.user.statut ?? 'Inconnu'} | Enfants : ${parent.childrenCount}",
"${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}",
),
isThreeLine: true,
trailing: Row(
@@ -141,9 +76,8 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
);
},
),
),
],
),
)
);
}
@@ -155,12 +89,13 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
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(
@@ -170,18 +105,13 @@ class _ParentManagementWidgetState extends State<ParentManagementWidget> {
labelText: "Statut",
border: OutlineInputBorder(),
),
value: _selectedStatus,
items: const [
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")),
DropdownMenuItem(value: "Actif", child: Text("Actif")),
DropdownMenuItem(value: "En attente", child: Text("En attente")),
DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")),
],
onChanged: (value) {
setState(() {
_selectedStatus = value;
_filter();
});
// TODO: Ajouter logique de filtrage
},
),
),
-19
View File
@@ -1,19 +0,0 @@
# Créer l’issue #84 (correctifs modale MDP) via l’API Gitea
1. Définir un token valide :
`export GITEA_TOKEN="votre_token"`
ou créer `.gitea-token` à la racine du projet avec le token seul.
2. Créer l’issue :
```bash
cd /chemin/vers/PetitsPas
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d @scripts/issue-84-payload.json \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
```
3. En cas de succès (HTTP 201), la réponse JSON contient le numéro de l’issue créée.
Payload utilisé : `scripts/issue-84-payload.json` (titre + corps depuis `scripts/issue-84-body.txt`).
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# Crée une issue Gitea via l'API.
# Usage: GITEA_TOKEN=xxx ./scripts/create-gitea-issue.sh
# Ou: mettre le token dans .gitea-token à la racine du projet.
set -e
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
REPO="jmartin/petitspas"
if [ -z "$GITEA_TOKEN" ]; then
if [ -f .gitea-token ]; then
GITEA_TOKEN=$(cat .gitea-token)
fi
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
exit 1
fi
TITLE="$1"
BODY="$2"
if [ -z "$TITLE" ]; then
echo "Usage: $0 \"Titre de l'issue\" \"Corps (optionnel)\""
exit 1
fi
# Build JSON (escape body for JSON)
BODY_ESC=$(echo "$BODY" | jq -Rs . 2>/dev/null || echo "null")
if [ "$BODY_ESC" = "null" ] || [ -z "$BODY" ]; then
PAYLOAD=$(jq -n --arg t "$TITLE" '{title: $t}')
else
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "$BODY" '{title: $t, body: $b}')
fi
RESP=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$BASE_URL/repos/$REPO/issues")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY_RESP=$(echo "$RESP" | sed '$d')
if [ "$HTTP_CODE" = "201" ]; then
ISSUE_NUM=$(echo "$BODY_RESP" | jq -r .number)
echo "Issue #$ISSUE_NUM créée."
echo "$BODY_RESP" | jq .
else
echo "Erreur HTTP $HTTP_CODE: $BODY_RESP"
exit 1
fi
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env bash
# Poste un commentaire sur une issue Gitea puis la ferme.
# Usage: GITEA_TOKEN=xxx ./scripts/gitea-close-issue-with-comment.sh <numéro> "Commentaire"
# Ou: mettre le token dans .gitea-token à la racine du projet.
# Exemple: ./scripts/gitea-close-issue-with-comment.sh 15 "Livré : panneau Paramètres opérationnel."
set -e
ISSUE="${1:?Usage: $0 <numéro_issue> \"Commentaire\"}"
COMMENT="${2:?Usage: $0 <numéro_issue> \"Commentaire\"}"
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
REPO="jmartin/petitspas"
if [ -z "$GITEA_TOKEN" ]; then
if [ -f .gitea-token ]; then
GITEA_TOKEN=$(cat .gitea-token)
fi
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
exit 1
fi
# 1) Poster le commentaire
echo "Ajout du commentaire sur l'issue #$ISSUE..."
# Échapper pour JSON (guillemets et backslash)
COMMENT_ESC=$(printf '%s' "$COMMENT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r//g')
PAYLOAD="{\"body\":\"$COMMENT_ESC\"}"
RESP=$(curl -s -w "\n%{http_code}" -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$BASE_URL/repos/$REPO/issues/$ISSUE/comments")
HTTP_CODE=$(echo "$RESP" | tail -1)
BODY=$(echo "$RESP" | sed '$d')
if [ "$HTTP_CODE" != "201" ]; then
echo "Erreur HTTP $HTTP_CODE lors du commentaire: $BODY"
exit 1
fi
echo "Commentaire ajouté."
# 2) Fermer l'issue
echo "Fermeture de l'issue #$ISSUE..."
RESP2=$(curl -s -w "\n%{http_code}" -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"$BASE_URL/repos/$REPO/issues/$ISSUE")
HTTP_CODE2=$(echo "$RESP2" | tail -1)
BODY2=$(echo "$RESP2" | sed '$d')
if [ "$HTTP_CODE2" = "200" ] || [ "$HTTP_CODE2" = "201" ]; then
echo "Issue #$ISSUE fermée."
else
echo "Erreur HTTP $HTTP_CODE2: $BODY2"
exit 1
fi
-14
View File
@@ -1,14 +0,0 @@
Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.
**Périmètre :**
- Ajustements visuels / UX de la modale (ChangePasswordDialog)
- Cohérence charte graphique, espacements, lisibilité
- Comportement (validation, messages d'erreur, fermeture)
- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages
**Tâches :**
- [ ] Revoir le design de la modale (relief, bordures, couleurs)
- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations
- [ ] Ajuster les textes et messages d'erreur
- [ ] Tester sur mobile et desktop
- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin
-1
View File
@@ -1 +0,0 @@
{"title": "[Frontend] Bug – Correctifs modale Changement MDP (première connexion admin)", "body": "Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.\n\n**Périmètre :**\n- Ajustements visuels / UX de la modale (ChangePasswordDialog)\n- Cohérence charte graphique, espacements, lisibilité\n- Comportement (validation, messages d'erreur, fermeture)\n- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages\n\n**Tâches :**\n- [ ] Revoir le design de la modale (relief, bordures, couleurs)\n- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations\n- [ ] Ajuster les textes et messages d'erreur\n- [ ] Tester sur mobile et desktop\n- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin\n"}