Files
wortis_tpe/lib/pages/main_navigation.dart

3974 lines
151 KiB
Dart
Raw Permalink Normal View History

2025-12-01 10:56:37 +01:00
// ===== lib/pages/main_navigation.dart =====
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:wtpe/pages/session_manager.dart';
import '../main.dart';
import 'home.dart';
import 'dashboard.dart';
import 'login.dart';
import '../widgets/responsive_helper.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:url_launcher/url_launcher.dart'; // Vous devrez ajouter cette dépendance
// ===== 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 AppColors {
static const Color primary = Color(0xFF006699);
static const Color secondary = Color(0xFF0088CC);
static const Color accent = Color(0xFF006699);
static const Color background = Color(0xFFF8FAFC);
static const Color surface = Colors.white;
static const Color success = Color(0xFF006699);
static const Color purple = Color(0xFF006699);
}
// ===== NAVIGATION ITEM MODEL =====
class NavigationItem {
final IconData icon;
final IconData activeIcon;
final String label;
final Color color;
NavigationItem({
required this.icon,
required this.activeIcon,
required this.label,
required this.color,
});
}
// ===== MAIN NAVIGATION PAGE =====
class MainNavigationPage extends StatefulWidget {
final int initialIndex;
const MainNavigationPage({super.key, this.initialIndex = 0});
@override
State<MainNavigationPage> createState() => _MainNavigationPageState();
}
class _MainNavigationPageState extends State<MainNavigationPage>
with TickerProviderStateMixin {
late int _currentIndex;
late PageController _pageController;
late AnimationController _animationController;
late AnimationController _iconAnimationController;
late Animation<double> _scaleAnimation;
late List<Animation<double>> _iconAnimations;
final List<NavigationItem> _navigationItems = [
NavigationItem(
icon: Icons.home_rounded,
activeIcon: Icons.home,
label: 'Services',
color: AppColors.primary,
),
NavigationItem(
icon: Icons.dashboard_outlined,
activeIcon: Icons.dashboard,
label: 'Dashboard',
color: AppColors.primary,
),
NavigationItem(
icon: Icons.person_outline_rounded,
activeIcon: Icons.person,
label: 'Profil',
color: AppColors.primary,
),
];
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: _currentIndex);
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_iconAnimationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
);
_iconAnimations =
_navigationItems.map((item) {
return Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(
parent: _iconAnimationController,
curve: Curves.elasticOut,
),
);
}).toList();
_animationController.forward();
}
@override
void dispose() {
_pageController.dispose();
_animationController.dispose();
_iconAnimationController.dispose();
super.dispose();
}
void _onItemTapped(int index) {
if (index != _currentIndex) {
setState(() {
_currentIndex = index;
});
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
_iconAnimationController.forward().then((_) {
_iconAnimationController.reverse();
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
children: const [
HomePage(showAppBar: false),
DashboardPage(showAppBar: false),
ProfilePage(),
],
),
bottomNavigationBar: _buildCustomBottomNavigation(),
);
}
Widget _buildCustomBottomNavigation() {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.15),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 0,
),
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: SizedBox(
height: 70,
child: Row(
children:
_navigationItems.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final isSelected = _currentIndex == index;
return Expanded(
child: _buildNavItem(item, index, isSelected),
);
}).toList(),
),
),
),
),
);
},
);
}
Widget _buildNavItem(NavigationItem item, int index, bool isSelected) {
return GestureDetector(
onTap: () => _onItemTapped(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
gradient:
isSelected
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
item.color.withOpacity(0.1),
item.color.withOpacity(0.05),
],
)
: null,
borderRadius: BorderRadius.circular(20),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: _iconAnimationController,
builder: (context, child) {
final scale =
isSelected && _iconAnimationController.isAnimating
? _iconAnimations[index].value
: 1.0;
return Transform.scale(
scale: scale,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color:
isSelected
? item.color.withOpacity(0.15)
: Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
isSelected ? item.activeIcon : item.icon,
color: isSelected ? item.color : Colors.grey[600],
size: 22,
),
),
);
},
),
const SizedBox(height: 2),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
style: TextStyle(
fontSize: isSelected ? 11 : 10,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? item.color : Colors.grey[600],
),
child: Text(item.label),
),
if (isSelected) ...[
const SizedBox(height: 1),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 16,
height: 2,
decoration: BoxDecoration(
color: item.color,
borderRadius: BorderRadius.circular(1),
),
),
],
],
),
),
);
}
}
// ===== PROFILE PAGE =====
// ===== PAGE PROFIL REDESIGNÉE =====
// Imports nécessaires (à ajuster selon votre structure)
// import 'package:your_app/controllers/auth_controller.dart';
// import 'package:your_app/utils/responsive_helper.dart';
// import 'package:your_app/utils/session_manager.dart';
// import 'package:your_app/utils/app_colors.dart';
// import 'package:your_app/pages/login_page.dart';
// ===== lib/pages/profile.dart AVEC SUPPORT ENTREPRISE COMPLET =====
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
// Constants API
static const String apiBaseUrl = 'https://api.live.wortis.cg/tpe';
static const Map<String, String> apiHeaders = {
'Content-Type': 'application/json',
};
// NOUVEAU: Couleur entreprise
static const Color enterpriseColor = Color(0xFF8B5CF6);
@override
void initState() {
super.initState();
_initializeOrientations();
_setupAnimations();
}
void _initializeOrientations() {
WidgetsBinding.instance.addPostFrameCallback((_) {
ResponsiveHelper.initializeOrientations(context);
});
}
void _setupAnimations() {
_animationController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeOutBack),
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isSmallScreen = context.isSmallPhone;
final isMediumScreen = context.isPhone && !context.isSmallPhone;
final isTablet = context.isTablet;
return Scaffold(
backgroundColor: AppColors.background,
body: Stack(
children: [
// Gradient d'arrière-plan
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.primary.withOpacity(0.05),
AppColors.background,
AppColors.background,
],
stops: [0.0, 0.3, 1.0],
),
),
),
// Contenu principal
FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: context.responsivePadding(
phone: EdgeInsets.fromLTRB(12, 40, 12, 120),
tablet: EdgeInsets.fromLTRB(20, 60, 20, 120),
),
child: Column(
children: [
// Header Profil avec support entreprise
_buildResponsiveProfileHeader(
isSmallScreen,
isMediumScreen,
isTablet,
),
SizedBox(height: isSmallScreen ? 16 : 24),
// Badge commission disponible
_buildCommissionBadge(),
// Section Actions
_buildResponsiveActionsSection(
isSmallScreen,
isMediumScreen,
isTablet,
),
SizedBox(height: isSmallScreen ? 16 : 24),
// Section Paramètres
_buildSettingsSection(
isSmallScreen,
isMediumScreen,
isTablet,
),
SizedBox(height: isSmallScreen ? 16 : 24),
// Bouton déconnexion
_buildResponsiveLogoutButton(
context,
isSmallScreen,
isMediumScreen,
),
],
),
),
),
],
),
),
),
],
),
);
}
// ============= HEADER PROFIL AVEC SUPPORT ENTREPRISE =============
Widget _buildResponsiveProfileHeader(
bool isSmallScreen,
bool isMediumScreen,
bool isTablet,
) {
return Consumer<AuthController>(
builder: (context, authController, child) {
final isEnterprise = authController.isEnterpriseMember;
final enterprise = authController.enterprise;
return Container(
width: double.infinity,
padding: EdgeInsets.all(
isSmallScreen ? 20 : (isMediumScreen ? 24 : 28),
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
isEnterprise
? [
enterpriseColor,
enterpriseColor.withOpacity(0.8),
enterpriseColor.withOpacity(0.6),
]
: [
AppColors.primary,
AppColors.primary.withOpacity(0.8),
AppColors.secondary,
],
stops: [0.0, 0.6, 1.0],
),
borderRadius: BorderRadius.circular(isSmallScreen ? 20 : 24),
boxShadow: [
BoxShadow(
color: (isEnterprise ? enterpriseColor : AppColors.primary)
.withOpacity(0.3),
blurRadius: isSmallScreen ? 15 : 25,
offset: Offset(0, isSmallScreen ? 8 : 12),
spreadRadius: isSmallScreen ? 1 : 2,
),
BoxShadow(
color: Colors.white.withOpacity(0.1),
blurRadius: 5,
offset: Offset(-2, -2),
),
],
),
child: Column(
children: [
// Avatar avec badge entreprise
Stack(
children: [
Container(
width: context.responsiveWidth(phone: 80, tablet: 100),
height: context.responsiveHeight(phone: 80, tablet: 100),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(
isSmallScreen ? 20 : 25,
),
border: Border.all(
color: Colors.white.withOpacity(0.4),
width: isSmallScreen ? 2 : 3,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Icon(
isEnterprise ? Icons.business : Icons.person_outline,
size: isSmallScreen ? 40 : (isMediumScreen ? 45 : 50),
color: Colors.white,
),
),
// Badge de statut
Positioned(
right: 0,
bottom: 0,
child: Container(
width: isSmallScreen ? 24 : 30,
height: isSmallScreen ? 24 : 30,
decoration: BoxDecoration(
color: AppColors.success,
borderRadius: BorderRadius.circular(
isSmallScreen ? 12 : 15,
),
border: Border.all(
color: Colors.white,
width: isSmallScreen ? 2 : 3,
),
boxShadow: [
BoxShadow(
color: AppColors.success.withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Icon(
Icons.check,
size: isSmallScreen ? 12 : 16,
color: Colors.white,
),
),
),
],
),
SizedBox(height: isSmallScreen ? 16 : 20),
// Badge type de compte
Container(
padding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 10 : 12,
vertical: isSmallScreen ? 4 : 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.25),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isEnterprise ? Icons.business : Icons.person,
size: isSmallScreen ? 12 : 14,
color: Colors.white.withOpacity(0.9),
),
SizedBox(width: isSmallScreen ? 4 : 6),
Text(
isEnterprise ? 'Compte Entreprise' : 'Agent Individuel',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: isSmallScreen ? 10 : 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
],
),
),
SizedBox(height: isSmallScreen ? 10 : 12),
// Nom de l'agent
ConstrainedBox(
constraints: BoxConstraints(maxWidth: double.infinity),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
authController.agentName ?? 'Agent Wortis',
style: TextStyle(
fontSize: isSmallScreen ? 22 : (isMediumScreen ? 25 : 28),
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 1,
),
),
),
// Nom de l'entreprise si membre
if (isEnterprise && enterprise?.nomEntreprise != null) ...[
SizedBox(height: isSmallScreen ? 6 : 8),
Container(
padding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 12 : 16,
vertical: isSmallScreen ? 6 : 8,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.25),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.apartment,
size: isSmallScreen ? 12 : 14,
color: Colors.white.withOpacity(0.9),
),
SizedBox(width: isSmallScreen ? 6 : 8),
Flexible(
child: Text(
enterprise!.nomEntreprise,
style: TextStyle(
fontSize: isSmallScreen ? 13 : 15,
color: Colors.white.withOpacity(0.95),
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
],
SizedBox(height: isSmallScreen ? 8 : 12),
// Badge ID
Container(
padding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 16 : 20,
vertical: isSmallScreen ? 8 : 10,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(25),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.badge,
size: isSmallScreen ? 14 : 16,
color: Colors.white.withOpacity(0.9),
),
SizedBox(width: isSmallScreen ? 6 : 8),
Flexible(
child: Text(
'ID: ${authController.agentId ?? "AGENT001"}',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
color: Colors.white.withOpacity(0.95),
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
SizedBox(height: isSmallScreen ? 12 : 16),
// Informations entreprise supplémentaires si membre
if (isEnterprise && enterprise != null) ...[
Container(
padding: EdgeInsets.all(isSmallScreen ? 12 : 14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.25),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
Text(
'${(enterprise as dynamic).nombreMembres ?? enterprise.membresIds.length}',
style: TextStyle(
fontSize: isSmallScreen ? 16 : 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 2),
Text(
'Membres',
style: TextStyle(
fontSize: isSmallScreen ? 10 : 11,
color: Colors.white.withOpacity(0.8),
),
),
],
),
Builder(
builder: (context) {
// Essayer d'accéder au domaine d'activité de manière sûre
final domaineActivite =
(enterprise as dynamic).domaineActivite;
if (domaineActivite != null &&
domaineActivite.toString().isNotEmpty) {
return Row(
children: [
SizedBox(width: isSmallScreen ? 16 : 24),
Container(
width: 1,
height: 30,
color: Colors.white.withOpacity(0.3),
),
SizedBox(width: isSmallScreen ? 16 : 24),
Flexible(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Secteur',
style: TextStyle(
fontSize: isSmallScreen ? 10 : 11,
color: Colors.white.withOpacity(0.8),
),
),
SizedBox(height: 2),
Text(
domaineActivite.toString(),
style: TextStyle(
fontSize: isSmallScreen ? 12 : 13,
fontWeight: FontWeight.w600,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
);
}
return SizedBox.shrink();
},
),
],
),
),
SizedBox(height: isSmallScreen ? 12 : 16),
],
// Indicateur de statut
Container(
padding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 12 : 16,
vertical: isSmallScreen ? 4 : 6,
),
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.success.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: isSmallScreen ? 6 : 8,
height: isSmallScreen ? 6 : 8,
decoration: BoxDecoration(
color: AppColors.success,
borderRadius: BorderRadius.circular(
isSmallScreen ? 3 : 4,
),
),
),
SizedBox(width: isSmallScreen ? 6 : 8),
Text(
isEnterprise ? 'Membre Actif' : 'Agent Actif',
style: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
);
},
);
}
// ============= NOTIFICATION BADGE COMMISSION =============
Widget _buildCommissionBadge() {
final authController = Provider.of<AuthController>(context, listen: false);
return FutureBuilder<Map<String, dynamic>>(
future: _getCommissionBalance(authController.agentId ?? ''),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!['success']) {
return SizedBox.shrink();
}
final availableCommission =
snapshot.data!['commission_disponible'] ?? 0.0;
if (availableCommission <= 0) {
return SizedBox.shrink();
}
return Container(
margin: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
child: GestureDetector(
onTap:
() => _showCommissionRechargeDialog(
context,
availableCommission,
snapshot.data!,
),
child: Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.success,
AppColors.success.withOpacity(0.8),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.success.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.monetization_on,
color: Colors.white,
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${availableCommission.toStringAsFixed(0)} XAF de commission disponible',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
'Transférez vers votre solde principal',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 13,
),
),
],
),
),
Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16),
],
),
),
),
);
},
);
}
// ============= SECTION ACTIONS =============
Widget _buildResponsiveActionsSection(
bool isSmallScreen,
bool isMediumScreen,
bool isTablet,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 4, bottom: isSmallScreen ? 12 : 16),
child: Text(
'Actions Rapides',
style: TextStyle(
fontSize: isSmallScreen ? 18 : (isMediumScreen ? 19 : 20),
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
_buildResponsiveMenuItem(
'Demander une recharge',
'Augmenter votre solde disponible',
Icons.add_circle_outline,
AppColors.accent,
() => _showRechargeOptionsDialog(context),
isPrimary: true,
delay: 100,
isSmallScreen: isSmallScreen,
isMediumScreen: isMediumScreen,
),
_buildResponsiveMenuItem(
'Historique des recharges',
'Suivez vos demandes de recharge',
Icons.history,
AppColors.secondary,
() => _showRechargeHistoryDialog(context),
delay: 200,
isSmallScreen: isSmallScreen,
isMediumScreen: isMediumScreen,
),
_buildResponsiveMenuItem(
'Historique des commissions',
'Consultez vos transferts de commission',
Icons.trending_up,
AppColors.success,
() => _showCommissionHistoryDialog(context),
delay: 250,
isSmallScreen: isSmallScreen,
isMediumScreen: isMediumScreen,
),
_buildResponsiveMenuItem(
'Support',
'Obtenez de l\'aide rapidement',
Icons.headset_mic,
AppColors.purple,
() => _showSupportDialog(context),
delay: 300,
isSmallScreen: isSmallScreen,
isMediumScreen: isMediumScreen,
),
],
);
}
Widget _buildResponsiveMenuItem(
String title,
String subtitle,
IconData icon,
Color color,
VoidCallback onTap, {
bool isPrimary = false,
int delay = 0,
bool isSmallScreen = false,
bool isMediumScreen = false,
}) {
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 600 + delay),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutBack,
builder: (context, animation, child) {
final clampedAnimation = animation.clamp(0.0, 1.0);
return Transform.translate(
offset: Offset(50 * (1 - clampedAnimation), 0),
child: Opacity(
opacity: clampedAnimation,
child: Container(
margin: EdgeInsets.only(bottom: isSmallScreen ? 12 : 16),
decoration: BoxDecoration(
gradient:
isPrimary
? LinearGradient(
colors: [color, color.withOpacity(0.8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
color: isPrimary ? null : AppColors.surface,
borderRadius: BorderRadius.circular(isSmallScreen ? 16 : 20),
border: Border.all(
color:
isPrimary ? Colors.transparent : color.withOpacity(0.15),
width: 2,
),
boxShadow: [
BoxShadow(
color:
isPrimary
? color.withOpacity(0.3)
: Colors.black.withOpacity(0.08),
blurRadius:
isPrimary
? (isSmallScreen ? 15 : 20)
: (isSmallScreen ? 10 : 15),
offset: Offset(
0,
isPrimary
? (isSmallScreen ? 6 : 8)
: (isSmallScreen ? 3 : 4),
),
spreadRadius: isPrimary ? (isSmallScreen ? 1 : 2) : 0,
),
if (!isPrimary)
BoxShadow(
color: Colors.white.withOpacity(0.9),
blurRadius: 3,
offset: Offset(-2, -2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(isSmallScreen ? 16 : 20),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 16 : 20,
vertical: isSmallScreen ? 16 : 20,
),
child: Row(
children: [
Container(
width:
isSmallScreen ? 48 : (isMediumScreen ? 52 : 56),
height:
isSmallScreen ? 48 : (isMediumScreen ? 52 : 56),
decoration: BoxDecoration(
color:
isPrimary
? Colors.white.withOpacity(0.2)
: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(
isSmallScreen ? 12 : 16,
),
border: Border.all(
color:
isPrimary
? Colors.white.withOpacity(0.3)
: color.withOpacity(0.2),
width: 1,
),
),
child: Icon(
icon,
color: isPrimary ? Colors.white : color,
size:
isSmallScreen ? 24 : (isMediumScreen ? 26 : 28),
),
),
SizedBox(width: isSmallScreen ? 12 : 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize:
isSmallScreen
? 14
: (isMediumScreen ? 15 : 16),
fontWeight: FontWeight.w700,
color:
isPrimary ? Colors.white : Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize:
isSmallScreen
? 11
: (isMediumScreen ? 12 : 13),
color:
isPrimary
? Colors.white.withOpacity(0.8)
: Colors.grey[600],
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Container(
width: isSmallScreen ? 28 : 32,
height: isSmallScreen ? 28 : 32,
decoration: BoxDecoration(
color:
isPrimary
? Colors.white.withOpacity(0.2)
: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(
isSmallScreen ? 8 : 10,
),
),
child: Icon(
Icons.arrow_forward_ios,
color: isPrimary ? Colors.white : color,
size: isSmallScreen ? 14 : 16,
),
),
],
),
),
),
),
),
),
);
},
);
}
// ============= BOUTON DÉCONNEXION =============
Widget _buildResponsiveLogoutButton(
BuildContext context,
bool isSmallScreen,
bool isMediumScreen,
) {
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 800),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutBack,
builder: (context, animation, child) {
return Transform.scale(
scale: animation,
child: Container(
width: double.infinity,
height: isSmallScreen ? 50 : (isMediumScreen ? 53 : 56),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red[400]!, Colors.red[500]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(isSmallScreen ? 14 : 16),
boxShadow: [
BoxShadow(
color: Colors.red.withOpacity(0.3),
blurRadius: isSmallScreen ? 15 : 20,
offset: Offset(0, isSmallScreen ? 6 : 8),
spreadRadius: isSmallScreen ? 1 : 2,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _logout(context),
borderRadius: BorderRadius.circular(isSmallScreen ? 14 : 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.logout,
color: Colors.white,
size: isSmallScreen ? 18 : 22,
),
SizedBox(width: isSmallScreen ? 8 : 12),
Text(
'Se Déconnecter',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
),
),
],
),
),
),
),
);
},
);
}
// ============= APIs COMMISSION =============
Future<Map<String, dynamic>> _getCommissionBalance(String agentId) async {
if (agentId.isEmpty) {
return {'success': false, 'commission_disponible': 0.0};
}
try {
final baseUrl = AuthController.baseUrl ?? apiBaseUrl;
final url = '$baseUrl/agent/$agentId/commission/balance';
print('Récupération commission - URL: $url');
final response = await http.get(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
print('Réponse balance commission - Status: ${response.statusCode}');
print('Réponse balance commission - Body: ${response.body}');
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true && data.containsKey('commission')) {
return {
'success': true,
'commission_disponible':
(data['commission']['commission_disponible'] ?? 0.0).toDouble(),
'commission_totale_gagnee':
(data['commission']['commission_totale_gagnee'] ?? 0.0)
.toDouble(),
'commission_totale_retiree':
(data['commission']['commission_totale_retiree'] ?? 0.0)
.toDouble(),
'nombre_transactions':
data['commission']['nombre_transactions'] ?? 0,
'derniers_retraits': data['commission']['derniers_retraits'] ?? [],
};
}
}
return {'success': false, 'commission_disponible': 0.0};
} catch (e) {
print('Erreur récupération commission: $e');
return {'success': false, 'commission_disponible': 0.0};
}
}
Future<Map<String, dynamic>> _getCommissionHistory(String agentId) async {
try {
final response = await http.get(
Uri.parse(
'${AuthController.baseUrl}/agent/$agentId/commission/history',
),
headers: AuthController.apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data;
} else {
return {'success': false, 'message': 'Erreur lors du chargement'};
}
} catch (e) {
return {'success': false, 'message': 'Erreur de connexion'};
}
}
// ============= DIALOGS RECHARGE =============
Future<void> _showRechargeOptionsDialog(BuildContext context) async {
final authController = Provider.of<AuthController>(context, listen: false);
await showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
elevation: 10,
child: Container(
width: MediaQuery.of(context).size.width * 0.85,
constraints: BoxConstraints(maxWidth: 400),
child: FutureBuilder<Map<String, dynamic>>(
future: _getCommissionBalance(authController.agentId ?? ''),
builder: (context, snapshot) {
double availableCommission = 0.0;
bool isLoading = !snapshot.hasData;
Map<String, dynamic> commissionData = {};
if (snapshot.hasData && snapshot.data!['success']) {
availableCommission =
snapshot.data!['commission_disponible'] ?? 0.0;
commissionData = snapshot.data!;
}
return Container(
padding: EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header avec icône
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.accent, AppColors.primary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.accent.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Icon(
Icons.account_balance_wallet,
color: Colors.white,
size: 32,
),
),
SizedBox(height: 16),
Text(
'Recharger mon compte',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
'Choisissez votre mode de recharge',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
// Affichage du solde et commission
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary.withOpacity(0.05),
AppColors.secondary.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.primary.withOpacity(0.15),
width: 1,
),
),
child: Column(
children: [
// Solde actuel
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.account_balance_wallet,
color: AppColors.primary,
size: 20,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Solde actuel',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2),
Text(
'${authController.balance.toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
),
],
),
SizedBox(height: 16),
Container(height: 1, color: Colors.grey[200]),
SizedBox(height: 16),
// Commission disponible
Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.success.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.trending_up,
color: AppColors.success,
size: 20,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Commission disponible',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2),
if (isLoading)
Container(
width: 80,
height: 18,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius:
BorderRadius.circular(4),
),
)
else
Text(
'${availableCommission.toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.success,
),
),
],
),
),
],
),
// Statistiques de commission si disponibles
if (!isLoading &&
commissionData.isNotEmpty) ...[
SizedBox(height: 12),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Text(
'${commissionData['commission_totale_gagnee']?.toStringAsFixed(0) ?? '0'}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.success,
),
),
Text(
'Gagnée',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
Column(
children: [
Text(
'${commissionData['commission_totale_retiree']?.toStringAsFixed(0) ?? '0'}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
Text(
'Utilisée',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
Column(
children: [
Text(
'${commissionData['nombre_transactions'] ?? 0}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Text(
'Transactions',
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
],
),
),
],
],
),
),
SizedBox(height: 24),
// Boutons d'action
Row(
children: [
// Demander une recharge
Expanded(
child: _buildRechargeOptionButton(
context: context,
title: 'Demander\nune recharge',
subtitle: 'Rechargeur requis',
icon: Icons.add_circle_outline,
color: AppColors.secondary,
onTap: () {
Navigator.of(context).pop();
_showRechargeRequestDialog(context);
},
),
),
SizedBox(width: 12),
// Utiliser la commission
Expanded(
child: _buildRechargeOptionButton(
context: context,
title: 'Utiliser la\ncommission',
subtitle: 'Transfert instantané',
icon: Icons.monetization_on,
color: AppColors.success,
onTap:
!isLoading && availableCommission > 0
? () {
Navigator.of(context).pop();
_showCommissionRechargeDialog(
context,
availableCommission,
commissionData,
);
}
: null,
isDisabled:
isLoading || availableCommission <= 0,
),
),
],
),
SizedBox(height: 16),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Fermer',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
),
],
),
),
);
},
),
),
),
);
}
Widget _buildRechargeOptionButton({
required BuildContext context,
required String title,
required String subtitle,
required IconData icon,
required Color color,
required VoidCallback? onTap,
bool isDisabled = false,
}) {
return GestureDetector(
onTap: isDisabled ? null : onTap,
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: isDisabled ? Colors.grey[100] : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDisabled ? Colors.grey[300]! : color.withOpacity(0.3),
width: 2,
),
boxShadow:
isDisabled
? []
: [
BoxShadow(
color: color.withOpacity(0.15),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isDisabled ? Colors.grey[300] : color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: isDisabled ? Colors.grey[500] : color,
size: 24,
),
),
SizedBox(height: 12),
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: isDisabled ? Colors.grey[500] : Colors.black87,
height: 1.2,
),
),
SizedBox(height: 4),
Text(
subtitle,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: isDisabled ? Colors.grey[400] : Colors.grey[600],
),
),
],
),
),
);
}
// Dialog pour demander une recharge traditionnelle
Future<void> _showRechargeRequestDialog(BuildContext context) async {
final amountController = TextEditingController();
final pinController = TextEditingController();
final screenSize = MediaQuery.of(context).size;
final isSmallScreen = screenSize.height < 600;
await showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(isSmallScreen ? 16 : 24),
),
elevation: 10,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 400,
maxHeight: screenSize.height * (isSmallScreen ? 0.9 : 0.8),
),
child: Container(
width: screenSize.width * 0.85,
padding: EdgeInsets.all(isSmallScreen ? 16 : 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
width: isSmallScreen ? 48 : 64,
height: isSmallScreen ? 48 : 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.secondary,
AppColors.accent,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(
isSmallScreen ? 16 : 20,
),
boxShadow: [
BoxShadow(
color: AppColors.secondary.withOpacity(0.3),
blurRadius: isSmallScreen ? 10 : 15,
offset: Offset(0, isSmallScreen ? 3 : 5),
),
],
),
child: Icon(
Icons.add_circle_outline,
color: Colors.white,
size: isSmallScreen ? 24 : 32,
),
),
SizedBox(height: isSmallScreen ? 12 : 16),
Text(
'Demander une recharge',
style: TextStyle(
fontSize: isSmallScreen ? 18 : 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
SizedBox(height: isSmallScreen ? 6 : 8),
Text(
'Un rechargeur validera votre demande',
style: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: isSmallScreen ? 16 : 20),
// Champ montant
TextField(
controller: amountController,
keyboardType: TextInputType.number,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
),
decoration: InputDecoration(
labelText: 'Montant à recharger',
hintText: 'Ex: 10000',
labelStyle: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
),
hintStyle: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
),
contentPadding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 12 : 16,
vertical: isSmallScreen ? 12 : 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.secondary.withOpacity(0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.secondary,
width: 2,
),
),
prefixIcon: Icon(
Icons.attach_money,
color: AppColors.secondary,
size: isSmallScreen ? 20 : 24,
),
suffixText: 'XAF',
suffixStyle: TextStyle(
color: AppColors.secondary,
fontWeight: FontWeight.w600,
fontSize: isSmallScreen ? 12 : 14,
),
),
autofocus: !isSmallScreen,
),
SizedBox(height: isSmallScreen ? 12 : 16),
// Champ PIN
TextField(
controller: pinController,
keyboardType: TextInputType.number,
obscureText: true,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
),
decoration: InputDecoration(
labelText: 'Code PIN',
hintText: '****',
labelStyle: TextStyle(
fontSize: isSmallScreen ? 12 : 14,
),
contentPadding: EdgeInsets.symmetric(
horizontal: isSmallScreen ? 12 : 16,
vertical: isSmallScreen ? 12 : 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.secondary.withOpacity(0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.secondary,
width: 2,
),
),
prefixIcon: Icon(
Icons.lock,
color: AppColors.secondary,
size: isSmallScreen ? 20 : 24,
),
),
),
SizedBox(height: isSmallScreen ? 12 : 16),
// Texte d'information
Text(
'Votre demande sera envoyée aux rechargeurs pour validation',
style: TextStyle(
fontSize: isSmallScreen ? 11 : 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
],
),
),
),
// Boutons fixes en bas
SizedBox(height: isSmallScreen ? 16 : 24),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: isSmallScreen ? 10 : 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Annuler',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
),
),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
final amount = amountController.text.trim();
final pin = pinController.text.trim();
if (amount.isEmpty || pin.isEmpty) {
if (mounted) {
_showErrorSnackBar(
context,
'Veuillez remplir tous les champs',
);
}
return;
}
final amountValue = double.tryParse(amount);
if (amountValue == null || amountValue <= 0) {
if (mounted) {
_showErrorSnackBar(
context,
'Montant invalide',
);
}
return;
}
Navigator.of(context).pop();
_processRechargeRequest(context, amount, pin);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.secondary,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
vertical: isSmallScreen ? 10 : 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
),
child: Text(
'Envoyer',
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
),
),
);
}
// Dialog pour utiliser la commission
Future<void> _showCommissionRechargeDialog(
BuildContext context,
double availableCommission,
Map<String, dynamic> commissionData,
) async {
final amountController = TextEditingController();
await showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
elevation: 10,
child: Container(
width: MediaQuery.of(context).size.width * 0.85,
constraints: BoxConstraints(maxWidth: 400),
padding: EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header animé
TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 600),
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.success, Colors.green[300]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppColors.success.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Icon(
Icons.monetization_on,
color: Colors.white,
size: 32,
),
),
);
},
),
SizedBox(height: 16),
Text(
'Utiliser ma commission',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
'Transférez vos commissions vers votre solde principal',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
SizedBox(height: 20),
// Affichage de la commission disponible
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.success.withOpacity(0.1),
AppColors.success.withOpacity(0.05),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.success.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.account_balance,
color: AppColors.success,
size: 18,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Commission disponible',
style: TextStyle(
fontSize: 13,
color: AppColors.success,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2),
Text(
'${availableCommission.toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.success,
),
),
],
),
),
],
),
),
SizedBox(height: 20),
// Champ de saisie du montant
TextField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Montant à transférer',
hintText:
'Ex: ${(availableCommission / 2).toStringAsFixed(0)}',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.success.withOpacity(0.3),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: AppColors.success,
width: 2,
),
),
prefixIcon: Icon(
Icons.account_balance_wallet,
color: AppColors.success,
),
suffixText: 'XAF',
suffixStyle: TextStyle(
color: AppColors.success,
fontWeight: FontWeight.w600,
),
),
autofocus: true,
),
SizedBox(height: 12),
// Boutons montants rapides
Row(
children: [
Expanded(
child: _buildQuickAmountButton(
context,
'25%',
(availableCommission * 0.25).toStringAsFixed(0),
amountController,
),
),
SizedBox(width: 8),
Expanded(
child: _buildQuickAmountButton(
context,
'50%',
(availableCommission * 0.5).toStringAsFixed(0),
amountController,
),
),
SizedBox(width: 8),
Expanded(
child: _buildQuickAmountButton(
context,
'Tout',
availableCommission.toStringAsFixed(0),
amountController,
),
),
],
),
SizedBox(height: 16),
Text(
'Ce montant sera instantanément ajouté à votre solde principal',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
SizedBox(height: 24),
// Boutons d'action
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Annuler',
style: TextStyle(fontSize: 16),
),
),
),
SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
final amount = double.tryParse(
amountController.text,
);
if (amount != null &&
amount > 0 &&
amount <= availableCommission) {
Navigator.of(context).pop();
_processCommissionTransfer(context, amount);
} else {
_showErrorSnackBar(
context,
'Montant invalide (max: ${availableCommission.toStringAsFixed(0)} XAF)',
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.success,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 3,
),
child: Text(
'Transférer',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
),
),
);
}
Widget _buildQuickAmountButton(
BuildContext context,
String label,
String amount,
TextEditingController controller,
) {
return GestureDetector(
onTap: () {
controller.text = amount;
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.success.withOpacity(0.3),
width: 1,
),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.success,
),
),
),
);
}
// Dialog historique des commissions
Future<void> _showCommissionHistoryDialog(BuildContext context) async {
final authController = Provider.of<AuthController>(context, listen: false);
showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: EdgeInsets.all(24),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Historique des commissions',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(Icons.close, color: Colors.grey),
),
],
),
SizedBox(height: 16),
Expanded(
child: FutureBuilder<Map<String, dynamic>>(
future: _getCommissionHistory(
authController.agentId ?? '',
),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.success,
),
),
);
}
if (!snapshot.hasData || !snapshot.data!['success']) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.history,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'Aucun historique trouvé',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
],
),
);
}
final withdrawals =
snapshot.data!['historique']['retraits'] as List;
return ListView.builder(
itemCount: withdrawals.length,
itemBuilder: (context, index) {
final withdrawal = withdrawals[index];
final date = DateTime.parse(
withdrawal['date_creation'],
);
return Container(
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.success.withOpacity(0.2),
width: 1,
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.trending_up,
color: AppColors.success,
size: 24,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Transfert de commission',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 4),
Text(
withdrawal['description'] ??
'Transfert vers solde',
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
SizedBox(height: 4),
Text(
_formatDate(date),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'+${withdrawal['montant'].toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.success,
),
),
SizedBox(height: 4),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.success.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(
8,
),
),
child: Text(
'Terminé',
style: TextStyle(
fontSize: 11,
color: AppColors.success,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
);
},
);
},
),
),
],
),
),
),
);
}
// ============= TRAITEMENT DES REQUÊTES =============
Future<void> _processRechargeRequest(
BuildContext context,
String montant,
String pin,
) async {
if (montant.isEmpty || pin.isEmpty) {
_showErrorSnackBar(context, 'Veuillez remplir tous les champs');
return;
}
final amountValue = double.tryParse(montant);
if (amountValue == null || amountValue <= 0) {
_showErrorSnackBar(context, 'Montant invalide');
return;
}
if (pin.length < 4) {
_showErrorSnackBar(context, 'PIN doit contenir au moins 4 chiffres');
return;
}
try {
final userId = await SessionManager().getUserId();
if (userId == null) {
_showErrorSnackBar(context, 'Session expirée, reconnectez-vous');
return;
}
await _demandeRecharge(userId, pin, montant, context);
} catch (e) {
print('Erreur processRechargeRequest : $e');
if (context.mounted) {
_showErrorSnackBar(context, 'Une erreur est survenue');
}
}
}
Future<void> _processCommissionTransfer(
BuildContext context,
double amount,
) async {
final authController = Provider.of<AuthController>(context, listen: false);
final agentId = authController.agentId;
if (agentId == null || agentId.isEmpty) {
_showErrorSnackBar(context, 'Erreur: Agent non connecté');
return;
}
// Dialog de chargement
showDialog(
context: context,
barrierDismissible: false,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.success,
),
strokeWidth: 3,
),
SizedBox(height: 16),
Text(
'Transfert en cours...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
'Veuillez patienter',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
),
);
try {
print(
'Envoi transfert commission - Agent ID: $agentId, Montant: $amount',
);
var headersList = {
'Accept': '*/*',
'User-Agent': 'FlutterApp (Wortis Agent)',
'Content-Type': 'application/json',
};
var url = Uri.parse(
'https://api.live.wortis.cg/tpe/recharge/$agentId/commission',
);
var body = {
"montant": amount,
"description": "Transfert de commission vers solde principal",
};
var req = http.Request('POST', url);
req.headers.addAll(headersList);
req.body = jsonEncode(body);
print('URL commission: $url');
print('Headers envoyés: $headersList');
print('Body envoyé: ${jsonEncode(body)}');
var res = await req.send();
final resBody = await res.stream.bytesToString();
// Fermer le dialog de chargement
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
print('Réponse commission - Status: ${res.statusCode}');
print('Réponse commission - Body: $resBody');
if (res.statusCode >= 200 && res.statusCode < 300) {
final data = jsonDecode(resBody);
if (data['success'] == true) {
// Rafraîchir le solde
await authController.refreshBalance();
// Message de succès
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
Icons.check_circle,
color: Colors.white,
size: 20,
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Transfert réussi !',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
Text(
'+${amount.toStringAsFixed(0)} XAF ajouté à votre solde',
style: TextStyle(fontSize: 12),
),
],
),
),
],
),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
margin: EdgeInsets.all(16),
duration: Duration(seconds: 4),
),
);
setState(() {});
}
} else {
if (context.mounted) {
_showErrorSnackBar(
context,
data['message'] ?? 'Erreur lors du transfert',
);
}
}
} else {
try {
final data = jsonDecode(resBody);
if (context.mounted) {
if (res.statusCode == 404) {
_showErrorSnackBar(
context,
'Service de commission non disponible',
);
} else if (res.statusCode == 401) {
_showErrorSnackBar(context, 'Session expirée, reconnectez-vous');
} else {
_showErrorSnackBar(
context,
data['message'] ?? 'Erreur de serveur (${res.statusCode})',
);
}
}
} catch (e) {
if (context.mounted) {
_showErrorSnackBar(
context,
'Erreur (${res.statusCode}): ${res.reasonPhrase}',
);
}
}
}
} on http.ClientException catch (e) {
if (Navigator.canPop(context)) Navigator.of(context).pop();
print('Erreur réseau commission: $e');
if (context.mounted) {
_showErrorSnackBar(context, 'Erreur de connexion réseau');
}
} on FormatException catch (e) {
if (Navigator.canPop(context)) Navigator.of(context).pop();
print('Erreur format JSON commission: $e');
if (context.mounted) {
_showErrorSnackBar(context, 'Erreur de format de réponse');
}
} catch (e) {
if (Navigator.canPop(context)) Navigator.of(context).pop();
print('Erreur transfert commission: $e');
if (context.mounted) {
_showErrorSnackBar(
context,
'Erreur lors du transfert: ${e.toString()}',
);
}
}
}
// ============= AUTRES MÉTHODES =============
Future<Map<String, dynamic>> _getTransactionHistory(String userId) async {
try {
final response = await http.get(
Uri.parse('$apiBaseUrl/agent/$userId/transactions'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return {
'success': true,
'transactions': data['transactions'] ?? [],
'total_commission': _calculateTotalCommission(
data['transactions'] ?? [],
),
};
} else {
return {'success': false, 'message': 'Erreur lors du chargement'};
}
} catch (e) {
return {'success': false, 'message': 'Erreur de connexion'};
}
}
Future<Map<String, dynamic>> _getRechargeHistory(String userId) async {
try {
final response = await http.get(
Uri.parse('$apiBaseUrl/recharge/history/agent/$userId'),
headers: apiHeaders,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return {'success': true, 'recharges': data['recharges'] ?? []};
} else {
return {'success': false, 'message': 'Erreur lors du chargement'};
}
} catch (e) {
return {'success': false, 'message': 'Erreur de connexion'};
}
}
double _calculateTotalCommission(List transactions) {
double totalTransaction = 0;
for (var transaction in transactions) {
if (transaction['statut'] == 'completed') {
totalTransaction += (transaction['montant'] ?? 0.0);
}
}
return totalTransaction * 0.0066;
}
Future<void> _showRechargeHistoryDialog(BuildContext context) async {
try {
final userId = await SessionManager().getUserId();
final result = await _getRechargeHistory(userId!);
if (!context.mounted) return;
if (result['success']) {
_showHistoryDialog(
context,
'Historique des recharges',
result['recharges'],
'recharge',
);
} else {
_showErrorSnackBar(context, result['message']);
}
} catch (e) {
_showErrorSnackBar(context, 'Erreur lors du chargement');
}
}
void _showHistoryDialog(
BuildContext context,
String title,
List items,
String type,
) {
showDialog(
context: context,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.surface, Colors.grey[50]!],
),
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.grey),
),
),
],
),
const SizedBox(height: 8),
Container(
height: 2,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.primary, AppColors.accent],
),
borderRadius: BorderRadius.circular(1),
),
),
const SizedBox(height: 16),
Expanded(
child:
items.isEmpty
? _buildEmptyState(type)
: ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) {
return AnimatedContainer(
duration: Duration(
milliseconds: 300 + (index * 100),
),
curve: Curves.easeOutBack,
child:
type == 'transaction'
? _buildEnhancedTransactionItem(
items[index],
index,
)
: _buildEnhancedRechargeHistoryItem(
items[index],
index,
),
);
},
),
),
],
),
),
),
);
}
Widget _buildEmptyState(String type) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
),
child: Icon(
type == 'transaction' ? Icons.receipt_long : Icons.history,
size: 40,
color: Colors.grey[400],
),
),
SizedBox(height: 16),
Text(
'Aucun élément dans l\'historique',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Text(
type == 'transaction'
? 'Vos transactions apparaîtront ici'
: 'Vos recharges apparaîtront ici',
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
Widget _buildEnhancedTransactionItem(
Map<String, dynamic> transaction,
int index,
) {
final serviceName = transaction['service_name'] ?? 'Service inconnu';
final montant = (transaction['montant_total_debite'] ?? 0.0).toDouble();
final commission = (transaction['commission_agent'] ?? 0.0).toDouble();
final statut = transaction['status'] ?? 'unknown';
final date =
DateTime.tryParse(transaction['created_at'] ?? '') ?? DateTime.now();
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 600 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutBack,
builder: (context, animation, child) {
final clampedAnimation = animation.clamp(0.0, 1.0);
return Transform.translate(
offset: Offset(30 * (1 - clampedAnimation), 0),
child: Opacity(
opacity: clampedAnimation,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _getStatusColor(statut).withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: _getStatusColor(statut).withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getStatusColor(statut),
_getStatusColor(statut).withOpacity(0.7),
],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: _getStatusColor(statut).withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Icon(
_getServiceIcon(serviceName),
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
serviceName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4),
Row(
children: [
Text(
'${montant.toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
Text(
'',
style: TextStyle(color: Colors.grey[500]),
),
Text(
'Commission: ${commission.toStringAsFixed(0)} XAF',
style: TextStyle(
fontSize: 12,
color: AppColors.success,
fontWeight: FontWeight.w500,
),
),
],
),
SizedBox(height: 4),
Text(
_formatDate(date),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(statut).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _getStatusColor(statut).withOpacity(0.3),
),
),
child: Text(
_getStatusText(statut),
style: TextStyle(
fontSize: 11,
color: _getStatusColor(statut),
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
},
);
}
Widget _buildEnhancedRechargeHistoryItem(
Map<String, dynamic> recharge,
int index,
) {
final montant = double.tryParse(recharge['montant'].toString()) ?? 0.0;
final status = recharge['status'] ?? '';
final date =
DateTime.tryParse(recharge['created_at'] ?? '') ?? DateTime.now();
return TweenAnimationBuilder<double>(
duration: Duration(milliseconds: 600 + (index * 100)),
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeOutBack,
builder: (context, animation, child) {
final clampedAnimation = animation.clamp(0.0, 1.0);
return Transform.translate(
offset: Offset(30 * (1 - clampedAnimation), 0),
child: Opacity(
opacity: clampedAnimation,
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _getStatusColor(status).withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: _getStatusColor(status).withOpacity(0.1),
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_getStatusColor(status),
_getStatusColor(status).withOpacity(0.7),
],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: _getStatusColor(status).withOpacity(0.3),
blurRadius: 8,
offset: Offset(0, 4),
),
],
),
child: Icon(
Icons.add_circle,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recharge de ${montant.toStringAsFixed(0)} XAF',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 8),
Text(
_formatDate(date),
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _getStatusColor(status).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _getStatusColor(status).withOpacity(0.3),
),
),
child: Text(
_getStatusText(status),
style: TextStyle(
fontSize: 11,
color: _getStatusColor(status),
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
},
);
}
// ============= UTILITAIRES =============
IconData _getServiceIcon(String serviceName) {
if (serviceName.toLowerCase().contains('internet')) {
return Icons.wifi;
} else if (serviceName.toLowerCase().contains('électricité') ||
serviceName.toLowerCase().contains('pelisa')) {
return Icons.electrical_services;
} else if (serviceName.toLowerCase().contains('eau')) {
return Icons.water_drop;
} else {
return Icons.receipt_long;
}
}
String _getStatusText(String status) {
switch (status.toLowerCase()) {
case 'completed':
return 'Terminé';
case 'approved':
return 'Approuvé';
case 'pending':
return 'En attente';
case 'rejected':
return 'Rejeté';
case 'cancelled':
return 'Annulé';
default:
return status.toUpperCase();
}
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'completed':
case 'approved':
return AppColors.success;
case 'pending':
return Colors.orange;
case 'rejected':
case 'cancelled':
return Colors.red;
default:
return Colors.grey;
}
}
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} à ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
}
Future<void> _demandeRecharge(
String? userId,
String pin,
String montant,
BuildContext context,
) async {
if (userId == null) {
_showErrorSnackBar(context, 'Erreur: Utilisateur non connecté');
return;
}
try {
print(
'Envoi demande recharge - URL: $apiBaseUrl/recharge/create/agent/$userId',
);
print(
'Montant: $montant, PIN: ${pin.isNotEmpty ? "****(${pin.length} chars)" : "vide"}',
);
final response = await http.post(
Uri.parse('$apiBaseUrl/recharge/create/agent/$userId'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode({'pin': pin, 'montant': montant}),
);
print('Réponse recharge - Status: ${response.statusCode}');
print('Réponse recharge - Body: ${response.body}');
if (response.statusCode == 201 || response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data['success'] == true || data.containsKey('recharge')) {
_showSuccessDialog(
context,
'Demande de recharge envoyée avec succès',
);
} else {
_showErrorSnackBar(
context,
data['message'] ?? 'Erreur lors de la demande',
);
}
} else {
final responseBody = jsonDecode(response.body);
final errorMessage = responseBody['message'] ?? 'Erreur inconnue';
if (errorMessage.toLowerCase().contains('pin') ||
errorMessage.toLowerCase().contains('code') ||
response.statusCode == 401) {
_showErrorSnackBar(context, 'PIN incorrect, veuillez réessayer');
} else if (response.statusCode == 404) {
_showErrorSnackBar(
context,
'Service non disponible, contactez le support',
);
} else {
_showErrorSnackBar(
context,
'Erreur (${response.statusCode}): $errorMessage',
);
}
}
} on http.ClientException catch (e) {
print('Erreur réseau: $e');
_showErrorSnackBar(context, 'Erreur de connexion réseau');
} on FormatException catch (e) {
print('Erreur format JSON: $e');
_showErrorSnackBar(context, 'Erreur de format de réponse');
} catch (e) {
print('Erreur générale: $e');
_showErrorSnackBar(context, 'Une erreur inattendue s\'est produite');
}
}
void _showSupportDialog(BuildContext context) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.purple.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.headset_mic, color: AppColors.purple),
),
SizedBox(width: 12),
Text('Support Technique'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Besoin d\'aide ? Contactez notre équipe de support :',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
SizedBox(height: 16),
_buildSupportOption(
Icons.phone,
'Téléphone',
'50 05',
AppColors.success,
),
SizedBox(height: 12),
_buildSupportOption(
Icons.email,
'Email',
'support@wortis.cg',
AppColors.primary,
),
SizedBox(height: 12),
_buildSupportOption(
Icons.chat,
'WhatsApp',
'+242 06 755 0505',
Colors.green,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
],
),
);
}
Widget _buildSupportOption(
IconData icon,
String title,
String value,
Color color,
) {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Row(
children: [
Icon(icon, color: color, size: 20),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.grey[700],
),
),
Text(
value,
style: TextStyle(
fontSize: 14,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
);
}
void _showSuccessDialog(BuildContext context, String message) {
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: true,
builder:
(context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.success.withOpacity(0.1),
Colors.green[50]!,
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColors.success,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: AppColors.success.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 5),
),
],
),
child: Icon(Icons.check, color: Colors.white, size: 32),
),
SizedBox(height: 16),
Text(
'Succès !',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.success,
),
),
SizedBox(height: 8),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey[700]),
),
SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.success,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(vertical: 12),
),
child: Text(
'Parfait !',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
);
}
void _showErrorSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
void _logout(BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
title: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.logout, color: Colors.red),
),
SizedBox(width: 12),
Text('Déconnexion'),
],
),
content: Text(
'Êtes-vous sûr de vouloir vous déconnecter de votre compte agent ?',
style: TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
final authController = Provider.of<AuthController>(
context,
listen: false,
);
await authController.logout();
if (context.mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text('Déconnexion'),
),
],
);
},
);
}
// ============= SECTION PARAMÈTRES =============
Widget _buildSettingsSection(
bool isSmallScreen,
bool isMediumScreen,
bool isTablet,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 4, bottom: isSmallScreen ? 12 : 16),
child: Text(
'Paramètres Kiosque',
style: TextStyle(
fontSize: isSmallScreen ? 18 : (isMediumScreen ? 19 : 20),
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
Container(
padding: EdgeInsets.all(isSmallScreen ? 16 : 20),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(isSmallScreen ? 16 : 20),
border: Border.all(
color: AppColors.primary.withOpacity(0.1),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: isSmallScreen ? 10 : 15,
offset: Offset(0, isSmallScreen ? 3 : 5),
),
],
),
child: Column(
children: [
_buildSettingTile(
icon: Icon(Icons.wifi_find, size: 24.0, color: Colors.blue),
title: 'Paramètres WiFi',
subtitle: 'Changer de réseau WiFi',
color: Colors.blue,
onTap: () => _openWifiSettings(),
isSmallScreen: isSmallScreen,
),
SizedBox(height: isSmallScreen ? 12 : 16),
Divider(color: Colors.grey[200], height: 1),
SizedBox(height: isSmallScreen ? 12 : 16),
_buildSettingTile(
icon: Icon(
Icons.brightness_6,
size: 24.0,
color: Colors.orange,
),
title: 'Luminosité',
subtitle: 'Ajuster la luminosité de l\'écran',
color: Colors.orange,
onTap: () => _showBrightnessDialog(context),
isSmallScreen: isSmallScreen,
),
SizedBox(height: isSmallScreen ? 12 : 16),
Divider(color: Colors.grey[200], height: 1),
SizedBox(height: isSmallScreen ? 12 : 16),
_buildSettingTile(
icon: FaIcon(
FontAwesomeIcons.whatsapp,
size: 24.0,
color: Colors.green,
),
title: 'Ouvrir WhatsApp',
subtitle: 'Ouvrir l\'application WhatsApp',
color: Colors.green,
onTap: () => _openWhatsApp(),
isSmallScreen: isSmallScreen,
),
],
),
),
],
);
}
Widget _buildSettingTile({
required Widget icon,
required String title,
required String subtitle,
required Color color,
required VoidCallback onTap,
required bool isSmallScreen,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.all(isSmallScreen ? 12 : 16),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2), width: 1),
),
child: Row(
children: [
Container(
width: isSmallScreen ? 40 : 48,
height: isSmallScreen ? 40 : 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Center(child: icon),
),
SizedBox(width: isSmallScreen ? 12 : 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: isSmallScreen ? 14 : 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: isSmallScreen ? 11 : 13,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: color,
size: isSmallScreen ? 14 : 16,
),
],
),
),
);
}
// ============= MÉTHODES D'ACTION =============
void _openWifiSettings() async {
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('exitKioskMode');
await platform.invokeMethod('openWifiSettings');
Future.delayed(Duration(seconds: 2), () async {
try {
await platform.invokeMethod('enableKioskMode');
} catch (e) {
print('Erreur réactivation kiosque: $e');
}
});
} catch (e) {
print('Erreur ouverture WiFi: $e');
_showErrorSnackBar(context, 'Impossible d\'ouvrir les paramètres WiFi');
}
}
void _showBrightnessDialog(BuildContext context) async {
double currentBrightness = 0.5;
try {
const platform = MethodChannel('com.wortis.agent/settings');
final brightness = await platform.invokeMethod('getSystemBrightness');
currentBrightness = (brightness as double).clamp(0.0, 1.0);
} catch (e) {
print('Erreur récupération luminosité: $e');
}
if (!mounted) return;
showDialog(
context: context,
builder:
(context) => StatefulBuilder(
builder:
(context, setState) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
title: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.brightness_6, color: Colors.orange),
),
SizedBox(width: 12),
Text('Réglage Luminosité'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Ajustez la luminosité de l\'écran',
style: TextStyle(color: Colors.grey[600]),
),
SizedBox(height: 20),
Row(
children: [
Icon(Icons.brightness_low, color: Colors.grey),
Expanded(
child: Slider(
value: currentBrightness,
onChanged: (value) {
setState(() {
currentBrightness = value;
});
_setBrightness(value);
},
activeColor: Colors.orange,
inactiveColor: Colors.orange.withOpacity(0.3),
),
),
Icon(Icons.brightness_high, color: Colors.orange),
],
),
SizedBox(height: 8),
Text(
'${(currentBrightness * 100).round()}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
],
),
),
);
}
void _setBrightness(double brightness) async {
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('setBrightness', {'brightness': brightness});
} catch (e) {
print('Erreur réglage luminosité: $e');
if (mounted) {
_showErrorSnackBar(context, 'Réglage de luminosité non disponible');
}
}
}
void _openWhatsApp() async {
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('exitKioskMode');
await platform.invokeMethod('openWhatsApp');
} catch (e) {
print('Erreur ouverture WhatsApp: $e');
try {
const platform = MethodChannel('com.wortis.agent/settings');
await platform.invokeMethod('enableKioskMode');
} catch (e2) {
print('Erreur réactivation kiosque: $e2');
}
if (mounted) {
_showErrorSnackBar(
context,
'WhatsApp non disponible ou erreur d\'ouverture',
);
}
}
}
}