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, ), ), ], ), SizedBox(height: 16), Container( height: 400, child: FutureBuilder>>( future: _database.getVentesParPointDeVente(), 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 par point de vente', style: TextStyle(color: Colors.grey)), ], ), ); } 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; }); } 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; showDialog( context: context, builder: (context) => AlertDialog( title: Text('Détails - $pointVenteNom'), content: Container( width: double.maxFinite, height: 500, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton.icon( onPressed: _toggleTodayFilter, 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: () => _selectDateRange(context), icon: const Icon(Icons.date_range, size: 20), label: Text(_dateRange != null ? isMobile ? 'Période' : 'Période sélectionnée' : isMobile ? '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, ), ), ), ], ), // Statistiques générales _buildStatRow('Chiffre d\'affaires:', '${NumberFormat('#,##0.00', 'fr_FR').format((pointVenteData['chiffre_affaires'] as num?)?.toDouble() ?? 0.0)} MGA'), _buildStatRow('Nombre de commandes:', '${pointVenteData['nombre_commandes'] ?? 0}'), _buildStatRow('Articles vendus:', '${pointVenteData['nombre_articles_vendus'] ?? 0}'), _buildStatRow('Quantité totale:', '${pointVenteData['quantite_totale_vendue'] ?? 0}'), _buildStatRow('Panier moyen:', '${NumberFormat('#,##0.00', 'fr_FR').format((pointVenteData['panier_moyen'] as num?)?.toDouble() ?? 0.0)} MGA'), SizedBox(height: 16), Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8), SizedBox(height: 16), // ✅ Top produits FutureBuilder>>( future: _database.getTopProduitsParPointDeVente(pointVenteId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { return Text('Aucun produit vendu', style: TextStyle(color: Colors.grey)); } final produits = snapshot.data!; return Column( children: produits.map((produit) => Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( produit['produit_nom'] ?? 'N/A', style: TextStyle(fontSize: 12), overflow: TextOverflow.ellipsis, ), ), Text( '${produit['quantite_vendue'] ?? 0} vendus', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), )).toList(), ); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Fermer'), ), ], ), ); } Widget _buildStatRow(String label, String value) { return Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle(fontSize: 12)), Text(value, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), ], ), ); } 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; } } }