2833 lines
90 KiB
Dart
2833 lines
90 KiB
Dart
|
|
// ===== 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';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|