You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1867 lines
68 KiB

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<DashboardPage> with SingleTickerProviderStateMixin {
DateTimeRange? _dateRange;
bool _showOnlyToday = false;
final AppDatabase _database = AppDatabase.instance;
final UserController _userController = Get.find<UserController>();
final GlobalKey _recentClientsKey = GlobalKey();
final GlobalKey _recentOrdersKey = GlobalKey();
final GlobalKey _lowStockKey = GlobalKey();
final GlobalKey _salesChartKey = GlobalKey();
late Future<Map<String, dynamic>> _statsFuture;
late Future<List<Commande>> _recentOrdersFuture;
late Future<List<Product>> _lowStockProductsFuture;
late Future<List<Client>> _recentClientsFuture;
late Future<List<Commande>> _allOrdersFuture;
late Future<Map<String, int>> _productsByCategoryFuture;
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_loadData();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(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<void> _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<Map<String, dynamic>> _groupOrdersByMonth(List<Commande> orders) {
final Map<String, double> 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<Map<String, int>>(
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<List<Commande>>(
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<List<Product>>(
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<PieChartSectionData> 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<Users?>(
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<Map<String, dynamic>>(
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<List<Commande>>(
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<List<DetailCommande>>(
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<List<Client>>(
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<List<Map<String, dynamic>>>(
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<void> _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
}
});
}
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) => StatefulBuilder(
builder: (context, setDialogState) => 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: () {
setDialogState(() {
_showOnlyToday = !_showOnlyToday;
if (_showOnlyToday) _dateRange = null;
});
},
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;
});
}
},
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,
),
),
),
],
),
SizedBox(height: 16),
// 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 avec filtre
FutureBuilder<List<Map<String, dynamic>>>(
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${_showOnlyToday ? ' aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', 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: [
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),
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<List<Product>>(
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;
}
}
}