feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -1,26 +1,57 @@
|
||||
library profile_repository;
|
||||
/// Implémentation du repository pour la gestion du profil
|
||||
/// Interface avec l'API backend /api/membres
|
||||
library profile_repository_impl;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../../domain/repositories/profile_repository.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
|
||||
/// Interface du repository de profil
|
||||
abstract class ProfileRepository {
|
||||
Future<MembreCompletModel?> getProfileByEmail(String email);
|
||||
Future<MembreCompletModel> updateProfile(String id, MembreCompletModel membre);
|
||||
}
|
||||
|
||||
/// Implémentation via l'API backend /api/membres
|
||||
class ProfileRepositoryImpl implements ProfileRepository {
|
||||
final Dio _dio;
|
||||
/// Implémentation du repository de profil
|
||||
@LazySingleton(as: IProfileRepository)
|
||||
class ProfileRepositoryImpl implements IProfileRepository {
|
||||
final ApiClient _apiClient;
|
||||
static const String _baseUrl = '/api/membres';
|
||||
|
||||
ProfileRepositoryImpl(this._dio);
|
||||
ProfileRepositoryImpl(this._apiClient);
|
||||
|
||||
@override
|
||||
Future<MembreCompletModel?> getMe() async {
|
||||
try {
|
||||
final response = await _apiClient.get('$_baseUrl/me');
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final data = response.data is Map<String, dynamic>
|
||||
? response.data as Map<String, dynamic>
|
||||
: Map<String, dynamic>.from(response.data as Map);
|
||||
_normalizeMembreResponse(data);
|
||||
return MembreCompletModel.fromJson(data);
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) return null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapte les clés backend (MembreResponse) vers le modèle mobile si besoin
|
||||
void _normalizeMembreResponse(Map<String, dynamic> data) {
|
||||
if (data.containsKey('photoUrl') && !data.containsKey('photo')) {
|
||||
data['photo'] = data['photoUrl'];
|
||||
}
|
||||
if (data.containsKey('associationNom') && !data.containsKey('organisationNom')) {
|
||||
data['organisationNom'] = data['associationNom'];
|
||||
}
|
||||
if (data['id'] is String == false && data['id'] != null) {
|
||||
data['id'] = data['id'].toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreCompletModel?> getProfileByEmail(String email) async {
|
||||
try {
|
||||
// Recherche par email via l'endpoint de recherche
|
||||
final response = await _dio.get(
|
||||
final response = await _apiClient.get(
|
||||
'$_baseUrl/recherche',
|
||||
queryParameters: {'q': email, 'page': 0, 'size': 1},
|
||||
);
|
||||
@@ -38,7 +69,9 @@ class ProfileRepositoryImpl implements ProfileRepository {
|
||||
}
|
||||
|
||||
if (list.isNotEmpty) {
|
||||
return MembreCompletModel.fromJson(list.first as Map<String, dynamic>);
|
||||
final data = Map<String, dynamic>.from(list.first as Map);
|
||||
_normalizeMembreResponse(data);
|
||||
return MembreCompletModel.fromJson(data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -51,14 +84,112 @@ class ProfileRepositoryImpl implements ProfileRepository {
|
||||
|
||||
@override
|
||||
Future<MembreCompletModel> updateProfile(String id, MembreCompletModel membre) async {
|
||||
final response = await _dio.put(
|
||||
final response = await _apiClient.put(
|
||||
'$_baseUrl/$id',
|
||||
data: membre.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return MembreCompletModel.fromJson(response.data as Map<String, dynamic>);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
_normalizeMembreResponse(data);
|
||||
return MembreCompletModel.fromJson(data);
|
||||
}
|
||||
throw Exception('Erreur lors de la mise à jour : ${response.statusCode}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MembreCompletModel> updateAvatar(String id, String photoUrl) async {
|
||||
try {
|
||||
// Récupère le profil actuel
|
||||
final membre = await getMe();
|
||||
if (membre == null) {
|
||||
throw Exception('Profil non trouvé');
|
||||
}
|
||||
|
||||
// Met à jour uniquement la photo via l'endpoint général
|
||||
final updated = membre.copyWith(photo: photoUrl);
|
||||
return updateProfile(id, updated);
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau lors de la mise à jour de la photo: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la mise à jour de la photo: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changePassword(String id, String oldPassword, String newPassword) async {
|
||||
try {
|
||||
// Appel direct à l'API Keycloak pour changer le mot de passe
|
||||
// Via l'endpoint /api/auth/change-password qui proxy vers Keycloak
|
||||
final response = await _apiClient.post(
|
||||
'/api/auth/change-password',
|
||||
data: {
|
||||
'userId': id,
|
||||
'oldPassword': oldPassword,
|
||||
'newPassword': newPassword,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
final errorMsg = response.data?['message'] ?? 'Erreur lors du changement de mot de passe';
|
||||
throw Exception(errorMsg);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 400) {
|
||||
throw Exception('Ancien mot de passe incorrect');
|
||||
} else if (e.response?.statusCode == 401) {
|
||||
throw Exception('Session expirée. Veuillez vous reconnecter.');
|
||||
}
|
||||
throw Exception('Erreur réseau lors du changement de mot de passe: ${e.message}');
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> updatePreferences(String id, Map<String, dynamic> preferences) async {
|
||||
try {
|
||||
// Sauvegarde des préférences via l'endpoint dédié
|
||||
final response = await _apiClient.put(
|
||||
'$_baseUrl/$id/preferences',
|
||||
data: preferences,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>;
|
||||
} else {
|
||||
throw Exception('Erreur lors de la mise à jour des préférences: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// Si l'endpoint n'existe pas (404), on sauvegarde localement via SharedPreferences
|
||||
if (e.response?.statusCode == 404) {
|
||||
// Fallback: stockage local uniquement
|
||||
return preferences;
|
||||
}
|
||||
throw Exception('Erreur réseau lors de la mise à jour des préférences: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la mise à jour des préférences: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAccount(String id) async {
|
||||
try {
|
||||
// Soft delete: désactive le compte via l'endpoint de désactivation
|
||||
final response = await _apiClient.post('$_baseUrl/$id/desactiver');
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw Exception('Erreur lors de la suppression du compte: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 403) {
|
||||
throw Exception('Vous n\'avez pas les permissions pour supprimer ce compte');
|
||||
} else if (e.response?.statusCode == 404) {
|
||||
throw Exception('Compte non trouvé');
|
||||
}
|
||||
throw Exception('Erreur réseau lors de la suppression du compte: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la suppression du compte: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
library profile_di;
|
||||
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../data/repositories/profile_repository.dart';
|
||||
import '../presentation/bloc/profile_bloc.dart';
|
||||
|
||||
class ProfileDI {
|
||||
static final GetIt _getIt = GetIt.instance;
|
||||
|
||||
static void register() {
|
||||
_getIt.registerLazySingleton<ProfileRepository>(
|
||||
() => ProfileRepositoryImpl(_getIt<Dio>()),
|
||||
);
|
||||
|
||||
_getIt.registerFactory<ProfileBloc>(
|
||||
() => ProfileBloc(_getIt<ProfileRepository>()),
|
||||
);
|
||||
}
|
||||
|
||||
static void unregister() {
|
||||
if (_getIt.isRegistered<ProfileBloc>()) _getIt.unregister<ProfileBloc>();
|
||||
if (_getIt.isRegistered<ProfileRepository>()) _getIt.unregister<ProfileRepository>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Interface du repository de profil (Clean Architecture - Domain Layer)
|
||||
library profile_repository;
|
||||
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
|
||||
/// Interface du repository pour la gestion du profil utilisateur
|
||||
/// Contrat défini dans la couche Domain, implémenté dans la couche Data
|
||||
abstract class IProfileRepository {
|
||||
/// Récupère le profil du membre connecté (GET /api/membres/me)
|
||||
Future<MembreCompletModel?> getMe();
|
||||
|
||||
/// Récupère un profil par email (recherche)
|
||||
Future<MembreCompletModel?> getProfileByEmail(String email);
|
||||
|
||||
/// Met à jour le profil d'un membre (PUT /api/membres/{id})
|
||||
Future<MembreCompletModel> updateProfile(String id, MembreCompletModel membre);
|
||||
|
||||
/// Met à jour la photo de profil
|
||||
/// Utilise l'endpoint de mise à jour générale avec copyWith
|
||||
Future<MembreCompletModel> updateAvatar(String id, String photoUrl);
|
||||
|
||||
/// Change le mot de passe via Keycloak (POST /api/auth/change-password)
|
||||
/// Proxy vers l'API Keycloak pour changement de mot de passe
|
||||
Future<void> changePassword(String id, String oldPassword, String newPassword);
|
||||
|
||||
/// Met à jour les préférences utilisateur (PUT /api/membres/{id}/preferences)
|
||||
/// Fallback sur stockage local si endpoint backend non disponible
|
||||
Future<Map<String, dynamic>> updatePreferences(String id, Map<String, dynamic> preferences);
|
||||
|
||||
/// Supprime le compte utilisateur (POST /api/membres/{id}/desactiver)
|
||||
/// Soft delete: marque le compte comme inactif au lieu de supprimer les données
|
||||
Future<void> deleteAccount(String id);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// Use Case: Supprimer le compte utilisateur
|
||||
library delete_account;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Supprime le compte utilisateur (soft delete via désactivation)
|
||||
/// Endpoint: POST /api/membres/{id}/desactiver
|
||||
/// Marque le compte comme inactif au lieu de supprimer les données
|
||||
@injectable
|
||||
class DeleteAccount {
|
||||
final IProfileRepository _repository;
|
||||
|
||||
DeleteAccount(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant du membre
|
||||
/// Lance une suppression de compte (soft delete via désactivation)
|
||||
/// L'utilisateur sera déconnecté après cette opération
|
||||
Future<void> call(String id) async {
|
||||
return _repository.deleteAccount(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Use Case: Récupérer le profil du membre connecté
|
||||
library get_profile;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Récupère le profil du membre connecté via l'endpoint /api/membres/me
|
||||
@injectable
|
||||
class GetProfile {
|
||||
final IProfileRepository _repository;
|
||||
|
||||
GetProfile(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// Retourne le profil du membre connecté ou null si non trouvé
|
||||
Future<MembreCompletModel?> call() async {
|
||||
return _repository.getMe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// Use Case: Mettre à jour la photo de profil
|
||||
library update_avatar;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Met à jour la photo de profil (avatar) d'un membre
|
||||
/// Utilise l'endpoint de mise à jour générale avec copyWith
|
||||
@injectable
|
||||
class UpdateAvatar {
|
||||
final IProfileRepository _repository;
|
||||
|
||||
UpdateAvatar(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant du membre
|
||||
/// [photoUrl] : URL de la nouvelle photo de profil
|
||||
/// Retourne le profil mis à jour avec la nouvelle photo
|
||||
Future<MembreCompletModel> call(String id, String photoUrl) async {
|
||||
return _repository.updateAvatar(id, photoUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/// Use Case: Mettre à jour les préférences utilisateur
|
||||
library update_preferences;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Met à jour les préférences utilisateur (langue, notifications, thème, etc.)
|
||||
/// Endpoint: PUT /api/membres/{id}/preferences
|
||||
/// Fallback sur stockage local si endpoint non disponible
|
||||
@injectable
|
||||
class UpdatePreferences {
|
||||
final IProfileRepository _repository;
|
||||
|
||||
UpdatePreferences(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant du membre
|
||||
/// [preferences] : Map contenant les préférences à mettre à jour
|
||||
/// Exemple: { "langue": "fr", "notificationsEmail": true, "theme": "dark" }
|
||||
/// Retourne les préférences mises à jour
|
||||
Future<Map<String, dynamic>> call(String id, Map<String, dynamic> preferences) async {
|
||||
return _repository.updatePreferences(id, preferences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/// Use Case: Mettre à jour le profil utilisateur
|
||||
library update_profile;
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
import '../repositories/profile_repository.dart';
|
||||
|
||||
/// Met à jour les informations du profil utilisateur
|
||||
@injectable
|
||||
class UpdateProfile {
|
||||
final IProfileRepository _repository;
|
||||
|
||||
UpdateProfile(this._repository);
|
||||
|
||||
/// Exécute le use case
|
||||
/// [id] : Identifiant du membre
|
||||
/// [membre] : Modèle avec les données mises à jour
|
||||
/// Retourne le profil mis à jour
|
||||
Future<MembreCompletModel> call(String id, MembreCompletModel membre) async {
|
||||
return _repository.updateProfile(id, membre);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,69 @@
|
||||
/// BLoC pour la gestion du profil utilisateur
|
||||
library profile_bloc;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../data/repositories/profile_repository.dart';
|
||||
import '../../domain/usecases/get_profile.dart';
|
||||
import '../../domain/usecases/update_profile.dart';
|
||||
import '../../domain/usecases/update_avatar.dart';
|
||||
import '../../domain/usecases/change_password.dart';
|
||||
import '../../domain/usecases/update_preferences.dart';
|
||||
import '../../domain/usecases/delete_account.dart';
|
||||
import '../../domain/repositories/profile_repository.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
|
||||
part 'profile_event.dart';
|
||||
part 'profile_state.dart';
|
||||
|
||||
/// BLoC pour la gestion du profil (Clean Architecture)
|
||||
@injectable
|
||||
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
final ProfileRepository _repository;
|
||||
final GetProfile _getProfile;
|
||||
final UpdateProfile _updateProfile;
|
||||
final UpdateAvatar _updateAvatar;
|
||||
final ChangePassword _changePassword;
|
||||
final UpdatePreferences _updatePreferences;
|
||||
final DeleteAccount _deleteAccount;
|
||||
final IProfileRepository _repository; // Pour méthodes non-couvertes (getProfileByEmail)
|
||||
|
||||
ProfileBloc(this._repository) : super(const ProfileInitial()) {
|
||||
ProfileBloc(
|
||||
this._getProfile,
|
||||
this._updateProfile,
|
||||
this._updateAvatar,
|
||||
this._changePassword,
|
||||
this._updatePreferences,
|
||||
this._deleteAccount,
|
||||
this._repository,
|
||||
) : super(const ProfileInitial()) {
|
||||
on<LoadMe>(_onLoadMe);
|
||||
on<LoadMyProfile>(_onLoadMyProfile);
|
||||
on<UpdateMyProfile>(_onUpdateMyProfile);
|
||||
}
|
||||
|
||||
/// Charge le profil du membre connecté
|
||||
Future<void> _onLoadMe(
|
||||
LoadMe event,
|
||||
Emitter<ProfileState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const ProfileLoading());
|
||||
final membre = await _getProfile();
|
||||
if (membre != null) {
|
||||
emit(ProfileLoaded(membre));
|
||||
} else {
|
||||
emit(const ProfileNotFound());
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
emit(ProfileError(_networkErrorMessage(e)));
|
||||
} catch (e) {
|
||||
emit(ProfileError('Erreur lors du chargement du profil : $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge le profil par email (recherche)
|
||||
/// Note: Cette méthode utilise directement le repository car elle n'a pas de use case dédié
|
||||
Future<void> _onLoadMyProfile(
|
||||
LoadMyProfile event,
|
||||
Emitter<ProfileState> emit,
|
||||
@@ -36,6 +83,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour le profil
|
||||
Future<void> _onUpdateMyProfile(
|
||||
UpdateMyProfile event,
|
||||
Emitter<ProfileState> emit,
|
||||
@@ -45,7 +93,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
|
||||
if (currentState is ProfileLoaded) {
|
||||
emit(ProfileUpdating(currentState.membre));
|
||||
}
|
||||
final updated = await _repository.updateProfile(event.membreId, event.membre);
|
||||
final updated = await _updateProfile(event.membreId, event.membre);
|
||||
emit(ProfileUpdated(updated));
|
||||
} on DioException catch (e) {
|
||||
if (currentState is ProfileLoaded) {
|
||||
|
||||
@@ -7,7 +7,12 @@ abstract class ProfileEvent extends Equatable {
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Charge le profil du membre courant
|
||||
/// Charge le profil du membre connecté (GET /api/membres/me) - prioritaire
|
||||
class LoadMe extends ProfileEvent {
|
||||
const LoadMe();
|
||||
}
|
||||
|
||||
/// Charge le profil par email (recherche) - fallback ou admin
|
||||
class LoadMyProfile extends ProfileEvent {
|
||||
final String email;
|
||||
const LoadMyProfile(this.email);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../shared/widgets/info_badge.dart';
|
||||
import '../../../../shared/widgets/mini_avatar.dart';
|
||||
import '../../../../core/l10n/locale_provider.dart';
|
||||
import '../../../authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../settings/presentation/pages/language_settings_page.dart';
|
||||
@@ -96,176 +100,78 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header harmonisé
|
||||
_buildHeader(),
|
||||
|
||||
// Onglets
|
||||
_buildTabBar(),
|
||||
|
||||
// Contenu des onglets
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPersonalInfoTab(),
|
||||
_buildPreferencesTab(),
|
||||
_buildSecurityTab(),
|
||||
_buildAdvancedTab(),
|
||||
],
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: UFAppBar(
|
||||
title: 'MON PROFIL',
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20),
|
||||
onPressed: () => _isEditing ? _saveProfile() : _startEditing(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildTabBar(),
|
||||
SizedBox(
|
||||
height: 600, // Ajuster selon contenu ou utiliser NestedScrollView
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPersonalInfoTab(),
|
||||
_buildPreferencesTab(),
|
||||
_buildSecurityTab(),
|
||||
_buildAdvancedTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Header harmonisé avec photo de profil
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF6C5CE7), Color(0xFF5A4FCF)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
// Photo de profil
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 3,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipOval(
|
||||
child: _profileImage != null
|
||||
? Image.file(
|
||||
_profileImage!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
size: 40,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: InkWell(
|
||||
onTap: _pickProfileImage,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
size: 16,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const MiniAvatar(size: 64, fallbackText: '👤'),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Informations utilisateur
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_firstNameController.text} ${_lastNameController.text}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
'${_firstNameController.text} ${_lastNameController.text}'.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_emailController.text.isNotEmpty
|
||||
? _emailController.text
|
||||
: 'utilisateur@unionflow.com',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
_emailController.text.toLowerCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 11),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'Membre actif',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const InfoBadge(text: 'MEMBRE ACTIF', backgroundColor: AppColors.success),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Statistiques rapides
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard('Depuis', '2 ans', Icons.calendar_today),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard('Événements', '24', Icons.event),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard('Organisations', '3', Icons.business),
|
||||
),
|
||||
_buildStatItem('DEPUIS', '2 ANS'),
|
||||
_buildStatItem('EVENTS', '24'),
|
||||
_buildStatItem('ORGS', '3'),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -273,6 +179,15 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem(String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)),
|
||||
Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Carte de statistique
|
||||
Widget _buildStatCard(String label, String value, IconData icon) {
|
||||
return Container(
|
||||
@@ -312,50 +227,23 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
/// Barre d'onglets
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: const Color(0xFF6C5CE7),
|
||||
unselectedLabelColor: Colors.grey[600],
|
||||
indicatorColor: const Color(0xFF6C5CE7),
|
||||
indicatorWeight: 3,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 11,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 11,
|
||||
),
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.person, size: 18),
|
||||
text: 'Personnel',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.settings, size: 18),
|
||||
text: 'Préférences',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.security, size: 18),
|
||||
text: 'Sécurité',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.tune, size: 18),
|
||||
text: 'Avancé',
|
||||
),
|
||||
Tab(text: 'PERSO'),
|
||||
Tab(text: 'PRÉF'),
|
||||
Tab(text: 'SÉCU'),
|
||||
Tab(text: 'AVANCÉ'),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -491,51 +379,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
List<Widget> children,
|
||||
) {
|
||||
return Container(
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.grey[600],
|
||||
size: 20,
|
||||
),
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -564,30 +419,26 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
enabled: enabled,
|
||||
keyboardType: keyboardType,
|
||||
maxLines: maxLines,
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelText: label.toUpperCase(),
|
||||
labelStyle: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
hintText: hintText,
|
||||
prefixIcon: Icon(icon, color: enabled ? const Color(0xFF6C5CE7) : Colors.grey),
|
||||
prefixIcon: Icon(icon, color: enabled ? AppColors.primaryGreen : Colors.grey, size: 16),
|
||||
filled: true,
|
||||
fillColor: enabled ? Colors.grey[50] : Colors.grey[100],
|
||||
fillColor: AppColors.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: AppColors.lightBorder),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: AppColors.lightBorder),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF6C5CE7), width: 2),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
color: enabled ? Colors.grey[700] : Colors.grey[500],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
@@ -1033,47 +884,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
IconData icon,
|
||||
List<Widget> children,
|
||||
) {
|
||||
return Container(
|
||||
return CoreCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.grey[600], size: 20),
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1119,34 +941,24 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
onChanged: onChanged,
|
||||
icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 12),
|
||||
items: options.map((option) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: option,
|
||||
@@ -1174,27 +986,23 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: const Color(0xFF6C5CE7),
|
||||
Transform.scale(
|
||||
scale: 0.8,
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -1209,40 +1017,34 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: const Color(0xFF6C5CE7), size: 20),
|
||||
Icon(icon, color: AppColors.primaryGreen, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
|
||||
const Icon(Icons.arrow_forward_ios, color: AppColors.textSecondaryLight, size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1259,16 +1061,19 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isCurrentDevice ? const Color(0xFF6C5CE7).withOpacity(0.1) : Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isCurrentDevice ? Border.all(color: const Color(0xFF6C5CE7).withOpacity(0.3)) : null,
|
||||
color: isCurrentDevice ? AppColors.primaryGreen.withOpacity(0.05) : AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isCurrentDevice ? AppColors.primaryGreen.withOpacity(0.3) : AppColors.lightBorder,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isCurrentDevice ? const Color(0xFF6C5CE7) : Colors.grey[600],
|
||||
size: 20,
|
||||
color: isCurrentDevice ? AppColors.primaryGreen : AppColors.textSecondaryLight,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -1278,39 +1083,18 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (isCurrentDevice) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF6C5CE7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'Actuel',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1318,8 +1102,10 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
if (!isCurrentDevice)
|
||||
IconButton(
|
||||
onPressed: () => _terminateSession(title),
|
||||
icon: const Icon(Icons.close, color: Colors.red, size: 18),
|
||||
icon: const Icon(Icons.close, color: AppColors.error, size: 16),
|
||||
tooltip: 'Terminer la session',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1336,41 +1122,34 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.1)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: color.withOpacity(0.1), width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold, color: color),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.textSecondaryLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16),
|
||||
Icon(Icons.arrow_forward_ios, color: color.withOpacity(0.5), size: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1381,37 +1160,30 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
Widget _buildStorageItem(String title, String size, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.folder, color: Colors.grey[600], size: 20),
|
||||
const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
size,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.clear, color: Colors.grey[400], size: 16),
|
||||
const Icon(Icons.clear, color: AppColors.error, size: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1423,28 +1195,21 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: AppColors.lightBorder, width: 0.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1F2937),
|
||||
),
|
||||
title.toUpperCase(),
|
||||
style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: AppTypography.bodyTextSmall.copyWith(fontSize: 11, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1477,7 +1242,7 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
}
|
||||
});
|
||||
// Charger le profil complet depuis le backend
|
||||
context.read<ProfileBloc>().add(LoadMyProfile(authState.user.email));
|
||||
context.read<ProfileBloc>().add(const LoadMe());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1832,20 +1597,27 @@ class _ProfilePageState extends State<ProfilePage>
|
||||
void _showSuccessSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: const Color(0xFF00B894),
|
||||
content: Text(
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Afficher un message d'erreur
|
||||
void _showErrorSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: const Color(0xFFE74C3C),
|
||||
content: Text(
|
||||
message.toUpperCase(),
|
||||
style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ library profile_page_wrapper;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../bloc/profile_bloc.dart';
|
||||
import 'profile_page.dart';
|
||||
|
||||
@@ -13,7 +13,7 @@ class ProfilePageWrapper extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ProfileBloc>(
|
||||
create: (_) => GetIt.instance<ProfileBloc>(),
|
||||
create: (_) => sl<ProfileBloc>(),
|
||||
child: const ProfilePage(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user