import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:youmazgestion/Components/appDrawer.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/controller/userController.dart'; import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/client.dart'; import 'package:youmazgestion/Models/produit.dart'; // Ajout de l'import manquant import 'package:intl/intl.dart'; class DashboardPage extends StatefulWidget { @override _DashboardPageState createState() => _DashboardPageState(); } class _DashboardPageState extends State with SingleTickerProviderStateMixin { DateTimeRange? _dateRange; bool _showOnlyToday = false; final AppDatabase _database = AppDatabase.instance; final UserController _userController = Get.find(); final GlobalKey _recentClientsKey = GlobalKey(); final GlobalKey _recentOrdersKey = GlobalKey(); final GlobalKey _lowStockKey = GlobalKey(); final GlobalKey _salesChartKey = GlobalKey(); late Future> _statsFuture; late Future> _recentOrdersFuture; late Future> _lowStockProductsFuture; late Future> _recentClientsFuture; late Future> _allOrdersFuture; late Future> _productsByCategoryFuture; late AnimationController _animationController; late Animation _fadeAnimation; @override void initState() { super.initState(); _loadData(); _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 800), ); _fadeAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ), ); // Démarrer l'animation après un léger délai Future.delayed(Duration(milliseconds: 50), () { if (mounted) { _animationController.forward(); } }); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _loadData() { _statsFuture = _database.getStatistiques(); // Plus besoin de calcul supplémentaire _recentOrdersFuture = _database.getCommandes().then((orders) => orders.take(5).toList()); _lowStockProductsFuture = _database.getProducts().then((products) { return products.where((p) => (p.stock ?? 0) < 10).toList(); }); _recentClientsFuture = _database.getClients().then((clients) => clients.take(5).toList()); _allOrdersFuture = _database.getCommandes(); _productsByCategoryFuture = _database.getProductCountByCategory(); } Future _showCategoryProductsDialog(String category) async { final products = await _database.getProductsByCategory(category); showDialog( context: context, builder: (context) => AlertDialog( title: Text('Produits dans $category'), content: Container( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return ListTile( leading: product.image != null && product.image!.isNotEmpty ? CircleAvatar(backgroundImage: NetworkImage(product.image!)) : CircleAvatar(child: Icon(Icons.inventory)), title: Text(product.name), subtitle: Text('Stock: ${product.stock}'), trailing: Text('${NumberFormat('#,##0', 'fr_FR').format(product.price)} MGA'), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Fermer'), ), ], ), ); } List> _groupOrdersByMonth(List orders) { final Map monthlySales = {}; for (final order in orders) { final monthYear = '${order.dateCommande.year}-${order.dateCommande.month.toString().padLeft(2, '0')}'; monthlySales.update( monthYear, (value) => value + order.montantTotal, ifAbsent: () => order.montantTotal, ); } return monthlySales.entries.map((entry) { return { 'month': entry.key, 'total': entry.value, }; }).toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Image.asset( 'assets/youmaz2.png', height: 40, // Ajustez la hauteur selon vos besoins ), centerTitle: true, elevation: 0, flexibleSpace: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [const Color.fromARGB(255, 15, 83, 160), const Color.fromARGB(255, 79, 165, 239)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ), actions: [ IconButton( icon: Icon(Icons.refresh, color: Colors.white), onPressed: () { _animationController.reset(); _loadData(); _animationController.forward(); }, ), ], ), drawer: CustomDrawer(), body: SingleChildScrollView( padding: EdgeInsets.all(16), child: FadeTransition( opacity: _fadeAnimation, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildUserInfo(), SizedBox(height: 20), _buildMiniStatistics(), SizedBox(height: 20), // Graphiques en ligne Row( children: [ Expanded( child: _buildSalesChart(), ), SizedBox(width: 16), Expanded( child: _buildStockChart(), ), ], ), SizedBox(height: 20), // Histogramme des catégories de produits _buildCategoryHistogram(), SizedBox(height: 20), // NOUVEAU: Widget des ventes par point de vente _buildVentesParPointDeVenteCard(), SizedBox(height: 20), // Section des données récentes _buildRecentDataSection(), ], ), ), ), ); } Widget _buildCategoryHistogram() { return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.category, color: Colors.blue), SizedBox(width: 8), Text( 'Produits par Catégorie', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: 16), Container( height: 200, child: FutureBuilder>( future: _productsByCategoryFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text('Aucune donnée disponible')); } final data = snapshot.data!; final categories = data.keys.toList(); final counts = data.values.toList(); return BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: counts.reduce((a, b) => a > b ? a : b).toDouble() * 1.2, barTouchData: BarTouchData( enabled: true, touchCallback: (FlTouchEvent event, response) { if (response != null && response.spot != null && event is FlTapUpEvent) { final category = categories[response.spot!.touchedBarGroupIndex]; _showCategoryProductsDialog(category); } }, touchTooltipData: BarTouchTooltipData( tooltipBgColor: Colors.blueGrey, getTooltipItem: (group, groupIndex, rod, rodIndex) { final category = categories[groupIndex]; final count = counts[groupIndex]; return BarTooltipItem( '$category\n$count produits', TextStyle(color: Colors.white), ); }, ), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < categories.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( categories[index].substring(0, 3).toUpperCase(), style: TextStyle( fontSize: 10, color: Colors.grey, ), ), ); } return Text(''); }, reservedSize: 40, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), style: TextStyle( fontSize: 10, color: Colors.grey, ), ); }, reservedSize: 40, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: true, border: Border.all( color: Colors.grey.withOpacity(0.3), width: 1, ), ), barGroups: categories.asMap().entries.map((entry) { final index = entry.key; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: counts[index].toDouble(), color: _getCategoryColor(index), width: 16, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: counts.reduce((a, b) => a > b ? a : b).toDouble() * 1.2, color: Colors.grey.withOpacity(0.1), ), ), ], showingTooltipIndicators: [0], ); }).toList(), ), ); }, ), ), ], ), ), ); } Color _getCategoryColor(int index) { final colors = [ Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal, Colors.pink, Colors.indigo, ]; return colors[index % colors.length]; } Widget _buildSalesChart() { return Card( key: _salesChartKey, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ... titre Container( height: 200, child: FutureBuilder>( future: _allOrdersFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.trending_up_outlined, size: 64, color: Colors.grey), SizedBox(height: 16), Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)), ], ), ); } final salesData = _groupOrdersByMonth(snapshot.data!); // Vérification si salesData est vide if (salesData.isEmpty) { return Center( child: Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)), ); } return BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2, barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipBgColor: Colors.blueGrey, getTooltipItem: (group, groupIndex, rod, rodIndex) { final month = salesData[groupIndex]['month']; final total = salesData[groupIndex]['total']; return BarTooltipItem( '$month\n${NumberFormat('#,##0', 'fr_FR').format(total)} MGA', TextStyle(color: Colors.white), ); }, ), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < salesData.length) { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( salesData[index]['month'].toString().split('-')[1], style: TextStyle( fontSize: 10, color: Colors.grey, ), ), ); } return Text(''); }, reservedSize: 40, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return Text( value.toInt().toString(), style: TextStyle( fontSize: 10, color: Colors.grey, ), ); }, reservedSize: 40, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: true, border: Border.all( color: Colors.grey.withOpacity(0.3), width: 1, ), ), barGroups: salesData.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: data['total'], color: Colors.blue, width: 16, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2, color: Colors.grey.withOpacity(0.1), ), ), ], showingTooltipIndicators: [0], ); }).toList(), ), ); }, ), ), ], ), ), ); } Widget _buildStockChart() { return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.inventory, color: Colors.blue), SizedBox(width: 8), Text( 'État du stock', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: 16), Container( height: 200, child: FutureBuilder>( future: _database.getProducts(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData) { return Center(child: Text('Aucune donnée disponible')); } final products = snapshot.data!; // Vérification si la liste est vide if (products.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey), SizedBox(height: 16), Text('Aucun produit en stock', style: TextStyle(color: Colors.grey)), ], ), ); } final lowStock = products.where((p) => (p.stock ?? 0) < 10).length; final inStock = products.length - lowStock; // Vérification pour éviter les sections vides List sections = []; if (lowStock > 0) { sections.add( PieChartSectionData( color: Colors.orange, value: lowStock.toDouble(), title: '$lowStock', radius: 20, titleStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white, ), ), ); } if (inStock > 0) { sections.add( PieChartSectionData( color: Colors.green, value: inStock.toDouble(), title: '$inStock', radius: 20, titleStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white, ), ), ); } // Si toutes les sections sont vides, afficher un message if (sections.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.info_outline, size: 64, color: Colors.grey), SizedBox(height: 16), Text('Aucune donnée de stock disponible', style: TextStyle(color: Colors.grey)), ], ), ); } return PieChart( PieChartData( sectionsSpace: 0, centerSpaceRadius: 40, sections: sections, pieTouchData: PieTouchData( enabled: true, // Activé pour permettre les interactions touchCallback: (FlTouchEvent event, pieTouchResponse) { // Gestion sécurisée des interactions if (pieTouchResponse != null && pieTouchResponse.touchedSection != null) { // Vous pouvez ajouter une logique ici si nécessaire } }, ), startDegreeOffset: 180, borderData: FlBorderData(show: false), ), ); }, ), ), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegendItem(Colors.orange, 'Stock faible'), SizedBox(width: 16), _buildLegendItem(Colors.green, 'En stock'), ], ), ], ), ), ); } Widget _buildLegendItem(Color color, String text) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), SizedBox(width: 4), Text( text, style: TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ); } Widget _buildUserInfo() { return FutureBuilder( future: _database.getUserById(_userController.userId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData) { return Text('Bienvenue'); } final user = snapshot.data!; return Row( children: [ CircleAvatar( radius: 30, backgroundColor: Colors.blue.shade100, child: Icon(Icons.person, size: 30, color: Colors.blue), ), SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Bienvenue, ${user.name}', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 4), Text( 'Rôle: ${user.roleName ?? 'Utilisateur'}', style: TextStyle(fontSize: 14, color: Colors.grey.shade600), ), ], ), ], ); }, ); } Widget _buildMiniStatistics() { return FutureBuilder>( future: _statsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Text('Erreur de chargement des statistiques'); } final stats = snapshot.data ?? {}; return Wrap( spacing: 12, runSpacing: 12, children: [ _buildMiniStatCard( title: 'Clients', value: '${stats['totalClients'] ?? 0}', icon: Icons.people, color: Colors.blue, ), _buildMiniStatCard( title: 'Commandes', value: '${stats['totalCommandes'] ?? 0}', icon: Icons.shopping_cart, color: Colors.green, ), _buildMiniStatCard( title: 'Produits', value: '${stats['totalProduits'] ?? 0}', icon: Icons.inventory, color: Colors.orange, ), _buildMiniStatCard( title: 'CA (MGA)', value: NumberFormat('#,##0', 'fr_FR').format(stats['chiffreAffaires'] ?? 0.0), icon: Icons.euro_symbol, color: Colors.purple, ), // ✅ NOUVELLE CARTE : Valeur totale du stock _buildMiniStatCard( title: 'Valeur Stock (MGA)', value: NumberFormat('#,##0', 'fr_FR').format(stats['valeurTotaleStock'] ?? 0.0), icon: Icons.inventory_2, color: Colors.teal, ), ], ); }, ); } Widget _buildMiniStatCard({required String title, required String value, required IconData icon, required Color color}) { return InkWell( onTap: () { // Animation au clic _animationController.reset(); _animationController.forward(); // Navigation based on the card type switch(title) { case 'Clients': // Scroll to recent clients section Scrollable.ensureVisible( _recentClientsKey.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); break; case 'Commandes': // Scroll to recent orders section Scrollable.ensureVisible( _recentOrdersKey.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); break; case 'Produits': // Scroll to low stock products section Scrollable.ensureVisible( _lowStockKey.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); break; case 'CA (MGA)': // Scroll to sales chart Scrollable.ensureVisible( _salesChartKey.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, ); break; } }, borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), ), child: Padding( padding: EdgeInsets.all(12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 32, color: color), SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: color, ), ), SizedBox(height: 4), Text( title, style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ), ), ); } Widget _buildRecentDataSection() { return Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _buildRecentOrdersCard()), SizedBox(width: 16), Expanded(child: _buildRecentClientsCard()), ], ), SizedBox(height: 16), _buildLowStockCard(), ], ); } Widget _buildRecentOrdersCard() { return Card( key: _recentOrdersKey, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.shopping_cart, color: Colors.green), SizedBox(width: 8), Text( 'Commandes récentes', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: 8), FutureBuilder>( future: _recentOrdersFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Padding( padding: EdgeInsets.all(8), child: Text('Aucune commande récente'), ); } final orders = snapshot.data!; return Column( children: orders.map((order) => FutureBuilder>( future: _database.getDetailsCommande(order.id!), builder: (context, detailsSnapshot) { if (detailsSnapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (detailsSnapshot.hasError || !detailsSnapshot.hasData || detailsSnapshot.data!.isEmpty) { return Padding( padding: EdgeInsets.all(8), child: Text('Aucun détail de commande disponible'), ); } final details = detailsSnapshot.data!; return InkWell( onTap: () { _animationController.reset(); _animationController.forward(); }, borderRadius: BorderRadius.circular(8), child: Container( margin: EdgeInsets.only(bottom: 8), padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: _getStatusColor(order.statut).withOpacity(0.2), child: Icon(Icons.receipt, color: _getStatusColor(order.statut)), ), title: Text( '${order.clientNomComplet}', style: TextStyle(fontSize: 14), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${NumberFormat('#,##0', 'fr_FR').format(order.montantTotal)} MGA', style: TextStyle(fontSize: 12), ), Text( '${order.dateCommande.toString().substring(0, 10)}', style: TextStyle(fontSize: 10, color: Colors.grey), ), ], ), trailing: Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _getStatusColor(order.statut).withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Text( order.statutLibelle, style: TextStyle( fontSize: 10, color: _getStatusColor(order.statut), ), ), ), ), // Affichage des produits commandés Column( crossAxisAlignment: CrossAxisAlignment.start, children: details.map((detail) => Padding( padding: EdgeInsets.only(left: 16, top: 4), child: Text( 'Produit: ${detail.produitNom}', style: TextStyle(fontSize: 12), ), )).toList(), ), ], ), ), ); }, )).toList(), ); }, ), ], ), ), ); } Widget _buildRecentClientsCard() { return Card( key: _recentClientsKey, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.people, color: Colors.blue), SizedBox(width: 8), Text( 'Clients récents', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: 8), FutureBuilder>( future: _recentClientsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Padding( padding: EdgeInsets.all(8), child: Text('Aucun client récent'), ); } final clients = snapshot.data!; return Column( children: clients.map((client) => InkWell( onTap: () { // Animation et action au clic _animationController.reset(); _animationController.forward(); // Vous pouvez ajouter une navigation vers le client ici }, borderRadius: BorderRadius.circular(8), child: Container( margin: EdgeInsets.only(bottom: 8), padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: ListTile( contentPadding: EdgeInsets.zero, leading: CircleAvatar( backgroundColor: Colors.blue.shade100, child: Icon(Icons.person, color: Colors.blue), ), title: Text( client.nomComplet.split(' ').first, style: TextStyle(fontSize: 14), ), subtitle: Text( client.email, style: TextStyle(fontSize: 12), overflow: TextOverflow.ellipsis, ), ), ), )).toList(), ); }, ), ], ), ), ); } //widget vente // 2. Ajoutez cette méthode dans la classe _DashboardPageState Widget _buildVentesParPointDeVenteCard() { return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.store, color: Colors.purple), SizedBox(width: 8), Text( 'Ventes par Point de Vente', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Spacer(), // Boutons de filtre dans le header Row( children: [ IconButton( onPressed: _toggleTodayFilter, icon: Icon( _showOnlyToday ? Icons.today : Icons.calendar_today, color: _showOnlyToday ? Colors.green : Colors.grey, ), tooltip: _showOnlyToday ? 'Toutes les dates' : 'Aujourd\'hui seulement', ), IconButton( onPressed: () => _selectDateRange(context), icon: Icon( Icons.date_range, color: _dateRange != null ? Colors.orange : Colors.grey, ), tooltip: 'Sélectionner une période', ), ], ), ], ), // Affichage de la période sélectionnée if (_showOnlyToday || _dateRange != null) Padding( padding: EdgeInsets.only(bottom: 8), child: Container( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.1), borderRadius: BorderRadius.circular(16), border: Border.all( color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _showOnlyToday ? Icons.today : Icons.date_range, size: 16, color: _showOnlyToday ? Colors.green : Colors.orange, ), SizedBox(width: 4), Text( _showOnlyToday ? 'Aujourd\'hui' : _dateRange != null ? '${DateFormat('dd/MM/yyyy').format(_dateRange!.start)} - ${DateFormat('dd/MM/yyyy').format(_dateRange!.end)}' : 'Toutes les dates', style: TextStyle( fontSize: 12, color: _showOnlyToday ? Colors.green : Colors.orange, fontWeight: FontWeight.w500, ), ), SizedBox(width: 4), InkWell( onTap: () { setState(() { _showOnlyToday = false; _dateRange = null; }); }, child: Icon( Icons.close, size: 16, color: _showOnlyToday ? Colors.green : Colors.orange, ), ), ], ), ), ), SizedBox(height: 16), Container( height: 400, child: FutureBuilder>>( future: _database.getVentesParPointDeVente( dateDebut: _dateRange?.start, dateFin: _dateRange?.end, aujourdHuiSeulement: _showOnlyToday, ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.store_mall_directory_outlined, size: 64, color: Colors.grey), SizedBox(height: 16), Text( 'Aucune donnée de vente${_showOnlyToday ? ' pour aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', style: TextStyle(color: Colors.grey), textAlign: TextAlign.center, ), ], ), ); } final ventesData = snapshot.data!; return SingleChildScrollView( child: Column( children: [ // Graphique en barres des chiffres d'affaires Container( height: 200, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: _getMaxChiffreAffaires(ventesData) * 1.2, barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData( tooltipBgColor: Colors.blueGrey, getTooltipItem: (group, groupIndex, rod, rodIndex) { final pointVente = ventesData[groupIndex]; final ca = pointVente['chiffre_affaires'] ?? 0.0; final nbCommandes = pointVente['nombre_commandes'] ?? 0; return BarTooltipItem( '${pointVente['point_vente_nom']}\n${NumberFormat('#,##0', 'fr_FR').format(ca)} MGA\n$nbCommandes commandes', TextStyle(color: Colors.white, fontSize: 12), ); }, ), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final index = value.toInt(); if (index >= 0 && index < ventesData.length) { final nom = ventesData[index]['point_vente_nom'] as String; return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( nom.length > 5 ? nom.substring(0, 5) : nom, style: TextStyle( fontSize: 10, color: Colors.grey, ), ), ); } return Text(''); }, reservedSize: 40, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { return Text( _formatCurrency(value), style: TextStyle( fontSize: 10, color: Colors.grey, ), ); }, reservedSize: 60, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: true, border: Border.all( color: Colors.grey.withOpacity(0.3), width: 1, ), ), barGroups: ventesData.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; final ca = (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: ca, color: _getPointVenteColor(index), width: 16, borderRadius: BorderRadius.circular(4), backDrawRodData: BackgroundBarChartRodData( show: true, toY: _getMaxChiffreAffaires(ventesData) * 1.2, color: Colors.grey.withOpacity(0.1), ), ), ], showingTooltipIndicators: [0], ); }).toList(), ), ), ), SizedBox(height: 20), // Tableau détaillé _buildTableauVentesPointDeVente(ventesData), ], ), ); }, ), ), ], ), ), ); } Widget _buildTableauVentesPointDeVente(List> ventesData) { return Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.withOpacity(0.3)), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ // En-tête du tableau Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), ), child: Row( children: [ Expanded(flex: 2, child: Text('Point de Vente', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), Expanded(flex: 2, child: Text('CA (MGA)', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), Expanded(flex: 1, child: Text('Cmd', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), Expanded(flex: 1, child: Text('Articles', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), Expanded(flex: 2, child: Text('Panier Moy.', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12))), ], ), ), // Lignes du tableau ...ventesData.asMap().entries.map((entry) { final index = entry.key; final data = entry.value; final isEven = index % 2 == 0; return InkWell( onTap: () => _showPointVenteDetails(data), child: Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: isEven ? Colors.grey.withOpacity(0.05) : Colors.white, ), child: Row( children: [ Expanded( flex: 2, child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: _getPointVenteColor(index), borderRadius: BorderRadius.circular(2), ), ), SizedBox(width: 8), Expanded( child: Text( data['point_vente_nom'] ?? 'N/A', style: TextStyle(fontSize: 12), overflow: TextOverflow.ellipsis, ), ), ], ), ), Expanded( flex: 2, child: Text( NumberFormat('#,##0.00', 'fr_FR').format( (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0, ), style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), ), Expanded( flex: 1, child: Text( '${data['nombre_commandes'] ?? 0}', style: TextStyle(fontSize: 12), ), ), Expanded( flex: 1, child: Text( '${data['nombre_articles_vendus'] ?? 0}', style: TextStyle(fontSize: 12), ), ), Expanded( flex: 2, child: Text( NumberFormat('#,##0.00', 'fr_FR').format( (data['panier_moyen'] as num?)?.toDouble() ?? 0.0, ), style: TextStyle(fontSize: 12), ), ), ], ), ), ); }).toList(), ], ), ); } // Méthodes utilitaires double _getMaxChiffreAffaires(List> ventesData) { if (ventesData.isEmpty) return 100.0; return ventesData .map((data) => (data['chiffre_affaires'] as num?)?.toDouble() ?? 0.0) .reduce((a, b) => a > b ? a : b); } Color _getPointVenteColor(int index) { final colors = [ Colors.blue, Colors.green, Colors.orange, Colors.purple, Colors.teal, Colors.pink, Colors.indigo, Colors.amber, Colors.cyan, Colors.lime, ]; return colors[index % colors.length]; } String _formatCurrency(double value) { if (value >= 1000000) { return '${(value / 1000000).toStringAsFixed(1)}M'; } else if (value >= 1000) { return '${(value / 1000).toStringAsFixed(1)}K'; } else { return value.toStringAsFixed(0); } } Future _selectDateRange(BuildContext context) async { final DateTimeRange? picked = await showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime.now().add(const Duration(days: 365)), initialDateRange: _dateRange ?? DateTimeRange( start: DateTime.now().subtract(const Duration(days: 30)), end: DateTime.now(), ), ); if (picked != null) { setState(() { _dateRange = picked; }); } } void _toggleTodayFilter() { setState(() { _showOnlyToday = !_showOnlyToday; if (_showOnlyToday) { _dateRange = null; // Reset date range when showing only today } }); } // NOUVELLES VARIABLES pour les filtres de sorties DateTimeRange? _sortiesDateRange; bool _sortiesShowOnlyToday = false; // ... autres variables existantes // NOUVELLE MÉTHODE: Sélectionner la période pour les sorties Future _selectSortiesDateRange(BuildContext context, StateSetter setDialogState) async { final DateTimeRange? picked = await showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime.now().add(const Duration(days: 365)), initialDateRange: _sortiesDateRange ?? DateTimeRange( start: DateTime.now().subtract(const Duration(days: 30)), end: DateTime.now(), ), ); if (picked != null) { setDialogState(() { _sortiesDateRange = picked; _sortiesShowOnlyToday = false; }); } } // NOUVELLE MÉTHODE: Toggle du filtre "aujourd'hui" pour les sorties void _toggleSortiesTodayFilter(StateSetter setDialogState) { setDialogState(() { _sortiesShowOnlyToday = !_sortiesShowOnlyToday; if (_sortiesShowOnlyToday) { _sortiesDateRange = null; } }); } // MÉTHODE MISE À JOUR: _showPointVenteDetails avec les nouveaux filtres void _showPointVenteDetails(Map pointVenteData) async { final isMobile = MediaQuery.of(context).size.width < 600; final pointVenteId = pointVenteData['point_vente_id'] as int; final pointVenteNom = pointVenteData['point_vente_nom'] as String; // VARIABLE LOCALE pour gérer l'expansion des cartes dans ce dialog uniquement Set dialogExpandedCommandes = {}; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => AlertDialog( title: Text('Détails - $pointVenteNom'), content: Container( width: double.maxFinite, height: 600, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // FILTRES UNIFIÉS Text('Filtres généraux:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: () { setDialogState(() { _showOnlyToday = !_showOnlyToday; if (_showOnlyToday) _dateRange = null; }); setState(() {}); // Pour rafraîchir le dashboard principal aussi }, icon: Icon( _showOnlyToday ? Icons.today : Icons.calendar_today, size: 20, ), label: Text(_showOnlyToday ? (isMobile ? 'Toutes dates' : 'Toutes les dates') : (isMobile ? 'Aujourd\'hui' : 'Aujourd\'hui seulement')), style: ElevatedButton.styleFrom( backgroundColor: _showOnlyToday ? Colors.green.shade600 : Colors.blue.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 12 : 16, vertical: 8, ), ), ), ElevatedButton.icon( onPressed: () async { final DateTimeRange? picked = await showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime.now().add(const Duration(days: 365)), initialDateRange: _dateRange ?? DateTimeRange( start: DateTime.now().subtract(const Duration(days: 30)), end: DateTime.now(), ), ); if (picked != null) { setDialogState(() { _dateRange = picked; _showOnlyToday = false; }); setState(() {}); // Pour rafraîchir le dashboard principal aussi } }, icon: const Icon(Icons.date_range, size: 20), label: Text(_dateRange != null ? (isMobile ? 'Période sélectionnée' : 'Période sélectionnée') : (isMobile ? 'Choisir période' : 'Choisir période')), style: ElevatedButton.styleFrom( backgroundColor: _dateRange != null ? Colors.orange.shade600 : Colors.grey.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 12 : 16, vertical: 8, ), ), ), if (_showOnlyToday || _dateRange != null) ElevatedButton.icon( onPressed: () { setDialogState(() { _showOnlyToday = false; _dateRange = null; }); setState(() {}); // Pour rafraîchir le dashboard principal aussi }, icon: const Icon(Icons.clear, size: 20), label: Text('Reset', style: TextStyle(fontSize: 12)), style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade600, foregroundColor: Colors.white, padding: EdgeInsets.symmetric( horizontal: isMobile ? 10 : 12, vertical: 8, ), ), ), ], ), // Affichage de la période sélectionnée if (_showOnlyToday || _dateRange != null) Container( margin: EdgeInsets.only(top: 12, bottom: 8), padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.1), borderRadius: BorderRadius.circular(16), border: Border.all( color: (_showOnlyToday ? Colors.green : Colors.orange).withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _showOnlyToday ? Icons.today : Icons.date_range, size: 16, color: _showOnlyToday ? Colors.green : Colors.orange, ), SizedBox(width: 6), Flexible( child: Text( _showOnlyToday ? 'Filtré sur aujourd\'hui' : _dateRange != null ? 'Du ${DateFormat('dd/MM/yyyy').format(_dateRange!.start)} au ${DateFormat('dd/MM/yyyy').format(_dateRange!.end)}' : 'Toutes les dates', style: TextStyle( fontSize: 12, color: _showOnlyToday ? Colors.green : Colors.orange, fontWeight: FontWeight.w500, ), ), ), ], ), ), SizedBox(height: 12), // Statistiques générales FutureBuilder>( future: _getDetailedPointVenteStats(pointVenteId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData) { return Text('Erreur de chargement des statistiques'); } final stats = snapshot.data!; return Column( children: [ _buildStatRow('Chiffre d\'affaires:', '${NumberFormat('#,##0.00', 'fr_FR').format((stats['chiffre_affaires'] as num?)?.toDouble() ?? 0.0)} MGA'), _buildStatRow('Nombre de commandes:', '${stats['nombre_commandes'] ?? 0}'), _buildStatRow('Articles vendus:', '${stats['nombre_articles_vendus'] ?? 0}'), _buildStatRow('Quantité totale:', '${stats['quantite_totale_vendue'] ?? 0}'), _buildStatRow('Panier moyen:', '${NumberFormat('#,##0.00', 'fr_FR').format((stats['panier_moyen'] as num?)?.toDouble() ?? 0.0)} MGA'), ], ); }, ), SizedBox(height: 12), // SECTION: Liste des commandes SizedBox(height: 16), Divider(), Text('Commandes récentes (max 10):', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), FutureBuilder>>( future: _database.getCommandesParPointDeVente( pointVenteId, dateDebut: _dateRange?.start, dateFin: _dateRange?.end, aujourdHuiSeulement: _showOnlyToday, limit: 10, ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Text('Erreur de chargement des commandes', style: TextStyle(color: Colors.red)); } if (!snapshot.hasData || snapshot.data!.isEmpty) { String message = 'Aucune commande trouvée'; if (_showOnlyToday) { message = 'Aucune commande aujourd\'hui'; } else if (_dateRange != null) { message = 'Aucune commande pour cette période'; } return Text(message, style: TextStyle(color: Colors.grey)); } final commandes = snapshot.data! .map((map) => Commande.fromMap(map)) .toList(); return Column( children: commandes.map((commande) => _buildCommandeCardForDialog(commande, dialogExpandedCommandes, setDialogState) ).toList(), ); }, ), // Section des sorties personnelles SizedBox(height: 16), Divider(), Text('Historique des sorties personnelles:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), FutureBuilder>>( future: _database.getHistoriqueSortiesPersonnelles( pointDeVenteId: pointVenteId, dateDebut: _dateRange?.start, dateFin: _dateRange?.end, aujourdHuiSeulement: _showOnlyToday, limit: 10, ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Text('Erreur de chargement des sorties', style: TextStyle(color: Colors.red)); } if (!snapshot.hasData || snapshot.data!.isEmpty) { String message = 'Aucune sortie personnelle trouvée'; if (_showOnlyToday) { message = 'Aucune sortie aujourd\'hui'; } else if (_dateRange != null) { message = 'Aucune sortie pour cette période'; } return Text(message, style: TextStyle(color: Colors.grey)); } final sorties = snapshot.data!; return Column( children: sorties.map((sortie) => Card( margin: EdgeInsets.symmetric(vertical: 2), child: Padding( padding: EdgeInsets.all(6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( sortie['produit_nom'] ?? 'N/A', style: TextStyle( fontWeight: FontWeight.w500, fontSize: 14, ), overflow: TextOverflow.ellipsis, ), ), Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _getStatusColorForSorties(sortie['statut']), borderRadius: BorderRadius.circular(12), ), child: Text( sortie['statut'] ?? 'N/A', style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ], ), SizedBox(height: 3), Row( children: [ Icon(Icons.inventory, size: 14, color: Colors.grey), SizedBox(width: 4), Text( 'Qté: ${sortie['quantite'] ?? 0}', style: TextStyle(fontSize: 14, color: Colors.black), ), SizedBox(width: 128), Icon(Icons.person, size: 14, color: Colors.grey), SizedBox(width: 4), Expanded( child: Text( '${sortie['admin_nom'] ?? ''} ${sortie['admin_nom_famille'] ?? ''}', style: TextStyle(fontSize: 14, color: Colors.grey), overflow: TextOverflow.ellipsis, ), ), ], ), if (sortie['date_sortie'] != null) ...[ SizedBox(height: 2), Row( children: [ Icon(Icons.access_time, size: 14, color: Colors.grey), SizedBox(width: 4), Text( _formatDateSortie(sortie['date_sortie']), style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ], if (sortie['motif'] != null && sortie['motif'].toString().isNotEmpty) ...[ SizedBox(height: 3), Text( 'Motif: ${sortie['motif']}', style: TextStyle( fontSize: 11, color: Colors.grey[600], fontStyle: FontStyle.italic, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ], ), ), )).toList(), ); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Fermer'), ), ], ), ), ); } Widget _buildCommandeCardForDialog( Commande commande, Set expandedCommandes, StateSetter setDialogState, ) { final int commandeId = commande.id ?? -1; // fallback si null final bool isExpanded = expandedCommandes.contains(commandeId); return FutureBuilder>( future: _database.getDetailsCommande(commandeId), builder: (context, snapshot) { double totalRemises = 0; bool aDesRemises = false; if (snapshot.hasData) { for (final detail in snapshot.data!) { totalRemises += detail.montantRemise; if (detail.aRemise) aDesRemises = true; } } return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, margin: const EdgeInsets.symmetric(vertical: 4), child: Material( elevation: isExpanded ? 4 : 2, borderRadius: BorderRadius.circular(8), child: Container( decoration: BoxDecoration( color: isExpanded ? Colors.grey.shade100 : Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: aDesRemises ? Border.all(color: Colors.orange.shade300, width: 1) : null, ), child: InkWell( onTap: () { setDialogState(() { if (isExpanded) { expandedCommandes.remove(commandeId); } else { expandedCommandes.add(commandeId); } }); }, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // En-tête Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: _getStatutColor(commande.statut).withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: aDesRemises ? Border.all(color: Colors.orange.shade300) : null, ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( aDesRemises ? Icons.discount : _getStatutIcon(commande.statut), size: 16, color: aDesRemises ? Colors.teal.shade700 : commande.statut == StatutCommande.annulee ? Colors.red : Colors.blue.shade600, ), Text( '#$commandeId', style: const TextStyle( fontSize: 8, fontWeight: FontWeight.bold, ), ), ], ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( commande.clientNomComplet, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Row( children: [ const Icon(Icons.calendar_today, size: 12, color: Colors.grey), const SizedBox(width: 4), Text( DateFormat('dd/MM/yyyy').format(commande.dateCommande), style: const TextStyle(fontSize: 11, color: Colors.grey), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getStatutColor(commande.statut).withOpacity(0.2), borderRadius: BorderRadius.circular(10), ), child: Text( commande.statutLibelle, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: commande.statut == StatutCommande.annulee ? Colors.red : Colors.blue.shade700, ), ), ), ], ), ], ), ), AnimatedRotation( turns: isExpanded ? 0.5 : 0, duration: const Duration(milliseconds: 300), child: Icon( Icons.expand_more, color: Colors.grey[600], ), ), ], ), const SizedBox(height: 8), // Montant Row( children: [ Icon(Icons.attach_money, size: 14, color: Colors.green.shade600), const SizedBox(width: 4), Text( '${NumberFormat('#,##0', 'fr_FR').format(commande.montantTotal)} MGA', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.green.shade700, ), ), if (totalRemises > 0) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.discount, size: 10, color: Colors.teal.shade700), const SizedBox(width: 2), Text( '-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)}', style: const TextStyle( fontSize: 9, fontWeight: FontWeight.bold, ), ), ], ), ), ] ], ), // Contenu étendu AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, child: isExpanded ? Column( children: [ const SizedBox(height: 12), Divider(color: Colors.grey[300]), if (commande.commandeurnom?.isNotEmpty ?? false) Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( children: [ Icon(Icons.person, size: 16, color: Colors.blue[600]), const SizedBox(width: 8), Text( '${commande.commandeurnom ?? ''} ${commande.commandeurPrenom ?? ''}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blue[700], ), ), ], ), ), const SizedBox(height: 8), if (snapshot.hasData && snapshot.data!.isNotEmpty) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.shopping_cart, size: 16, color: Colors.green[600]), const SizedBox(width: 8), Text( 'Produits commandés:', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green[700], ), ), ], ), const SizedBox(height: 6), ...snapshot.data!.map((detail) => Padding( padding: const EdgeInsets.only(left: 24, bottom: 4), child: Row( children: [ const Icon(Icons.circle, size: 4, color: Colors.green), const SizedBox(width: 8), Expanded( child: Text( '${detail.produitNom} (x${detail.quantite})', style: TextStyle( fontSize: 11, color: Colors.green[700], ), ), ), Text( '${NumberFormat('#,##0', 'fr_FR').format(detail.sousTotal)} MGA', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.green[700], ), ), ], ), )) ], ), ], ) : const SizedBox.shrink(), ), ], ), ), ), ), ), ); }, ); } Color _getStatutColor(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return Colors.orange.shade100; case StatutCommande.confirmee: return Colors.blue.shade100; case StatutCommande.annulee: return Colors.red.shade100; default: return Colors.grey.shade100; } } IconData _getStatutIcon(StatutCommande statut) { switch (statut) { case StatutCommande.enAttente: return Icons.pending; case StatutCommande.confirmee: return Icons.check_circle; case StatutCommande.annulee: return Icons.cancel; default: return Icons.help; } } // Méthodes utilitaires à ajouter dans votre classe Color _getStatusColorForSorties(String? statut) { switch (statut?.toLowerCase()) { case 'en_attente': return Colors.orange; case 'approuve': return Colors.green; case 'refuse': return Colors.red; default: return Colors.grey; } } // Version corrigée de _formatDate qui gère à la fois DateTime et String String _formatDateSortie(dynamic dateValue) { if (dateValue == null) return 'N/A'; DateTime? date; // Gestion des différents types de données if (dateValue is DateTime) { date = dateValue; } else if (dateValue is String) { if (dateValue.isEmpty) return 'N/A'; try { // Essayer différents formats de parsing if (dateValue.contains('T')) { // Format ISO 8601 (ex: 2024-01-15T10:30:00.000Z) date = DateTime.parse(dateValue); } else if (dateValue.contains('-') && dateValue.contains(':')) { // Format personnalisé (ex: 15-01-2024 10:30:00) final inputFormat = DateFormat('dd-MM-yyyy HH:mm:ss'); date = inputFormat.parse(dateValue); } else { // Essayer le parsing par défaut date = DateTime.tryParse(dateValue); } } catch (e) { print('Erreur de parsing de date: $e pour la valeur: $dateValue'); return 'Date invalide'; } } else { return 'Format de date non supporté'; } if (date == null) return 'Date invalide'; try { final outputFormat = DateFormat('dd/MM/yyyy HH:mm'); return outputFormat.format(date); } catch (e) { print('Erreur de formatage de date: $e'); return 'Erreur de format'; } } // Version alternative plus robuste String _formatDateSortieAlternative(dynamic dateValue) { if (dateValue == null) return 'N/A'; try { DateTime date; if (dateValue is DateTime) { date = dateValue; } else if (dateValue is String) { // Convertir en DateTime selon différents formats possibles if (dateValue.contains('T')) { date = DateTime.parse(dateValue); } else { // Essayer le format dd-MM-yyyy HH:mm:ss final parts = dateValue.split(' '); if (parts.length >= 2) { final datePart = parts[0]; final timePart = parts[1]; final dateComponents = datePart.split('-'); final timeComponents = timePart.split(':'); if (dateComponents.length == 3 && timeComponents.length >= 2) { date = DateTime( int.parse(dateComponents[2]), // année int.parse(dateComponents[1]), // mois int.parse(dateComponents[0]), // jour int.parse(timeComponents[0]), // heure int.parse(timeComponents[1]), // minute timeComponents.length > 2 ? int.parse(timeComponents[2].split('.')[0]) : 0, // seconde ); } else { date = DateTime.now(); } } else { date = DateTime.parse(dateValue); } } } else { return 'Format non supporté'; } return DateFormat('dd/MM/yyyy HH:mm').format(date); } catch (e) { print('Erreur de formatage de date: $e pour la valeur: $dateValue'); return 'Format invalide'; } } Future> _getDetailedPointVenteStats(int pointVenteId) async { final ventesData = await _database.getVentesParPointDeVente( dateDebut: _dateRange?.start, dateFin: _dateRange?.end, aujourdHuiSeulement: _showOnlyToday, ); final pointVenteStats = ventesData.firstWhere( (data) => data['point_vente_id'] == pointVenteId, orElse: () => { 'chiffre_affaires': 0.0, 'nombre_commandes': 0, 'nombre_articles_vendus': 0, 'quantite_totale_vendue': 0, 'panier_moyen': 0.0, }, ); return pointVenteStats; } Widget _buildStatRow(String title, String value) { return Container( padding: EdgeInsets.symmetric(vertical: 2, horizontal: 4), // Padding minimal child: Row( children: [ Text( title, style: TextStyle( fontWeight: FontWeight.w500, fontSize: 15, ), ), SizedBox(width: 6), // Espace minimal entre titre et valeur Expanded( child: Text( value, textAlign: TextAlign.right, style: TextStyle( fontSize: 15, color: Colors.grey[700], ), overflow: TextOverflow.ellipsis, ), ), ], ), ); } Widget _buildLowStockCard() { return Card( key: _lowStockKey, elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.warning, color: Colors.orange), SizedBox(width: 8), Text( 'Produits en rupture', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), SizedBox(height: 8), FutureBuilder>( future: _lowStockProductsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Padding( padding: EdgeInsets.all(8), child: Text('Aucun produit en rupture de stock'), ); } final products = snapshot.data!; return Column( children: products.map((product) => InkWell( onTap: () { // Animation et action au clic _animationController.reset(); _animationController.forward(); // Vous pouvez ajouter une navigation vers le produit ici }, borderRadius: BorderRadius.circular(8), child: Container( margin: EdgeInsets.only(bottom: 8), padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ), child: ListTile( contentPadding: EdgeInsets.zero, leading: product.image != null && product.image!.isNotEmpty ? CircleAvatar( backgroundImage: NetworkImage(product.image!), radius: 20, ) : CircleAvatar( backgroundColor: Colors.orange.shade100, child: Icon(Icons.inventory, color: Colors.orange), ), title: Text( product.name, style: TextStyle(fontSize: 14), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Stock: ${product.stock ?? 0}', style: TextStyle(fontSize: 12), ), Text( 'Catégorie: ${product.category}', style: TextStyle(fontSize: 10, color: Colors.grey), ), ], ), trailing: Text( '${NumberFormat('#,##0', 'fr_FR').format(product.price)} MGA', style: TextStyle(fontSize: 12), ), ), ), )).toList(), ); }, ), ], ), ), ); } Color _getStatusColor(StatutCommande status) { switch (status) { case StatutCommande.enAttente: return Colors.orange; case StatutCommande.confirmee: return Colors.blue; case StatutCommande.annulee: return Colors.red; default: return Colors.grey; } } }