// ===== lib/pages/dashboard.dart - VERSION COMPLÈTE AVEC SUPPORT ENTREPRISE ===== import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import '../main.dart'; import '../widgets/wortis_logo.dart'; import 'login.dart'; import '../widgets/responsive_helper.dart'; class DashboardPage extends StatefulWidget { final bool showAppBar; const DashboardPage({super.key, this.showAppBar = true}); @override _DashboardPageState createState() => _DashboardPageState(); } class _DashboardPageState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _fadeAnimation; late Animation _slideAnimation; static const String baseUrl = 'https://api.live.wortis.cg/tpe'; static const String apiBaseUrl = '$baseUrl'; static const Map apiHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; // Couleurs static const Color primaryColor = Color(0xFF006699); static const Color secondaryColor = Color(0xFF0088CC); static const Color accentColor = Color(0xFFFF6B35); static const Color backgroundColor = Color(0xFFF8FAFC); static const Color surfaceColor = Colors.white; static const Color successColor = Color(0xFF38A169); static const Color warningColor = Color(0xFFF59E0B); static const Color dangerColor = Color(0xFFEF4444); static const Color enterpriseColor = Color( 0xFF8B5CF6, ); // Violet pour entreprise bool _isLoading = false; bool _hasError = false; String _errorMessage = ''; DateTime? _lastUpdate; Map? _balanceData; Map? _statsData; Map? _enterpriseData; List> _recentTransactions = []; List> _enterpriseMembers = []; @override void initState() { super.initState(); _initializeOrientations(); _setupAnimations(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadDashboardData(); }); } void _initializeOrientations() { WidgetsBinding.instance.addPostFrameCallback((_) { ResponsiveHelper.initializeOrientations(context); }); } void _setupAnimations() { _animationController = AnimationController( duration: Duration(milliseconds: 1200), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _slideAnimation = Tween( begin: Offset(0, 0.2), end: Offset.zero, ).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); _animationController.forward(); } @override void dispose() { _animationController.dispose(); super.dispose(); } // ===== NOUVELLES MÉTHODES API ===== Future _checkApiHealth() async { try { final response = await http .get(Uri.parse('$apiBaseUrl/health'), headers: apiHeaders) .timeout(Duration(seconds: 5)); print('🏥 Health check: ${response.statusCode}'); return response.statusCode == 200; } catch (e) { print('❌ Erreur santé API: $e'); return false; } } Future?> _getAgentBalance(String agentId) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/agent/$agentId/balance'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('💰 Balance response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); print('📊 Balance data: $data'); if (data['success']) { return data; } else { throw Exception(data['message'] ?? 'Erreur récupération solde'); } } else { throw Exception('Erreur HTTP ${response.statusCode}'); } } catch (e) { print('❌ Erreur récupération solde: $e'); rethrow; } } Future?> _getAgentStats(String agentId) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/agent/$agentId/stats'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('📊 Stats response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success']) { return data; } else { print('⚠️ Stats API success=false: ${data['message']}'); return _getEmptyStats(); } } else { throw Exception('Erreur HTTP stats ${response.statusCode}'); } } catch (e) { print('❌ Erreur récupération stats: $e'); return _getEmptyStats(); } } Map _getEmptyStats() { return { 'success': true, 'stats': { 'solde_actuel': 0.0, 'statistiques_jour': { 'total_transactions': 0, 'total_montant': 0.0, 'total_commissions': 0.0, }, 'statistiques_mois': { 'total_transactions': 0, 'total_montant': 0.0, 'total_commissions': 0.0, }, 'services_favoris': [], }, }; } Future>> _getAgentTransactions( String agentId, { int limit = 5, }) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/agent/$agentId/transactions?limit=$limit'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('📝 Transactions response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success']) { return List>.from(data['transactions'] ?? []); } else { print('⚠️ Transactions API success=false: ${data['message']}'); return []; } } else { throw Exception('Erreur HTTP transactions ${response.statusCode}'); } } catch (e) { print('❌ Erreur récupération transactions: $e'); return []; } } // ===== NOUVELLES MÉTHODES ENTREPRISE ===== Future?> _getEnterpriseBalance( String enterpriseId, ) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/enterprise/$enterpriseId/balance'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('🏢 Enterprise balance response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success']) { return data; } else { throw Exception( data['message'] ?? 'Erreur récupération solde entreprise', ); } } else { throw Exception('Erreur HTTP ${response.statusCode}'); } } catch (e) { print('❌ Erreur récupération solde entreprise: $e'); return null; } } Future>> _getEnterpriseMembers( String enterpriseId, ) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/enterprise/$enterpriseId/members'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('👥 Enterprise members response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success']) { return List>.from(data['members'] ?? []); } else { return []; } } else { return []; } } catch (e) { print('❌ Erreur récupération membres: $e'); return []; } } Future?> _getMemberInfo(String agentId) async { try { final response = await http .get( Uri.parse('$apiBaseUrl/member/$agentId/info'), headers: apiHeaders, ) .timeout(Duration(seconds: 10)); print('👤 Member info response: ${response.statusCode}'); if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['success']) { return data; } } return null; } catch (e) { print('❌ Erreur récupération info membre: $e'); return null; } } // ===== CHARGEMENT DES DONNÉES ===== Future _loadDashboardData() async { final authController = Provider.of(context, listen: false); final agentId = authController.agentId; if (agentId == null) { if (mounted) { setState(() { _hasError = true; _errorMessage = 'Agent ID non disponible'; }); } return; } if (mounted) { setState(() { _isLoading = true; _hasError = false; _errorMessage = ''; }); } try { // Vérifier la santé de l'API final isApiHealthy = await _checkApiHealth(); if (!isApiHealthy) { throw Exception( 'API non disponible. Vérifiez que le serveur fonctionne sur $baseUrl', ); } print('🔄 Chargement des données pour agent: $agentId'); // Charger les données de base final balanceData = await _getAgentBalance(agentId); final statsData = await _getAgentStats(agentId); final transactions = await _getAgentTransactions(agentId, limit: 5); // Charger les données entreprise si membre Map? enterpriseData; List> enterpriseMembers = []; if (balanceData != null) { final accountType = balanceData['type'] ?? 'agent'; final enterpriseId = balanceData['enterprise_id']; print('📋 Account type: $accountType'); print('🏢 Enterprise ID: $enterpriseId'); if (accountType == 'enterprise_member' && enterpriseId != null) { print('🔍 Chargement données entreprise: $enterpriseId'); // Charger le solde entreprise enterpriseData = await _getEnterpriseBalance(enterpriseId); // Charger les membres si admin if (balanceData['is_admin_entreprise'] == true) { enterpriseMembers = await _getEnterpriseMembers(enterpriseId); } } } if (mounted) { setState(() { _balanceData = balanceData; _statsData = statsData; _enterpriseData = enterpriseData; _recentTransactions = transactions; _enterpriseMembers = enterpriseMembers; _lastUpdate = DateTime.now(); _isLoading = false; _hasError = false; }); } // Mettre à jour le solde dans AuthController _updateAuthControllerBalance(authController); print('✅ Données dashboard chargées avec succès'); _debugApiData(); } catch (e) { if (mounted) { setState(() { _isLoading = false; _hasError = true; _errorMessage = e.toString().replaceAll('Exception: ', ''); }); } print('❌ Erreur chargement dashboard: $e'); } } void _updateAuthControllerBalance(AuthController authController) { if (_balanceData == null) return; final accountType = _balanceData!['type'] ?? 'agent'; if (accountType == 'enterprise_member') { // Utiliser le solde entreprise if (_enterpriseData != null && _enterpriseData!['solde_entreprise'] != null) { final enterpriseBalance = (_enterpriseData!['solde_entreprise'] as num).toDouble(); authController.updateBalance(enterpriseBalance); print('💰 Solde entreprise mis à jour: $enterpriseBalance FCFA'); } else if (_balanceData!['enterprise'] != null && _balanceData!['enterprise']['solde_entreprise'] != null) { final enterpriseBalance = (_balanceData!['enterprise']['solde_entreprise'] as num).toDouble(); authController.updateBalance(enterpriseBalance); print( '💰 Solde entreprise mis à jour (depuis balance): $enterpriseBalance FCFA', ); } } else { // Utiliser le solde personnel if (_balanceData!['solde'] != null) { final personalBalance = (_balanceData!['solde'] as num).toDouble(); authController.updateBalance(personalBalance); print('💰 Solde personnel mis à jour: $personalBalance FCFA'); } } } Future _refreshDashboard() async { await _loadDashboardData(); } void _debugApiData() { print('🔍 ========== DEBUG COMPLET =========='); print('📊 Balance Data: $_balanceData'); print('📈 Stats Data: $_statsData'); print('🏢 Enterprise Data: $_enterpriseData'); print('📝 Transactions: ${_recentTransactions.length}'); print('👥 Members: ${_enterpriseMembers.length}'); print('❌ Has Error: $_hasError'); print('⏳ Is Loading: $_isLoading'); print('====================================='); } // ===== ACTIONS UTILISATEUR ===== void _logout() async { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon(Icons.logout, color: primaryColor), SizedBox(width: 12), Text('Déconnexion'), ], ), content: Text('Êtes-vous sûr de vouloir vous déconnecter ?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('Annuler'), ), ElevatedButton( onPressed: () async { Navigator.of(context).pop(); final authController = Provider.of( context, listen: false, ); await authController.logout(); Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (context) => LoginPage()), ); }, style: ElevatedButton.styleFrom( backgroundColor: dangerColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text('Déconnexion', style: TextStyle(color: Colors.white)), ), ], ); }, ); } void _showApiStatus() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon(Icons.api, color: primaryColor), SizedBox(width: 12), Text('État de l\'API'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow('URL', apiBaseUrl), SizedBox(height: 8), _buildInfoRow( 'Dernière MAJ', _lastUpdate?.toString() ?? 'Jamais', ), SizedBox(height: 8), Row( children: [ Icon( _hasError ? Icons.error : Icons.check_circle, color: _hasError ? dangerColor : successColor, ), SizedBox(width: 8), Text(_hasError ? 'Erreur' : 'Connecté'), ], ), if (_hasError) ...[ SizedBox(height: 8), Text( 'Erreur: $_errorMessage', style: TextStyle(color: dangerColor), ), ], ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('OK'), ), ElevatedButton( onPressed: () { Navigator.of(context).pop(); _loadDashboardData(); }, style: ElevatedButton.styleFrom(backgroundColor: primaryColor), child: Text('Retester', style: TextStyle(color: Colors.white)), ), ], ); }, ); } Widget _buildInfoRow(String label, String value) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$label: ', style: TextStyle(fontWeight: FontWeight.bold)), Expanded(child: Text(value, style: TextStyle(color: Colors.grey[600]))), ], ); } void _showEnterpriseDetails() { if (_enterpriseData == null) return; showDialog( context: context, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Row( children: [ Icon(Icons.business, color: enterpriseColor), SizedBox(width: 12), Expanded( child: Text( _enterpriseData!['nom_entreprise'] ?? 'Entreprise', style: TextStyle(fontSize: 18), ), ), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDetailRow( 'ID Entreprise', _enterpriseData!['enterprise_id'], ), _buildDetailRow( 'Solde', '${_formatMoney(_enterpriseData!['solde_entreprise'])} FCFA', ), _buildDetailRow( 'Commission', '${_formatMoney(_enterpriseData!['commission_entreprise'])} FCFA', ), _buildDetailRow( 'Membres', '${_enterpriseData!['total_members'] ?? 0}', ), _buildDetailRow( 'Transactions', '${_enterpriseData!['statistics']?['total_transactions'] ?? 0}', ), _buildDetailRow( 'Volume Total', '${_formatMoney(_enterpriseData!['statistics']?['total_volume'])} FCFA', ), _buildDetailRow( 'Statut', _enterpriseData!['status'] ?? 'active', ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('Fermer'), ), ], ); }, ); } Widget _buildDetailRow(String label, dynamic value) { return Padding( padding: EdgeInsets.symmetric(vertical: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$label: ', style: TextStyle(fontWeight: FontWeight.w600)), Expanded( child: Text( value.toString(), style: TextStyle(color: Colors.grey[700]), ), ), ], ), ); } void _showEnterpriseMembers() { if (_enterpriseMembers.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Aucun membre à afficher'))); return; } showDialog( context: context, builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), title: Text( 'Membres de l\'entreprise (${_enterpriseMembers.length})', ), content: Container( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: _enterpriseMembers.length, itemBuilder: (context, index) { final member = _enterpriseMembers[index]; return ListTile( leading: CircleAvatar( backgroundColor: primaryColor.withOpacity(0.1), child: Icon(Icons.person, color: primaryColor), ), title: Text(member['nom'] ?? 'N/A'), subtitle: Text(member['agent_id'] ?? 'N/A'), trailing: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( member['role_dans_entreprise'] ?? 'Membre', style: TextStyle(fontSize: 10), ), if (member['is_admin_entreprise'] == true) Container( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: enterpriseColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( 'Admin', style: TextStyle( fontSize: 9, color: enterpriseColor, fontWeight: FontWeight.bold, ), ), ), ], ), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('Fermer'), ), ], ); }, ); } String _formatMoney(dynamic value) { if (value == null) return '0'; final amount = (value is num) ? value.toDouble() : double.tryParse(value.toString()) ?? 0.0; return amount .toStringAsFixed(0) .replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]} ', ); } // ===== BUILD UI ===== @override Widget build(BuildContext context) { return Scaffold( backgroundColor: backgroundColor, appBar: widget.showAppBar ? _buildAppBar() : null, body: _isLoading && _balanceData == null ? _buildLoadingScreen() : _hasError && _balanceData == null ? _buildErrorScreen() : RefreshIndicator( onRefresh: _refreshDashboard, child: FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: SingleChildScrollView( physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.fromLTRB( 16, widget.showAppBar ? 16 : 60, 16, widget.showAppBar ? 16 : 120, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildWelcomeHeader(), SizedBox(height: 24), if (_hasError) _buildErrorBanner(), _buildFinancialSummary(), SizedBox(height: 24), _buildPerformanceOverview(), SizedBox(height: 24), _buildSalesAnalytics(), SizedBox(height: 24), if (_enterpriseData != null) ...[ _buildEnterpriseSection(), SizedBox(height: 24), ], _buildRecentActivity(), SizedBox(height: 24), if (_lastUpdate != null) _buildLastUpdateInfo(), ], ), ), ), ), ), ); } // ===== APP BAR ===== PreferredSizeWidget _buildAppBar() { return AppBar( title: Consumer( builder: (context, auth, child) { final isEnterprise = _balanceData?['type'] == 'enterprise_member'; return Row( children: [ Text('Dashboard Wortis'), if (isEnterprise) ...[ SizedBox(width: 8), Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: enterpriseColor.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.business, size: 12, color: Colors.white), SizedBox(width: 4), Text( 'Entreprise', style: TextStyle(fontSize: 10, color: Colors.white), ), ], ), ), ], ], ); }, ), automaticallyImplyLeading: false, actions: [ if (_enterpriseData != null) IconButton( icon: Icon(Icons.business_center), onPressed: _showEnterpriseDetails, tooltip: 'Détails entreprise', ), if (_enterpriseMembers.isNotEmpty) IconButton( icon: Badge( label: Text('${_enterpriseMembers.length}'), child: Icon(Icons.group), ), onPressed: _showEnterpriseMembers, tooltip: 'Membres', ), IconButton( icon: _isLoading ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Icon(Icons.refresh), onPressed: _isLoading ? null : _refreshDashboard, ), IconButton(icon: Icon(Icons.info_outline), onPressed: _showApiStatus), IconButton(icon: Icon(Icons.logout), onPressed: _logout), ], ); } // ===== WELCOME HEADER ===== Widget _buildWelcomeHeader() { return Consumer( builder: (context, authController, child) { final screenWidth = MediaQuery.of(context).size.width; final isTablet = screenWidth > 600; final isEnterprise = _balanceData?['type'] == 'enterprise_member'; final agentName = _balanceData?['nom'] ?? authController.agentName ?? 'Agent'; final enterpriseName = _enterpriseData?['nom_entreprise'] ?? _balanceData?['enterprise']?['nom_entreprise']; return Container( width: double.infinity, padding: EdgeInsets.all(isTablet ? 24 : 20), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isEnterprise ? [enterpriseColor, enterpriseColor.withOpacity(0.7)] : [primaryColor, secondaryColor], ), borderRadius: BorderRadius.circular(isTablet ? 24 : 20), boxShadow: [ BoxShadow( color: (isEnterprise ? enterpriseColor : primaryColor) .withOpacity(0.3), blurRadius: 15, offset: Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ WortisLogoWidget( size: isTablet ? 60 : 50, isWhite: true, withShadow: false, ), SizedBox(width: isTablet ? 16 : 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Badge type de compte Container( padding: EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isEnterprise ? Icons.business : Icons.person, color: Colors.white, size: 12, ), SizedBox(width: 4), Text( isEnterprise ? 'Compte Entreprise' : 'Agent Individuel', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600, ), ), ], ), ), SizedBox(height: 8), // Nom de l'agent Text( agentName, style: TextStyle( color: Colors.white, fontSize: isTablet ? 24 : 20, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), // Nom de l'entreprise si membre if (isEnterprise && enterpriseName != null) ...[ SizedBox(height: 4), Row( children: [ Icon( Icons.apartment, color: Colors.white.withOpacity(0.8), size: 14, ), SizedBox(width: 6), Expanded( child: Text( enterpriseName, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: isTablet ? 14 : 12, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ], ), ), ], ), SizedBox(height: isTablet ? 16 : 12), // Statut de connexion Row( children: [ Icon( _hasError ? Icons.cloud_off : Icons.cloud_done, color: Colors.white.withOpacity(0.8), size: isTablet ? 16 : 14, ), SizedBox(width: isTablet ? 6 : 4), Expanded( child: Text( _getStatusText(), style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: isTablet ? 12 : 11, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ); }, ); } String _getStatusText() { if (_hasError) return 'Mode hors ligne'; if (_isLoading) return 'Synchronisation...'; if (_lastUpdate != null) { final diff = DateTime.now().difference(_lastUpdate!); if (diff.inMinutes < 1) { return 'Données actualisées'; } else if (diff.inMinutes < 60) { return 'MAJ il y a ${diff.inMinutes} min'; } else { return 'MAJ il y a ${diff.inHours}h'; } } return 'En attente de données'; } // ===== FINANCIAL SUMMARY ===== Widget _buildFinancialSummary() { final isTablet = context.isTablet; final isSmallScreen = context.isSmallPhone; return Consumer( builder: (context, authController, child) { final isEnterprise = _balanceData?['type'] == 'enterprise_member'; // Déterminer le solde à afficher double solde; if (isEnterprise && _enterpriseData != null) { solde = (_enterpriseData!['solde_entreprise'] as num?)?.toDouble() ?? 0.0; } else if (isEnterprise && _balanceData?['enterprise']?['solde_entreprise'] != null) { solde = (_balanceData!['enterprise']['solde_entreprise'] as num) .toDouble(); } else { solde = (_balanceData?['solde'] as num?)?.toDouble() ?? 0.0; } final agentName = _balanceData?['nom'] ?? authController.agentName ?? 'Agent'; final agentId = _balanceData?['agent_id'] ?? authController.agentId ?? 'N/A'; final stats = _statsData?['stats']; final statsMois = stats?['statistiques_mois'] ?? {}; final volumeTotal = (statsMois['total_montant'] ?? 0.0).toDouble(); final commissionsTotal = (statsMois['total_commissions'] ?? 0.0).toDouble(); final totalTransactions = statsMois['total_transactions'] ?? 0; return Container( width: double.infinity, padding: EdgeInsets.all(isTablet ? 28 : (isSmallScreen ? 16 : 20)), decoration: BoxDecoration( gradient: isEnterprise ? LinearGradient( colors: [enterpriseColor.withOpacity(0.05), surfaceColor], ) : null, color: isEnterprise ? null : surfaceColor, borderRadius: BorderRadius.circular(20), border: isEnterprise ? Border.all(color: enterpriseColor.withOpacity(0.2)) : null, boxShadow: [ BoxShadow( color: (isEnterprise ? enterpriseColor : primaryColor) .withOpacity(0.08), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête avec badge Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Résumé Financier', style: TextStyle( fontSize: context.responsiveFontSize( phone: 16, tablet: 20, ), fontWeight: FontWeight.bold, color: Colors.black87, ), ), SizedBox(width: 8), if (isEnterprise) Container( padding: EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: enterpriseColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: enterpriseColor.withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.business, color: enterpriseColor, size: 12, ), SizedBox(width: 4), Text( 'Entreprise', style: TextStyle( color: enterpriseColor, fontSize: 10, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), SizedBox(height: 2), Text( 'Agent: $agentName', style: TextStyle( fontSize: isTablet ? 15 : (isSmallScreen ? 12 : 14), color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), if (isEnterprise && _enterpriseData?['nom_entreprise'] != null) ...[ SizedBox(height: 2), Row( children: [ Icon( Icons.apartment, size: 12, color: enterpriseColor, ), SizedBox(width: 4), Expanded( child: Text( _enterpriseData!['nom_entreprise'], style: TextStyle( fontSize: isSmallScreen ? 11 : 12, color: enterpriseColor, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), ], ), ], ], ), ), if (!_hasError && !isSmallScreen) Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: successColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, size: 12, color: successColor, ), SizedBox(width: 4), Text( 'En ligne', style: TextStyle( color: successColor, fontSize: 10, fontWeight: FontWeight.w600, ), ), ], ), ), ], ), SizedBox(height: isTablet ? 24 : (isSmallScreen ? 16 : 20)), // Solde principal LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth; final soldeText = solde.toStringAsFixed(0); double fontSize; if (availableWidth < 250) { fontSize = 24; } else if (availableWidth < 350) { fontSize = 32; } else if (isTablet) { fontSize = 48; } else { fontSize = 36; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _formatMoney(solde), style: TextStyle( fontSize: fontSize, fontWeight: FontWeight.bold, color: isEnterprise ? enterpriseColor : primaryColor, ), ), SizedBox(width: 8), Padding( padding: EdgeInsets.only(bottom: fontSize * 0.15), child: Text( 'FCFA', style: TextStyle( fontSize: fontSize * 0.4, fontWeight: FontWeight.w600, color: Colors.grey[600], ), ), ), ], ), ), SizedBox(height: 8), Row( children: [ Text( isEnterprise ? 'Solde entreprise' : 'Solde disponible', style: TextStyle( fontSize: isTablet ? 15 : (isSmallScreen ? 12 : 13), color: Colors.grey[600], ), ), Text( ' • Agent $agentId', style: TextStyle( fontSize: isTablet ? 15 : (isSmallScreen ? 12 : 13), color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ], ), ], ); }, ), SizedBox(height: isTablet ? 24 : (isSmallScreen ? 16 : 20)), // Statistiques financières Container( padding: EdgeInsets.all( isTablet ? 18 : (isSmallScreen ? 12 : 14), ), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(12), ), child: _buildResponsiveFinancialStats( volumeTotal, commissionsTotal, totalTransactions, isSmallScreen, isTablet, ), ), ], ), ); }, ); } Widget _buildResponsiveFinancialStats( double volumeTotal, double commissionsTotal, int totalTransactions, bool isSmallScreen, bool isTablet, ) { final screenWidth = MediaQuery.of(context).size.width; if (isSmallScreen) { return Column( children: [ _buildFinancialItemCompact( 'Volume mensuel', '${(volumeTotal / 1000).toStringAsFixed(0)}k FCFA', primaryColor, isSmallScreen: true, ), SizedBox(height: 8), Row( children: [ Expanded( child: _buildFinancialItemCompact( 'Commissions', '${commissionsTotal.toStringAsFixed(0)} FCFA', accentColor, isSmallScreen: true, ), ), SizedBox(width: 8), Expanded( child: _buildFinancialItemCompact( 'Transactions', '$totalTransactions', successColor, isSmallScreen: true, ), ), ], ), SizedBox(height: 8), _buildFinancialItemCompact( 'Taux commission', '${volumeTotal > 0 ? (commissionsTotal / volumeTotal * 100).toStringAsFixed(2) : '0.00'}%', warningColor, isSmallScreen: true, ), ], ); } if (screenWidth < 500) { return Column( children: [ Row( children: [ Expanded( child: _buildFinancialItemCompact( 'Volume mensuel', '${(volumeTotal / 1000).toStringAsFixed(0)}k FCFA', primaryColor, ), ), SizedBox(width: 8), Expanded( child: _buildFinancialItemCompact( 'Commissions', '${commissionsTotal.toStringAsFixed(0)} FCFA', accentColor, ), ), ], ), SizedBox(height: 12), Row( children: [ Expanded( child: _buildFinancialItemCompact( 'Transactions', '$totalTransactions', successColor, ), ), SizedBox(width: 8), Expanded( child: _buildFinancialItemCompact( 'Taux commission', '${volumeTotal > 0 ? (commissionsTotal / volumeTotal * 100).toStringAsFixed(2) : '0.00'}%', warningColor, ), ), ], ), ], ); } return Row( children: [ Expanded( child: _buildFinancialItemCompact( 'Volume mensuel', '${(volumeTotal / 1000).toStringAsFixed(0)}k FCFA', primaryColor, isTablet: isTablet, ), ), SizedBox(width: isTablet ? 16 : 8), Expanded( child: _buildFinancialItemCompact( 'Commissions', '${commissionsTotal.toStringAsFixed(0)} FCFA', accentColor, isTablet: isTablet, ), ), SizedBox(width: isTablet ? 16 : 8), Expanded( child: _buildFinancialItemCompact( 'Transactions', '$totalTransactions', successColor, isTablet: isTablet, ), ), SizedBox(width: isTablet ? 16 : 8), Expanded( child: _buildFinancialItemCompact( 'Taux commission', '${volumeTotal > 0 ? (commissionsTotal / volumeTotal * 100).toStringAsFixed(2) : '0.00'}%', warningColor, isTablet: isTablet, ), ), ], ); } Widget _buildFinancialItemCompact( String label, String value, Color color, { bool isSmallScreen = false, bool isTablet = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( label, style: TextStyle( fontSize: isSmallScreen ? 9 : (isTablet ? 12 : 10), color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ), SizedBox(height: 2), FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( value, style: TextStyle( fontSize: isSmallScreen ? 12 : (isTablet ? 16 : 13), fontWeight: FontWeight.bold, color: color, ), ), ), ], ); } // ===== PERFORMANCE OVERVIEW ===== Widget _buildPerformanceOverview() { final screenWidth = MediaQuery.of(context).size.width; final isTablet = screenWidth > 600; final stats = _statsData?['stats']; final statsMois = stats?['statistiques_mois'] ?? {}; final ventesMois = statsMois['total_transactions'] ?? 0; final revenuseMois = (statsMois['total_montant'] ?? 0.0).toDouble(); final commissionsMois = (statsMois['total_commissions'] ?? 0.0).toDouble(); final objectifMensuel = 1000000.0; final objectifPourcent = revenuseMois > 0 ? ((revenuseMois / objectifMensuel) * 100).clamp(0.0, 100.0) : 0.0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Performances Mensuelles', style: TextStyle( fontSize: isTablet ? 22 : 20, fontWeight: FontWeight.bold, color: Colors.black87, ), ), Container( padding: EdgeInsets.symmetric( horizontal: isTablet ? 10 : 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue[200]!), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.calendar_month, size: 12, color: Colors.blue[600]), SizedBox(width: 4), Text( '${DateTime.now().month.toString().padLeft(2, '0')}/${DateTime.now().year}', style: TextStyle( fontSize: 10, color: Colors.blue[600], fontWeight: FontWeight.w600, ), ), ], ), ), ], ), SizedBox(height: 16), isTablet ? _buildTabletPerformanceCards( ventesMois, revenuseMois, commissionsMois, objectifPourcent, ) : _buildMobilePerformanceCards( ventesMois, revenuseMois, commissionsMois, objectifPourcent, ), ], ); } Widget _buildTabletPerformanceCards( int ventes, double revenus, double commissions, double objectif, ) { return Row( children: [ Expanded( child: _buildPerformanceCard( 'Transactions', '$ventes', ventes > 0 ? '$ventes transaction${ventes > 1 ? 's' : ''}' : 'Aucune transaction', ventes > 0 ? successColor : Colors.grey, Icons.trending_up, ), ), SizedBox(width: 16), Expanded( child: _buildPerformanceCard( 'Volume traité', '${(revenus / 1000).toStringAsFixed(0)}k FCFA', revenus > 0 ? 'Volume mensuel' : 'Pas de volume', revenus > 0 ? primaryColor : Colors.grey, Icons.monetization_on, ), ), SizedBox(width: 16), Expanded( child: _buildPerformanceCard( 'Commissions', '${commissions.toStringAsFixed(0)} FCFA', commissions > 0 ? '${(commissions / revenus * 100).toStringAsFixed(1)}% du volume' : 'Aucune commission', commissions > 0 ? accentColor : Colors.grey, Icons.account_balance_wallet, ), ), SizedBox(width: 16), Expanded( child: _buildPerformanceCard( 'Objectif mensuel', '${objectif.toStringAsFixed(1)}%', revenus > 0 ? '${(revenus / 1000).toStringAsFixed(0)}k/1000k' : 'Objectif: 1M FCFA', objectif > 80 ? successColor : objectif > 50 ? warningColor : Colors.grey, Icons.track_changes, ), ), ], ); } Widget _buildMobilePerformanceCards( int ventes, double revenus, double commissions, double objectif, ) { return Column( children: [ Row( children: [ Expanded( child: _buildPerformanceCard( 'Transactions', '$ventes', ventes > 0 ? '$ventes transaction${ventes > 1 ? 's' : ''}' : 'Aucune', ventes > 0 ? successColor : Colors.grey, Icons.trending_up, ), ), SizedBox(width: 12), Expanded( child: _buildPerformanceCard( 'Volume traité', '${(revenus / 1000).toStringAsFixed(0)}k', revenus > 0 ? 'FCFA' : 'Pas de volume', revenus > 0 ? primaryColor : Colors.grey, Icons.monetization_on, ), ), ], ), SizedBox(height: 12), Row( children: [ Expanded( child: _buildPerformanceCard( 'Commissions', commissions.toStringAsFixed(0), commissions > 0 ? 'FCFA gagné' : 'Aucune', commissions > 0 ? accentColor : Colors.grey, Icons.account_balance_wallet, ), ), SizedBox(width: 12), Expanded( child: _buildPerformanceCard( 'Objectif', '${objectif.toStringAsFixed(0)}%', revenus > 0 ? 'Atteint' : '1M FCFA', objectif > 80 ? successColor : objectif > 50 ? warningColor : Colors.grey, Icons.track_changes, ), ), ], ), ], ); } Widget _buildPerformanceCard( String title, String value, String trend, Color color, IconData icon, ) { final screenWidth = MediaQuery.of(context).size.width; final isTablet = screenWidth > 600; return Container( padding: EdgeInsets.all(isTablet ? 20 : 16), decoration: BoxDecoration( color: surfaceColor, borderRadius: BorderRadius.circular(16), border: Border.all(color: color.withOpacity(0.1)), boxShadow: [ BoxShadow( color: color.withOpacity(0.08), blurRadius: 10, offset: Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon(icon, color: color, size: isTablet ? 24 : 22), if (isTablet) Flexible( child: Text( trend, style: TextStyle( fontSize: 12, color: color, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), SizedBox(height: 12), Text( value, style: TextStyle( fontSize: isTablet ? 20 : 16, fontWeight: FontWeight.bold, color: color, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), SizedBox(height: 4), Text( title, style: TextStyle( fontSize: isTablet ? 13 : 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (!isTablet) ...[ SizedBox(height: 4), Text( trend, style: TextStyle( fontSize: 10, color: color, fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), ); } // ===== SALES ANALYTICS ===== Widget _buildSalesAnalytics() { final services = _statsData?['stats']?['services_favoris'] as List? ?? []; return Container( padding: EdgeInsets.all(24), decoration: BoxDecoration( color: surfaceColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Analyse des Services', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), if (services.isNotEmpty) Text( '${services.length} service${services.length > 1 ? 's' : ''}', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), SizedBox(height: 20), if (services.isEmpty) _buildEmptyServicesAnalytics() else _buildServicesAnalytics(services), ], ), ); } Widget _buildEmptyServicesAnalytics() { return Column( children: [ Icon(Icons.analytics_outlined, size: 48, color: Colors.grey[400]), SizedBox(height: 12), Text( 'Aucune transaction encore', style: TextStyle( fontSize: 16, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), SizedBox(height: 8), Text( 'Vos statistiques apparaîtront ici après vos premières transactions', textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), ], ); } Widget _buildServicesAnalytics(List services) { final total = services.fold( 0, (sum, service) => sum + (service['count'] as int? ?? 0), ); return Row( children: services.take(3).map((service) { final count = service['count'] as int? ?? 0; final percentage = total > 0 ? (count / total) : 0.0; final serviceName = service['_id'] as String? ?? 'Service'; Color serviceColor = primaryColor; if (serviceName.toLowerCase().contains('internet')) { serviceColor = primaryColor; } else if (serviceName.toLowerCase().contains('électricité') || serviceName.toLowerCase().contains('e2c') || serviceName.toLowerCase().contains('pelisa')) { serviceColor = accentColor; } else { serviceColor = successColor; } return Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: 8), child: _buildSalesItem( serviceName, '${(percentage * 100).toStringAsFixed(0)}%', serviceColor, percentage, ), ), ); }).toList(), ); } Widget _buildSalesItem( String service, String percentage, Color color, double progress, ) { return Column( children: [ Text( service, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), SizedBox(height: 12), Stack( alignment: Alignment.center, children: [ SizedBox( width: 60, height: 60, child: CircularProgressIndicator( value: progress, strokeWidth: 6, backgroundColor: color.withOpacity(0.1), valueColor: AlwaysStoppedAnimation(color), ), ), Text( percentage, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: color, ), ), ], ), ], ); } // ===== ENTERPRISE SECTION (NOUVEAU) ===== Widget _buildEnterpriseSection() { if (_enterpriseData == null) return SizedBox.shrink(); final stats = _enterpriseData!['statistics'] ?? {}; final totalMembers = _enterpriseData!['total_members'] ?? 0; final totalTransactions = stats['total_transactions'] ?? 0; final totalVolume = (stats['total_volume'] ?? 0.0).toDouble(); final totalCommissions = (stats['total_commissions'] ?? 0.0).toDouble(); final dailyTransactions = stats['daily_transactions'] ?? 0; final isAdmin = _balanceData?['is_admin_entreprise'] == true; return Container( padding: EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [enterpriseColor.withOpacity(0.05), surfaceColor], ), borderRadius: BorderRadius.circular(20), border: Border.all(color: enterpriseColor.withOpacity(0.2)), boxShadow: [ BoxShadow( color: enterpriseColor.withOpacity(0.08), blurRadius: 15, offset: Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ Icon(Icons.business, color: enterpriseColor, size: 24), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _enterpriseData!['nom_entreprise'] ?? 'Entreprise', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: enterpriseColor, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( 'ID: ${_enterpriseData!['enterprise_id']}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ), ], ), ), if (isAdmin) Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: enterpriseColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( 'ADMIN', style: TextStyle( fontSize: 10, color: enterpriseColor, fontWeight: FontWeight.bold, ), ), ), ], ), SizedBox(height: 20), // Stats entreprise Row( children: [ Expanded( child: _buildEnterpriseStatCard( 'Membres', '$totalMembers', Icons.group, enterpriseColor, ), ), SizedBox(width: 12), Expanded( child: _buildEnterpriseStatCard( 'Transactions', '$totalTransactions', Icons.receipt_long, primaryColor, ), ), ], ), SizedBox(height: 12), Row( children: [ Expanded( child: _buildEnterpriseStatCard( 'Volume Total', '${(totalVolume / 1000).toStringAsFixed(0)}k', Icons.trending_up, successColor, ), ), SizedBox(width: 12), Expanded( child: _buildEnterpriseStatCard( 'Commissions', '${totalCommissions.toStringAsFixed(0)}', Icons.account_balance_wallet, accentColor, ), ), ], ), if (dailyTransactions > 0) ...[ SizedBox(height: 12), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: successColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.today, size: 16, color: successColor), SizedBox(width: 8), Text( '$dailyTransactions transaction${dailyTransactions > 1 ? 's' : ''} aujourd\'hui', style: TextStyle( fontSize: 13, color: successColor, fontWeight: FontWeight.w600, ), ), ], ), ), ], // Boutons d'action if (isAdmin && _enterpriseMembers.isNotEmpty) ...[ SizedBox(height: 16), Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: _showEnterpriseMembers, icon: Icon(Icons.group, size: 18), label: Text( 'Voir les membres (${_enterpriseMembers.length})', ), style: ElevatedButton.styleFrom( backgroundColor: enterpriseColor, foregroundColor: Colors.white, padding: EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), SizedBox(width: 8), ElevatedButton( onPressed: _showEnterpriseDetails, child: Icon(Icons.info_outline, size: 18), style: ElevatedButton.styleFrom( backgroundColor: enterpriseColor.withOpacity(0.1), foregroundColor: enterpriseColor, padding: EdgeInsets.all(12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ], ), ], ], ), ); } Widget _buildEnterpriseStatCard( String label, String value, IconData icon, Color color, ) { return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: surfaceColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 16, color: color), Spacer(), Text( value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: color, ), ), ], ), SizedBox(height: 4), Text( label, style: TextStyle( fontSize: 11, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), ], ), ); } // ===== RECENT ACTIVITY ===== Widget _buildRecentActivity() { final isSmallScreen = context.isSmallPhone; final isTablet = context.isTablet; return Container( padding: EdgeInsets.all(isTablet ? 24 : (isSmallScreen ? 16 : 20)), decoration: BoxDecoration( color: surfaceColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 15, offset: Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( 'Activité Récente', style: TextStyle( fontSize: context.responsiveFontSize(phone: 16, tablet: 20), fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), if (!isSmallScreen) TextButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Historique complet - Bientôt disponible', ), ), ); }, child: Text('Voir tout', style: TextStyle(fontSize: 12)), ), ], ), SizedBox(height: 12), if (_recentTransactions.isEmpty) _buildEmptyTransactions() else ...[ Text( 'Dernières transactions (${_recentTransactions.length})', style: TextStyle( fontSize: isSmallScreen ? 10 : 12, color: Colors.grey[600], ), ), SizedBox(height: 8), ..._recentTransactions.map( (transaction) => _buildResponsiveActivityItem( transaction, isSmallScreen, isTablet, ), ), ], ], ), ); } Widget _buildResponsiveActivityItem( Map transaction, bool isSmallScreen, bool isTablet, ) { final serviceName = transaction['service_name'] as String? ?? 'Service inconnu'; final montantTotal = (transaction['montant_total_debite'] as num?)?.toDouble() ?? 0.0; final commission = (transaction['commission_agent'] as num?)?.toDouble() ?? 0.0; final clientRef = transaction['client_reference'] as String? ?? ''; final statut = transaction['status'] as String? ?? 'unknown'; final accountType = transaction['account_type'] as String?; final balanceSource = transaction['balance_source'] as String?; final dateStr = transaction['created_at'] is String ? transaction['created_at'] as String : transaction['created_at']?['\$date'] as String? ?? ''; DateTime? transactionDate; try { transactionDate = DateTime.parse(dateStr); } catch (e) { transactionDate = DateTime.now(); } final timeStr = '${transactionDate.hour.toString().padLeft(2, '0')}:${transactionDate.minute.toString().padLeft(2, '0')}'; final dayStr = '${transactionDate.day.toString().padLeft(2, '0')}/${transactionDate.month.toString().padLeft(2, '0')}'; Color color = primaryColor; IconData icon = Icons.wifi; if (statut == 'completed') { color = successColor; icon = Icons.check_circle; } else if (statut == 'pending') { color = warningColor; icon = Icons.pending; } else if (statut == 'failed') { color = dangerColor; icon = Icons.error; } final isEnterpriseTransaction = accountType == 'enterprise_member' || balanceSource == 'enterprise'; return Container( margin: EdgeInsets.symmetric(vertical: isSmallScreen ? 6 : 8), padding: EdgeInsets.all(isSmallScreen ? 12 : (isTablet ? 20 : 16)), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: isEnterpriseTransaction ? enterpriseColor.withOpacity(0.3) : color.withOpacity(0.2), ), ), child: Column( children: [ Row( children: [ Container( width: isSmallScreen ? 36 : (isTablet ? 50 : 42), height: isSmallScreen ? 36 : (isTablet ? 50 : 42), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( icon, color: color, size: isSmallScreen ? 18 : (isTablet ? 24 : 20), ), ), SizedBox(width: isSmallScreen ? 8 : 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( serviceName, style: TextStyle( fontSize: isSmallScreen ? 13 : (isTablet ? 16 : 14), fontWeight: FontWeight.bold, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (isEnterpriseTransaction) Container( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: enterpriseColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.business, size: 8, color: enterpriseColor, ), SizedBox(width: 2), Text( 'ENT', style: TextStyle( fontSize: 8, color: enterpriseColor, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), SizedBox(height: 2), Text( '$dayStr à $timeStr • Réf: $clientRef', style: TextStyle( fontSize: isSmallScreen ? 10 : (isTablet ? 12 : 11), color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), FittedBox( child: Text( '${montantTotal.toStringAsFixed(0)} FCFA', style: TextStyle( fontSize: isSmallScreen ? 12 : (isTablet ? 16 : 14), fontWeight: FontWeight.bold, color: color, ), ), ), ], ), if (!isSmallScreen) ...[ SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Container( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: successColor.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Text( 'Commission: +${commission.toStringAsFixed(0)} FCFA', style: TextStyle( fontSize: isTablet ? 12 : 10, color: successColor, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), ), SizedBox(width: 8), Container( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Text( statut.toUpperCase(), style: TextStyle( fontSize: isTablet ? 11 : 9, color: color, fontWeight: FontWeight.bold, ), ), ), ], ), ] else ...[ SizedBox(height: 4), Row( children: [ Icon(Icons.monetization_on, size: 12, color: successColor), SizedBox(width: 4), Text( '+${commission.toStringAsFixed(0)} FCFA', style: TextStyle( fontSize: 10, color: successColor, fontWeight: FontWeight.w600, ), ), Spacer(), Container( padding: EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( statut.toUpperCase(), style: TextStyle( fontSize: 8, color: color, fontWeight: FontWeight.bold, ), ), ), ], ), ], ], ), ); } Widget _buildEmptyTransactions() { return Padding( padding: EdgeInsets.symmetric(vertical: 32), child: Column( children: [ Icon(Icons.receipt_long_outlined, size: 48, color: Colors.grey[400]), SizedBox(height: 12), Text( 'Aucune transaction récente', style: TextStyle( fontSize: 16, color: Colors.grey[600], fontWeight: FontWeight.w500, ), ), SizedBox(height: 8), Text( 'Vos dernières transactions apparaîtront ici', style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), ], ), ); } // ===== UTILITY WIDGETS ===== Widget _buildLoadingScreen() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(primaryColor), ), SizedBox(height: 16), Text( 'Chargement des données...', style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), ], ), ); } Widget _buildErrorScreen() { return Center( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.cloud_off, size: 64, color: dangerColor.withOpacity(0.7), ), SizedBox(height: 16), Text( 'Impossible de charger les données', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: dangerColor, ), textAlign: TextAlign.center, ), SizedBox(height: 8), Text( _errorMessage, textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton.icon( onPressed: _loadDashboardData, icon: Icon(Icons.refresh), label: Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: primaryColor, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), SizedBox(width: 12), TextButton.icon( onPressed: _showApiStatus, icon: Icon(Icons.info_outline), label: Text('Détails'), ), ], ), ], ), ), ); } Widget _buildErrorBanner() { return Container( margin: EdgeInsets.only(bottom: 16), padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.orange[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.orange[200]!), ), child: Row( children: [ Icon(Icons.warning, color: Colors.orange[600], size: 20), SizedBox(width: 8), Expanded( child: Text( 'Données partiellement indisponibles - Mode hors ligne', style: TextStyle(color: Colors.orange[700], fontSize: 12), ), ), IconButton( onPressed: _loadDashboardData, icon: Icon(Icons.refresh, color: Colors.orange[600], size: 18), ), ], ), ); } Widget _buildLastUpdateInfo() { if (_lastUpdate == null) return SizedBox.shrink(); return Padding( padding: EdgeInsets.only(top: 16), child: Center( child: Container( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _hasError ? Icons.error_outline : Icons.check_circle_outline, size: 14, color: _hasError ? Colors.orange : successColor, ), SizedBox(width: 6), Text( 'Dernière MAJ: ${_lastUpdate!.hour.toString().padLeft(2, '0')}:${_lastUpdate!.minute.toString().padLeft(2, '0')}', style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), ), ), ); } }