Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8104e7e4 | ||
|
|
c4d93ee458 | ||
|
|
1fca0cf132 | ||
|
|
b16dd4b55c | ||
|
|
8682421453 | ||
|
|
dfe7daed14 | ||
|
|
11aa66feff | ||
|
|
d23f3c9f4f |
@@ -0,0 +1,18 @@
|
|||||||
|
# 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);
|
return this.assistantesMaternellesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
@ApiOperation({ summary: 'Récupérer la liste des nounous' })
|
||||||
@ApiResponse({ status: 200, description: '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 {
|
export class ParentsController {
|
||||||
constructor(private readonly parentsService: ParentsService) {}
|
constructor(private readonly parentsService: ParentsService) {}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@Get()
|
@Get()
|
||||||
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
||||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class GestionnairesController {
|
|||||||
return this.gestionnairesService.create(dto);
|
return this.gestionnairesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Liste des gestionnaires' })
|
@ApiOperation({ summary: 'Liste des gestionnaires' })
|
||||||
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
|
@ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] })
|
||||||
@Get()
|
@Get()
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { GestionnairesService } from './gestionnaires.service';
|
|||||||
import { GestionnairesController } from './gestionnaires.controller';
|
import { GestionnairesController } from './gestionnaires.controller';
|
||||||
import { Users } from 'src/entities/users.entity';
|
import { Users } from 'src/entities/users.entity';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Users])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Users]),
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
controllers: [GestionnairesController],
|
controllers: [GestionnairesController],
|
||||||
providers: [GestionnairesService],
|
providers: [GestionnairesService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class UserController {
|
|||||||
|
|
||||||
// Lister tous les utilisateurs (super_admin uniquement)
|
// Lister tous les utilisateurs (super_admin uniquement)
|
||||||
@Get()
|
@Get()
|
||||||
@Roles(RoleType.SUPER_ADMIN)
|
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||||
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
|
@ApiOperation({ summary: 'Lister tous les utilisateurs' })
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.userService.findAll();
|
return this.userService.findAll();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ParentsModule } from '../parents/parents.module';
|
|||||||
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
||||||
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
|
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
|
||||||
import { Parents } from 'src/entities/parents.entity';
|
import { Parents } from 'src/entities/parents.entity';
|
||||||
|
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature(
|
imports: [TypeOrmModule.forFeature(
|
||||||
@@ -20,6 +21,7 @@ import { Parents } from 'src/entities/parents.entity';
|
|||||||
]), forwardRef(() => AuthModule),
|
]), forwardRef(() => AuthModule),
|
||||||
ParentsModule,
|
ParentsModule,
|
||||||
AssistantesMaternellesModule,
|
AssistantesMaternellesModule,
|
||||||
|
GestionnairesModule,
|
||||||
],
|
],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService],
|
providers: [UserService],
|
||||||
|
|||||||
@@ -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<List<AppUser>> getGestionnaires() async {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.gestionnaires}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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<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?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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,6 +5,14 @@ class AppUser {
|
|||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
final bool changementMdpObligatoire;
|
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({
|
AppUser({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -13,20 +21,53 @@ class AppUser {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.changementMdpObligatoire = false,
|
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) {
|
factory AppUser.fromJson(Map<String, dynamic> json) {
|
||||||
|
final id = json['id']?.toString();
|
||||||
|
final email = json['email']?.toString();
|
||||||
|
final role = json['role']?.toString();
|
||||||
|
if (id == null || id.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: id manquant');
|
||||||
|
}
|
||||||
|
if (email == null || email.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: email manquant');
|
||||||
|
}
|
||||||
|
if (role == null || role.isEmpty) {
|
||||||
|
throw Exception('Profil invalide: rôle manquant');
|
||||||
|
}
|
||||||
return AppUser(
|
return AppUser(
|
||||||
id: json['id'] as String,
|
id: id,
|
||||||
email: json['email'] as String,
|
email: email,
|
||||||
role: json['role'] as String,
|
role: role,
|
||||||
createdAt: json['createdAt'] != null
|
createdAt: json['cree_le'] != null
|
||||||
? DateTime.parse(json['createdAt'] as String)
|
? DateTime.tryParse(json['cree_le'].toString()) ?? DateTime.now()
|
||||||
: DateTime.now(),
|
: (json['createdAt'] != null
|
||||||
updatedAt: json['updatedAt'] != null
|
? DateTime.tryParse(json['createdAt'].toString()) ?? DateTime.now()
|
||||||
? DateTime.parse(json['updatedAt'] as String)
|
: DateTime.now()),
|
||||||
: DateTime.now(),
|
updatedAt: json['modifie_le'] != null
|
||||||
changementMdpObligatoire: json['changement_mdp_obligatoire'] as bool? ?? false,
|
? DateTime.tryParse(json['modifie_le'].toString()) ?? DateTime.now()
|
||||||
|
: (json['updatedAt'] != null
|
||||||
|
? DateTime.tryParse(json['updatedAt'].toString()) ?? DateTime.now()
|
||||||
|
: DateTime.now()),
|
||||||
|
changementMdpObligatoire:
|
||||||
|
json['changement_mdp_obligatoire'] == true,
|
||||||
|
nom: json['nom']?.toString(),
|
||||||
|
prenom: json['prenom']?.toString(),
|
||||||
|
statut: json['statut']?.toString(),
|
||||||
|
telephone: json['telephone']?.toString(),
|
||||||
|
photoUrl: json['photo_url']?.toString(),
|
||||||
|
adresse: json['adresse']?.toString(),
|
||||||
|
ville: json['ville']?.toString(),
|
||||||
|
codePostal: json['code_postal']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +79,16 @@ class AppUser {
|
|||||||
'createdAt': createdAt.toIso8601String(),
|
'createdAt': createdAt.toIso8601String(),
|
||||||
'updatedAt': updatedAt.toIso8601String(),
|
'updatedAt': updatedAt.toIso8601String(),
|
||||||
'changement_mdp_obligatoire': changementMdpObligatoire,
|
'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,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/assistante_maternelle_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_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/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/admin/parametres_panel.dart';
|
||||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||||
@@ -104,7 +105,7 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
|||||||
case 2:
|
case 2:
|
||||||
return const AssistanteMaternelleManagementWidget();
|
return const AssistanteMaternelleManagementWidget();
|
||||||
case 3:
|
case 3:
|
||||||
return const Center(child: Text('👨💼 Administrateurs'));
|
return const AdminManagementWidget();
|
||||||
default:
|
default:
|
||||||
return const Center(child: Text('Page non trouvée'));
|
return const Center(child: Text('Page non trouvée'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,20 +236,20 @@ class _LoginPageState extends State<LoginScreen> with WidgetsBindingObserver {
|
|||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
margin: const EdgeInsets.only(bottom: 15),
|
margin: const EdgeInsets.only(bottom: 15),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.red[50],
|
color: Colors.red.shade50,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.red[300]!),
|
border: Border.all(color: Colors.red.shade300),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.error_outline, color: Colors.red[700], size: 20),
|
Icon(Icons.error_outline, color: Colors.red.shade700, size: 20),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_errorMessage!,
|
_errorMessage!,
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.red[700],
|
color: Colors.red.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ class ApiConfig {
|
|||||||
static const String users = '/users';
|
static const String users = '/users';
|
||||||
static const String userProfile = '/users/profile';
|
static const String userProfile = '/users/profile';
|
||||||
static const String userChildren = '/users/children';
|
static const String userChildren = '/users/children';
|
||||||
|
static const String gestionnaires = '/gestionnaires';
|
||||||
|
static const String parents = '/parents';
|
||||||
|
static const String assistantesMaternelles = '/assistantes-maternelles';
|
||||||
|
|
||||||
// Configuration (admin)
|
// Configuration (admin)
|
||||||
static const String configuration = '/configuration';
|
static const String configuration = '/configuration';
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is Exception) rethrow;
|
if (e is Exception) rethrow;
|
||||||
throw Exception('Erreur réseau: impossible de se connecter au serveur');
|
if (e is Error) throw Exception('Erreur interne: ${e.toString()}');
|
||||||
|
throw Exception('Erreur réseau: impossible de se connecter au serveur ($e)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +57,22 @@ class AuthService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final raw = jsonDecode(response.body);
|
||||||
|
if (raw is! Map<String, dynamic>) {
|
||||||
|
throw Exception('Profil invalide: réponse serveur inattendue');
|
||||||
|
}
|
||||||
|
// Accepter réponse directe ou wrapper { data: {...} }
|
||||||
|
final data = raw.containsKey('data') && raw['data'] is Map<String, dynamic>
|
||||||
|
? raw['data'] as Map<String, dynamic>
|
||||||
|
: raw;
|
||||||
return AppUser.fromJson(data);
|
return AppUser.fromJson(data);
|
||||||
} else {
|
} else {
|
||||||
throw Exception('Erreur lors de la récupération du profil');
|
throw Exception('Erreur lors de la récupération du profil');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is Exception) rethrow;
|
if (e is Exception) rethrow;
|
||||||
throw Exception('Erreur réseau: impossible de récupérer le profil');
|
if (e is Error) throw Exception('Erreur interne: ${e.toString()}');
|
||||||
|
throw Exception('Erreur réseau: impossible de récupérer le profil ($e)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +107,8 @@ class AuthService {
|
|||||||
await _saveCurrentUser(user);
|
await _saveCurrentUser(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is Exception) rethrow;
|
if (e is Exception) rethrow;
|
||||||
throw Exception('Erreur réseau: impossible de changer le mot de passe');
|
if (e is Error) throw Exception('Erreur interne: ${e.toString()}');
|
||||||
|
throw Exception('Erreur réseau: impossible de changer le mot de passe ($e)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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,72 +1,142 @@
|
|||||||
import 'package:flutter/material.dart';
|
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});
|
const AssistanteMaternelleManagementWidget({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<AssistanteMaternelleManagementWidget> createState() =>
|
||||||
final assistantes = [
|
_AssistanteMaternelleManagementWidgetState();
|
||||||
{
|
}
|
||||||
"nom": "Marie Dupont",
|
|
||||||
"numeroAgrement": "AG123456",
|
|
||||||
"zone": "Paris 14",
|
|
||||||
"capacite": 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nom": "Claire Martin",
|
|
||||||
"numeroAgrement": "AG654321",
|
|
||||||
"zone": "Lyon 7",
|
|
||||||
"capacite": 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
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) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 🔎 Zone de filtre
|
// 🔎 Zone de filtre
|
||||||
_buildFilterSection(),
|
_buildFilterSection(),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 📋 Liste des assistantes
|
// 📋 Liste des assistantes
|
||||||
ListView.builder(
|
if (_isLoading)
|
||||||
shrinkWrap: true,
|
const Center(child: CircularProgressIndicator())
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
else if (_error != null)
|
||||||
itemCount: assistantes.length,
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
itemBuilder: (context, index) {
|
else if (_filteredAssistantes.isEmpty)
|
||||||
final assistante = assistantes[index];
|
const Center(child: Text("Aucune assistante maternelle trouvée."))
|
||||||
return Card(
|
else
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
Expanded(
|
||||||
child: ListTile(
|
child: ListView.builder(
|
||||||
leading: const Icon(Icons.face),
|
itemCount: _filteredAssistantes.length,
|
||||||
title: Text(assistante['nom'].toString()),
|
itemBuilder: (context, index) {
|
||||||
subtitle: Text(
|
final assistante = _filteredAssistantes[index];
|
||||||
"N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"),
|
return Card(
|
||||||
trailing: Row(
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: ListTile(
|
||||||
children: [
|
leading: CircleAvatar(
|
||||||
IconButton(
|
backgroundImage: assistante.user.photoUrl != null
|
||||||
icon: const Icon(Icons.edit),
|
? NetworkImage(assistante.user.photoUrl!)
|
||||||
onPressed: () {
|
: null,
|
||||||
// TODO: Ajouter modification
|
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(
|
SizedBox(
|
||||||
width: 200,
|
width: 200,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _zoneController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: "Zone géographique",
|
labelText: "Zone géographique",
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
|
||||||
// TODO: Ajouter logique de filtrage par zone
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 200,
|
width: 200,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _capacityController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: "Capacité minimum",
|
labelText: "Capacité minimum",
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (value) {
|
|
||||||
// TODO: Ajouter logique de filtrage par capacité
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,70 @@
|
|||||||
import 'package:flutter/material.dart';
|
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';
|
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);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -14,9 +75,10 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
|||||||
// 🔹 Barre du haut avec bouton
|
// 🔹 Barre du haut avec bouton
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
decoration: InputDecoration(
|
controller: _searchController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
hintText: "Rechercher un gestionnaire...",
|
hintText: "Rechercher un gestionnaire...",
|
||||||
prefixIcon: Icon(Icons.search),
|
prefixIcon: Icon(Icons.search),
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -26,7 +88,7 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Rediriger vers la page de création
|
// TODO: Rediriger vers la page de création
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text("Créer un gestionnaire"),
|
label: const Text("Créer un gestionnaire"),
|
||||||
@@ -36,17 +98,25 @@ class GestionnaireManagementWidget extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 🔹 Liste des gestionnaires
|
// 🔹 Liste des gestionnaires
|
||||||
Expanded(
|
if (_isLoading)
|
||||||
child: ListView.builder(
|
const Center(child: CircularProgressIndicator())
|
||||||
itemCount: 5, // À remplacer par liste dynamique
|
else if (_error != null)
|
||||||
itemBuilder: (context, index) {
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
return GestionnaireCard(
|
else if (_filteredGestionnaires.isEmpty)
|
||||||
name: "Dupont $index",
|
const Center(child: Text("Aucun gestionnaire trouvé."))
|
||||||
email: "dupont$index@mail.com",
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,83 +1,149 @@
|
|||||||
import 'package:flutter/material.dart';
|
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});
|
const ParentManagementWidget({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<ParentManagementWidget> createState() => _ParentManagementWidgetState();
|
||||||
// 🔁 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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
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) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
_buildSearchSection(),
|
||||||
_buildSearchSection(),
|
const SizedBox(height: 16),
|
||||||
|
if (_isLoading)
|
||||||
const SizedBox(height: 16),
|
const Center(child: CircularProgressIndicator())
|
||||||
|
else if (_error != null)
|
||||||
ListView.builder(
|
Center(child: Text('Erreur: $_error', style: const TextStyle(color: Colors.red)))
|
||||||
shrinkWrap: true,
|
else if (_filteredParents.isEmpty)
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
const Center(child: Text("Aucun parent trouvé."))
|
||||||
itemCount: parents.length,
|
else
|
||||||
itemBuilder: (context, index) {
|
Expanded(
|
||||||
final parent = parents[index];
|
child: ListView.builder(
|
||||||
return Card(
|
itemCount: _filteredParents.length,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
itemBuilder: (context, index) {
|
||||||
child: ListTile(
|
final parent = _filteredParents[index];
|
||||||
leading: const Icon(Icons.person_outline),
|
return Card(
|
||||||
title: Text(parent['nom'].toString()),
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
subtitle: Text(
|
child: ListTile(
|
||||||
"${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}",
|
leading: CircleAvatar(
|
||||||
),
|
backgroundImage: parent.user.photoUrl != null
|
||||||
isThreeLine: true,
|
? NetworkImage(parent.user.photoUrl!)
|
||||||
trailing: Row(
|
: null,
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: parent.user.photoUrl == null
|
||||||
children: [
|
? const Icon(Icons.person)
|
||||||
IconButton(
|
: null,
|
||||||
icon: const Icon(Icons.visibility),
|
),
|
||||||
tooltip: "Voir dossier",
|
title: Text(parent.user.fullName.isNotEmpty
|
||||||
onPressed: () {
|
? parent.user.fullName
|
||||||
// TODO: Voir le statut du dossier
|
: '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(
|
SizedBox(
|
||||||
width: 220,
|
width: 220,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: "Nom du parent",
|
labelText: "Nom du parent",
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
|
||||||
// TODO: Ajouter logique de recherche
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -105,13 +170,18 @@ class ParentManagementWidget extends StatelessWidget {
|
|||||||
labelText: "Statut",
|
labelText: "Statut",
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
value: _selectedStatus,
|
||||||
items: const [
|
items: const [
|
||||||
DropdownMenuItem(value: "Actif", child: Text("Actif")),
|
DropdownMenuItem(value: null, child: Text("Tous")),
|
||||||
DropdownMenuItem(value: "En attente", child: Text("En attente")),
|
DropdownMenuItem(value: "actif", child: Text("Actif")),
|
||||||
DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")),
|
DropdownMenuItem(value: "en_attente", child: Text("En attente")),
|
||||||
|
DropdownMenuItem(value: "suspendu", child: Text("Suspendu")),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
// TODO: Ajouter logique de filtrage
|
setState(() {
|
||||||
|
_selectedStatus = value;
|
||||||
|
_filter();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 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`).
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"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"}
|
||||||
Reference in New Issue
Block a user