Files
wortis_tpe/lib/pages/rechargeur_dashboard.dart

2241 lines
72 KiB
Dart
Raw Permalink Normal View History

2025-12-01 10:56:37 +01:00
// ===== lib/pages/rechargeur_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 RechargeurColors {
static const Color primary = Color(0xFF38A169);
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 warning = Color(0xFFF59E0B);
static const Color success = Color(0xFF38A169);
static const Color error = Colors.red;
}
// ===== RECHARGEUR DASHBOARD =====
class RechargeurDashboard extends StatefulWidget {
const RechargeurDashboard({super.key});
@override
State<RechargeurDashboard> createState() => _RechargeurDashboardState();
}
class _RechargeurDashboardState extends State<RechargeurDashboard>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
bool _isLoading = false;
List<Map<String, dynamic>> _pendingRecharges = [];
Map<String, dynamic> _rechargeStats = {};
@override
void initState() {
super.initState();
_setupAnimations();
_loadPendingRecharges();
_loadRechargeStats(); // Ajoutez cette ligne
}
@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<bool> _approveRechargeApi(
String rechargeId,
String agentId,
String pin,
) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/recharge/approve/$rechargeId/agent/$agentId'),
headers: apiHeaders,
body: jsonEncode({
'status': 'approved',
'approved_by':
Provider.of<AuthController>(context, listen: false).agentId,
'approved_at': DateTime.now().toIso8601String(),
'pin': pin,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['success'] ?? false;
}
return false;
} catch (e) {
print('❌ Erreur approbation recharge: $e');
return false;
}
}
Future<bool> _rejectRechargeApi(
String rechargeId,
String agentId,
String pin, {
String? reason,
}) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/recharge/reject/$rechargeId/agent/$agentId'),
headers: apiHeaders,
body: jsonEncode({
'status': 'rejected',
'rejected_by':
Provider.of<AuthController>(context, listen: false).agentId,
'rejected_at': DateTime.now().toIso8601String(),
'rejection_reason': reason ?? 'Demande rejetée par le rechargeur',
'pin': pin,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['success'] ?? false;
}
return false;
} catch (e) {
print('❌ Erreur rejet recharge: $e');
return false;
}
}
Future<Map<String, dynamic>> _processRechargeApi(
String agentId,
double amount,
String pin,
) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/recharge/process'),
headers: apiHeaders,
body: jsonEncode({
'agent_id': agentId,
'montant': amount,
'rechargeur_id':
Provider.of<AuthController>(context, listen: false).agentId,
'processed_at': DateTime.now().toIso8601String(),
'pin': pin,
}),
);
final data = jsonDecode(response.body);
return {
'success':
response.statusCode == 200 ? (data['success'] ?? false) : false,
'message': data['message'] ?? '',
'transaction_id': data['transaction_id'],
'new_balance': data['new_balance'],
'error_code': response.statusCode != 200 ? response.statusCode : null,
};
} catch (e) {
print('❌ Erreur traitement recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
Future<void> _loadPendingRecharges() async {
setState(() {
_isLoading = true;
});
try {
final recharges = await RechargeApiService.getAllRecharges();
setState(() {
_pendingRecharges =
recharges.where((r) => r['status'] == 'pending').toList();
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
_showSnackBar(
'Erreur lors du chargement des recharges',
RechargeurColors.error,
);
}
}
Future<void> _loadRechargeStats() async {
try {
final authController = Provider.of<AuthController>(
context,
listen: false,
);
final stats = await RechargeApiService.getRechargeStats(
rechargeurId: authController.agentId,
startDate: DateTime.now().subtract(const Duration(days: 30)),
endDate: DateTime.now(),
);
print('**************************************$stats');
if (stats['success'] == true) {
setState(() {
_rechargeStats = stats['stats'] ?? {};
});
}
} catch (e) {
print('❌ Erreur chargement statistiques: $e');
}
}
// ===== 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: RechargeurColors.accent),
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: RechargeurColors.accent,
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 _showRechargeDialog({
String? initialAgentId,
String? initialAmount,
String? errorMessage,
}) {
final agentIdController = TextEditingController(text: initialAgentId ?? '');
final amountController = TextEditingController(text: initialAmount ?? '');
final pinController = TextEditingController();
showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: LayoutBuilder(
builder: (context, constraints) {
final maxWidth =
constraints.maxWidth > 400 ? 400.0 : constraints.maxWidth;
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(
Icons.add_circle,
color: RechargeurColors.primary,
),
SizedBox(width: 8),
Text(
'Recharger un agent',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 24),
if (errorMessage != null) ...[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.red.withOpacity(0.3),
),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
errorMessage,
style: const TextStyle(
fontSize: 13,
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
const SizedBox(height: 16),
],
TextField(
controller: agentIdController,
decoration: InputDecoration(
labelText: 'ID Agent',
hintText: 'WRT1234',
border: const OutlineInputBorder(),
prefixIcon: const Icon(
Icons.person,
color: RechargeurColors.primary,
),
errorText:
errorMessage?.contains('Agent') == true
? 'ID Agent invalide'
: null,
),
),
const SizedBox(height: 16),
TextField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Montant (FCFA)',
hintText: '50000',
border: const OutlineInputBorder(),
prefixIcon: const Icon(
Icons.monetization_on,
color: RechargeurColors.primary,
),
errorText:
errorMessage?.contains('Montant') == true
? 'Montant invalide'
: null,
),
),
const SizedBox(height: 16),
TextField(
controller: pinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 4,
decoration: InputDecoration(
labelText: 'Code PIN',
hintText: '****',
border: const OutlineInputBorder(),
prefixIcon: const Icon(
Icons.lock,
color: RechargeurColors.primary,
),
counterText: '',
errorText:
errorMessage?.contains('PIN') == true
? 'PIN invalide'
: null,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: RechargeurColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.info_outline,
color: RechargeurColors.primary,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'La recharge sera effectuée immédiatement après validation du PIN',
style: TextStyle(
fontSize: 13,
color: RechargeurColors.primary.withOpacity(
0.8,
),
),
),
),
],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_processRecharge(
agentIdController.text,
amountController.text,
pinController.text,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: RechargeurColors.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text('Recharger'),
),
],
),
],
),
),
);
},
),
),
);
}
Future<void> _processRecharge(
String agentId,
String amount,
String pin,
) async {
String? errorMessage;
// Validation des champs
if (agentId.isEmpty) {
errorMessage = 'L\'ID de l\'agent est requis';
} else if (amount.isEmpty) {
errorMessage = 'Le montant est requis';
} else if (pin.isEmpty) {
errorMessage = 'Le code PIN est requis';
} else if (pin.length != 4) {
errorMessage = 'Le PIN doit contenir exactement 4 chiffres';
} else if (!RegExp(r'^\d{4}$').hasMatch(pin)) {
errorMessage = 'Le PIN ne doit contenir que des chiffres';
}
final numericAmount = double.tryParse(amount);
if (errorMessage == null) {
if (numericAmount == null || numericAmount <= 0) {
errorMessage = 'Le montant doit être un nombre positif';
} else if (numericAmount > 10000000) {
errorMessage = 'Le montant ne peut pas dépasser 10,000,000 FCFA';
} else if (numericAmount < 1) {
errorMessage = 'Le montant minimum est de 1 FCFA';
}
}
if (!RegExp(r'^[A-Z0-9]{3,10}$').hasMatch(agentId) &&
errorMessage == null) {
errorMessage =
'L\'ID agent doit contenir 3-10 caractères alphanumériques en majuscules';
}
// Si erreur de validation, réouvrir le dialog
if (errorMessage != null) {
_showRechargeDialog(
initialAgentId: agentId,
initialAmount: amount,
errorMessage: errorMessage,
);
return;
}
setState(() {
_isLoading = true;
});
try {
final result = await _processRechargeApi(agentId, numericAmount!, pin);
if (result['success'] == true) {
_showSnackBar(
'✅ Recharge de ${numericAmount.toStringAsFixed(0)} XAF effectuée pour $agentId',
RechargeurColors.success,
);
_loadPendingRecharges();
_loadRechargeStats();
} else {
String apiErrorMessage =
result['message'] ?? 'Erreur lors de la recharge';
// Réouvrir le dialog avec l'erreur API
_showRechargeDialog(
initialAgentId: agentId,
initialAmount: amount,
errorMessage: apiErrorMessage,
);
}
} catch (e) {
_showRechargeDialog(
initialAgentId: agentId,
initialAmount: amount,
errorMessage: 'Erreur de connexion',
);
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _approveRecharge(Map<String, dynamic> recharge) async {
final rechargeId =
recharge['_id'] is Map
? recharge['_id']['\$oid'] ?? recharge['_id'].toString()
: recharge['_id'].toString();
final agentId = recharge['agent_id'];
final amount = recharge['montant'];
final pinController = TextEditingController();
final result = await showDialog<String>(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.security, color: Colors.green),
SizedBox(width: 8),
Text('Confirmer avec PIN'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Approuver la recharge de $amount XAF pour l\'agent $agentId',
),
const SizedBox(height: 16),
TextField(
controller: pinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 4,
decoration: const InputDecoration(
labelText: 'Code PIN',
hintText: '****',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock, color: Colors.green),
counterText: '',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, pinController.text),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
child: const Text('Approuver'),
),
],
),
);
if (result != null && result.isNotEmpty) {
setState(() {
_isLoading = true;
});
try {
final success = await _approveRechargeApi(rechargeId, agentId, result);
if (success) {
_showSnackBar(
'✅ Recharge approuvée: $agentId - $amount XAF',
Colors.green,
);
_loadPendingRecharges();
_loadRechargeStats();
} else {
_showSnackBar(
'PIN incorrect ou erreur lors de l\'approbation',
RechargeurColors.error,
);
}
} catch (e) {
_showSnackBar('Erreur lors de l\'approbation', RechargeurColors.error);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
void _showHistoryDialog() {
showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Historique des recharges',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
const Divider(),
Expanded(
child: FutureBuilder<Map<String, dynamic>>(
future: RechargeApiService.getRechargeHistory(
rechargeurId:
Provider.of<AuthController>(
context,
listen: false,
).agentId,
),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (!snapshot.hasData ||
snapshot.data!['success'] != true) {
return const Center(
child: Text(
'Erreur lors du chargement de l\'historique',
),
);
}
final recharges = List<Map<String, dynamic>>.from(
snapshot.data!['recharges'] ?? [],
);
if (recharges.isEmpty) {
return const Center(
child: Text('Aucune recharge dans l\'historique'),
);
}
return ListView.builder(
itemCount: recharges.length,
itemBuilder: (context, index) {
final recharge = recharges[index];
return _buildHistoryItem(recharge);
},
);
},
),
),
],
),
),
),
);
}
Widget _buildHistoryItem(Map<String, dynamic> recharge) {
final agentId = recharge['agent_id'] ?? '';
final amount = recharge['montant'] ?? 0.0;
final amountValue = double.tryParse(amount.toString()) ?? 0.0;
final status = recharge['status'] ?? '';
final date =
DateTime.tryParse(recharge['created_at']['\$date'] ?? '') ??
DateTime.now();
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: RechargeUtils.getStatusColor(status).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
RechargeUtils.getStatusIcon(status),
color: RechargeUtils.getStatusColor(status),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Agent $agentId',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Text(
'${amountValue.toStringAsFixed(0)} XAF • ${RechargeUtils.formatRelativeDate(date)}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: RechargeUtils.getStatusColor(status).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10,
color: RechargeUtils.getStatusColor(status),
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
Future<void> _rejectRecharge(Map<String, dynamic> recharge) async {
final rechargeId =
recharge['_id'] is Map
? recharge['_id']['\$oid'] ?? recharge['_id'].toString()
: recharge['_id'].toString();
final agentId = recharge['agent_id'];
final reasonController = TextEditingController();
final pinController = TextEditingController();
final result = await showDialog<Map<String, String>>(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: const Row(
children: [
Icon(Icons.security, color: Colors.red),
SizedBox(width: 8),
Text('Confirmer le rejet'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Rejeter la demande de recharge pour l\'agent $agentId'),
const SizedBox(height: 16),
TextField(
controller: reasonController,
decoration: const InputDecoration(
labelText: 'Raison du rejet (optionnel)',
hintText: 'Ex: Solde insuffisant',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 16),
TextField(
controller: pinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 4,
decoration: const InputDecoration(
labelText: 'Code PIN',
hintText: '****',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock, color: Colors.red),
counterText: '',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed:
() => Navigator.pop(context, {
'reason': reasonController.text,
'pin': pinController.text,
}),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Rejeter'),
),
],
),
);
if (result != null && result['pin']!.isNotEmpty) {
setState(() {
_isLoading = true;
});
try {
final success = await _rejectRechargeApi(
rechargeId,
agentId,
result['pin']!,
reason: result['reason']!.isEmpty ? null : result['reason'],
);
if (success) {
_showSnackBar('❌ Recharge rejetée pour $agentId', Colors.red);
_loadPendingRecharges();
_loadRechargeStats(); // Ajoutez cette ligne
} else {
_showSnackBar(
'PIN incorrect ou erreur lors du rejet',
RechargeurColors.error,
);
}
} catch (e) {
_showSnackBar('Erreur lors du rejet', RechargeurColors.error);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: RechargeurColors.background,
body: FadeTransition(
opacity: _fadeAnimation,
child: SafeArea(
child: RefreshIndicator(
onRefresh: () async {
await _loadPendingRecharges();
await _loadRechargeStats(); // Ajoutez cette ligne
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 30),
_buildRechargeStats(),
const SizedBox(height: 30),
_buildRechargeActions(),
const SizedBox(height: 30),
_buildPendingRecharges(),
],
),
),
),
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
RechargeurColors.primary,
RechargeurColors.primary.withOpacity(0.8),
],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: RechargeurColors.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.add_circle,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'ESPACE RECHARGEUR',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
authController.agentName ?? 'Rechargeur',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Solde: ${authController.activeBalance.toStringAsFixed(0)} XAF',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
IconButton(
onPressed: _logout,
icon: const Icon(Icons.logout, color: Colors.white),
tooltip: 'Déconnexion',
),
],
);
},
),
);
}
Widget _buildRechargeStats() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistiques de recharge',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
// === STATISTIQUES GLOBALES ===
Row(
children: [
Expanded(
child: _buildStatCard(
'Total recharges',
'${_rechargeStats['total_recharges'] ?? 0}',
Icons.receipt_long,
RechargeurColors.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Volume total',
'${(_rechargeStats['total_amount'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.account_balance_wallet,
RechargeurColors.secondary,
),
),
],
),
const SizedBox(height: 12),
// === STATISTIQUES DE SUCCÈS ===
Row(
children: [
Expanded(
child: _buildStatCard(
'Recharges réussies',
'${_rechargeStats['success_count'] ?? 0}',
Icons.check_circle,
RechargeurColors.success,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Volume réussi',
'${(_rechargeStats['success_amount'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.trending_up,
RechargeurColors.success,
),
),
],
),
const SizedBox(height: 12),
// === TAUX ET MOYENNES ===
Row(
children: [
Expanded(
child: _buildStatCard(
'Taux de réussite',
'${(_rechargeStats['success_rate'] ?? 0.0).toStringAsFixed(1)}%',
Icons.analytics,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Montant moyen',
'${(_rechargeStats['average_amount'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.calculate,
Colors.orange,
),
),
],
),
const SizedBox(height: 12),
// === STATUT ACTUEL ===
Row(
children: [
Expanded(
child: _buildStatCard(
'En attente',
'${_rechargeStats['pending_count'] ?? 0}',
Icons.pending,
RechargeurColors.warning,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'Rejetées',
'${_rechargeStats['rejected_count'] ?? 0}',
Icons.cancel,
RechargeurColors.error,
),
),
],
),
const SizedBox(height: 16),
// === SECTION AUJOURD'HUI ===
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.today, color: Colors.blue, size: 20),
const SizedBox(width: 8),
Text(
'Aujourd\'hui (${DateTime.now().day}/${DateTime.now().month})',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildMiniStatCard(
'Recharges',
'${_rechargeStats['daily_count'] ?? 0}',
Icons.receipt,
Colors.blue,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Volume',
'${(_rechargeStats['daily_volume'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.monetization_on,
Colors.green,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Réussies',
'${_rechargeStats['daily_success'] ?? 0}',
Icons.check,
Colors.green,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildMiniStatCard(
'Taux',
'${(_rechargeStats['daily_success_rate'] ?? 0.0).toStringAsFixed(1)}%',
Icons.percent,
Colors.blue,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Moyenne',
'${(_rechargeStats['daily_average'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.trending_up,
Colors.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'En attente',
'${_rechargeStats['daily_pending'] ?? 0}',
Icons.pending,
Colors.orange,
),
),
],
),
],
),
),
const SizedBox(height: 16),
// === SECTION CE MOIS ===
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.purple.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.calendar_month, color: Colors.purple, size: 20),
const SizedBox(width: 8),
Text(
'Ce mois (${_getMonthName(DateTime.now().month)} ${DateTime.now().year})',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.purple,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildMiniStatCard(
'Recharges',
'${_rechargeStats['monthly_count'] ?? 0}',
Icons.receipt,
Colors.purple,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Volume',
'${(_rechargeStats['monthly_volume'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.account_balance_wallet,
Colors.green,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Réussies',
'${_rechargeStats['monthly_success'] ?? 0}',
Icons.verified,
Colors.green,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildMiniStatCard(
'Taux',
'${(_rechargeStats['monthly_success_rate'] ?? 0.0).toStringAsFixed(1)}%',
Icons.analytics,
Colors.blue,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Moyenne',
'${(_rechargeStats['monthly_average'] ?? 0.0).toStringAsFixed(0)} XAF',
Icons.calculate,
Colors.orange,
),
),
const SizedBox(width: 8),
Expanded(
child: _buildMiniStatCard(
'Échecs',
'${_rechargeStats['monthly_failed'] ?? 0}',
Icons.error,
Colors.red,
),
),
],
),
],
),
),
],
);
}
// Widget pour les mini-cartes de statistiques
Widget _buildMiniStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: color,
),
textAlign: TextAlign.center,
),
Text(
title,
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
// Fonction utilitaire pour obtenir le nom du mois
String _getMonthName(int month) {
const months = [
'',
'Janvier',
'Février',
'Mars',
'Avril',
'Mai',
'Juin',
'Juillet',
'Août',
'Septembre',
'Octobre',
'Novembre',
'Décembre',
];
return months[month];
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: RechargeurColors.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(
'↗ +8%',
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 _buildRechargeActions() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Actions de recharge',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildActionButton(
'Recharger un agent',
'Créditer le solde',
Icons.person_add,
RechargeurColors.primary,
onTap: _showRechargeDialog,
),
),
// const SizedBox(width: 12),
// Expanded(
// child: _buildActionButton(
// 'Scanner QR Code',
// 'Recharge rapide',
// Icons.qr_code_scanner,
// RechargeurColors.secondary,
// ),
// ),
],
),
// const SizedBox(height: 12),
// Row(
// children: [
// Expanded(
// child: _buildActionButton(
// 'Historique',
// 'Voir toutes les recharges',
// Icons.history,
// Colors.grey[600]!,
// onTap: _showHistoryDialog, // Ajoutez cette ligne
// ),
// )
// ],
// ),
],
);
}
Widget _buildActionButton(
String title,
String subtitle,
IconData icon,
Color color, {
VoidCallback? onTap,
}) {
return GestureDetector(
onTap:
onTap ??
() =>
_showSnackBar('$title - Fonction en développement', Colors.blue),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: RechargeurColors.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,
children: [
Icon(icon, color: color, size: 28),
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 _buildPendingRecharges() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: RechargeurColors.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: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recharges en attente',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: RechargeurColors.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_pendingRecharges.length} demandes',
style: const TextStyle(
fontSize: 11,
color: RechargeurColors.warning,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 16),
if (_isLoading)
const Center(child: CircularProgressIndicator())
else if (_pendingRecharges.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
'Aucune recharge en attente',
style: TextStyle(color: Colors.grey),
),
),
)
else
..._pendingRecharges.map((recharge) => _buildPendingItem(recharge)),
],
),
);
}
Widget _buildPendingItem(Map<String, dynamic> recharge) {
final agentId = recharge['agent_id'] ?? '';
final amount = recharge['montant'] ?? '';
final status = recharge['status'] ?? '';
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: RechargeurColors.warning.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: RechargeurColors.warning.withOpacity(0.2)),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: RechargeurColors.warning.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.person,
color: RechargeurColors.warning,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Agent $agentId',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Text(
'Demande: $amount XAF • ${status.toUpperCase()}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
Row(
children: [
IconButton(
onPressed: () => _approveRecharge(recharge),
icon: const Icon(
Icons.check_circle,
color: Colors.green,
size: 20,
),
tooltip: 'Approuver',
),
IconButton(
onPressed: () => _rejectRecharge(recharge),
icon: const Icon(Icons.cancel, color: Colors.red, size: 20),
tooltip: 'Rejeter',
),
],
),
],
),
);
}
}
// ===== SERVICES API ADDITIONNELS =====
/// Service pour gérer les recharges
class RechargeApiService {
/// Récupérer toutes les recharges
static Future<List<Map<String, dynamic>>> getAllRecharges() async {
try {
final response = await http.get(
Uri.parse('$baseUrl/recharge/get_recharges'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return List<Map<String, dynamic>>.from(data['all_recharges'] ?? []);
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
}
} catch (e) {
print('❌ Erreur récupération recharges: $e');
rethrow;
}
}
/// Récupérer les recharges par statut
static Future<List<Map<String, dynamic>>> getRechargesByStatus(
String status,
) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/recharge/get_recharges?status=$status'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final allRecharges = List<Map<String, dynamic>>.from(
data['all_recharges'] ?? [],
);
return allRecharges.where((r) => r['status'] == status).toList();
} else {
throw Exception('Erreur HTTP ${response.statusCode}');
}
} catch (e) {
print('❌ Erreur récupération recharges par statut: $e');
rethrow;
}
}
/// Approuver une recharge
static Future<Map<String, dynamic>> approveRecharge(
String rechargeId, {
required String approvedBy,
}) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/recharge/approve/$rechargeId'),
headers: apiHeaders,
body: jsonEncode({
'status': 'approved',
'approved_by': approvedBy,
'approved_at': DateTime.now().toIso8601String(),
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': data['success'] ?? false,
'message': data['message'] ?? 'Recharge approuvée avec succès',
'data': data['data'],
};
} else {
return {
'success': false,
'message': data['message'] ?? 'Erreur lors de l\'approbation',
'error_code': response.statusCode,
};
}
} catch (e) {
print('❌ Erreur approbation recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
/// Rejeter une recharge
static Future<Map<String, dynamic>> rejectRecharge(
String rechargeId, {
required String rejectedBy,
String? reason,
}) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/recharge/reject/$rechargeId'),
headers: apiHeaders,
body: jsonEncode({
'status': 'rejected',
'rejected_by': rejectedBy,
'rejected_at': DateTime.now().toIso8601String(),
'rejection_reason': reason ?? 'Demande rejetée par le rechargeur',
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': data['success'] ?? false,
'message': data['message'] ?? 'Recharge rejetée avec succès',
'data': data['data'],
};
} else {
return {
'success': false,
'message': data['message'] ?? 'Erreur lors du rejet',
'error_code': response.statusCode,
};
}
} catch (e) {
print('❌ Erreur rejet recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
/// Traiter une nouvelle recharge directe
static Future<Map<String, dynamic>> processDirectRecharge(
String agentId,
double amount, {
required String processedBy,
}) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/recharge/process'),
headers: apiHeaders,
body: jsonEncode({
'agent_id': agentId,
'montant': amount,
'rechargeur_id': processedBy,
'processed_at': DateTime.now().toIso8601String(),
'status': 'completed',
'type': 'direct',
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': data['success'] ?? false,
'message': data['message'] ?? 'Recharge effectuée avec succès',
'data': data['data'],
'transaction_id': data['transaction_id'],
};
} else {
return {
'success': false,
'message': data['message'] ?? 'Erreur lors de la recharge',
'error_code': response.statusCode,
};
}
} catch (e) {
print('❌ Erreur traitement recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
/// Récupérer les statistiques de recharge
static Future<Map<String, dynamic>> getRechargeStats({
String? rechargeurId,
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final queryParams = <String, String>{};
if (rechargeurId != null) queryParams['rechargeur_id'] = rechargeurId;
if (startDate != null) {
queryParams['start_date'] = startDate.toIso8601String();
}
if (endDate != null) queryParams['end_date'] = endDate.toIso8601String();
final uri = Uri.parse(
'$baseUrl/recharge/stats',
).replace(queryParameters: queryParams);
final response = await http.get(uri, headers: apiHeaders);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return {'success': true, 'stats': data['stats'] ?? {}};
} else {
return {
'success': false,
'message': 'Erreur lors de la récupération des statistiques',
};
}
} catch (e) {
print('❌ Erreur récupération stats recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
/// Récupérer l'historique des recharges avec pagination
static Future<Map<String, dynamic>> getRechargeHistory({
String? rechargeurId,
String? agentId,
String? status,
int page = 1,
int limit = 20,
}) async {
try {
final queryParams = <String, String>{
'page': page.toString(),
'limit': limit.toString(),
};
if (rechargeurId != null) queryParams['rechargeur_id'] = rechargeurId;
if (agentId != null) queryParams['agent_id'] = agentId;
if (status != null) queryParams['status'] = status;
final uri = Uri.parse(
'$baseUrl/recharge/history',
).replace(queryParameters: queryParams);
final response = await http.get(uri, headers: apiHeaders);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return {
'success': true,
'recharges': data['recharges'] ?? [],
'pagination': data['pagination'] ?? {},
'total': data['total'] ?? 0,
};
} else {
return {
'success': false,
'message': 'Erreur lors de la récupération de l\'historique',
};
}
} catch (e) {
print('❌ Erreur récupération historique recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
/// Valider qu'un agent existe avant recharge
static Future<Map<String, dynamic>> validateAgent(String agentId) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/agent/$agentId/validate'),
headers: apiHeaders,
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': data['success'] ?? false,
'exists': data['exists'] ?? false,
'agent_name': data['agent_name'],
'agent_status': data['agent_status'],
'current_balance': data['current_balance'],
};
} else {
return {
'success': false,
'exists': false,
'message': data['message'] ?? 'Agent introuvable',
};
}
} catch (e) {
print('❌ Erreur validation agent: $e');
return {
'success': false,
'exists': false,
'message': 'Erreur de connexion: $e',
};
}
}
/// Annuler une recharge en attente
static Future<Map<String, dynamic>> cancelPendingRecharge(
String rechargeId, {
required String cancelledBy,
String? reason,
}) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/recharge/cancel/$rechargeId'),
headers: apiHeaders,
body: jsonEncode({
'status': 'cancelled',
'cancelled_by': cancelledBy,
'cancelled_at': DateTime.now().toIso8601String(),
'cancellation_reason': reason ?? 'Annulée par le rechargeur',
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
return {
'success': data['success'] ?? false,
'message': data['message'] ?? 'Recharge annulée avec succès',
};
} else {
return {
'success': false,
'message': data['message'] ?? 'Erreur lors de l\'annulation',
};
}
} catch (e) {
print('❌ Erreur annulation recharge: $e');
return {'success': false, 'message': 'Erreur de connexion: $e'};
}
}
}
// ===== MODÈLES DE DONNÉES =====
/// Modèle pour une recharge
class RechargeModel {
final String id;
final String agentId;
final double montant;
final String status;
final DateTime? createdAt;
final DateTime? processedAt;
final String? processedBy;
final String? rejectionReason;
RechargeModel({
required this.id,
required this.agentId,
required this.montant,
required this.status,
this.createdAt,
this.processedAt,
this.processedBy,
this.rejectionReason,
});
factory RechargeModel.fromJson(Map<String, dynamic> json) {
return RechargeModel(
id: json['_id'] is Map ? json['_id']['\$oid'] : json['_id'].toString(),
agentId: json['agent_id'] ?? '',
montant: double.tryParse(json['montant'].toString()) ?? 0.0,
status: json['status'] ?? 'pending',
createdAt:
json['created_at'] != null
? DateTime.tryParse(json['created_at'])
: null,
processedAt:
json['processed_at'] != null
? DateTime.tryParse(json['processed_at'])
: null,
processedBy: json['processed_by'],
rejectionReason: json['rejection_reason'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'agent_id': agentId,
'montant': montant,
'status': status,
'created_at': createdAt?.toIso8601String(),
'processed_at': processedAt?.toIso8601String(),
'processed_by': processedBy,
'rejection_reason': rejectionReason,
};
}
bool get isPending => status == 'pending';
bool get isApproved => status == 'approved';
bool get isRejected => status == 'rejected';
bool get isCancelled => status == 'cancelled';
bool get isCompleted => status == 'completed';
String get statusDisplayName {
switch (status) {
case 'pending':
return 'En attente';
case 'approved':
return 'Approuvée';
case 'rejected':
return 'Rejetée';
case 'cancelled':
return 'Annulée';
case 'completed':
return 'Terminée';
default:
return status.toUpperCase();
}
}
Color get statusColor {
switch (status) {
case 'pending':
return RechargeurColors.warning;
case 'approved':
case 'completed':
return RechargeurColors.success;
case 'rejected':
case 'cancelled':
return RechargeurColors.error;
default:
return Colors.grey;
}
}
}
/// Modèle pour les statistiques de recharge
class RechargeStatsModel {
final int totalRecharges;
final double totalAmount;
final int pendingCount;
final int approvedCount;
final int rejectedCount;
final double averageAmount;
final Map<String, int> rechargesByDay;
RechargeStatsModel({
required this.totalRecharges,
required this.totalAmount,
required this.pendingCount,
required this.approvedCount,
required this.rejectedCount,
required this.averageAmount,
required this.rechargesByDay,
});
factory RechargeStatsModel.fromJson(Map<String, dynamic> json) {
return RechargeStatsModel(
totalRecharges: json['total_recharges'] ?? 0,
totalAmount: (json['total_amount'] ?? 0.0).toDouble(),
pendingCount: json['pending_count'] ?? 0,
approvedCount: json['approved_count'] ?? 0,
rejectedCount: json['rejected_count'] ?? 0,
averageAmount: (json['average_amount'] ?? 0.0).toDouble(),
rechargesByDay: Map<String, int>.from(json['recharges_by_day'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'total_recharges': totalRecharges,
'total_amount': totalAmount,
'pending_count': pendingCount,
'approved_count': approvedCount,
'rejected_count': rejectedCount,
'average_amount': averageAmount,
'recharges_by_day': rechargesByDay,
};
}
}
// ===== UTILITAIRES =====
/// Utilitaires pour les recharges
class RechargeUtils {
/// Formater un montant en FCFA
static String formatAmount(double amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]} ')} FCFA';
}
/// Valider un ID d'agent
static bool isValidAgentId(String agentId) {
return agentId.isNotEmpty &&
agentId.length >= 3 &&
agentId.length <= 10 &&
RegExp(r'^[A-Z0-9]+$').hasMatch(agentId);
}
/// Valider un montant de recharge
static bool isValidAmount(double amount) {
return amount > 0 && amount <= 10000000; // Max 10M FCFA
}
/// Obtenir la couleur selon le statut
static Color getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'pending':
return RechargeurColors.warning;
case 'approved':
case 'completed':
return RechargeurColors.success;
case 'rejected':
case 'cancelled':
return RechargeurColors.error;
default:
return Colors.grey;
}
}
/// Obtenir l'icône selon le statut
static IconData getStatusIcon(String status) {
switch (status.toLowerCase()) {
case 'pending':
return Icons.pending;
case 'approved':
case 'completed':
return Icons.check_circle;
case 'rejected':
case 'cancelled':
return Icons.cancel;
default:
return Icons.help_outline;
}
}
/// Formater une date relative
static String formatRelativeDate(DateTime date) {
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';
}
}
}