|
|
|
@ -1100,7 +1100,6 @@ Widget _buildMiniStatistics() { |
|
|
|
|
|
|
|
//widget vente |
|
|
|
// 2. Ajoutez cette méthode dans la classe _DashboardPageState |
|
|
|
|
|
|
|
Widget _buildVentesParPointDeVenteCard() { |
|
|
|
return Card( |
|
|
|
elevation: 4, |
|
|
|
@ -1123,13 +1122,91 @@ Widget _buildVentesParPointDeVenteCard() { |
|
|
|
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<List<Map<String, dynamic>>>( |
|
|
|
future: _database.getVentesParPointDeVente(), |
|
|
|
future: _database.getVentesParPointDeVente( |
|
|
|
dateDebut: _dateRange?.start, |
|
|
|
dateFin: _dateRange?.end, |
|
|
|
aujourdHuiSeulement: _showOnlyToday, |
|
|
|
), |
|
|
|
builder: (context, snapshot) { |
|
|
|
if (snapshot.connectionState == ConnectionState.waiting) { |
|
|
|
return Center(child: CircularProgressIndicator()); |
|
|
|
@ -1142,7 +1219,11 @@ Widget _buildVentesParPointDeVenteCard() { |
|
|
|
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)), |
|
|
|
Text( |
|
|
|
'Aucune donnée de vente${_showOnlyToday ? ' pour aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', |
|
|
|
style: TextStyle(color: Colors.grey), |
|
|
|
textAlign: TextAlign.center, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
@ -1438,16 +1519,20 @@ String _formatCurrency(double value) { |
|
|
|
void _toggleTodayFilter() { |
|
|
|
setState(() { |
|
|
|
_showOnlyToday = !_showOnlyToday; |
|
|
|
if (_showOnlyToday) { |
|
|
|
_dateRange = null; // Reset date range when showing only today |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void _showPointVenteDetails(Map<String, dynamic> 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( |
|
|
|
builder: (context) => StatefulBuilder( |
|
|
|
builder: (context, setDialogState) => AlertDialog( |
|
|
|
title: Text('Détails - $pointVenteNom'), |
|
|
|
content: Container( |
|
|
|
width: double.maxFinite, |
|
|
|
@ -1461,7 +1546,12 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async { |
|
|
|
runSpacing: 8, |
|
|
|
children: [ |
|
|
|
ElevatedButton.icon( |
|
|
|
onPressed: _toggleTodayFilter, |
|
|
|
onPressed: () { |
|
|
|
setDialogState(() { |
|
|
|
_showOnlyToday = !_showOnlyToday; |
|
|
|
if (_showOnlyToday) _dateRange = null; |
|
|
|
}); |
|
|
|
}, |
|
|
|
icon: Icon( |
|
|
|
_showOnlyToday ? Icons.today : Icons.calendar_today, |
|
|
|
size: 20, |
|
|
|
@ -1485,7 +1575,25 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async { |
|
|
|
), |
|
|
|
), |
|
|
|
ElevatedButton.icon( |
|
|
|
onPressed: () => _selectDateRange(context), |
|
|
|
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; |
|
|
|
}); |
|
|
|
} |
|
|
|
}, |
|
|
|
icon: const Icon(Icons.date_range, size: 20), |
|
|
|
label: Text(_dateRange != null |
|
|
|
? isMobile |
|
|
|
@ -1507,30 +1615,53 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async { |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
// 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), |
|
|
|
|
|
|
|
// Statistiques générales avec FutureBuilder pour refresh automatique |
|
|
|
FutureBuilder<Map<String, dynamic>>( |
|
|
|
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: 16), |
|
|
|
Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)), |
|
|
|
SizedBox(height: 8), |
|
|
|
|
|
|
|
// ✅ Top produits |
|
|
|
// Top produits avec filtre |
|
|
|
FutureBuilder<List<Map<String, dynamic>>>( |
|
|
|
future: _database.getTopProduitsParPointDeVente(pointVenteId), |
|
|
|
future: _database.getTopProduitsParPointDeVente( |
|
|
|
pointVenteId, |
|
|
|
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 Text('Aucun produit vendu', style: TextStyle(color: Colors.grey)); |
|
|
|
return Text('Aucun produit vendu${_showOnlyToday ? ' aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', style: TextStyle(color: Colors.grey)); |
|
|
|
} |
|
|
|
|
|
|
|
final produits = snapshot.data!; |
|
|
|
@ -1562,15 +1693,47 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async { |
|
|
|
), |
|
|
|
), |
|
|
|
actions: [ |
|
|
|
if (_showOnlyToday || _dateRange != null) |
|
|
|
TextButton( |
|
|
|
onPressed: () { |
|
|
|
setDialogState(() { |
|
|
|
_showOnlyToday = false; |
|
|
|
_dateRange = null; |
|
|
|
}); |
|
|
|
}, |
|
|
|
child: Text('Réinitialiser'), |
|
|
|
), |
|
|
|
TextButton( |
|
|
|
onPressed: () => Navigator.pop(context), |
|
|
|
child: Text('Fermer'), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Future<Map<String, dynamic>> _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 label, String value) { |
|
|
|
return Padding( |
|
|
|
padding: EdgeInsets.symmetric(vertical: 4), |
|
|
|
|