Browse Source

31072025

28062025_02
andrymodeste 4 months ago
parent
commit
f509af1716
  1. 6
      lib/Models/Client.dart
  2. 3
      lib/Models/produit.dart
  3. 116
      lib/Services/stock_managementDatabase.dart
  4. 808
      lib/Views/Dashboard.dart
  5. 58
      lib/Views/gestionStock.dart
  6. 13
      lib/Views/newCommand.dart
  7. 4
      lib/config/DatabaseConfig.dart

6
lib/Models/Client.dart

@ -88,6 +88,8 @@ class Commande {
final String? notes;
final DateTime? dateLivraison;
final int? commandeurId;
final String? commandeurnom;
final String? commandeurPrenom;
final int? validateurId;
final String? clientNom;
final String? clientPrenom;
@ -104,6 +106,8 @@ class Commande {
this.notes,
this.dateLivraison,
this.commandeurId,
this.commandeurnom,
this.commandeurPrenom,
this.validateurId,
this.clientNom,
this.clientPrenom,
@ -156,6 +160,8 @@ class Commande {
? Client._parseDateTime(map['dateLivraison'])
: null,
commandeurId: map['commandeurId'] as int?,
commandeurnom: map['commandeurnom'] as String?,
commandeurPrenom: map['commandeurPrenom'] as String?,
validateurId: map['validateurId'] as int?,
clientNom: map['clientNom'] as String?,
clientPrenom: map['clientPrenom'] as String?,

3
lib/Models/produit.dart

@ -13,6 +13,7 @@ class Product {
String? qrCode;
final String? reference;
final int? pointDeVenteId;
final String? pointDeVentelib;
final String? marque;
final String? ram;
final String? memoireInterne;
@ -29,6 +30,7 @@ class Product {
this.qrCode,
this.reference,
this.pointDeVenteId,
this.pointDeVentelib,
this.marque,
this.ram,
this.memoireInterne,
@ -82,6 +84,7 @@ class Product {
qrCode: map['qrCode'] as String?,
reference: map['reference'] as String?,
pointDeVenteId: map['point_de_vente_id'] as int?,
pointDeVentelib: map['pointDeVentelib'] as String?,
marque: map['marque'] as String?,
ram: map['ram'] as String?,
memoireInterne: map['memoire_interne'] as String?,

116
lib/Services/stock_managementDatabase.dart

@ -513,7 +513,11 @@ String _formatDate(DateTime date) {
Future<List<Product>> getProducts() async {
final db = await database;
final result = await db.query('SELECT * FROM products ORDER BY name ASC');
final result = await db.query('''
SELECT p.*,pv.nom as pointDeVentelib
FROM products p
LEFT JOIN points_de_vente pv ON p.point_de_vente_id = pv.id
ORDER BY name ASC''');
return result.map((row) => Product.fromMap(row.fields)).toList();
}
@ -916,6 +920,75 @@ Future<double> getValeurTotaleStock() async {
return result.map((row) => DetailCommande.fromMap(row.fields)).toList();
}
Future<List<Map<String, dynamic>>> getCommandesParPointDeVente(
int pointVenteId, {
DateTime? dateDebut,
DateTime? dateFin,
bool aujourdHuiSeulement = false,
int limit = 50,
}) async {
final db = await database;
try {
String whereClause = 'WHERE u.point_de_vente_id = ? AND c.statut != 5';
List<dynamic> whereArgs = [pointVenteId];
if (aujourdHuiSeulement) {
final today = DateTime.now();
final startOfDay = DateTime(today.year, today.month, today.day);
final endOfDay = DateTime(today.year, today.month, today.day, 23, 59, 59);
whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?';
whereArgs.addAll([
_formatDate(startOfDay),
_formatDate(endOfDay),
]);
} else if (dateDebut != null && dateFin != null) {
final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?';
whereArgs.addAll([
_formatDate(dateDebut),
_formatDate(adjustedEndDate),
]);
} else if (dateDebut != null) {
whereClause += ' AND c.dateCommande >= ?';
whereArgs.add(_formatDate(dateDebut));
} else if (dateFin != null) {
final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
whereClause += ' AND c.dateCommande <= ?';
whereArgs.add(_formatDate(adjustedEndDate));
}
whereClause += ' ORDER BY c.dateCommande DESC LIMIT ?';
whereArgs.add(limit);
final result = await db.query('''
SELECT c.*,
cl.nom AS clientNom,
cl.prenom AS clientPrenom,
u.name AS commandeurnom,
u.lastName AS commandeurPrenom,
CASE
WHEN c.statut = 0 THEN 'En attente'
WHEN c.statut = 1 THEN 'Confirmée'
WHEN c.statut = 2 THEN 'Annulée'
ELSE 'Inconnu'
END AS statut_libelle,
pv.nom AS point_vente_nom,
u.name AS commandeur_nom
FROM commandes c
LEFT JOIN clients cl ON c.clientId = cl.id
INNER JOIN users u ON c.commandeurId = u.id
LEFT JOIN points_de_vente pv ON u.point_de_vente_id = pv.id
$whereClause
''', whereArgs);
return result.map((row) => row.fields).toList();
} catch (e) {
print('Erreur récupération commandes: $e');
return [];
}
}
// --- RECHERCHE PRODUITS ---
@ -948,6 +1021,12 @@ Future<double> getValeurTotaleStock() async {
return result.map((row) => row['category'] as String).toList();
}
Future<List<Map<String, dynamic>>> getPointsDeVentes() async {
final db = await database;
final result = await db.query('SELECT DISTINCT id, nom FROM pointsdevente ORDER BY nom ASC');
return result.map((row) => row.fields).toList();
}
Future<List<Product>> getProductsByCategory(String category) async {
final db = await database;
final result = await db.query(
@ -3212,10 +3291,16 @@ Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(
return [];
}
}
// 1. Mise à jour de la méthode dans stock_managementDatabase.dart
Future<List<Map<String, dynamic>>> getHistoriqueSortiesPersonnelles({
int? adminId,
String? statut,
int? pointDeVenteId,
DateTime? dateDebut,
DateTime? dateFin,
bool aujourdHuiSeulement = false,
int limit = 50,
}) async {
final db = await database;
@ -3225,7 +3310,6 @@ Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(
List<dynamic> params = [];
// Filtre par point de vente seulement si pointDeVenteId n'est pas null
// (null signifie que l'utilisateur a pointDeVenteId = 0 et peut tout voir)
if (pointDeVenteId != null) {
whereClause = 'WHERE sp.point_de_vente_id = ?';
params.add(pointDeVenteId);
@ -3241,6 +3325,34 @@ Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(
params.add(statut);
}
// Nouveau filtre par date
if (aujourdHuiSeulement) {
final today = DateTime.now();
final startOfDay = DateTime(today.year, today.month, today.day);
final endOfDay = DateTime(today.year, today.month, today.day, 23, 59, 59);
whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') +
' sp.date_sortie >= ? AND sp.date_sortie <= ?';
params.add(startOfDay.toIso8601String());
params.add(endOfDay.toIso8601String());
} else if (dateDebut != null && dateFin != null) {
final startOfDay = DateTime(dateDebut.year, dateDebut.month, dateDebut.day);
final endOfDay = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') +
' sp.date_sortie >= ? AND sp.date_sortie <= ?';
params.add(startOfDay.toIso8601String());
params.add(endOfDay.toIso8601String());
} else if (dateDebut != null) {
final startOfDay = DateTime(dateDebut.year, dateDebut.month, dateDebut.day);
whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie >= ?';
params.add(startOfDay.toIso8601String());
} else if (dateFin != null) {
final endOfDay = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.date_sortie <= ?';
params.add(endOfDay.toIso8601String());
}
final result = await db.query('''
SELECT sp.*,
p.name as produit_nom,

808
lib/Views/Dashboard.dart

@ -1524,6 +1524,45 @@ void _toggleTodayFilter() {
}
});
}
// 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<void> _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<String, dynamic> pointVenteData) async {
final isMobile = MediaQuery.of(context).size.width < 600;
final pointVenteId = pointVenteData['point_vente_id'] as int;
@ -1536,11 +1575,16 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
title: Text('Détails - $pointVenteNom'),
content: Container(
width: double.maxFinite,
height: 500,
height: 600, // Augmenté pour inclure les commandes
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,
@ -1551,18 +1595,15 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
_showOnlyToday = !_showOnlyToday;
if (_showOnlyToday) _dateRange = null;
});
setState(() {});
},
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'),
? (isMobile ? 'Toutes dates' : 'Toutes les dates')
: (isMobile ? 'Aujourd\'hui' : 'Aujourd\'hui seulement')),
style: ElevatedButton.styleFrom(
backgroundColor: _showOnlyToday
? Colors.green.shade600
@ -1592,16 +1633,13 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
_dateRange = picked;
_showOnlyToday = false;
});
setState(() {});
}
},
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'),
? (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
@ -1613,12 +1651,71 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
),
),
),
if (_showOnlyToday || _dateRange != null)
ElevatedButton.icon(
onPressed: () {
setDialogState(() {
_showOnlyToday = false;
_dateRange = null;
});
setState(() {});
},
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,
),
),
),
],
),
SizedBox(height: 16),
// 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 avec FutureBuilder pour refresh automatique
// Statistiques générales
FutureBuilder<Map<String, dynamic>>(
future: _getDetailedPointVenteStats(pointVenteId),
builder: (context, snapshot) {
@ -1643,47 +1740,182 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
},
),
SizedBox(height: 12),
// NOUVELLE SECTION: Liste des commandes
SizedBox(height: 16),
Text('Top 5 des produits:', style: TextStyle(fontWeight: FontWeight.bold)),
Divider(),
Text('Commandes récentes (max 10):',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
// Top produits avec filtre
FutureBuilder<List<Map<String, dynamic>>>(
future: _database.getTopProduitsParPointDeVente(
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 || !snapshot.hasData || snapshot.data!.isEmpty) {
return Text('Aucun produit vendu${_showOnlyToday ? ' aujourd\'hui' : _dateRange != null ? ' pour cette période' : ''}', style: TextStyle(color: Colors.grey));
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 produits = snapshot.data!;
final commandes = snapshot.data!
.map((map) => Commande.fromMap(map))
.toList();
return Column(
children: produits.map((produit) => Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
children: commandes.map(_buildCommandeCardForDialog).toList(),
);
},
),
// Section des sorties personnelles
SizedBox(height: 16),
Divider(),
Text('Historique des sorties personnelles:',
style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
// ... (reste du code pour les sorties, inchangé)
FutureBuilder<List<Map<String, dynamic>>>(
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(
produit['produit_nom'] ?? 'N/A',
style: TextStyle(fontSize: 12),
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(
'${produit['quantite_vendue'] ?? 0} vendus',
_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(),
);
},
@ -1693,26 +1925,502 @@ void _showPointVenteDetails(Map<String, dynamic> pointVenteData) async {
),
),
actions: [
if (_showOnlyToday || _dateRange != null)
TextButton(
onPressed: () {
setDialogState(() {
_showOnlyToday = false;
_dateRange = null;
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
],
),
),
);
}
// Ajoutez cette variable d'état dans votre classe _DashboardPageState
Set<int> _expandedCommandes = <int>{};
// Remplacez votre méthode _buildCommandeCardForDialog par celle-ci :
Widget _buildCommandeCardForDialog(Commande commande) {
final bool isExpanded = _expandedCommandes.contains(commande.id);
return FutureBuilder<List<DetailCommande>>(
future: _database.getDetailsCommande(commande.id!),
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: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: InkWell(
onTap: () {
setState(() {
if (isExpanded) {
_expandedCommandes.remove(commande.id);
} else {
_expandedCommandes.add(commande.id!);
}
});
},
child: Text('Réinitialiser'),
borderRadius: BorderRadius.circular(8),
child: Card(
margin: EdgeInsets.symmetric(vertical: 4),
elevation: isExpanded ? 4 : 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: aDesRemises
? BorderSide(color: Colors.orange.shade300, width: 1)
: BorderSide.none,
),
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête de la commande (toujours visible)
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, width: 1)
: 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(
'#${commande.id}',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Fermer'),
),
],
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
commande.clientNomComplet,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Row(
children: [
Icon(Icons.calendar_today, size: 12, color: Colors.grey),
SizedBox(width: 4),
Text(
DateFormat('dd/MM/yyyy').format(commande.dateCommande),
style: TextStyle(fontSize: 11, color: Colors.grey),
),
SizedBox(width: 12),
Container(
padding: 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,
),
),
),
],
),
],
),
),
// Icône d'expansion
AnimatedRotation(
turns: isExpanded ? 0.5 : 0,
duration: Duration(milliseconds: 300),
child: Icon(
Icons.expand_more,
color: Colors.grey[600],
),
),
],
),
SizedBox(height: 8),
// Montant (toujours visible)
Row(
children: [
Icon(Icons.attach_money, size: 14, color: Colors.green.shade600),
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) ...[
SizedBox(width: 8),
Container(
padding: 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),
SizedBox(width: 2),
Text(
'-${NumberFormat('#,##0', 'fr_FR').format(totalRemises)}',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
color: Colors.teal.shade700,
),
),
],
),
),
],
],
),
// Détails étendus (visibles seulement si expanded)
AnimatedCrossFade(
duration: Duration(milliseconds: 300),
crossFadeState: isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: SizedBox.shrink(),
secondChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
Divider(color: Colors.grey[300]),
SizedBox(height: 8),
// Informations du commandeur
if (commande.commandeurnom != null && commande.commandeurnom!.isNotEmpty)
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Icon(Icons.person, size: 16, color: Colors.blue[600]),
SizedBox(width: 8),
Text(
'Commandeur: ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
Expanded(
child: Text(
'${commande.commandeurnom ?? ''} ${commande.commandeurPrenom ?? ''}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.blue[700],
),
),
),
],
),
),
SizedBox(height: 8),
// Statut détaillé
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
),
child: Row(
children: [
Icon(
_getStatutIcon(commande.statut),
size: 16,
color: commande.statut == StatutCommande.annulee
? Colors.red
: Colors.blue[600],
),
SizedBox(width: 8),
Text(
'Statut: ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
Text(
commande.statutLibelle,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: commande.statut == StatutCommande.annulee
? Colors.red
: Colors.blue[700],
),
),
],
),
),
SizedBox(height: 8),
// Liste des produits commandés
if (snapshot.hasData && snapshot.data!.isNotEmpty) ...[
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.shopping_cart, size: 16, color: Colors.green[600]),
SizedBox(width: 8),
Text(
'Produits commandés:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.green[700],
),
),
],
),
SizedBox(height: 6),
...snapshot.data!.map((detail) => Padding(
padding: EdgeInsets.only(left: 24, bottom: 4),
child: Row(
children: [
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Colors.green[600],
borderRadius: BorderRadius.circular(2),
),
),
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],
),
),
],
),
)).toList(),
],
),
),
],
],
),
),
],
),
),
),
),
);
},
);
}
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<Map<String, dynamic>> _getDetailedPointVenteStats(int pointVenteId) async {
final ventesData = await _database.getVentesParPointDeVente(
dateDebut: _dateRange?.start,
@ -1734,14 +2442,30 @@ Future<Map<String, dynamic>> _getDetailedPointVenteStats(int pointVenteId) async
return pointVenteStats;
}
Widget _buildStatRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
Widget _buildStatRow(String title, String value) {
return Container(
padding: EdgeInsets.symmetric(vertical: 2, horizontal: 4), // Padding minimal
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 12)),
Text(value, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
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,
),
),
],
),
);

58
lib/Views/gestionStock.dart

@ -19,6 +19,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
List<Product> _products = [];
List<Product> _filteredProducts = [];
String? _selectedCategory;
int? _selectedIdPointDeVente; // Nouveau filtre
final TextEditingController _searchController = TextEditingController();
bool _showLowStockOnly = false;
bool _sortByStockAscending = false;
@ -46,10 +47,12 @@ class _GestionStockPageState extends State<GestionStockPage> {
(product.reference?.toLowerCase().contains(query) ?? false);
final matchesCategory = _selectedCategory == null ||
product.category == _selectedCategory;
final matchesPointDeVente = _selectedIdPointDeVente == null ||
product.pointDeVenteId == _selectedIdPointDeVente; // Nouveau filtre
final matchesStockFilter = !_showLowStockOnly ||
(product.stock ?? 0) <= 5; // Seuil pour stock faible
return matchesSearch && matchesCategory && matchesStockFilter;
return matchesSearch && matchesCategory && matchesPointDeVente && matchesStockFilter;
}).toList();
// Trier les produits
@ -112,7 +115,7 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
const SizedBox(height: 12),
// Filtres
// Filtres - Première ligne
Row(
children: [
// Filtre par catégorie
@ -156,6 +159,53 @@ class _GestionStockPageState extends State<GestionStockPage> {
),
const SizedBox(width: 12),
// Filtre par point de vente
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _database.getPointsDeVente(), // Vous devez implémenter cette méthode
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final pointsDeVente = snapshot.data!;
return DropdownButtonFormField<int>(
value: _selectedIdPointDeVente,
decoration: InputDecoration(
labelText: 'Point de vente',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
items: [
const DropdownMenuItem<int>(
value: null,
child: Text('Tous les points de vente'),
),
...pointsDeVente.map((point) {
return DropdownMenuItem<int>(
value: point['id'],
child: Text(point['nom'] ?? 'Point ${point['id']}'),
);
}),
],
onChanged: (value) {
setState(() {
_selectedIdPointDeVente = value;
_filterProducts();
});
},
);
},
),
),
],
),
const SizedBox(height: 12),
// Filtres - Deuxième ligne
Row(
children: [
// Toggle pour stock faible seulement
Container(
decoration: BoxDecoration(
@ -330,8 +380,8 @@ class _GestionStockPageState extends State<GestionStockPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Catégorie: ${product.category}'),
if (product.reference != null && product.reference!.isNotEmpty)
Text('Réf: ${product.reference!}'),
if (product.pointDeVentelib != null)
Text('Point de vente: ${product.pointDeVentelib}'),
],
),
trailing: Row(

13
lib/Views/newCommand.dart

@ -11,6 +11,8 @@ import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/controller/userController.dart';
import 'package:intl/intl.dart';
import 'commandManagement.dart';
class NouvelleCommandePage extends StatefulWidget {
const NouvelleCommandePage({super.key});
@ -1047,6 +1049,8 @@ Widget _buildProductDetailRow(String label, String value) {
// 8. Modifier _clearFormAndCart pour vider le nouveau panier
void _clearFormAndCart() {
if (!mounted) return; // Évite l'appel à setState si le widget est déjà démonté
setState(() {
// Vider les contrôleurs client
_nomController.clear();
@ -1072,6 +1076,7 @@ Widget _buildProductDetailRow(String label, String value) {
}
Future<void> _showClientSuggestions(String query, {required bool isNom}) async {
if (query.length < 3) {
_hideAllSuggestions();
@ -4043,9 +4048,15 @@ Future<void> _submitOrder() async {
),
),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context); // Ferme le dialogue actuel
_clearFormAndCart();
_loadProducts();
// Redirige vers la page GestionCommandesPage
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const GestionCommandesPage()),
);
},
child: Text(
'OK',

4
lib/config/DatabaseConfig.dart

@ -11,8 +11,8 @@ class DatabaseConfig {
static const String localDatabase = 'guycom';
// Production (public) MySQL settings
// static const String prodHost = '185.70.105.157';
static const String prodHost = '102.17.52.31';
static const String prodHost = '185.70.105.157';
// static const String prodHost = '102.17.52.31';
static const String prodUsername = 'guycom';
static const String prodPassword = '3iV59wjRdbuXAPR';
static const String prodDatabase = 'guycom';

Loading…
Cancel
Save