Files
wortis_tpe/lib/pages/dashboard.dart

2696 lines
89 KiB
Dart

// ===== 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<DashboardPage>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
static const String apiBaseUrl = '$baseUrl';
static const Map<String, String> 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<String, dynamic>? _balanceData;
Map<String, dynamic>? _statsData;
Map<String, dynamic>? _enterpriseData;
List<Map<String, dynamic>> _recentTransactions = [];
List<Map<String, dynamic>> _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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_slideAnimation = Tween<Offset>(
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<bool> _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<Map<String, dynamic>?> _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<Map<String, dynamic>?> _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<String, dynamic> _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<List<Map<String, dynamic>>> _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<Map<String, dynamic>>.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<Map<String, dynamic>?> _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<List<Map<String, dynamic>>> _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<Map<String, dynamic>>.from(data['members'] ?? []);
} else {
return [];
}
} else {
return [];
}
} catch (e) {
print('❌ Erreur récupération membres: $e');
return [];
}
}
Future<Map<String, dynamic>?> _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<void> _loadDashboardData() async {
final authController = Provider.of<AuthController>(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<String, dynamic>? enterpriseData;
List<Map<String, dynamic>> 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<void> _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<AuthController>(
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<AuthController>(
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<Color>(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<AuthController>(
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<AuthController>(
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<int>(
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>(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<String, dynamic> 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<Color>(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]),
),
],
),
),
),
);
}
}