From f509af1716da9630a1a8456addd9abe502a54632 Mon Sep 17 00:00:00 2001 From: andrymodeste Date: Thu, 31 Jul 2025 21:56:32 +0200 Subject: [PATCH] 31072025 --- lib/Models/Client.dart | 6 + lib/Models/produit.dart | 3 + lib/Services/stock_managementDatabase.dart | 222 ++++-- lib/Views/Dashboard.dart | 830 +++++++++++++++++++-- lib/Views/gestionStock.dart | 58 +- lib/Views/newCommand.dart | 59 +- lib/config/DatabaseConfig.dart | 4 +- 7 files changed, 1044 insertions(+), 138 deletions(-) diff --git a/lib/Models/Client.dart b/lib/Models/Client.dart index f05299b..7e20e37 100644 --- a/lib/Models/Client.dart +++ b/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?, diff --git a/lib/Models/produit.dart b/lib/Models/produit.dart index 9a36195..e2ee90a 100644 --- a/lib/Models/produit.dart +++ b/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?, diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index b82c3b5..998cea1 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -513,7 +513,11 @@ String _formatDate(DateTime date) { Future> 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 getValeurTotaleStock() async { return result.map((row) => DetailCommande.fromMap(row.fields)).toList(); } +Future>> 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 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 getValeurTotaleStock() async { return result.map((row) => row['category'] as String).toList(); } + Future>> 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> getProductsByCategory(String category) async { final db = await database; final result = await db.query( @@ -2932,7 +3011,7 @@ Future>> getTopProduitsParPointDeVente( } // 6. Méthode pour récupérer les statistiques des transferts - Future> getStatistiquesTransferts() async { + Future> getStatistiquesTransferts() async { final db = await database; try { @@ -3212,60 +3291,93 @@ Future>> getTopProduitsParPointDeVente( return []; } } - Future>> getHistoriqueSortiesPersonnelles({ - int? adminId, - String? statut, - int? pointDeVenteId, - int limit = 50, - }) async { - final db = await database; - - try { - String whereClause = ''; - List 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); - } - - if (adminId != null) { - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.admin_id = ?'; - params.add(adminId); - } - - if (statut != null) { - whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.statut = ?'; - params.add(statut); - } - - final result = await db.query(''' - SELECT sp.*, - p.name as produit_nom, - p.reference as produit_reference, - u_admin.name as admin_nom, - u_admin.lastname as admin_nom_famille, - u_approb.name as approbateur_nom, - u_approb.lastname as approbateur_nom_famille, - pv.nom as point_vente_nom - FROM sorties_stock_personnelles sp - JOIN products p ON sp.produit_id = p.id - JOIN users u_admin ON sp.admin_id = u_admin.id - LEFT JOIN users u_approb ON sp.approbateur_id = u_approb.id - LEFT JOIN points_de_vente pv ON sp.point_de_vente_id = pv.id - $whereClause - ORDER BY sp.date_sortie DESC - LIMIT ? - ''', [...params, limit]); - - return result.map((row) => row.fields).toList(); - } catch (e) { - print('Erreur récupération historique sorties: $e'); - return []; - } + + + // 1. Mise à jour de la méthode dans stock_managementDatabase.dart +Future>> getHistoriqueSortiesPersonnelles({ + int? adminId, + String? statut, + int? pointDeVenteId, + DateTime? dateDebut, + DateTime? dateFin, + bool aujourdHuiSeulement = false, + int limit = 50, +}) async { + final db = await database; + + try { + String whereClause = ''; + List params = []; + + // Filtre par point de vente seulement si pointDeVenteId n'est pas null + if (pointDeVenteId != null) { + whereClause = 'WHERE sp.point_de_vente_id = ?'; + params.add(pointDeVenteId); + } + + if (adminId != null) { + whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.admin_id = ?'; + params.add(adminId); } + + if (statut != null) { + whereClause += (whereClause.isEmpty ? 'WHERE' : ' AND') + ' sp.statut = ?'; + 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, + p.reference as produit_reference, + u_admin.name as admin_nom, + u_admin.lastname as admin_nom_famille, + u_approb.name as approbateur_nom, + u_approb.lastname as approbateur_nom_famille, + pv.nom as point_vente_nom + FROM sorties_stock_personnelles sp + JOIN products p ON sp.produit_id = p.id + JOIN users u_admin ON sp.admin_id = u_admin.id + LEFT JOIN users u_approb ON sp.approbateur_id = u_approb.id + LEFT JOIN points_de_vente pv ON sp.point_de_vente_id = pv.id + $whereClause + ORDER BY sp.date_sortie DESC + LIMIT ? + ''', [...params, limit]); + + return result.map((row) => row.fields).toList(); + } catch (e) { + print('Erreur récupération historique sorties: $e'); + return []; + } +} Future> getStatistiquesSortiesPersonnelles() async { final db = await database; diff --git a/lib/Views/Dashboard.dart b/lib/Views/Dashboard.dart index 5cff380..e1f7687 100644 --- a/lib/Views/Dashboard.dart +++ b/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 _selectSortiesDateRange(BuildContext context, StateSetter setDialogState) async { + final DateTimeRange? picked = await showDateRangePicker( + context: context, + firstDate: DateTime(2020), + lastDate: DateTime.now().add(const Duration(days: 365)), + initialDateRange: _sortiesDateRange ?? + DateTimeRange( + start: DateTime.now().subtract(const Duration(days: 30)), + end: DateTime.now(), + ), + ); + + if (picked != null) { + setDialogState(() { + _sortiesDateRange = picked; + _sortiesShowOnlyToday = false; + }); + } + } + + // NOUVELLE MÉTHODE: Toggle du filtre "aujourd'hui" pour les sorties + void _toggleSortiesTodayFilter(StateSetter setDialogState) { + setDialogState(() { + _sortiesShowOnlyToday = !_sortiesShowOnlyToday; + if (_sortiesShowOnlyToday) { + _sortiesDateRange = null; + } + }); + } + + // MÉTHODE MISE À JOUR: _showPointVenteDetails avec les nouveaux filtres void _showPointVenteDetails(Map pointVenteData) async { final isMobile = MediaQuery.of(context).size.width < 600; final pointVenteId = pointVenteData['point_vente_id'] as int; @@ -1536,11 +1575,16 @@ void _showPointVenteDetails(Map 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 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 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 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>( future: _getDetailedPointVenteStats(pointVenteId), builder: (context, snapshot) { @@ -1643,46 +1740,181 @@ void _showPointVenteDetails(Map 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>>( - 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) { + return Text('Erreur de chargement des commandes', + style: TextStyle(color: Colors.red)); + } + + if (!snapshot.hasData || snapshot.data!.isEmpty) { + String message = 'Aucune commande trouvée'; + if (_showOnlyToday) { + message = 'Aucune commande aujourd\'hui'; + } else if (_dateRange != null) { + message = 'Aucune commande pour cette période'; + } + + return Text(message, style: TextStyle(color: Colors.grey)); + } + + final commandes = snapshot.data! + .map((map) => Commande.fromMap(map)) + .toList(); + + return Column( + children: commandes.map(_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>>( + 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 || !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 sorties', + style: TextStyle(color: Colors.red)); } - final produits = snapshot.data!; + 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: 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, + children: sorties.map((sortie) => Card( + margin: EdgeInsets.symmetric(vertical: 2), + child: Padding( + padding: EdgeInsets.all(6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + sortie['produit_nom'] ?? 'N/A', + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _getStatusColorForSorties(sortie['statut']), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + sortie['statut'] ?? 'N/A', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), - ), - Text( - '${produit['quantite_vendue'] ?? 0} vendus', - style: TextStyle(fontSize: 12, color: Colors.grey), - ), - ], + SizedBox(height: 3), + Row( + children: [ + Icon(Icons.inventory, size: 14, color: Colors.grey), + SizedBox(width: 4), + Text( + 'Qté: ${sortie['quantite'] ?? 0}', + style: TextStyle(fontSize: 14, color: Colors.black), + ), + SizedBox(width: 128), + Icon(Icons.person, size: 14, color: Colors.grey), + SizedBox(width: 4), + Expanded( + child: Text( + '${sortie['admin_nom'] ?? ''} ${sortie['admin_nom_famille'] ?? ''}', + style: TextStyle(fontSize: 14, color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + if (sortie['date_sortie'] != null) ...[ + SizedBox(height: 2), + Row( + children: [ + Icon(Icons.access_time, size: 14, color: Colors.grey), + SizedBox(width: 4), + Text( + _formatDateSortie(sortie['date_sortie']), + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ], + if (sortie['motif'] != null && sortie['motif'].toString().isNotEmpty) ...[ + SizedBox(height: 3), + Text( + 'Motif: ${sortie['motif']}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), ), )).toList(), ); @@ -1693,16 +1925,6 @@ void _showPointVenteDetails(Map 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'), @@ -1710,9 +1932,495 @@ void _showPointVenteDetails(Map pointVenteData) async { ], ), ), + ); +} +// Ajoutez cette variable d'état dans votre classe _DashboardPageState +Set _expandedCommandes = {}; + +// Remplacez votre méthode _buildCommandeCardForDialog par celle-ci : +Widget _buildCommandeCardForDialog(Commande commande) { + final bool isExpanded = _expandedCommandes.contains(commande.id); + + return FutureBuilder>( + 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!); + } + }); + }, + 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, + ), + ), + ], + ), + ), + 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> _getDetailedPointVenteStats(int pointVenteId) async { final ventesData = await _database.getVentesParPointDeVente( dateDebut: _dateRange?.start, @@ -1734,14 +2442,30 @@ Future> _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, + ), + ), ], ), ); diff --git a/lib/Views/gestionStock.dart b/lib/Views/gestionStock.dart index fb767b3..5eba467 100644 --- a/lib/Views/gestionStock.dart +++ b/lib/Views/gestionStock.dart @@ -19,6 +19,7 @@ class _GestionStockPageState extends State { List _products = []; List _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 { (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 { ), const SizedBox(height: 12), - // Filtres + // Filtres - Première ligne Row( children: [ // Filtre par catégorie @@ -156,6 +159,53 @@ class _GestionStockPageState extends State { ), const SizedBox(width: 12), + // Filtre par point de vente + Expanded( + child: FutureBuilder>>( + 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( + value: _selectedIdPointDeVente, + decoration: InputDecoration( + labelText: 'Point de vente', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + items: [ + const DropdownMenuItem( + value: null, + child: Text('Tous les points de vente'), + ), + ...pointsDeVente.map((point) { + return DropdownMenuItem( + 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 { 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( diff --git a/lib/Views/newCommand.dart b/lib/Views/newCommand.dart index 5134b4a..c705dcf 100644 --- a/lib/Views/newCommand.dart +++ b/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,29 +1049,32 @@ Widget _buildProductDetailRow(String label, String value) { // 8. Modifier _clearFormAndCart pour vider le nouveau panier void _clearFormAndCart() { - setState(() { - // Vider les contrôleurs client - _nomController.clear(); - _prenomController.clear(); - _emailController.clear(); - _telephoneController.clear(); - _adresseController.clear(); - - // Vider le nouveau panier - _panierDetails.clear(); - - // Réinitialiser le commercial au premier de la liste - if (_commercialUsers.isNotEmpty) { - _selectedCommercialUser = _commercialUsers.first; - } - - // Masquer toutes les suggestions - _hideAllSuggestions(); - - // Réinitialiser l'état de chargement - _isLoading = false; - }); - } + if (!mounted) return; // Évite l'appel à setState si le widget est déjà démonté + + setState(() { + // Vider les contrôleurs client + _nomController.clear(); + _prenomController.clear(); + _emailController.clear(); + _telephoneController.clear(); + _adresseController.clear(); + + // Vider le nouveau panier + _panierDetails.clear(); + + // Réinitialiser le commercial au premier de la liste + if (_commercialUsers.isNotEmpty) { + _selectedCommercialUser = _commercialUsers.first; + } + + // Masquer toutes les suggestions + _hideAllSuggestions(); + + // Réinitialiser l'état de chargement + _isLoading = false; + }); +} + Future _showClientSuggestions(String query, {required bool isNom}) async { @@ -4043,9 +4048,15 @@ Future _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', diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index 2651345..1735531 100644 --- a/lib/config/DatabaseConfig.dart +++ b/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';