2696 lines
89 KiB
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]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|