Files
wortis_tpe/lib/pages/admin_dashboard.dart

2833 lines
90 KiB
Dart
Raw Permalink Normal View History

2025-12-01 10:56:37 +01:00
// ===== lib/pages/admin_dashboard.dart =====
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import '../main.dart';
import '../widgets/wortis_logo.dart';
import 'login.dart';
// ===== CONFIGURATION API =====
const String baseUrl = 'https://api.live.wortis.cg/tpe';
const String apiBaseUrl = '$baseUrl/api';
const Map<String, String> apiHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
// ===== COULEURS GLOBALES =====
class AdminColors {
static const Color primary = Color(0xFFE53E3E);
static const Color secondary = Color(0xFF006699);
static const Color accent = Color(0xFFFF6B35);
static const Color background = Color(0xFFF8FAFC);
static const Color surface = Colors.white;
static const Color success = Color(0xFF38A169);
static const Color warning = Color(0xFFF59E0B);
static const Color error = Color(0xFFE53E3E);
static const Color info = Color(0xFF3182CE);
}
// ===== ADMIN DASHBOARD =====
class AdminDashboard extends StatefulWidget {
const AdminDashboard({super.key});
@override
State<AdminDashboard> createState() => _AdminDashboardState();
}
class _AdminDashboardState extends State<AdminDashboard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
bool _isLoading = false;
int _selectedIndex = 0;
// Données du dashboard
Map<String, dynamic> _systemStats = {};
List<Map<String, dynamic>> _recentActivities = [];
List<Map<String, dynamic>> _agents = [];
List<Map<String, dynamic>> _allRecharges = [];
List<Map<String, dynamic>> _allTransactions = [];
Map<String, dynamic> _rechargeStats = {};
Map<String, dynamic> _transactionStats = {};
@override
void initState() {
super.initState();
_setupAnimations();
_loadDashboardData();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _setupAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_animationController.forward();
}
// ===== MÉTHODES API =====
Future<void> _loadDashboardData() async {
setState(() {
_isLoading = true;
});
try {
await Future.wait([
_loadSystemStats(),
_loadRecentActivities(),
_loadAgents(),
_loadAllRecharges(),
_loadAllTransactions(), // Ajoutez cette ligne
_loadRechargeStats(), // Ajoutez cette ligne
_loadTransactionStats(), // Ajoutez cette ligne
]);
} catch (e) {
print('❌ Erreur chargement dashboard admin: $e');
_showSnackBar('Erreur lors du chargement des données', AdminColors.error);
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _loadSystemStats() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/system/stats'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true) {
setState(() {
_systemStats = data['stats'] ?? {};
});
}
}
} catch (e) {
print('❌ Erreur stats système: $e');
// Données par défaut en cas d'erreur
setState(() {
_systemStats = {
'active_agents': 247,
'daily_transactions': 1543,
'total_volume': 45200000.0,
'total_commissions': 2100000.0,
'success_rate': 98.5,
'pending_recharges': 12,
};
});
}
}
Future<void> _loadRecentActivities() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/activities/recent'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true) {
setState(() {
_recentActivities = List<Map<String, dynamic>>.from(
data['activities'] ?? [],
);
});
}
}
} catch (e) {
print('❌ Erreur activités récentes: $e');
// Données par défaut
setState(() {
_recentActivities = [
{
'type': 'agent_created',
'title': 'Nouvel agent créé',
'description': 'AGENT1245 - Jean Martin',
'icon': 'person_add',
'color': 'green',
'time': '2min',
'created_at':
DateTime.now()
.subtract(const Duration(minutes: 2))
.toIso8601String(),
},
{
'type': 'recharge_approved',
'title': 'Recharge validée',
'description': 'AGENT0987 - 50,000 F',
'icon': 'check_circle',
'color': 'blue',
'time': '5min',
'created_at':
DateTime.now()
.subtract(const Duration(minutes: 5))
.toIso8601String(),
},
];
});
}
}
Future<void> _loadAgents() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/agents'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true) {
setState(() {
_agents = List<Map<String, dynamic>>.from(data['agents'] ?? []);
});
}
}
} catch (e) {
print('❌ Erreur chargement agents: $e');
}
}
Future<void> _loadAllRecharges() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/recharge/get_recharges'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_allRecharges = List<Map<String, dynamic>>.from(
data['all_recharges'] ?? [],
);
});
}
} catch (e) {
print('❌ Erreur chargement recharges: $e');
}
}
Future<void> _loadAllTransactions() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/transactions'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_allTransactions = List<Map<String, dynamic>>.from(
data['transactions'] ?? [],
);
});
}
} catch (e) {
print('❌ Erreur chargement transactions: $e');
}
}
Future<void> _loadRechargeStats() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/recharges/stats'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_rechargeStats = data['stats'] ?? {};
});
}
} catch (e) {
print('❌ Erreur stats recharges: $e');
}
}
Future<void> _loadTransactionStats() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/transactions/stats'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_transactionStats = data['stats'] ?? {};
});
}
} catch (e) {
print('❌ Erreur stats transactions: $e');
}
}
Future<void> _createAgent(Map<String, String> agentData) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/admin/agents/create'),
headers: apiHeaders,
body: jsonEncode(agentData),
);
if (response.statusCode == 201) {
final data = jsonDecode(response.body);
if (data['success'] == true) {
_showSnackBar('Agent créé avec succès', AdminColors.success);
_loadAgents();
} else {
_showSnackBar(
data['message'] ?? 'Erreur lors de la création',
AdminColors.error,
);
}
} else {
_showSnackBar(
'Erreur lors de la création de l\'agent',
AdminColors.error,
);
}
} catch (e) {
print('❌ Erreur création agent: $e');
_showSnackBar('Erreur de connexion', AdminColors.error);
}
}
Future<void> _updateAgentStatus(String agentId, String status) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/admin/agents/$agentId/status'),
headers: apiHeaders,
body: jsonEncode({
'status': status,
'updated_by':
Provider.of<AuthController>(context, listen: false).agentId,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true) {
_showSnackBar('Statut agent mis à jour', AdminColors.success);
_loadAgents();
} else {
_showSnackBar(
data['message'] ?? 'Erreur mise à jour',
AdminColors.error,
);
}
}
} catch (e) {
print('❌ Erreur mise à jour statut: $e');
_showSnackBar('Erreur de connexion', AdminColors.error);
}
}
Future<void> _exportData(String type) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/admin/export/$type'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
_showSnackBar('Export $type généré avec succès', AdminColors.success);
} else {
_showSnackBar('Erreur lors de l\'export', AdminColors.error);
}
} catch (e) {
print('❌ Erreur export: $e');
_showSnackBar('Erreur de connexion', AdminColors.error);
}
}
// ===== MÉTHODES UI =====
void _logout() async {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.logout, color: AdminColors.primary),
SizedBox(width: 12),
Text('Déconnexion'),
],
),
content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AdminColors.primary,
foregroundColor: Colors.white,
),
child: const Text('Déconnexion'),
),
],
);
},
);
if (result == true) {
final authController = Provider.of<AuthController>(
context,
listen: false,
);
await authController.logout();
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
}
}
}
void _showSnackBar(String message, Color color) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: color,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
void _showCreateAgentDialog() {
final nameController = TextEditingController();
final phoneController = TextEditingController();
final emailController = TextEditingController();
final typeController = TextEditingController(text: 'agent');
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.person_add, color: AdminColors.secondary),
SizedBox(width: 8),
Text('Créer un agent'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Nom complet',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
const SizedBox(height: 16),
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Téléphone',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: typeController.text,
decoration: const InputDecoration(
labelText: 'Type d\'agent',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: const [
DropdownMenuItem(
value: 'agent',
child: Text('Agent standard'),
),
DropdownMenuItem(
value: 'rechargeur',
child: Text('Rechargeur'),
),
DropdownMenuItem(
value: 'admin',
child: Text('Administrateur'),
),
],
onChanged:
(value) => typeController.text = value ?? 'agent',
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_createAgent({
'nom': nameController.text,
'telephone': phoneController.text,
'email': emailController.text,
'type': typeController.text,
});
},
style: ElevatedButton.styleFrom(
backgroundColor: AdminColors.secondary,
foregroundColor: Colors.white,
),
child: const Text('Créer'),
),
],
),
);
}
void _showAgentDetails(Map<String, dynamic> agent) {
showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.95,
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
children: [
// Header avec avatar et infos principales
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AdminColors.secondary,
AdminColors.secondary.withOpacity(0.8),
],
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 2,
),
),
child: const Icon(
Icons.person,
size: 40,
color: Colors.white,
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
agent['nom'] ?? 'Nom non disponible',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'ID: ${agent['agent_id'] ?? 'N/A'}',
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${agent['type'] ?? 'agent'}'.toUpperCase(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(
Icons.close,
color: Colors.white,
size: 28,
),
),
],
),
),
// Contenu scrollable
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Statistiques rapides
Row(
children: [
Expanded(
child: _buildAgentStatCard(
'Solde',
'${(agent['solde'] ?? 0).toStringAsFixed(0)} F',
Icons.account_balance_wallet,
AdminColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildAgentStatCard(
'Transactions',
'${agent['total_transactions'] ?? 0}',
Icons.receipt_long,
AdminColors.info,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildAgentStatCard(
'Commissions',
'${(agent['total_commissions'] ?? 0).toStringAsFixed(0)} F',
Icons.monetization_on,
AdminColors.accent,
),
),
],
),
const SizedBox(height: 24),
// Informations détaillées
const Text(
'Informations détaillées',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
_buildDetailCard([
_buildDetailRow(
'Nom complet',
agent['nom'] ?? 'N/A',
),
_buildDetailRow(
'ID Agent',
agent['agent_id'] ?? 'N/A',
),
_buildDetailRow(
'Téléphone',
agent['telephone'] ?? 'N/A',
),
_buildDetailRow(
'Email',
agent['email'] ?? 'Non renseigné',
),
_buildDetailRow('Type', agent['role'] ?? 'agent'),
_buildDetailRow(
'Statut',
agent['status'] ?? 'active',
),
_buildDetailRow(
'Membre depuis',
agent['created_at'] != null
? _formatDuration(
DateTime.parse(agent['created_at']),
)
: 'Non disponible',
),
_buildDetailRow(
'Dernière connexion',
agent['last_login'] != null
? _formatDate(agent['last_login'])
: 'Jamais connecté',
),
_buildDetailRow(
'Dernière activité',
agent['updated_at'] != null
? _formatDate(agent['updated_at']?['\$date'])
: 'Non disponible',
),
]),
const SizedBox(height: 24),
// Actions
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_updateAgentStatus(
agent['agent_id'],
agent['status'] == 'active'
? 'inactive'
: 'active',
);
},
icon: Icon(
agent['status'] == 'active'
? Icons.block
: Icons.check_circle,
),
label: Text(
agent['status'] == 'active'
? 'Désactiver'
: 'Activer',
),
style: ElevatedButton.styleFrom(
backgroundColor:
agent['status'] == 'active'
? AdminColors.error
: AdminColors.success,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed:
() => _showSnackBar(
'Modification agent - En développement',
AdminColors.info,
),
icon: const Icon(Icons.edit),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
backgroundColor: AdminColors.secondary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
),
),
),
],
),
],
),
),
),
],
),
),
),
);
}
Widget _buildAgentStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
textAlign: TextAlign.center,
),
Text(
title,
style: const TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildDetailCard(List<Widget> children) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(children: children),
);
}
String _formatDuration(DateTime createdAt) {
final duration = DateTime.now().difference(createdAt);
if (duration.inDays > 365) {
return '${(duration.inDays / 365).floor()} an${(duration.inDays / 365).floor() > 1 ? 's' : ''}';
} else if (duration.inDays > 30) {
return '${(duration.inDays / 30).floor()} mois';
} else {
return '${duration.inDays} jour${duration.inDays > 1 ? 's' : ''}';
}
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AdminColors.background,
body: FadeTransition(
opacity: _fadeAnimation,
child: SafeArea(
child: RefreshIndicator(
onRefresh: _loadDashboardData,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 30),
_buildNavigationTabs(),
const SizedBox(height: 20),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else
_buildTabContent(),
],
),
),
),
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AdminColors.primary, AdminColors.primary.withOpacity(0.8)],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AdminColors.primary.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 8),
),
],
),
child: Consumer<AuthController>(
builder: (context, authController, child) {
return Row(
children: [
const WortisLogoWidget(
size: 60,
isWhite: true,
withShadow: false,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.admin_panel_settings,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'PANNEAU ADMIN',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
authController.agentName ?? 'Administrateur',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'ID: ${authController.agentId ?? "ADMIN"}',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
),
),
],
),
),
IconButton(
onPressed: _logout,
icon: const Icon(Icons.logout, color: Colors.white),
tooltip: 'Déconnexion',
),
],
);
},
),
);
}
Widget _buildNavigationTabs() {
final tabs = [
{'title': 'Vue d\'ensemble', 'icon': Icons.dashboard},
{'title': 'Agents', 'icon': Icons.people},
{'title': 'Recharges', 'icon': Icons.account_balance_wallet},
{
'title': 'Transactions',
'icon': Icons.receipt_long,
}, // Ajoutez cet onglet
{'title': 'Rapports', 'icon': Icons.bar_chart},
];
return Container(
height: 60,
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Row(
children:
tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isSelected = _selectedIndex == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedIndex = index),
child: Container(
decoration: BoxDecoration(
color:
isSelected
? AdminColors.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
margin: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
tab['icon'] as IconData,
color:
isSelected
? AdminColors.primary
: Colors.grey[600],
size: 20,
),
const SizedBox(height: 4),
Text(
tab['title'] as String,
style: TextStyle(
fontSize: 10,
fontWeight:
isSelected ? FontWeight.w600 : FontWeight.w400,
color:
isSelected
? AdminColors.primary
: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}).toList(),
),
);
}
Widget _buildTabContent() {
switch (_selectedIndex) {
case 0:
return _buildOverviewTab();
case 1:
return _buildAgentsTab();
case 2:
return _buildRechargesTab();
case 3:
return _buildTransactionsTab(); // Ajoutez cette ligne
case 4: // Changez de 3 à 4
return _buildReportsTab();
default:
return _buildOverviewTab();
}
}
Widget _buildOverviewTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatsOverview(),
const SizedBox(height: 30),
_buildQuickActions(),
const SizedBox(height: 30),
_buildRecentActivity(),
],
);
}
Widget _buildStatsOverview() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Vue d\'ensemble du système',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Agents actifs',
'${_systemStats['active_agents'] ?? 0}',
Icons.people,
AdminColors.secondary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Transactions/jour',
'${_systemStats['daily_transactions'] ?? 0}',
Icons.trending_up,
AdminColors.success,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard(
'Volume total',
'${((_systemStats['total_volume'] ?? 0.0) / 1000000).toStringAsFixed(1)}M F',
Icons.monetization_on,
AdminColors.warning,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Taux de réussite',
'${(_systemStats['success_rate'] ?? 0.0).toStringAsFixed(1)}%',
Icons.check_circle,
AdminColors.primary,
),
),
],
),
],
);
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.1)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: color, size: 24),
const Text(
'↗ +12%',
style: TextStyle(
fontSize: 11,
color: Colors.green,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildQuickActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions rapides',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
childAspectRatio: 1.3,
children: [
_buildActionCard(
'Créer un agent',
'Nouveau compte agent',
Icons.person_add,
AdminColors.secondary,
onTap: _showCreateAgentDialog,
),
_buildActionCard(
'Export données',
'Télécharger rapports',
Icons.download,
AdminColors.info,
onTap: () => _showExportDialog(),
),
_buildActionCard(
'Statistiques',
'Analyses détaillées',
Icons.analytics,
AdminColors.success,
),
_buildActionCard(
'Configuration',
'Paramètres système',
Icons.settings,
AdminColors.primary,
),
],
),
],
);
}
Widget _buildActionCard(
String title,
String subtitle,
IconData icon,
Color color, {
VoidCallback? onTap,
}) {
return GestureDetector(
onTap:
onTap ??
() => _showSnackBar(
'$title - Fonction en développement',
AdminColors.info,
),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
),
);
}
Widget _buildRecentActivity() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Activité récente',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
if (_recentActivities.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'Aucune activité récente',
style: TextStyle(color: Colors.grey),
),
),
)
else
..._recentActivities.map(
(activity) => _buildActivityItem(
activity['title'] ?? '',
activity['description'] ?? '',
_getIconFromString(activity['icon'] ?? 'info'),
_getColorFromString(activity['color'] ?? 'grey'),
activity['time'] ?? '',
),
),
],
),
);
}
Widget _buildActivityItem(
String title,
String subtitle,
IconData icon,
Color color,
String time,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(
subtitle,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
Text(time, style: TextStyle(fontSize: 11, color: Colors.grey[500])),
],
),
);
}
Widget _buildAgentsTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Gestion des agents',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
ElevatedButton.icon(
onPressed: _showCreateAgentDialog,
icon: const Icon(Icons.add, size: 18),
label: const Text('Nouvel agent'),
style: ElevatedButton.styleFrom(
backgroundColor: AdminColors.secondary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Rechercher un agent...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: AdminColors.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${_agents.length} agents',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AdminColors.secondary,
),
),
),
],
),
const SizedBox(height: 16),
if (_agents.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: Column(
children: [
Icon(
Icons.people_outline,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
'Aucun agent trouvé',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _agents.length,
itemBuilder: (context, index) {
final agent = _agents[index];
return _buildAgentCard(agent);
},
),
],
),
),
],
);
}
Widget _buildAgentCard(Map<String, dynamic> agent) {
final status = agent['status'] ?? 'active';
final statusColor =
status == 'active' ? AdminColors.success : AdminColors.error;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
status == 'active' ? Icons.person : Icons.person_off,
color: statusColor,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
agent['nom'] ?? 'Nom non disponible',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'ID: ${agent['agent_id'] ?? 'N/A'}${agent['role'] ?? 'agent'}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 4),
Text(
'Solde: ${(agent['solde'] ?? 0).toStringAsFixed(0)} F',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AdminColors.secondary,
),
),
],
),
),
const SizedBox(width: 12),
IconButton(
onPressed: () => _showAgentDetails(agent),
icon: const Icon(Icons.visibility, color: AdminColors.secondary),
tooltip: 'Voir détails',
),
],
),
);
}
Widget _buildRechargesTab() {
// Grouper les recharges par rechargeur
final rechargesByRechargeur = <String, List<Map<String, dynamic>>>{};
for (final recharge in _allRecharges) {
final rechargeur =
recharge['approved_by'] ?? recharge['rejected_by'] ?? 'Non assigné';
if (!rechargesByRechargeur.containsKey(rechargeur)) {
rechargesByRechargeur[rechargeur] = [];
}
rechargesByRechargeur[rechargeur]!.add(recharge);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Supervision des recharges',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AdminColors.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${rechargesByRechargeur.length} rechargeurs',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AdminColors.secondary,
),
),
),
],
),
const SizedBox(height: 16),
// Statistiques globales recharges
_buildRechargeGlobalStats(),
const SizedBox(height: 20),
// Liste des rechargeurs
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Recharges par rechargeur',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
if (rechargesByRechargeur.isEmpty)
const Center(
child: Text(
'Aucune recharge trouvée',
style: TextStyle(color: Colors.grey),
),
)
else
...rechargesByRechargeur.entries.map(
(entry) => _buildRechargeurCard(entry.key, entry.value),
),
],
),
),
],
);
}
Widget _buildRechargeGlobalStats() {
final totalAmount = _allRecharges.fold<double>(
0.0,
(sum, r) =>
sum + (double.tryParse(r['montant']?.toString() ?? '0') ?? 0.0),
);
final successfulRecharges =
_allRecharges
.where((r) => ['approved', 'completed'].contains(r['status']))
.toList();
final successAmount = successfulRecharges.fold<double>(
0.0,
(sum, r) =>
sum + (double.tryParse(r['montant']?.toString() ?? '0') ?? 0.0),
);
final successRate =
_allRecharges.isNotEmpty
? (successfulRecharges.length / _allRecharges.length * 100)
: 0.0;
return Row(
children: [
Expanded(
child: _buildRechargeStatCard(
'Volume total',
'${(totalAmount / 1000000).toStringAsFixed(1)}M F',
Icons.monetization_on,
AdminColors.secondary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildRechargeStatCard(
'Volume réussi',
'${(successAmount / 1000000).toStringAsFixed(1)}M F',
Icons.check_circle,
AdminColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildRechargeStatCard(
'Taux de réussite',
'${successRate.toStringAsFixed(1)}%',
Icons.trending_up,
AdminColors.info,
),
),
],
);
}
Widget _buildRechargeurCard(
String rechargeur,
List<Map<String, dynamic>> recharges,
) {
final successfulRecharges =
recharges
.where((r) => ['approved', 'completed'].contains(r['status']))
.toList();
final totalAmount = successfulRecharges.fold<double>(
0.0,
(sum, r) =>
sum + (double.tryParse(r['montant']?.toString() ?? '0') ?? 0.0),
);
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: ExpansionTile(
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AdminColors.secondary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.person,
color: AdminColors.secondary,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rechargeur,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(
'${recharges.length} recharges • ${totalAmount.toStringAsFixed(0)} F réussis',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
],
),
children:
recharges
.map((recharge) => _buildRechargeListItem(recharge))
.toList(),
),
);
}
Widget _buildRechargeListItem(Map<String, dynamic> recharge) {
final status = recharge['status'] ?? '';
final statusColor = _getRechargeStatusColor(status);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getRechargeStatusIcon(status),
color: statusColor,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Agent ${recharge['agent_id'] ?? 'N/A'}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${recharge['montant'] ?? 0} F • ${_formatDate(recharge['created_at'])}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildTransactionsTab() {
// Grouper les transactions par service
final transactionsByService = <String, List<Map<String, dynamic>>>{};
for (final transaction in _allTransactions) {
final service = transaction['service_name'] ?? 'Service inconnu';
if (!transactionsByService.containsKey(service)) {
transactionsByService[service] = [];
}
transactionsByService[service]!.add(transaction);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Gestion des transactions',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AdminColors.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${transactionsByService.length} services',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AdminColors.info,
),
),
),
],
),
const SizedBox(height: 16),
// Statistiques globales transactions
_buildTransactionGlobalStats(),
const SizedBox(height: 20),
// Liste des services
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Transactions par service',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
if (transactionsByService.isEmpty)
const Center(
child: Text(
'Aucune transaction trouvée',
style: TextStyle(color: Colors.grey),
),
)
else
...transactionsByService.entries.map(
(entry) => _buildServiceCard(entry.key, entry.value),
),
],
),
),
],
);
}
Widget _buildTransactionGlobalStats() {
final successfulTransactions =
_allTransactions.where((t) => t['status'] == 'completed').toList();
print(successfulTransactions);
final totalVolume = successfulTransactions.fold<double>(
0.0,
(sum, t) =>
sum + (double.tryParse(t['montant']?.toString() ?? '0') ?? 0.0),
);
final totalCommissions = successfulTransactions.fold<double>(
0.0,
(sum, t) =>
sum +
(double.tryParse(t['commission_agent']?.toString() ?? '0') ?? 0.0),
);
final successRate =
_allTransactions.isNotEmpty
? (successfulTransactions.length / _allTransactions.length * 100)
: 0.0;
return Row(
children: [
Expanded(
child: _buildTransactionStatCard(
'Volume total',
'${(totalVolume / 1000000).toStringAsFixed(1)}M F',
Icons.monetization_on,
AdminColors.secondary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildTransactionStatCard(
'Commissions',
'${((totalVolume * 0.66) / 100000).toStringAsFixed(0)}k F',
Icons.account_balance,
AdminColors.accent,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildTransactionStatCard(
'Taux de réussite',
'${successRate.toStringAsFixed(1)}%',
Icons.trending_up,
AdminColors.success,
),
),
],
);
}
Widget _buildServiceCard(
String service,
List<Map<String, dynamic>> transactions,
) {
final successfulTransactions =
transactions.where((t) => t['status'] == 'completed').toList();
final totalVolume = successfulTransactions.fold<double>(
0.0,
(sum, t) =>
sum + (double.tryParse(t['montant']?.toString() ?? '0') ?? 0.0),
);
Color serviceColor = AdminColors.info;
IconData serviceIcon = Icons.widgets;
if (service.toLowerCase().contains('internet')) {
serviceColor = AdminColors.secondary;
serviceIcon = Icons.wifi;
} else if (service.toLowerCase().contains('électricité') ||
service.toLowerCase().contains('eneo')) {
serviceColor = AdminColors.warning;
serviceIcon = Icons.electrical_services;
} else if (service.toLowerCase().contains('eau')) {
serviceColor = AdminColors.info;
serviceIcon = Icons.water_drop;
}
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: ExpansionTile(
title: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: serviceColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(serviceIcon, color: serviceColor, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
service,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(
'${transactions.length} transactions • ${totalVolume.toStringAsFixed(0)} F',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
],
),
children:
transactions
.take(10)
.map((transaction) => _buildTransactionListItem(transaction))
.toList(),
),
);
}
Widget _buildTransactionListItem(Map<String, dynamic> transaction) {
final status = transaction['status'] ?? '';
final statusColor = _getTransactionStatusColor(status);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getTransactionStatusIcon(status),
color: statusColor,
size: 16,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Agent ${transaction['agent_id'] ?? 'N/A'}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${transaction['montant'] ?? 0} F • ${_formatDate(transaction['created_at'])}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Widget _buildTransactionStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
title,
style: TextStyle(fontSize: 9, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildTransactionCard(Map<String, dynamic> transaction) {
final status = transaction['status'] ?? '';
final statusColor = _getTransactionStatusColor(status);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor.withOpacity(0.2)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
_getTransactionStatusIcon(status),
color: statusColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Agent ${transaction['agent_id'] ?? 'N/A'}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'Service: ${transaction['service_name'] ?? 'N/A'}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
Text(
'Montant: ${transaction['montant'] ?? 0} F',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AdminColors.secondary,
),
),
if (transaction['created_at'] != null)
Text(
'Date: ${_formatDate(transaction['created_at'])}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
),
],
),
);
}
Widget _buildRechargeStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
title,
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildRechargeCard(Map<String, dynamic> recharge) {
final status = recharge['status'] ?? '';
final statusColor = _getRechargeStatusColor(status);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: statusColor.withOpacity(0.2)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
_getRechargeStatusIcon(status),
color: statusColor,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'Agent ${recharge['agent_id'] ?? 'N/A'}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: statusColor,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 4),
Text(
'Montant: ${recharge['montant'] ?? 0} F',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AdminColors.secondary,
),
),
if (recharge['created_at'] != null)
Text(
'Créé: ${_formatDate(recharge['created_at'])}',
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
),
],
),
);
}
Widget _buildReportsTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Rapports et analyses',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildReportCard(
'Rapport financier',
'Transactions et revenus',
Icons.monetization_on,
AdminColors.success,
onTap: () => _exportData('financial'),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildReportCard(
'Rapport agents',
'Performance des agents',
Icons.people,
AdminColors.secondary,
onTap: () => _exportData('agents'),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildReportCard(
'Rapport recharges',
'Historique complet',
Icons.account_balance_wallet,
AdminColors.warning,
onTap: () => _exportData('recharges'),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildReportCard(
'Rapport système',
'Logs et performances',
Icons.computer,
AdminColors.info,
onTap: () => _exportData('system'),
),
),
],
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistiques détaillées',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
_buildDetailedStats(),
],
),
),
],
);
}
Widget _buildReportCard(
String title,
String subtitle,
IconData icon,
Color color, {
VoidCallback? onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AdminColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(icon, color: color, size: 24),
Icon(Icons.download, color: color.withOpacity(0.7), size: 16),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
),
],
),
),
);
}
Widget _buildDetailedStats() {
return Column(
children: [
_buildStatRow(
'Volume total traité',
'${((_systemStats['total_volume'] ?? 0.0) / 1000000).toStringAsFixed(2)} M FCFA',
),
_buildStatRow(
'Nombre d\'agents',
'${_systemStats['active_agents'] ?? 0}',
),
_buildStatRow(
'Transactions aujourd\'hui',
'${_systemStats['daily_transactions'] ?? 0}',
),
_buildStatRow(
'Taux de réussite',
'${(_systemStats['success_rate'] ?? 0.0).toStringAsFixed(1)}%',
),
_buildStatRow(
'Recharges en attente',
'${_systemStats['pending_recharges'] ?? 0}',
),
_buildStatRow('Dernière mise à jour', 'Maintenant'),
],
);
}
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 14, color: Colors.grey[700])),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
);
}
void _showExportDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.download, color: AdminColors.info),
SizedBox(width: 8),
Text('Export de données'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Choisissez le type de données à exporter:'),
const SizedBox(height: 16),
ListTile(
leading: const Icon(
Icons.monetization_on,
color: AdminColors.success,
),
title: const Text('Rapport financier'),
subtitle: const Text('Transactions et revenus'),
onTap: () {
Navigator.pop(context);
_exportData('financial');
},
),
ListTile(
leading: const Icon(
Icons.people,
color: AdminColors.secondary,
),
title: const Text('Liste des agents'),
subtitle: const Text('Tous les agents et leurs données'),
onTap: () {
Navigator.pop(context);
_exportData('agents');
},
),
ListTile(
leading: const Icon(
Icons.account_balance_wallet,
color: AdminColors.warning,
),
title: const Text('Historique recharges'),
subtitle: const Text('Toutes les recharges'),
onTap: () {
Navigator.pop(context);
_exportData('recharges');
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
],
),
);
}
// ===== UTILITAIRES =====
IconData _getIconFromString(String iconName) {
switch (iconName) {
case 'person_add':
return Icons.person_add;
case 'check_circle':
return Icons.check_circle;
case 'description':
return Icons.description;
default:
return Icons.info;
}
}
Color _getColorFromString(String colorName) {
switch (colorName) {
case 'green':
return AdminColors.success;
case 'blue':
return AdminColors.secondary;
case 'orange':
return AdminColors.warning;
case 'red':
return AdminColors.error;
default:
return Colors.grey;
}
}
Color _getRechargeStatusColor(String status) {
switch (status) {
case 'pending':
return AdminColors.warning;
case 'approved':
case 'completed':
return AdminColors.success;
case 'rejected':
return AdminColors.error;
default:
return Colors.grey;
}
}
IconData _getRechargeStatusIcon(String status) {
switch (status) {
case 'pending':
return Icons.pending;
case 'approved':
case 'completed':
return Icons.check_circle;
case 'rejected':
return Icons.cancel;
default:
return Icons.help_outline;
}
}
Color _getTransactionStatusColor(String status) {
switch (status) {
case 'completed':
return AdminColors.success;
case 'pending':
return AdminColors.warning;
case 'failed':
return AdminColors.error;
default:
return Colors.grey;
}
}
IconData _getTransactionStatusIcon(String status) {
switch (status) {
case 'completed':
return Icons.check_circle;
case 'pending':
return Icons.pending;
case 'failed':
return Icons.error;
default:
return Icons.help_outline;
}
}
String _formatDate(String? dateString) {
if (dateString == null) return 'N/A';
try {
final date = DateTime.parse(dateString);
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 0) {
return '${difference.inDays}j';
} else if (difference.inHours > 0) {
return '${difference.inHours}h';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}min';
} else {
return 'Maintenant';
}
} catch (e) {
return 'N/A';
}
}
}