import ' package:flutter/material.dart ' ;
import ' package:get/get.dart ' ;
import ' package:qr_code_scanner_plus/qr_code_scanner_plus.dart ' ;
import ' package:youmazgestion/Components/app_bar.dart ' ;
import ' package:youmazgestion/Components/appDrawer.dart ' ;
import ' package:youmazgestion/Components/newCommandComponents/CadeauDialog.dart ' ;
import ' package:youmazgestion/Components/newCommandComponents/RemiseDialog.dart ' ;
import ' package:youmazgestion/Models/client.dart ' ;
import ' package:youmazgestion/Models/users.dart ' ;
import ' package:youmazgestion/Models/produit.dart ' ;
import ' package:youmazgestion/Services/stock_managementDatabase.dart ' ;
import ' package:youmazgestion/controller/userController.dart ' ;
class NouvelleCommandePage extends StatefulWidget {
const NouvelleCommandePage ( { super . key } ) ;
@ override
_NouvelleCommandePageState createState ( ) = > _NouvelleCommandePageState ( ) ;
}
class _NouvelleCommandePageState extends State < NouvelleCommandePage > {
final AppDatabase _appDatabase = AppDatabase . instance ;
final _formKey = GlobalKey < FormState > ( ) ;
bool _isLoading = false ;
// Contrôleurs client
final TextEditingController _nomController = TextEditingController ( ) ;
final TextEditingController _prenomController = TextEditingController ( ) ;
final TextEditingController _emailController = TextEditingController ( ) ;
final TextEditingController _telephoneController = TextEditingController ( ) ;
final TextEditingController _adresseController = TextEditingController ( ) ;
// Contrôleurs pour les filtres
final TextEditingController _searchNameController = TextEditingController ( ) ;
final TextEditingController _searchImeiController = TextEditingController ( ) ;
final TextEditingController _searchReferenceController = TextEditingController ( ) ;
List < Map < String , dynamic > > _pointsDeVente = [ ] ;
String ? _selectedPointDeVente ;
final UserController _userController = Get . find < UserController > ( ) ;
// Panier
final List < Product > _products = [ ] ;
final List < Product > _filteredProducts = [ ] ;
final Map < int , int > _quantites = { } ;
final Map < int , DetailCommande > _panierDetails = { } ;
// Variables de filtre
bool _showOnlyInStock = false ;
// Utilisateurs commerciaux
List < Users > _commercialUsers = [ ] ;
Users ? _selectedCommercialUser ;
// Variables pour les suggestions clients
bool _showNomSuggestions = false ;
bool _showTelephoneSuggestions = false ;
// Variables pour le scanner (identiques à ProductManagementPage)
QRViewController ? _qrController ;
bool _isScanning = false ;
final GlobalKey _qrKey = GlobalKey ( debugLabel: ' QR ' ) ;
@ override
void initState ( ) {
super . initState ( ) ;
_loadProducts ( ) ;
_loadCommercialUsers ( ) ;
_loadPointsDeVenteWithDefault ( ) ; // Charger les points de vente
_searchNameController . addListener ( _filterProducts ) ;
_searchImeiController . addListener ( _filterProducts ) ;
_searchReferenceController . addListener ( _filterProducts ) ;
}
Future < void > _loadPointsDeVenteWithDefault ( ) async {
try {
final points = await _appDatabase . getPointsDeVente ( ) ;
setState ( ( ) {
_pointsDeVente = points ;
if ( points . isNotEmpty ) {
if ( _userController . pointDeVenteId > 0 ) {
final userPointDeVente = points . firstWhere (
( point ) = > point [ ' id ' ] = = _userController . pointDeVenteId ,
orElse: ( ) = > < String , dynamic > { } ,
) ;
if ( userPointDeVente . isNotEmpty ) {
_selectedPointDeVente = userPointDeVente [ ' nom ' ] as String ;
} else {
_selectedPointDeVente = points [ 0 ] [ ' nom ' ] as String ;
}
} else {
_selectedPointDeVente = points [ 0 ] [ ' nom ' ] as String ;
}
}
} ) ;
_filterProducts ( ) ; // Appliquer le filtre dès le chargement
} catch ( e ) {
Get . snackbar ( ' Erreur ' , ' Impossible de charger les points de vente: $ e ' ) ;
print ( ' ❌ Erreur chargement points de vente: $ e ' ) ;
}
}
bool _isUserSuperAdmin ( ) {
return _userController . role = = ' Super Admin ' ;
}
bool _isProduitCommandable ( Product product ) {
if ( _isUserSuperAdmin ( ) ) {
return true ; // Les superadmins peuvent tout commander
}
// Les autres utilisateurs ne peuvent commander que les produits de leur PV
return product . pointDeVenteId = = _userController . pointDeVenteId ;
}
// 🎯 MÉTHODE UTILITAIRE: Obtenir l'ID du point de vente sélectionné
int ? _getSelectedPointDeVenteId ( ) {
if ( _selectedPointDeVente = = null ) return null ;
final pointDeVente = _pointsDeVente . firstWhere (
( point ) = > point [ ' nom ' ] = = _selectedPointDeVente ,
orElse: ( ) = > < String , dynamic > { } ,
) ;
return pointDeVente . isNotEmpty ? pointDeVente [ ' id ' ] as int : null ;
}
// 2. Ajoutez cette méthode pour charger les points de vente
// 2. Ajoutez cette méthode pour charger les points de vente
Future < void > _loadPointsDeVente ( ) async {
try {
final points = await _appDatabase . getPointsDeVente ( ) ;
setState ( ( ) {
_pointsDeVente = points ;
if ( points . isNotEmpty ) {
_selectedPointDeVente = points . first [ ' nom ' ] as String ;
}
} ) ;
} catch ( e ) {
Get . snackbar ( ' Erreur ' , ' Impossible de charger les points de vente: $ e ' ) ;
}
}
// ==Gestion des remise
// 3. Ajouter ces méthodes pour gérer les remises
Future < void > _showRemiseDialog ( Product product ) async {
final detailExistant = _panierDetails [ product . id ! ] ;
final result = await showDialog < dynamic > (
context: context ,
builder: ( context ) = > RemiseDialog (
product: product ,
quantite: detailExistant ? . quantite ? ? 1 ,
prixUnitaire: product . price ,
detailExistant: detailExistant ,
) ,
) ;
if ( result ! = null ) {
if ( result = = ' supprimer ' ) {
_supprimerRemise ( product . id ! ) ;
} else if ( result is Map < String , dynamic > ) {
_appliquerRemise ( product . id ! , result ) ;
}
}
}
void _appliquerRemise ( int productId , Map < String , dynamic > remiseData ) {
final detailExistant = _panierDetails [ productId ] ;
if ( detailExistant = = null ) return ;
final detailAvecRemise = detailExistant . appliquerRemise (
type: remiseData [ ' type ' ] as RemiseType ,
valeur: remiseData [ ' valeur ' ] as double ,
) ;
setState ( ( ) {
_panierDetails [ productId ] = detailAvecRemise ;
} ) ;
Get . snackbar (
' Remise appliquée ' ,
' Remise de ${ detailAvecRemise . remiseDescription } appliquée ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
}
void _supprimerRemise ( int productId ) {
final detailExistant = _panierDetails [ productId ] ;
if ( detailExistant = = null ) return ;
setState ( ( ) {
_panierDetails [ productId ] = detailExistant . supprimerRemise ( ) ;
} ) ;
Get . snackbar (
' Remise supprimée ' ,
' La remise a été supprimée ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . blue . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
}
// Ajout des produits au pannier
// 4. Modifier la méthode pour ajouter des produits au panier
// 🎯 MODIFIÉ: Validation avant ajout au panier
// 🎯 MODIFIÉ: Validation avant ajout au panier (inchangée)
void _ajouterAuPanier ( Product product , int quantite ) {
// 🔒 VÉRIFICATION SÉCURITÉ: Non-superadmin ne peut commander que ses produits
if ( ! _isProduitCommandable ( product ) ) {
Get . snackbar (
' Produit non commandable ' ,
' Ce produit appartient à un autre point de vente. Seuls les produits de votre point de vente " ${ _userController . pointDeVenteDesignation } " sont commandables. ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
icon: const Icon ( Icons . info , color: Colors . white ) ,
duration: const Duration ( seconds: 5 ) ,
) ;
return ;
}
// Vérifier le stock disponible
if ( product . stock ! = null & & quantite > product . stock ! ) {
Get . snackbar (
' Stock insuffisant ' ,
' Quantité demandée non disponible ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
return ;
}
setState ( ( ) {
final detail = DetailCommande . sansRemise (
commandeId: 0 ,
produitId: product . id ! ,
quantite: quantite ,
prixUnitaire: product . price ,
produitNom: product . name ,
produitReference: product . reference ,
) ;
_panierDetails [ product . id ! ] = detail ;
} ) ;
}
// 🎯 MODIFIÉ: Validation lors de la modification de quantité
void _modifierQuantite ( int productId , int nouvelleQuantite ) {
final detailExistant = _panierDetails [ productId ] ;
if ( detailExistant = = null ) return ;
final product = _products . firstWhere ( ( p ) = > p . id = = productId ) ;
// 🔒 VÉRIFICATION SÉCURITÉ supplémentaire
if ( ! _isProduitCommandable ( product ) ) {
Get . snackbar (
' Modification impossible ' ,
' Vous ne pouvez modifier que les produits de votre point de vente ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
) ;
return ;
}
if ( nouvelleQuantite < = 0 ) {
setState ( ( ) {
_panierDetails . remove ( productId ) ;
} ) ;
return ;
}
// ... reste du code existant pour la modification
if ( product . stock ! = null & & nouvelleQuantite > product . stock ! ) {
Get . snackbar (
' Stock insuffisant ' ,
' Quantité maximum: ${ product . stock } ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange ,
colorText: Colors . white ,
) ;
return ;
}
final nouveauSousTotal = nouvelleQuantite * detailExistant . prixUnitaire ;
setState ( ( ) {
if ( detailExistant . estCadeau ) {
// Pour un cadeau, le prix final reste à 0
_panierDetails [ productId ] = DetailCommande (
id: detailExistant . id ,
commandeId: detailExistant . commandeId ,
produitId: detailExistant . produitId ,
quantite: nouvelleQuantite ,
prixUnitaire: detailExistant . prixUnitaire ,
sousTotal: nouveauSousTotal ,
prixFinal: 0.0 ,
estCadeau: true ,
produitNom: detailExistant . produitNom ,
produitReference: detailExistant . produitReference ,
) ;
} else if ( detailExistant . aRemise ) {
// Recalculer la remise si elle existe
final detail = DetailCommande (
id: detailExistant . id ,
commandeId: detailExistant . commandeId ,
produitId: detailExistant . produitId ,
quantite: nouvelleQuantite ,
prixUnitaire: detailExistant . prixUnitaire ,
sousTotal: nouveauSousTotal ,
prixFinal: nouveauSousTotal ,
produitNom: detailExistant . produitNom ,
produitReference: detailExistant . produitReference ,
) . appliquerRemise (
type: detailExistant . remiseType ! ,
valeur: detailExistant . remiseValeur ,
) ;
_panierDetails [ productId ] = detail ;
} else {
// Article normal sans remise
_panierDetails [ productId ] = DetailCommande (
id: detailExistant . id ,
commandeId: detailExistant . commandeId ,
produitId: detailExistant . produitId ,
quantite: nouvelleQuantite ,
prixUnitaire: detailExistant . prixUnitaire ,
sousTotal: nouveauSousTotal ,
prixFinal: nouveauSousTotal ,
produitNom: detailExistant . produitNom ,
produitReference: detailExistant . produitReference ,
) ;
}
} ) ;
}
// === NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE (identiques à ProductManagementPage) ===
void _startAutomaticScanning ( ) {
if ( _isScanning ) return ;
setState ( ( ) {
_isScanning = true ;
} ) ;
Get . to ( ( ) = > _buildAutomaticScannerPage ( ) ) ? . then ( ( _ ) {
setState ( ( ) {
_isScanning = false ;
} ) ;
} ) ;
}
Widget _buildAutomaticScannerPage ( ) {
return Scaffold (
appBar: AppBar (
title: const Text ( ' Scanner Produit ' ) ,
backgroundColor: Colors . green . shade700 ,
foregroundColor: Colors . white ,
leading: IconButton (
icon: const Icon ( Icons . close ) ,
onPressed: ( ) {
_qrController ? . dispose ( ) ;
Get . back ( ) ;
} ,
) ,
actions: [
IconButton (
icon: const Icon ( Icons . flash_on ) ,
onPressed: ( ) async {
await _qrController ? . toggleFlash ( ) ;
} ,
) ,
IconButton (
icon: const Icon ( Icons . flip_camera_ios ) ,
onPressed: ( ) async {
await _qrController ? . flipCamera ( ) ;
} ,
) ,
] ,
) ,
body: Stack (
children: [
// Scanner view
QRView (
key: _qrKey ,
onQRViewCreated: _onAutomaticQRViewCreated ,
overlay: QrScannerOverlayShape (
borderColor: Colors . green ,
borderRadius: 10 ,
borderLength: 30 ,
borderWidth: 10 ,
cutOutSize: 250 ,
) ,
) ,
// Instructions overlay
Positioned (
bottom: 100 ,
left: 20 ,
right: 20 ,
child: Container (
padding: const EdgeInsets . all ( 16 ) ,
decoration: BoxDecoration (
color: Colors . black . withOpacity ( 0.7 ) ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: const Column (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . qr_code_scanner , color: Colors . white , size: 40 ) ,
SizedBox ( height: 8 ) ,
Text (
' Scanner automatiquement un produit ' ,
style: TextStyle (
color: Colors . white ,
fontSize: 16 ,
fontWeight: FontWeight . w500 ,
) ,
textAlign: TextAlign . center ,
) ,
SizedBox ( height: 4 ) ,
Text (
' Pointez vers QR Code, IMEI ou code-barres ' ,
style: TextStyle (
color: Colors . white70 ,
fontSize: 14 ,
) ,
textAlign: TextAlign . center ,
) ,
] ,
) ,
) ,
) ,
] ,
) ,
) ;
}
void _onAutomaticQRViewCreated ( QRViewController controller ) {
_qrController = controller ;
controller . scannedDataStream . listen ( ( scanData ) {
if ( scanData . code ! = null & & scanData . code ! . isNotEmpty ) {
// Pauser le scanner pour éviter les scans multiples
controller . pauseCamera ( ) ;
// Fermer la page du scanner
Get . back ( ) ;
// Traiter le résultat avec identification automatique
_processScannedData ( scanData . code ! ) ;
}
} ) ;
}
Future < void > _processScannedData ( String scannedData ) async {
try {
// Montrer un indicateur de chargement
Get . dialog (
AlertDialog (
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
CircularProgressIndicator ( color: Colors . green . shade700 ) ,
const SizedBox ( height: 16 ) ,
const Text ( ' Identification du produit... ' ) ,
const SizedBox ( height: 8 ) ,
Text (
' Code: $ scannedData ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
fontFamily: ' monospace ' ,
) ,
) ,
] ,
) ,
) ,
barrierDismissible: false ,
) ;
// Attendre un court instant pour l'effet visuel
await Future . delayed ( const Duration ( milliseconds: 300 ) ) ;
// Recherche automatique du produit par différents critères
Product ? foundProduct = await _findProductAutomatically ( scannedData ) ;
// Fermer l'indicateur de chargement
Get . back ( ) ;
if ( foundProduct = = null ) {
_showProductNotFoundDialog ( scannedData ) ;
return ;
}
// Vérifier le stock
if ( foundProduct . stock ! = null & & foundProduct . stock ! < = 0 ) {
Get . snackbar (
' Stock insuffisant ' ,
' Le produit " ${ foundProduct . name } " n \' est plus en stock ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
icon: const Icon ( Icons . warning_amber , color: Colors . white ) ,
) ;
return ;
}
final detailExistant = _panierDetails [ foundProduct ! . id ! ] ;
// Vérifier si le produit peut être ajouté (stock disponible)
final currentQuantity = _quantites [ foundProduct . id ] ? ? 0 ;
if ( foundProduct . stock ! = null & & currentQuantity > = foundProduct . stock ! ) {
Get . snackbar (
' Stock limite atteint ' ,
' Quantité maximum atteinte pour " ${ foundProduct . name } " ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
icon: const Icon ( Icons . warning_amber , color: Colors . white ) ,
) ;
return ;
}
// Ajouter le produit au panier
_modifierQuantite ( foundProduct . id ! , currentQuantity + 1 ) ;
// Afficher le dialogue de succès
_showProductFoundAndAddedDialog ( foundProduct , currentQuantity + 1 ) ;
} catch ( e ) {
// Fermer l'indicateur de chargement si il est encore ouvert
if ( Get . isDialogOpen ! ) Get . back ( ) ;
Get . snackbar (
' Erreur ' ,
' Une erreur est survenue: ${ e . toString ( ) } ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
) ;
}
}
Future < Product ? > _findProductAutomatically ( String scannedData ) async {
// Nettoyer les données scannées
final cleanedData = scannedData . trim ( ) ;
// 1. Essayer de trouver par IMEI exact
for ( var product in _products ) {
if ( product . imei ? . toLowerCase ( ) . trim ( ) = = cleanedData . toLowerCase ( ) ) {
return product ;
}
}
// 2. Essayer de trouver par référence exacte
for ( var product in _products ) {
if ( product . reference ? . toLowerCase ( ) . trim ( ) = = cleanedData . toLowerCase ( ) ) {
return product ;
}
}
// 3. Si c'est une URL QR code, extraire la référence
if ( cleanedData . contains ( ' stock.guycom.mg/ ' ) ) {
final reference = cleanedData . split ( ' / ' ) . last ;
for ( var product in _products ) {
if ( product . reference ? . toLowerCase ( ) . trim ( ) = = reference . toLowerCase ( ) ) {
return product ;
}
}
}
// 4. Recherche par correspondance partielle dans le nom
for ( var product in _products ) {
if ( product . name . toLowerCase ( ) . contains ( cleanedData . toLowerCase ( ) ) & &
cleanedData . length > = 3 ) {
return product ;
}
}
// 5. Utiliser la base de données pour une recherche plus approfondie
try {
// Recherche par IMEI dans la base
final productByImei = await _appDatabase . getProductByIMEI ( cleanedData ) ;
if ( productByImei ! = null ) {
return productByImei ;
}
// Recherche par référence dans la base
final productByRef = await _appDatabase . getProductByReference ( cleanedData ) ;
if ( productByRef ! = null ) {
return productByRef ;
}
} catch ( e ) {
print ( ' Erreur recherche base de données: $ e ' ) ;
}
return null ;
}
void _showProductFoundAndAddedDialog ( Product product , int newQuantity ) {
Get . dialog (
AlertDialog (
title: Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . green . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon ( Icons . check_circle , color: Colors . green . shade700 ) ,
) ,
const SizedBox ( width: 12 ) ,
const Expanded ( child: Text ( ' Produit identifié et ajouté ! ' ) ) ,
] ,
) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
product . name ,
style: const TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
) ,
const SizedBox ( height: 8 ) ,
if ( product . imei ! = null & & product . imei ! . isNotEmpty )
Text ( ' IMEI: ${ product . imei } ' ) ,
if ( product . reference ! = null & & product . reference ! . isNotEmpty )
Text ( ' Référence: ${ product . reference } ' ) ,
Text ( ' Prix: ${ product . price . toStringAsFixed ( 2 ) } MGA ' ) ,
Text ( ' Quantité dans le panier: $ newQuantity ' ) ,
if ( product . stock ! = null )
Text ( ' Stock restant: ${ product . stock ! - newQuantity } ' ) ,
const SizedBox ( height: 12 ) ,
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . green . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Row (
children: [
Icon ( Icons . auto_awesome ,
color: Colors . green . shade700 , size: 16 ) ,
const SizedBox ( width: 8 ) ,
const Expanded (
child: Text (
' Produit identifié automatiquement ' ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Get . back ( ) ,
child: const Text ( ' Continuer ' ) ,
) ,
ElevatedButton (
onPressed: ( ) {
Get . back ( ) ;
_showCartBottomSheet ( ) ;
} ,
style: ElevatedButton . styleFrom (
backgroundColor: Colors . green . shade700 ,
foregroundColor: Colors . white ,
) ,
child: const Text ( ' Voir le panier ' ) ,
) ,
ElevatedButton (
onPressed: ( ) {
Get . back ( ) ;
_startAutomaticScanning ( ) ; // Scanner un autre produit
} ,
style: ElevatedButton . styleFrom (
backgroundColor: Colors . blue . shade700 ,
foregroundColor: Colors . white ,
) ,
child: const Text ( ' Scanner encore ' ) ,
) ,
] ,
) ,
) ;
}
void _showProductNotFoundDialog ( String scannedData ) {
Get . dialog (
AlertDialog (
title: Row (
children: [
Icon ( Icons . search_off , color: Colors . red . shade600 ) ,
const SizedBox ( width: 8 ) ,
const Text ( ' Produit non trouvé ' ) ,
] ,
) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const Text ( ' Aucun produit trouvé avec ce code: ' ) ,
const SizedBox ( height: 8 ) ,
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade100 ,
borderRadius: BorderRadius . circular ( 4 ) ,
) ,
child: Text (
scannedData ,
style: const TextStyle (
fontFamily: ' monospace ' ,
fontWeight: FontWeight . bold ,
) ,
) ,
) ,
const SizedBox ( height: 12 ) ,
Text (
' Vérifiez que le code est correct ou que le produit existe dans la base de données. ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
) ,
) ,
const SizedBox ( height: 8 ) ,
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
' Types de codes supportés: ' ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . bold ,
color: Colors . blue . shade700 ,
) ,
) ,
const SizedBox ( height: 4 ) ,
Text (
' • QR Code produit \n • IMEI (téléphones) \n • Référence produit \n • Code-barres ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . blue . shade600 ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Get . back ( ) ,
child: const Text ( ' Fermer ' ) ,
) ,
ElevatedButton (
onPressed: ( ) {
Get . back ( ) ;
_startAutomaticScanning ( ) ;
} ,
style: ElevatedButton . styleFrom (
backgroundColor: Colors . green . shade700 ,
foregroundColor: Colors . white ,
) ,
child: const Text ( ' Scanner à nouveau ' ) ,
) ,
] ,
) ,
) ;
}
Widget _buildAutoScanInfoCard ( ) {
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 8 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . green . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
Icons . auto_awesome ,
color: Colors . green . shade700 ,
size: 20 ,
) ,
) ,
const SizedBox ( width: 12 ) ,
const Expanded (
child: Text (
' Scanner automatiquement: QR Code, IMEI, Référence ou code-barres ' ,
style: TextStyle (
fontSize: 14 ,
color: Color . fromARGB ( 255 , 9 , 56 , 95 ) ,
) ,
) ,
) ,
ElevatedButton . icon (
onPressed: _isScanning ? null : _startAutomaticScanning ,
icon: _isScanning
? const SizedBox (
width: 16 ,
height: 16 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
color: Colors . white ,
) ,
)
: const Icon ( Icons . qr_code_scanner , size: 18 ) ,
label: Text ( _isScanning ? ' Scan... ' : ' Scanner ' ) ,
style: ElevatedButton . styleFrom (
backgroundColor: _isScanning ? Colors . grey : Colors . green . shade700 ,
foregroundColor: Colors . white ,
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// === FIN DES NOUVELLES MÉTHODES DE SCAN AUTOMATIQUE ===
// 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 ;
} ) ;
}
Future < void > _showClientSuggestions ( String query , { required bool isNom } ) async {
if ( query . length < 3 ) {
_hideAllSuggestions ( ) ;
return ;
}
setState ( ( ) {
if ( isNom ) {
_showNomSuggestions = true ;
_showTelephoneSuggestions = false ;
} else {
_showTelephoneSuggestions = true ;
_showNomSuggestions = false ;
}
} ) ;
}
void _hideNomSuggestions ( ) {
if ( mounted & & _showNomSuggestions ) {
setState ( ( ) {
_showNomSuggestions = false ;
} ) ;
}
}
void _hideTelephoneSuggestions ( ) {
if ( mounted & & _showTelephoneSuggestions ) {
setState ( ( ) {
_showTelephoneSuggestions = false ;
} ) ;
}
}
void _hideAllSuggestions ( ) {
_hideNomSuggestions ( ) ;
_hideTelephoneSuggestions ( ) ;
}
// 🎯 MODIFIÉ: Chargement de TOUS les produits (visibilité totale)
Future < void > _loadProducts ( ) async {
final products = await _appDatabase . getProducts ( ) ;
setState ( ( ) {
_products . clear ( ) ;
// ✅ TOUS les utilisateurs voient TOUS les produits
_products . addAll ( products ) ;
print ( " ✅ Produits chargés: ${ products . length } (tous visibles) " ) ;
_filteredProducts . clear ( ) ;
_filteredProducts . addAll ( _products ) ;
} ) ;
}
Future < void > _loadCommercialUsers ( ) async {
final commercialUsers = await _appDatabase . getCommercialUsers ( ) ;
setState ( ( ) {
_commercialUsers = commercialUsers ;
if ( _commercialUsers . isNotEmpty ) {
_selectedCommercialUser = _commercialUsers . first ;
}
} ) ;
}
// 🎯 MODIFIÉ: Filtrage avec visibilité totale mais indication des restrictions
void _filterProducts ( ) {
final nameQuery = _searchNameController . text . toLowerCase ( ) ;
final imeiQuery = _searchImeiController . text . toLowerCase ( ) ;
final referenceQuery = _searchReferenceController . text . toLowerCase ( ) ;
final selectedPointDeVenteId = _getSelectedPointDeVenteId ( ) ;
setState ( ( ) {
_filteredProducts . clear ( ) ;
for ( var product in _products ) {
bool matchesName = nameQuery . isEmpty | |
product . name . toLowerCase ( ) . contains ( nameQuery ) ;
bool matchesImei = imeiQuery . isEmpty | |
( product . imei ? . toLowerCase ( ) . contains ( imeiQuery ) ? ? false ) ;
bool matchesReference = referenceQuery . isEmpty | |
( product . reference ? . toLowerCase ( ) . contains ( referenceQuery ) ? ? false ) ;
bool matchesStock =
! _showOnlyInStock | | ( product . stock ! = null & & product . stock ! > 0 ) ;
// Appliquer le filtre par point de vente uniquement si un point est sélectionné
bool matchesPointDeVente = true ;
if ( selectedPointDeVenteId ! = null ) {
matchesPointDeVente = product . pointDeVenteId = = selectedPointDeVenteId ;
}
if ( matchesName & &
matchesImei & &
matchesReference & &
matchesStock & &
matchesPointDeVente ) {
_filteredProducts . add ( product ) ;
}
}
} ) ;
print ( " 🔍 Filtrage: ${ _filteredProducts . length } produits visibles " ) ;
}
void _toggleStockFilter ( ) {
setState ( ( ) {
_showOnlyInStock = ! _showOnlyInStock ;
} ) ;
_filterProducts ( ) ;
}
// 🎯 MÉTHODE UTILITAIRE: Reset des filtres avec point de vente utilisateur
void _clearFilters ( ) {
setState ( ( ) {
_searchNameController . clear ( ) ;
_searchImeiController . clear ( ) ;
_searchReferenceController . clear ( ) ;
_showOnlyInStock = false ;
// Réinitialiser au point de vente de l'utilisateur connecté
if ( _userController . pointDeVenteId > 0 ) {
final userPointDeVente = _pointsDeVente . firstWhere (
( point ) = > point [ ' id ' ] = = _userController . pointDeVenteId ,
orElse: ( ) = > < String , dynamic > { } ,
) ;
if ( userPointDeVente . isNotEmpty ) {
_selectedPointDeVente = userPointDeVente [ ' nom ' ] as String ;
} else {
_selectedPointDeVente = null ; // Fallback si le point de vente n'existe plus
}
} else {
_selectedPointDeVente = null ;
}
} ) ;
_filterProducts ( ) ;
print ( " 🔄 Filtres réinitialisés - Point de vente: $ _selectedPointDeVente " ) ;
}
// 11. Modifiez la section des filtres pour inclure le bouton de réinitialisation
Widget _buildFilterSection ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 16 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 16.0 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Icon ( Icons . filter_list , color: Colors . blue . shade700 ) ,
const SizedBox ( width: 8 ) ,
const Text (
' Filtres de recherche ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
color: Color . fromARGB ( 255 , 9 , 56 , 95 ) ,
) ,
) ,
const Spacer ( ) ,
TextButton . icon (
onPressed: _clearFilters ,
icon: const Icon ( Icons . clear , size: 18 ) ,
label: isMobile ? const SizedBox ( ) : const Text ( ' Réinitialiser ' ) ,
style: TextButton . styleFrom (
foregroundColor: Colors . grey . shade600 ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 16 ) ,
// Champ de recherche par nom
TextField (
controller: _searchNameController ,
decoration: InputDecoration (
labelText: ' Rechercher par nom ' ,
prefixIcon: const Icon ( Icons . search ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
const SizedBox ( height: 12 ) ,
if ( ! isMobile ) . . . [
// Version desktop - champs sur la même ligne
Row (
children: [
Expanded (
child: TextField (
controller: _searchImeiController ,
decoration: InputDecoration (
labelText: ' IMEI ' ,
prefixIcon: const Icon ( Icons . phone_android ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: TextField (
controller: _searchReferenceController ,
decoration: InputDecoration (
labelText: ' Référence ' ,
prefixIcon: const Icon ( Icons . qr_code ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
) ,
] ,
) ,
] else . . . [
// Version mobile - champs empilés
TextField (
controller: _searchImeiController ,
decoration: InputDecoration (
labelText: ' IMEI ' ,
prefixIcon: const Icon ( Icons . phone_android ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
const SizedBox ( height: 12 ) ,
TextField (
controller: _searchReferenceController ,
decoration: InputDecoration (
labelText: ' Référence ' ,
prefixIcon: const Icon ( Icons . qr_code ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
] ,
const SizedBox ( height: 16 ) ,
// Boutons de filtre adaptés pour mobile
Wrap (
spacing: 8 ,
runSpacing: 8 ,
children: [
ElevatedButton . icon (
onPressed: _toggleStockFilter ,
icon: Icon (
_showOnlyInStock ? Icons . inventory : Icons . inventory_2 ,
size: 20 ,
) ,
label: Text ( _showOnlyInStock
? isMobile ? ' Tous ' : ' Afficher tous '
: isMobile ? ' En stock ' : ' Stock disponible ' ) ,
style: ElevatedButton . styleFrom (
backgroundColor: _showOnlyInStock
? Colors . green . shade600
: Colors . blue . shade600 ,
foregroundColor: Colors . white ,
padding: EdgeInsets . symmetric (
horizontal: isMobile ? 12 : 16 ,
vertical: 8
) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 8 ) ,
// Compteur de résultats avec indicateurs de filtres actifs
Container (
padding: const EdgeInsets . symmetric (
horizontal: 12 ,
vertical: 8
) ,
decoration: BoxDecoration (
color: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 20 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
' ${ _filteredProducts . length } produit(s) ' ,
style: TextStyle (
color: Colors . blue . shade700 ,
fontWeight: FontWeight . w600 ,
fontSize: isMobile ? 12 : 14 ,
) ,
) ,
// Indicateurs de filtres actifs
if ( _selectedPointDeVente ! = null | | _showOnlyInStock | |
_searchNameController . text . isNotEmpty | |
_searchImeiController . text . isNotEmpty | |
_searchReferenceController . text . isNotEmpty ) . . . [
const SizedBox ( height: 4 ) ,
Wrap (
spacing: 4 ,
children: [
if ( _selectedPointDeVente ! = null )
_buildFilterChip ( ' PV: $ _selectedPointDeVente ' ) ,
if ( _showOnlyInStock )
_buildFilterChip ( ' En stock ' ) ,
if ( _searchNameController . text . isNotEmpty )
_buildFilterChip ( ' Nom: ${ _searchNameController . text } ' ) ,
if ( _searchImeiController . text . isNotEmpty )
_buildFilterChip ( ' IMEI: ${ _searchImeiController . text } ' ) ,
if ( _searchReferenceController . text . isNotEmpty )
_buildFilterChip ( ' Réf: ${ _searchReferenceController . text } ' ) ,
] ,
) ,
] ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
Widget _buildFilterChip ( String label ) {
return Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . orange . shade100 ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: Colors . orange . shade300 ) ,
) ,
child: Text (
label ,
style: TextStyle (
fontSize: 10 ,
color: Colors . orange . shade700 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ;
}
Widget _buildFloatingCartButton ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
final cartItemCount = _panierDetails . values . where ( ( d ) = > d . quantite > 0 ) . length ;
if ( isMobile ) {
return Row (
mainAxisAlignment: MainAxisAlignment . end ,
children: [
FloatingActionButton (
heroTag: " scan_btn " ,
onPressed: _isScanning ? null : _startAutomaticScanning ,
backgroundColor: _isScanning ? Colors . grey : Colors . green . shade700 ,
foregroundColor: Colors . white ,
mini: true ,
child: _isScanning
? const SizedBox (
width: 16 ,
height: 16 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
color: Colors . white ,
) ,
)
: const Icon ( Icons . qr_code_scanner ) ,
) ,
const SizedBox ( width: 8 ) ,
FloatingActionButton . extended (
onPressed: _showCartBottomSheet ,
icon: const Icon ( Icons . shopping_cart ) ,
label: Text ( ' $ cartItemCount ' ) ,
backgroundColor: Colors . blue . shade800 ,
foregroundColor: Colors . white ,
) ,
] ,
) ;
} else {
return FloatingActionButton . extended (
onPressed: _showCartBottomSheet ,
icon: const Icon ( Icons . shopping_cart ) ,
label: Text ( ' Panier ( $ cartItemCount ) ' ) ,
backgroundColor: Colors . blue . shade800 ,
foregroundColor: Colors . white ,
) ;
}
}
// Nouvelle méthode pour afficher les filtres sur mobile
void _showMobileFilters ( BuildContext context ) {
showModalBottomSheet (
context: context ,
isScrollControlled: true ,
builder: ( context ) = > SingleChildScrollView (
padding: EdgeInsets . only (
bottom: MediaQuery . of ( context ) . viewInsets . bottom ,
) ,
child: Column (
children: [
_buildPointDeVenteFilter ( ) ,
_buildFilterSection ( ) ,
] ,
) ,
) ,
) ;
}
void _showClientFormDialog ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
// Variables locales pour les suggestions dans le dialog
bool showNomSuggestions = false ;
bool showPrenomSuggestions = false ;
bool showEmailSuggestions = false ;
bool showTelephoneSuggestions = false ;
List < Client > localClientSuggestions = [ ] ;
// GlobalKeys pour positionner les overlays
final GlobalKey nomFieldKey = GlobalKey ( ) ;
final GlobalKey prenomFieldKey = GlobalKey ( ) ;
final GlobalKey emailFieldKey = GlobalKey ( ) ;
final GlobalKey telephoneFieldKey = GlobalKey ( ) ;
Get . dialog (
StatefulBuilder (
builder: ( context , setDialogState ) {
return Stack (
children: [
AlertDialog (
title: Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon ( Icons . person_add , color: Colors . blue . shade700 ) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Text (
isMobile ? ' Client ' : ' Informations Client ' ,
style: TextStyle ( fontSize: isMobile ? 16 : 18 ) ,
) ,
) ,
] ,
) ,
content: Container (
width: isMobile ? double . maxFinite : 600 ,
constraints: BoxConstraints (
maxHeight: MediaQuery . of ( context ) . size . height * 0.7 ,
) ,
child: SingleChildScrollView (
child: Form (
key: _formKey ,
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Champ Nom avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey (
key: nomFieldKey ,
controller: _nomController ,
label: ' Nom ' ,
validator: ( value ) = > value ? . isEmpty ? ? true
? ' Veuillez entrer un nom ' : null ,
onChanged: ( value ) async {
if ( value . length > = 2 ) {
final suggestions = await _appDatabase . suggestClients ( value ) ;
setDialogState ( ( ) {
localClientSuggestions = suggestions ;
showNomSuggestions = suggestions . isNotEmpty ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
} ) ;
} else {
setDialogState ( ( ) {
showNomSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
}
} ,
) ,
const SizedBox ( height: 12 ) ,
// Champ Prénom avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey (
key: prenomFieldKey ,
controller: _prenomController ,
label: ' Prénom ' ,
validator: ( value ) = > value ? . isEmpty ? ? true
? ' Veuillez entrer un prénom ' : null ,
onChanged: ( value ) async {
if ( value . length > = 2 ) {
final suggestions = await _appDatabase . suggestClients ( value ) ;
setDialogState ( ( ) {
localClientSuggestions = suggestions ;
showPrenomSuggestions = suggestions . isNotEmpty ;
showNomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
} ) ;
} else {
setDialogState ( ( ) {
showPrenomSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
}
} ,
) ,
const SizedBox ( height: 12 ) ,
// Champ Email avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey (
key: emailFieldKey ,
controller: _emailController ,
label: ' Email ' ,
keyboardType: TextInputType . emailAddress ,
validator: ( value ) {
if ( value ? . isEmpty ? ? true ) return ' Veuillez entrer un email ' ;
if ( ! RegExp ( r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$' ) . hasMatch ( value ! ) ) {
return ' Email invalide ' ;
}
return null ;
} ,
onChanged: ( value ) async {
if ( value . length > = 3 ) {
final suggestions = await _appDatabase . suggestClients ( value ) ;
setDialogState ( ( ) {
localClientSuggestions = suggestions ;
showEmailSuggestions = suggestions . isNotEmpty ;
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showTelephoneSuggestions = false ;
} ) ;
} else {
setDialogState ( ( ) {
showEmailSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
}
} ,
) ,
const SizedBox ( height: 12 ) ,
// Champ Téléphone avec suggestions (SANS bouton recherche)
_buildTextFormFieldWithKey (
key: telephoneFieldKey ,
controller: _telephoneController ,
label: ' Téléphone ' ,
keyboardType: TextInputType . phone ,
validator: ( value ) = > value ? . isEmpty ? ? true
? ' Veuillez entrer un téléphone ' : null ,
onChanged: ( value ) async {
if ( value . length > = 3 ) {
final suggestions = await _appDatabase . suggestClients ( value ) ;
setDialogState ( ( ) {
localClientSuggestions = suggestions ;
showTelephoneSuggestions = suggestions . isNotEmpty ;
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
} ) ;
} else {
setDialogState ( ( ) {
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
}
} ,
) ,
const SizedBox ( height: 12 ) ,
_buildTextFormField (
controller: _adresseController ,
label: ' Adresse ' ,
maxLines: 2 ,
validator: ( value ) = > value ? . isEmpty ? ? true
? ' Veuillez entrer une adresse ' : null ,
) ,
const SizedBox ( height: 12 ) ,
_buildCommercialDropdown ( ) ,
] ,
) ,
) ,
) ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Get . back ( ) ,
child: const Text ( ' Annuler ' ) ,
) ,
ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: Colors . blue . shade800 ,
foregroundColor: Colors . white ,
padding: EdgeInsets . symmetric (
horizontal: isMobile ? 16 : 20 ,
vertical: isMobile ? 10 : 12
) ,
) ,
onPressed: ( ) {
if ( _formKey . currentState ! . validate ( ) ) {
// Fermer toutes les suggestions avant de soumettre
setDialogState ( ( ) {
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
Get . back ( ) ;
_submitOrder ( ) ;
}
} ,
child: Text (
isMobile ? ' Valider ' : ' Valider la commande ' ,
style: TextStyle ( fontSize: isMobile ? 12 : 14 ) ,
) ,
) ,
] ,
) ,
// Overlay pour les suggestions du nom
if ( showNomSuggestions )
_buildSuggestionOverlay (
fieldKey: nomFieldKey ,
suggestions: localClientSuggestions ,
onClientSelected: ( client ) {
_fillFormWithClient ( client ) ;
setDialogState ( ( ) {
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
onDismiss: ( ) {
setDialogState ( ( ) {
showNomSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
) ,
// Overlay pour les suggestions du prénom
if ( showPrenomSuggestions )
_buildSuggestionOverlay (
fieldKey: prenomFieldKey ,
suggestions: localClientSuggestions ,
onClientSelected: ( client ) {
_fillFormWithClient ( client ) ;
setDialogState ( ( ) {
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
onDismiss: ( ) {
setDialogState ( ( ) {
showPrenomSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
) ,
// Overlay pour les suggestions de l'email
if ( showEmailSuggestions )
_buildSuggestionOverlay (
fieldKey: emailFieldKey ,
suggestions: localClientSuggestions ,
onClientSelected: ( client ) {
_fillFormWithClient ( client ) ;
setDialogState ( ( ) {
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
onDismiss: ( ) {
setDialogState ( ( ) {
showEmailSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
) ,
// Overlay pour les suggestions du téléphone
if ( showTelephoneSuggestions )
_buildSuggestionOverlay (
fieldKey: telephoneFieldKey ,
suggestions: localClientSuggestions ,
onClientSelected: ( client ) {
_fillFormWithClient ( client ) ;
setDialogState ( ( ) {
showNomSuggestions = false ;
showPrenomSuggestions = false ;
showEmailSuggestions = false ;
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
onDismiss: ( ) {
setDialogState ( ( ) {
showTelephoneSuggestions = false ;
localClientSuggestions = [ ] ;
} ) ;
} ,
) ,
] ,
) ;
} ,
) ,
) ;
}
// Widget pour créer un TextFormField avec une clé
Widget _buildTextFormFieldWithKey ( {
required GlobalKey key ,
required TextEditingController controller ,
required String label ,
TextInputType ? keyboardType ,
int maxLines = 1 ,
String ? Function ( String ? ) ? validator ,
void Function ( String ) ? onChanged ,
} ) {
return Container (
key: key ,
child: _buildTextFormField (
controller: controller ,
label: label ,
keyboardType: keyboardType ,
maxLines: maxLines ,
validator: validator ,
onChanged: onChanged ,
) ,
) ;
}
// Widget pour l'overlay des suggestions
// Widget pour l'overlay des suggestions
Widget _buildSuggestionOverlay ( {
required GlobalKey fieldKey ,
required List < Client > suggestions ,
required Function ( Client ) onClientSelected ,
required VoidCallback onDismiss ,
} ) {
return Positioned . fill (
child: GestureDetector (
onTap: onDismiss ,
child: Material (
color: Colors . transparent ,
child: Builder (
builder: ( context ) {
// Obtenir la position du champ
final RenderBox ? renderBox = fieldKey . currentContext ? . findRenderObject ( ) as RenderBox ? ;
if ( renderBox = = null ) return const SizedBox ( ) ;
final position = renderBox . localToGlobal ( Offset . zero ) ;
final size = renderBox . size ;
return Stack (
children: [
Positioned (
left: position . dx ,
top: position . dy + size . height + 4 ,
width: size . width ,
child: GestureDetector (
onTap: ( ) { } , // Empêcher la fermeture au tap sur la liste
child: Container (
constraints: const BoxConstraints (
maxHeight: 200 , // Hauteur maximum pour la scrollabilité
) ,
decoration: BoxDecoration (
color: Colors . white ,
border: Border . all ( color: Colors . grey . shade300 ) ,
borderRadius: BorderRadius . circular ( 8 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.15 ) ,
blurRadius: 8 ,
offset: const Offset ( 0 , 4 ) ,
) ,
] ,
) ,
child: ClipRRect (
borderRadius: BorderRadius . circular ( 8 ) ,
child: Scrollbar (
thumbVisibility: suggestions . length > 3 ,
child: ListView . separated (
padding: EdgeInsets . zero ,
shrinkWrap: true ,
itemCount: suggestions . length ,
separatorBuilder: ( context , index ) = > Divider (
height: 1 ,
color: Colors . grey . shade200 ,
) ,
itemBuilder: ( context , index ) {
final client = suggestions [ index ] ;
return ListTile (
dense: true ,
contentPadding: const EdgeInsets . symmetric (
horizontal: 12 ,
vertical: 4 ,
) ,
leading: CircleAvatar (
radius: 16 ,
backgroundColor: Colors . blue . shade100 ,
child: Icon (
Icons . person ,
size: 16 ,
color: Colors . blue . shade700 ,
) ,
) ,
title: Text (
' ${ client . nom } ${ client . prenom } ' ,
style: const TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
subtitle: Text (
' ${ client . telephone } • ${ client . email } ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
) ,
) ,
onTap: ( ) = > onClientSelected ( client ) ,
hoverColor: Colors . blue . shade50 ,
) ;
} ,
) ,
) ,
) ,
) ,
) ,
) ,
] ,
) ;
} ,
) ,
) ,
) ,
) ;
}
// Méthode pour remplir le formulaire avec les données du client
void _fillFormWithClient ( Client client ) {
_nomController . text = client . nom ;
_prenomController . text = client . prenom ;
_emailController . text = client . email ;
_telephoneController . text = client . telephone ;
_adresseController . text = client . adresse ? ? ' ' ;
Get . snackbar (
' Client trouvé ' ,
' Les informations ont été remplies automatiquement ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
}
Widget _buildTextFormField ( {
required TextEditingController controller ,
required String label ,
TextInputType ? keyboardType ,
String ? Function ( String ? ) ? validator ,
int ? maxLines ,
void Function ( String ) ? onChanged ,
} ) {
return TextFormField (
controller: controller ,
decoration: InputDecoration (
labelText: label ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . white ,
) ,
keyboardType: keyboardType ,
validator: validator ,
maxLines: maxLines ,
onChanged: onChanged ,
) ;
}
Widget _buildCommercialDropdown ( ) {
return DropdownButtonFormField < Users > (
value: _selectedCommercialUser ,
decoration: InputDecoration (
labelText: ' Commercial ' ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
filled: true ,
fillColor: Colors . white ,
) ,
items: _commercialUsers . map ( ( Users user ) {
return DropdownMenuItem < Users > (
value: user ,
child: Text ( ' ${ user . name } ${ user . lastName } ' ) ,
) ;
} ) . toList ( ) ,
onChanged: ( Users ? newValue ) {
setState ( ( ) {
_selectedCommercialUser = newValue ;
} ) ;
} ,
validator: ( value ) = > value = = null ? ' Veuillez sélectionner un commercial ' : null ,
) ;
}
Widget _buildUserPointDeVenteInfo ( ) {
if ( _userController . pointDeVenteId < = 0 ) {
return const SizedBox . shrink ( ) ;
}
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 8 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
Icons . store ,
color: Colors . blue . shade700 ,
size: 20 ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const Text (
' Votre point de vente ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w600 ,
color: Color . fromARGB ( 255 , 9 , 56 , 95 ) ,
) ,
) ,
const SizedBox ( height: 4 ) ,
Text (
_userController . pointDeVenteDesignation ,
style: TextStyle (
fontSize: 12 ,
color: Colors . blue . shade700 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
] ,
) ,
) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 4 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: Colors . blue . shade200 ) ,
) ,
child: Text (
' ID: ${ _userController . pointDeVenteId } ' ,
style: TextStyle (
fontSize: 10 ,
color: Colors . blue . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// 6. Ajoutez cette méthode pour filtrer les produits par point de vente
// 🎯 MODIFIÉ: Dropdown avec gestion améliorée
Widget _buildPointDeVenteFilter ( ) {
if ( ! _isUserSuperAdmin ( ) ) {
return const SizedBox . shrink ( ) ; // Cacher pour les non-admins
}
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 8 ) ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 12 ) ) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Icon ( Icons . filter_list , color: Colors . green . shade700 ) ,
const SizedBox ( width: 8 ) ,
const Text ( ' Filtrer par point de vente (Admin) ' ,
style: TextStyle ( fontSize: 14 , fontWeight: FontWeight . w600 ) ) ,
] ,
) ,
const SizedBox ( height: 8 ) ,
DropdownButtonFormField < String > (
value: _selectedPointDeVente ,
decoration: InputDecoration ( labelText: ' Point de vente ' ) ,
items: [
const DropdownMenuItem (
value: null , child: Text ( ' Tous les points de vente ' ) ) ,
. . . _pointsDeVente . map ( ( point ) {
return DropdownMenuItem (
value: point [ ' nom ' ] as String ,
child: Text ( point [ ' nom ' ] as String ) ,
) ;
} ) . toList ( ) ,
] ,
onChanged: ( value ) {
setState ( ( ) {
_selectedPointDeVente = value ;
_filterProducts ( ) ;
} ) ;
} ,
) ,
] ,
) ,
) ,
) ;
}
// 🎯 MODIFIÉ: Interface utilisateur adaptée selon le rôle
// 🎯 NOUVEAU: Header d'information adapté
Widget _buildRoleBasedHeader ( ) {
final commandableCount = _products . where ( ( p ) = > _isProduitCommandable ( p ) ) . length ;
final totalCount = _products . length ;
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 8 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Column (
children: [
Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: _isUserSuperAdmin ( )
? Colors . purple . shade100
: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
_isUserSuperAdmin ( ) ? Icons . admin_panel_settings : Icons . visibility ,
color: _isUserSuperAdmin ( )
? Colors . purple . shade700
: Colors . blue . shade700 ,
size: 20 ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
_isUserSuperAdmin ( ) ? ' Mode Administrateur ' : ' Mode Consultation étendue ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w600 ,
color: _isUserSuperAdmin ( )
? Colors . purple . shade700
: Colors . blue . shade700 ,
) ,
) ,
const SizedBox ( height: 4 ) ,
Text (
_isUserSuperAdmin ( )
? ' Tous les produits sont visibles et commandables '
: ' Tous les produits sont visibles • Commandes limitées à votre PV ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
) ,
) ,
] ,
) ,
) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 4 ) ,
decoration: BoxDecoration (
color: _isUserSuperAdmin ( )
? Colors . purple . shade50
: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all (
color: _isUserSuperAdmin ( )
? Colors . purple . shade200
: Colors . blue . shade200
) ,
) ,
child: Text (
_userController . role . toUpperCase ( ) ,
style: TextStyle (
fontSize: 10 ,
color: _isUserSuperAdmin ( )
? Colors . purple . shade600
: Colors . blue . shade600 ,
fontWeight: FontWeight . w600 ,
) ,
) ,
) ,
] ,
) ,
// Statistiques de produits
const SizedBox ( height: 12 ) ,
Row (
children: [
// Produits visibles
Expanded (
child: Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: Colors . blue . shade200 ) ,
) ,
child: Row (
children: [
Icon ( Icons . visibility , size: 16 , color: Colors . blue . shade600 ) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Text (
' $ totalCount produit(s) visibles ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . blue . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ,
] ,
) ,
) ,
) ,
if ( ! _isUserSuperAdmin ( ) ) . . . [
const SizedBox ( width: 8 ) ,
// Produits commandables
Expanded (
child: Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . green . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: Colors . green . shade200 ) ,
) ,
child: Row (
children: [
Icon ( Icons . shopping_cart , size: 16 , color: Colors . green . shade600 ) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Text (
' $ commandableCount commandables ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . green . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ,
] ,
) ,
) ,
) ,
] ,
] ,
) ,
] ,
) ,
) ,
) ;
}
Widget _buildProductList ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
return _filteredProducts . isEmpty
? _buildEmptyState ( )
: ListView . builder (
padding: const EdgeInsets . all ( 16.0 ) ,
itemCount: _filteredProducts . length ,
itemBuilder: ( context , index ) {
final product = _filteredProducts [ index ] ;
final quantity = _quantites [ product . id ] ? ? 0 ;
return _buildProductListItem ( product , quantity , isMobile ) ;
} ,
) ;
}
Widget _buildEmptyState ( ) {
return Center (
child: Padding (
padding: const EdgeInsets . all ( 32.0 ) ,
child: Column (
children: [
Icon (
Icons . search_off ,
size: 64 ,
color: Colors . grey . shade400 ,
) ,
const SizedBox ( height: 16 ) ,
Text (
' Aucun produit trouvé ' ,
style: TextStyle (
fontSize: 18 ,
fontWeight: FontWeight . w500 ,
color: Colors . grey . shade600 ,
) ,
) ,
const SizedBox ( height: 8 ) ,
Text (
' Modifiez vos critères de recherche ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . grey . shade500 ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// 🎯 MODIFIÉ: Interface produit avec indication visuelle de la commandabilité
Widget _buildProductListItem ( Product product , int quantity , bool isMobile ) {
final bool isOutOfStock = product . stock ! = null & & product . stock ! < = 0 ;
final detailPanier = _panierDetails [ product . id ! ] ;
final int currentQuantity = detailPanier ? . quantite ? ? 0 ;
final isCurrentUserPointDeVente = product . pointDeVenteId = = _userController . pointDeVenteId ;
final isProduitCommandable = _isProduitCommandable ( product ) ;
return FutureBuilder < String ? > (
future: _appDatabase . getPointDeVenteNomById ( product . pointDeVenteId ? ? 0 ) ,
builder: ( context , snapshot ) {
String pointDeVenteText = ' Chargement... ' ;
if ( snapshot . connectionState = = ConnectionState . done ) {
if ( snapshot . hasError ) {
pointDeVenteText = ' Erreur de chargement ' ;
} else {
pointDeVenteText = snapshot . data ? ? ' Non spécifié ' ;
}
}
return Card (
margin: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 4 ) ,
elevation: 2 ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
side: isCurrentUserPointDeVente
? BorderSide ( color: Colors . orange . shade300 , width: 2 )
: ! isProduitCommandable
? BorderSide ( color: Colors . grey . shade300 , width: 1.5 )
: BorderSide . none ,
) ,
child: Opacity (
opacity: isProduitCommandable ? 1.0 : 0.7 ,
child: Container (
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 8 ) ,
border: isOutOfStock
? Border . all ( color: Colors . red . shade200 , width: 1.5 )
: detailPanier ? . estCadeau = = true
? Border . all ( color: Colors . green . shade300 , width: 2 )
: detailPanier ? . aRemise = = true
? Border . all ( color: Colors . orange . shade300 , width: 2 )
: isCurrentUserPointDeVente
? Border . all ( color: Colors . orange . shade300 , width: 2 )
: ! isProduitCommandable
? Border . all ( color: Colors . grey . shade200 , width: 1 )
: null ,
) ,
child: Padding (
padding: const EdgeInsets . all ( 12.0 ) ,
child: Column (
children: [
Row (
children: [
Container (
width: isMobile ? 40 : 50 ,
height: isMobile ? 40 : 50 ,
decoration: BoxDecoration (
color: ! isProduitCommandable
? Colors . grey . shade100
: isOutOfStock
? Colors . red . shade50
: detailPanier ? . estCadeau = = true
? Colors . green . shade50
: detailPanier ? . aRemise = = true
? Colors . orange . shade50
: isCurrentUserPointDeVente
? Colors . orange . shade50
: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
! isProduitCommandable
? Icons . lock_outline
: detailPanier ? . estCadeau = = true
? Icons . card_giftcard
: detailPanier ? . aRemise = = true
? Icons . discount
: isCurrentUserPointDeVente
? Icons . store
: Icons . shopping_bag ,
size: isMobile ? 20 : 24 ,
color: ! isProduitCommandable
? Colors . grey . shade500
: isOutOfStock
? Colors . red
: detailPanier ? . estCadeau = = true
? Colors . green . shade700
: detailPanier ? . aRemise = = true
? Colors . orange . shade700
: isCurrentUserPointDeVente
? Colors . orange . shade700
: Colors . blue ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Expanded (
child: Text (
product . name ,
style: TextStyle (
fontWeight: FontWeight . bold ,
fontSize: isMobile ? 14 : 16 ,
color: ! isProduitCommandable
? Colors . grey . shade600
: isOutOfStock
? Colors . red . shade700
: null ,
) ,
) ,
) ,
// Indicateurs de statut
if ( ! isProduitCommandable & & ! _isUserSuperAdmin ( ) )
Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade200 ,
borderRadius: BorderRadius . circular ( 10 ) ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . lock_outline , size: 10 , color: Colors . grey . shade600 ) ,
const SizedBox ( width: 2 ) ,
Text (
' AUTRE PV ' ,
style: TextStyle (
fontSize: 9 ,
fontWeight: FontWeight . bold ,
color: Colors . grey . shade600 ,
) ,
) ,
] ,
) ,
) ,
if ( detailPanier ? . estCadeau = = true )
Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . green . shade100 ,
borderRadius: BorderRadius . circular ( 10 ) ,
) ,
child: Text (
' CADEAU ' ,
style: TextStyle (
fontSize: 10 ,
fontWeight: FontWeight . bold ,
color: Colors . green . shade700 ,
) ,
) ,
) ,
if ( isCurrentUserPointDeVente & & detailPanier ? . estCadeau ! = true & & isProduitCommandable )
Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . orange . shade100 ,
borderRadius: BorderRadius . circular ( 10 ) ,
) ,
child: Text (
' MON PV ' ,
style: TextStyle (
fontSize: 10 ,
fontWeight: FontWeight . bold ,
color: Colors . orange . shade700 ,
) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
// ===== PRIX AVEC GESTION CADEAUX/REMISES =====
Row (
children: [
if ( detailPanier ? . estCadeau = = true ) . . . [
Text (
' Gratuit ' ,
style: TextStyle (
color: Colors . green . shade700 ,
fontWeight: FontWeight . bold ,
fontSize: isMobile ? 12 : 14 ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Text (
' ${ product . price . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . grey . shade500 ,
fontWeight: FontWeight . w600 ,
fontSize: isMobile ? 11 : 13 ,
decoration: TextDecoration . lineThrough ,
) ,
) ,
] else . . . [
Text (
' ${ product . price . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . green . shade700 ,
fontWeight: FontWeight . w600 ,
fontSize: isMobile ? 12 : 14 ,
decoration: detailPanier ? . aRemise = = true
? TextDecoration . lineThrough
: null ,
) ,
) ,
if ( detailPanier ? . aRemise = = true ) . . . [
const SizedBox ( width: 8 ) ,
Text (
' ${ ( detailPanier ! . prixFinal / detailPanier . quantite ) . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . orange . shade700 ,
fontWeight: FontWeight . bold ,
fontSize: isMobile ? 12 : 14 ,
) ,
) ,
] ,
] ,
] ,
) ,
// Affichage remise
if ( detailPanier ? . aRemise = = true & & ! detailPanier ! . estCadeau )
Text (
' Remise: ${ detailPanier ! . remiseDescription } ' ,
style: TextStyle (
fontSize: isMobile ? 10 : 12 ,
color: Colors . orange . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
// Stock
if ( product . stock ! = null )
Text (
' Stock: ${ product . stock } ${ isOutOfStock ? ' (Rupture) ' : ' ' } ' ,
style: TextStyle (
fontSize: isMobile ? 10 : 12 ,
color: isOutOfStock
? Colors . red . shade600
: Colors . grey . shade600 ,
fontWeight: isOutOfStock ? FontWeight . w600 : FontWeight . normal ,
) ,
) ,
// ===== AFFICHAGE IMEI ET RÉFÉRENCE =====
if ( product . imei ! = null & & product . imei ! . isNotEmpty )
Text (
' IMEI: ${ product . imei } ' ,
style: TextStyle (
fontSize: isMobile ? 9 : 11 ,
color: Colors . grey . shade600 ,
fontFamily: ' monospace ' ,
) ,
) ,
if ( product . reference ! = null & & product . reference ! . isNotEmpty )
Text (
' Réf: ${ product . reference } ' ,
style: TextStyle (
fontSize: isMobile ? 9 : 11 ,
color: Colors . grey . shade600 ,
) ,
) ,
// Point de vente
const SizedBox ( height: 4 ) ,
Row (
children: [
Icon (
Icons . store ,
size: 12 ,
color: isCurrentUserPointDeVente
? Colors . orange . shade700
: ! isProduitCommandable
? Colors . grey . shade500
: Colors . grey . shade600
) ,
const SizedBox ( width: 4 ) ,
Expanded (
child: Text (
' PV: $ pointDeVenteText ' ,
style: TextStyle (
fontSize: isMobile ? 9 : 11 ,
color: isCurrentUserPointDeVente
? Colors . orange . shade700
: ! isProduitCommandable
? Colors . grey . shade500
: Colors . grey . shade600 ,
fontWeight: isCurrentUserPointDeVente
? FontWeight . w600
: FontWeight . normal ,
) ,
) ,
) ,
if ( ! isProduitCommandable & & ! _isUserSuperAdmin ( ) )
Icon (
Icons . lock_outline ,
size: 12 ,
color: Colors . grey . shade500 ,
) ,
] ,
) ,
] ,
) ,
) ,
// ===== CONTRÔLES QUANTITÉ ET ACTIONS =====
Column (
children: [
// Boutons d'actions (seulement si commandable ET dans le panier)
if ( isProduitCommandable & & currentQuantity > 0 ) . . . [
Row (
mainAxisSize: MainAxisSize . min ,
children: [
// Bouton cadeau
Container (
margin: const EdgeInsets . only ( right: 4 ) ,
child: IconButton (
icon: Icon (
detailPanier ? . estCadeau = = true
? Icons . card_giftcard
: Icons . card_giftcard_outlined ,
size: isMobile ? 16 : 18 ,
color: detailPanier ? . estCadeau = = true
? Colors . green . shade700
: Colors . grey . shade600 ,
) ,
onPressed: isOutOfStock ? null : ( ) = > _basculerStatutCadeau ( product . id ! ) ,
tooltip: detailPanier ? . estCadeau = = true
? ' Retirer le statut cadeau '
: ' Marquer comme cadeau ' ,
style: IconButton . styleFrom (
backgroundColor: detailPanier ? . estCadeau = = true
? Colors . green . shade100
: Colors . grey . shade100 ,
minimumSize: Size ( isMobile ? 32 : 36 , isMobile ? 32 : 36 ) ,
) ,
) ,
) ,
// Bouton remise (seulement pour les articles non-cadeaux)
if ( ! detailPanier ! . estCadeau )
Container (
margin: const EdgeInsets . only ( right: 4 ) ,
child: IconButton (
icon: Icon (
detailPanier . aRemise
? Icons . discount
: Icons . local_offer ,
size: isMobile ? 16 : 18 ,
color: detailPanier . aRemise
? Colors . orange . shade700
: Colors . grey . shade600 ,
) ,
onPressed: isOutOfStock ? null : ( ) = > _showRemiseDialog ( product ) ,
tooltip: detailPanier . aRemise
? ' Modifier la remise '
: ' Ajouter une remise ' ,
style: IconButton . styleFrom (
backgroundColor: detailPanier . aRemise
? Colors . orange . shade100
: Colors . grey . shade100 ,
minimumSize: Size ( isMobile ? 32 : 36 , isMobile ? 32 : 36 ) ,
) ,
) ,
) ,
// Bouton pour ajouter un cadeau à un autre produit
Container (
margin: const EdgeInsets . only ( left: 4 ) ,
child: IconButton (
icon: Icon (
Icons . add_circle_outline ,
size: isMobile ? 16 : 18 ,
color: Colors . green . shade600 ,
) ,
onPressed: isOutOfStock ? null : ( ) = > _showCadeauDialog ( product ) ,
tooltip: ' Ajouter un cadeau ' ,
style: IconButton . styleFrom (
backgroundColor: Colors . green . shade50 ,
minimumSize: Size ( isMobile ? 32 : 36 , isMobile ? 32 : 36 ) ,
) ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 8 ) ,
] ,
// Contrôles de quantité (seulement si commandable)
if ( isProduitCommandable )
Container (
decoration: BoxDecoration (
color: isOutOfStock
? Colors . grey . shade100
: detailPanier ? . estCadeau = = true
? Colors . green . shade50
: isCurrentUserPointDeVente
? Colors . orange . shade50
: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 20 ) ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
IconButton (
icon: Icon ( Icons . remove , size: isMobile ? 16 : 18 ) ,
onPressed: isOutOfStock ? null : ( ) {
if ( currentQuantity > 0 ) {
_modifierQuantite ( product . id ! , currentQuantity - 1 ) ;
}
} ,
) ,
Text (
currentQuantity . toString ( ) ,
style: TextStyle (
fontWeight: FontWeight . bold ,
fontSize: isMobile ? 12 : 14 ,
) ,
) ,
IconButton (
icon: Icon ( Icons . add , size: isMobile ? 16 : 18 ) ,
onPressed: isOutOfStock ? null : ( ) {
if ( product . stock = = null | | currentQuantity < product . stock ! ) {
if ( currentQuantity = = 0 ) {
_ajouterAuPanier ( product , 1 ) ;
} else {
_modifierQuantite ( product . id ! , currentQuantity + 1 ) ;
}
} else {
Get . snackbar (
' Stock insuffisant ' ,
' Quantité demandée non disponible ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
}
} ,
) ,
] ,
) ,
)
else
// Message informatif pour produits non-commandables
Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 8 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade100 ,
borderRadius: BorderRadius . circular ( 20 ) ,
border: Border . all ( color: Colors . grey . shade300 ) ,
) ,
child: Column (
children: [
Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . info_outline , size: 14 , color: Colors . grey . shade600 ) ,
const SizedBox ( width: 4 ) ,
Text (
' Consultation ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . grey . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
ElevatedButton . icon (
icon: const Icon ( Icons . swap_horiz , size: 14 ) ,
label: const Text ( ' Demander transfert ' ) ,
style: ElevatedButton . styleFrom (
backgroundColor: ( product . stock ! = null & & product . stock ! > = 1 )
? Colors . blue . shade700
: Colors . grey . shade400 ,
foregroundColor: Colors . white ,
padding: const EdgeInsets . symmetric ( horizontal: 8 , vertical: 4 ) ,
) ,
onPressed: ( product . stock ! = null & & product . stock ! > = 1 )
? ( ) = > _showDemandeTransfertDialog ( product )
: ( ) {
Get . snackbar (
' Stock insuffisant ' ,
' Impossible de demander un transfert : produit en rupture de stock ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange . shade600 ,
colorText: Colors . white ,
) ;
} ,
) ,
] ,
) ,
) ,
] ,
) ,
] ,
) ,
] ,
) ,
) ,
) ,
) ,
) ;
} ,
) ;
}
// 🎨 INTERFACE AMÉLIORÉE: Dialog moderne pour demande de transfert
Future < void > _showDemandeTransfertDialog ( Product product ) async {
final quantiteController = TextEditingController ( text: ' 1 ' ) ;
final notesController = TextEditingController ( ) ;
final _formKey = GlobalKey < FormState > ( ) ;
// Récupérer les infos du point de vente source
final pointDeVenteSource = await _appDatabase . getPointDeVenteNomById ( product . pointDeVenteId ? ? 0 ) ;
final pointDeVenteDestination = await _appDatabase . getPointDeVenteNomById ( _userController . pointDeVenteId ) ;
await showDialog (
context: context ,
barrierDismissible: false ,
builder: ( context ) = > AlertDialog (
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 16 ) ) ,
contentPadding: EdgeInsets . zero ,
content: Container (
width: 400 ,
child: SingleChildScrollView (
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
// En-tête avec design moderne
Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 20 ) ,
decoration: BoxDecoration (
gradient: LinearGradient (
colors: [ Colors . blue . shade600 , Colors . blue . shade700 ] ,
begin: Alignment . topLeft ,
end: Alignment . bottomRight ,
) ,
borderRadius: const BorderRadius . only (
topLeft: Radius . circular ( 16 ) ,
topRight: Radius . circular ( 16 ) ,
) ,
) ,
child: Column (
children: [
Icon (
Icons . swap_horizontal_circle ,
size: 48 ,
color: Colors . white ,
) ,
const SizedBox ( height: 8 ) ,
Text (
' Demande de transfert ' ,
style: TextStyle (
fontSize: 20 ,
fontWeight: FontWeight . bold ,
color: Colors . white ,
) ,
) ,
Text (
' Transférer un produit entre points de vente ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . white . withOpacity ( 0.9 ) ,
) ,
) ,
] ,
) ,
) ,
// Contenu principal
Padding (
padding: const EdgeInsets . all ( 20 ) ,
child: Form (
key: _formKey ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
// Informations du produit
Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 16 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade50 ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: Colors . grey . shade200 ) ,
) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
Icons . inventory_2 ,
color: Colors . blue . shade700 ,
size: 20 ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
' Produit à transférer ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
Text (
product . name ,
style: const TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 12 ) ,
Row (
children: [
Expanded (
child: _buildInfoCard (
' Prix unitaire ' ,
' ${ product . price . toStringAsFixed ( 2 ) } MGA ' ,
Icons . attach_money ,
Colors . green ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: _buildInfoCard (
' Stock disponible ' ,
' ${ product . stock ? ? 0 } ' ,
Icons . inventory ,
product . stock ! = null & & product . stock ! > 0
? Colors . green
: Colors . red ,
) ,
) ,
] ,
) ,
if ( product . reference ! = null & & product . reference ! . isNotEmpty ) . . . [
const SizedBox ( height: 8 ) ,
Text (
' Référence: ${ product . reference } ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . grey . shade600 ,
fontFamily: ' monospace ' ,
) ,
) ,
] ,
] ,
) ,
) ,
const SizedBox ( height: 20 ) ,
// Informations de transfert
Container (
width: double . infinity ,
padding: const EdgeInsets . all ( 16 ) ,
decoration: BoxDecoration (
color: Colors . orange . shade50 ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: Colors . orange . shade200 ) ,
) ,
child: Column (
children: [
Row (
children: [
Icon ( Icons . arrow_forward , color: Colors . orange . shade700 ) ,
const SizedBox ( width: 8 ) ,
Text (
' Informations de transfert ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . bold ,
color: Colors . orange . shade700 ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 12 ) ,
Row (
children: [
Expanded (
child: _buildTransferStep (
' DE ' ,
pointDeVenteSource ? ? ' Chargement... ' ,
Icons . store_outlined ,
Colors . red . shade600 ,
) ,
) ,
Container (
margin: const EdgeInsets . symmetric ( horizontal: 8 ) ,
child: Icon (
Icons . arrow_forward ,
color: Colors . orange . shade700 ,
size: 24 ,
) ,
) ,
Expanded (
child: _buildTransferStep (
' VERS ' ,
pointDeVenteDestination ? ? ' Chargement... ' ,
Icons . store ,
Colors . green . shade600 ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
const SizedBox ( height: 20 ) ,
// Champ quantité avec design amélioré
Text (
' Quantité à transférer ' ,
style: TextStyle (
fontSize: 14 ,
fontWeight: FontWeight . w600 ,
color: Colors . grey . shade700 ,
) ,
) ,
const SizedBox ( height: 8 ) ,
Container (
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: Colors . grey . shade300 ) ,
) ,
child: Row (
children: [
IconButton (
onPressed: ( ) {
int currentQty = int . tryParse ( quantiteController . text ) ? ? 1 ;
if ( currentQty > 1 ) {
quantiteController . text = ( currentQty - 1 ) . toString ( ) ;
}
} ,
icon: Icon ( Icons . remove , color: Colors . grey . shade600 ) ,
) ,
Expanded (
child: TextFormField (
controller: quantiteController ,
decoration: const InputDecoration (
border: InputBorder . none ,
contentPadding: EdgeInsets . symmetric ( horizontal: 16 ) ,
hintText: ' Quantité ' ,
) ,
textAlign: TextAlign . center ,
keyboardType: TextInputType . number ,
style: const TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
) ,
validator: ( value ) {
if ( value = = null | | value . isEmpty ) {
return ' Veuillez entrer une quantité ' ;
}
final qty = int . tryParse ( value ) ? ? 0 ;
if ( qty < = 0 ) {
return ' Quantité invalide ' ;
}
if ( product . stock ! = null & & qty > product . stock ! ) {
return ' Quantité supérieure au stock disponible ' ;
}
return null ;
} ,
) ,
) ,
IconButton (
onPressed: ( ) {
int currentQty = int . tryParse ( quantiteController . text ) ? ? 1 ;
int maxStock = product . stock ? ? 999 ;
if ( currentQty < maxStock ) {
quantiteController . text = ( currentQty + 1 ) . toString ( ) ;
}
} ,
icon: Icon ( Icons . add , color: Colors . grey . shade600 ) ,
) ,
] ,
) ,
) ,
// Boutons d'action avec design moderne
Row (
children: [
Expanded (
child: TextButton (
onPressed: ( ) = > Navigator . pop ( context ) ,
style: TextButton . styleFrom (
padding: const EdgeInsets . symmetric ( vertical: 16 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
side: BorderSide ( color: Colors . grey . shade300 ) ,
) ,
) ,
child: Text (
' Annuler ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . w600 ,
color: Colors . grey . shade700 ,
) ,
) ,
) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
flex: 2 ,
child: ElevatedButton . icon (
onPressed: ( ) async {
if ( ! _formKey . currentState ! . validate ( ) ) return ;
final qty = int . tryParse ( quantiteController . text ) ? ? 0 ;
if ( qty < = 0 ) {
Get . snackbar (
' Erreur ' ,
' Quantité invalide ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
return ;
}
try {
setState ( ( ) = > _isLoading = true ) ;
Navigator . pop ( context ) ;
await _appDatabase . createDemandeTransfert (
produitId: product . id ! ,
pointDeVenteSourceId: product . pointDeVenteId ! ,
pointDeVenteDestinationId: _userController . pointDeVenteId ,
demandeurId: _userController . userId ,
quantite: qty ,
notes: notesController . text . isNotEmpty
? notesController . text
: ' Demande de transfert depuis l \' application mobile ' ,
) ;
Get . snackbar (
' Demande envoyée ✅ ' ,
' Votre demande de transfert de $ qty unité(s) a été enregistrée et sera traitée prochainement. ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green ,
colorText: Colors . white ,
duration: const Duration ( seconds: 4 ) ,
icon: const Icon ( Icons . check_circle , color: Colors . white ) ,
) ;
} catch ( e ) {
Get . snackbar (
' Erreur ' ,
' Impossible d \' envoyer la demande: ${ e . toString ( ) } ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
duration: const Duration ( seconds: 4 ) ,
) ;
} finally {
setState ( ( ) = > _isLoading = false ) ;
}
} ,
icon: const Icon ( Icons . send , color: Colors . white ) ,
label: const Text (
' Envoyer la demande ' ,
style: TextStyle (
fontSize: 16 ,
fontWeight: FontWeight . bold ,
color: Colors . white ,
) ,
) ,
style: ElevatedButton . styleFrom (
backgroundColor: Colors . blue . shade600 ,
padding: const EdgeInsets . symmetric ( vertical: 16 ) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
elevation: 2 ,
) ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
) ,
] ,
) ,
) ,
) ,
) ,
) ;
}
// 🎨 Widget pour les cartes d'information
Widget _buildInfoCard ( String label , String value , IconData icon , Color color ) {
return Container (
padding: const EdgeInsets . all ( 12 ) ,
decoration: BoxDecoration (
color: color . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: color . withOpacity ( 0.3 ) ) ,
) ,
child: Column (
children: [
Icon ( icon , color: color , size: 20 ) ,
const SizedBox ( height: 4 ) ,
Text (
label ,
style: TextStyle (
fontSize: 10 ,
color: Colors . grey . shade600 ,
fontWeight: FontWeight . w500 ,
) ,
textAlign: TextAlign . center ,
) ,
Text (
value ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . bold ,
color: color ,
) ,
textAlign: TextAlign . center ,
) ,
] ,
) ,
) ;
}
// 🎨 Widget pour les étapes de transfert
Widget _buildTransferStep ( String label , String pointDeVente , IconData icon , Color color ) {
return Container (
padding: const EdgeInsets . all ( 12 ) ,
decoration: BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: color . withOpacity ( 0.3 ) ) ,
) ,
child: Column (
children: [
Container (
padding: const EdgeInsets . all ( 6 ) ,
decoration: BoxDecoration (
color: color . withOpacity ( 0.1 ) ,
borderRadius: BorderRadius . circular ( 6 ) ,
) ,
child: Icon ( icon , color: color , size: 16 ) ,
) ,
const SizedBox ( height: 6 ) ,
Text (
label ,
style: TextStyle (
fontSize: 10 ,
color: Colors . grey . shade600 ,
fontWeight: FontWeight . bold ,
) ,
) ,
Text (
pointDeVente ,
style: TextStyle (
fontSize: 11 ,
fontWeight: FontWeight . w600 ,
color: color ,
) ,
textAlign: TextAlign . center ,
maxLines: 2 ,
overflow: TextOverflow . ellipsis ,
) ,
] ,
) ,
) ;
}
// 🎨 BOUTON AMÉLIORÉ dans le widget principal
// Remplacez le bouton "Demander transfert" existant par celui-ci :
void _showCartBottomSheet ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
Get . bottomSheet (
Container (
height: MediaQuery . of ( context ) . size . height * ( isMobile ? 0.85 : 0.7 ) ,
padding: const EdgeInsets . all ( 16 ) ,
decoration: const BoxDecoration (
color: Colors . white ,
borderRadius: BorderRadius . vertical ( top: Radius . circular ( 20 ) ) ,
) ,
child: Column (
children: [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text (
' Votre Panier ' ,
style: TextStyle (
fontSize: isMobile ? 18 : 20 ,
fontWeight: FontWeight . bold
) ,
) ,
IconButton (
icon: const Icon ( Icons . close ) ,
onPressed: ( ) = > Get . back ( ) ,
) ,
] ,
) ,
const Divider ( ) ,
Expanded ( child: _buildCartItemsList ( ) ) ,
const Divider ( ) ,
_buildCartTotalSection ( ) ,
const SizedBox ( height: 16 ) ,
_buildSubmitButton ( ) ,
] ,
) ,
) ,
isScrollControlled: true ,
) ;
}
// 6. Modifier _buildCartItemsList pour afficher les remises
Widget _buildCartItemsList ( ) {
final itemsInCart = _panierDetails . entries . where ( ( e ) = > e . value . quantite > 0 ) . toList ( ) ;
if ( itemsInCart . isEmpty ) {
return const Center (
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . shopping_cart_outlined , size: 60 , color: Colors . grey ) ,
SizedBox ( height: 16 ) ,
Text (
' Votre panier est vide ' ,
style: TextStyle ( fontSize: 16 , color: Colors . grey ) ,
) ,
] ,
) ,
) ;
}
return ListView . builder (
itemCount: itemsInCart . length ,
itemBuilder: ( context , index ) {
final entry = itemsInCart [ index ] ;
final detail = entry . value ;
final product = _products . firstWhere ( ( p ) = > p . id = = entry . key ) ;
return Dismissible (
key: Key ( entry . key . toString ( ) ) ,
background: Container (
color: Colors . red . shade100 ,
alignment: Alignment . centerRight ,
padding: const EdgeInsets . only ( right: 20 ) ,
child: const Icon ( Icons . delete , color: Colors . red ) ,
) ,
direction: DismissDirection . endToStart ,
onDismissed: ( direction ) {
setState ( ( ) {
_panierDetails . remove ( entry . key ) ;
} ) ;
Get . snackbar (
' Produit retiré ' ,
' ${ product . name } a été retiré du panier ' ,
snackPosition: SnackPosition . BOTTOM ,
) ;
} ,
child: Card (
margin: const EdgeInsets . only ( bottom: 8 ) ,
elevation: 1 ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
side: detail . estCadeau
? BorderSide ( color: Colors . green . shade300 , width: 1.5 )
: detail . aRemise
? BorderSide ( color: Colors . orange . shade300 , width: 1.5 )
: BorderSide . none ,
) ,
child: ListTile (
contentPadding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
leading: Container (
width: 40 ,
height: 40 ,
decoration: BoxDecoration (
color: detail . estCadeau
? Colors . green . shade50
: detail . aRemise
? Colors . orange . shade50
: Colors . blue . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon (
detail . estCadeau
? Icons . card_giftcard
: detail . aRemise
? Icons . discount
: Icons . shopping_bag ,
size: 20 ,
color: detail . estCadeau
? Colors . green . shade700
: detail . aRemise
? Colors . orange . shade700
: Colors . blue . shade700 ,
) ,
) ,
title: Row (
children: [
Expanded ( child: Text ( product . name ) ) ,
if ( detail . estCadeau )
Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . green . shade100 ,
borderRadius: BorderRadius . circular ( 10 ) ,
) ,
child: Text (
' CADEAU ' ,
style: TextStyle (
fontSize: 10 ,
fontWeight: FontWeight . bold ,
color: Colors . green . shade700 ,
) ,
) ,
) ,
] ,
) ,
subtitle: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Row (
children: [
Text ( ' ${ detail . quantite } x ' ) ,
if ( detail . estCadeau ) . . . [
Text (
' GRATUIT ' ,
style: TextStyle (
color: Colors . green . shade700 ,
fontWeight: FontWeight . bold ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Text (
' ( ${ detail . prixUnitaire . toStringAsFixed ( 2 ) } MGA) ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . grey . shade500 ,
decoration: TextDecoration . lineThrough ,
) ,
) ,
] else if ( detail . aRemise ) . . . [
Text (
' ${ detail . prixUnitaire . toStringAsFixed ( 2 ) } ' ,
style: const TextStyle (
decoration: TextDecoration . lineThrough ,
color: Colors . grey ,
) ,
) ,
const Text ( ' → ' ) ,
Text (
' ${ ( detail . prixFinal / detail . quantite ) . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . orange . shade700 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] else
Text ( ' ${ detail . prixUnitaire . toStringAsFixed ( 2 ) } MGA ' ) ,
] ,
) ,
if ( detail . aRemise & & ! detail . estCadeau )
Text (
' Remise: ${ detail . remiseDescription } (- ${ detail . montantRemise . toStringAsFixed ( 2 ) } MGA) ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . orange . shade600 ,
fontStyle: FontStyle . italic ,
) ,
) ,
if ( detail . estCadeau )
Row (
children: [
Icon (
Icons . card_giftcard ,
size: 12 ,
color: Colors . green . shade600 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
' Article offert gracieusement ' ,
style: TextStyle (
fontSize: 11 ,
color: Colors . green . shade600 ,
fontStyle: FontStyle . italic ,
) ,
) ,
] ,
) ,
] ,
) ,
trailing: Column (
mainAxisAlignment: MainAxisAlignment . center ,
crossAxisAlignment: CrossAxisAlignment . end ,
children: [
if ( detail . estCadeau ) . . . [
Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon (
Icons . card_giftcard ,
size: 16 ,
color: Colors . green . shade700 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
' GRATUIT ' ,
style: TextStyle (
fontWeight: FontWeight . bold ,
color: Colors . green . shade700 ,
fontSize: 14 ,
) ,
) ,
] ,
) ,
Text (
' Valeur: ${ detail . sousTotal . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontSize: 10 ,
color: Colors . grey . shade500 ,
fontStyle: FontStyle . italic ,
) ,
) ,
] else if ( detail . aRemise & & detail . sousTotal ! = detail . prixFinal ) . . . [
Text (
' ${ detail . sousTotal . toStringAsFixed ( 2 ) } MGA ' ,
style: const TextStyle (
fontSize: 11 ,
decoration: TextDecoration . lineThrough ,
color: Colors . grey ,
) ,
) ,
Text (
' ${ detail . prixFinal . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontWeight: FontWeight . bold ,
color: Colors . orange . shade700 ,
fontSize: 14 ,
) ,
) ,
] else
Text (
' ${ detail . prixFinal . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontWeight: FontWeight . bold ,
color: Colors . blue . shade800 ,
fontSize: 14 ,
) ,
) ,
] ,
) ,
onTap: ( ) {
if ( detail . estCadeau ) {
_basculerStatutCadeau ( product . id ! ) ;
} else {
_showRemiseDialog ( product ) ;
}
} ,
) ,
) ,
) ;
} ,
) ;
}
// 7. Modifier _buildCartTotalSection pour afficher les totaux avec remises
Widget _buildCartTotalSection ( ) {
double sousTotal = 0 ;
double totalRemises = 0 ;
double totalCadeaux = 0 ;
double total = 0 ;
int nombreCadeaux = 0 ;
_panierDetails . forEach ( ( productId , detail ) {
sousTotal + = detail . sousTotal ;
if ( detail . estCadeau ) {
totalCadeaux + = detail . sousTotal ;
nombreCadeaux + = detail . quantite ;
} else {
totalRemises + = detail . montantRemise ;
}
total + = detail . prixFinal ;
} ) ;
return Column (
children: [
// Sous-total
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text ( ' Sous-total: ' , style: TextStyle ( fontSize: 14 ) ) ,
Text (
' ${ sousTotal . toStringAsFixed ( 2 ) } MGA ' ,
style: const TextStyle ( fontSize: 14 ) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
] ,
// Remises totales
if ( totalRemises > 0 ) . . . [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Text (
' Remises totales: ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . orange . shade700 ,
) ,
) ,
Text (
' - ${ totalRemises . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . orange . shade700 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
] ,
// Cadeaux offerts
if ( totalCadeaux > 0 ) . . . [
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Row (
children: [
Icon (
Icons . card_giftcard ,
size: 16 ,
color: Colors . green . shade700 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
' Cadeaux offerts ( $ nombreCadeaux ): ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . green . shade700 ,
) ,
) ,
] ,
) ,
Text (
' - ${ totalCadeaux . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
fontSize: 14 ,
color: Colors . green . shade700 ,
fontWeight: FontWeight . bold ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 4 ) ,
] ,
if ( totalRemises > 0 | | totalCadeaux > 0 )
const Divider ( height: 16 ) ,
// Total final
Row (
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
const Text (
' Total: ' ,
style: TextStyle ( fontSize: 18 , fontWeight: FontWeight . bold ) ,
) ,
Text (
' ${ total . toStringAsFixed ( 2 ) } MGA ' ,
style: const TextStyle (
fontSize: 18 ,
fontWeight: FontWeight . bold ,
color: Colors . green ,
) ,
) ,
] ,
) ,
const SizedBox ( height: 8 ) ,
// Résumé
Text (
' ${ _panierDetails . values . where ( ( d ) = > d . quantite > 0 ) . length } article(s) ' ,
style: TextStyle ( color: Colors . grey . shade600 ) ,
) ,
// Économies totales
if ( totalRemises > 0 | | totalCadeaux > 0 ) . . . [
const SizedBox ( height: 4 ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 12 , vertical: 6 ) ,
decoration: BoxDecoration (
color: Colors . green . shade50 ,
borderRadius: BorderRadius . circular ( 12 ) ,
border: Border . all ( color: Colors . green . shade200 ) ,
) ,
child: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon (
Icons . savings ,
size: 16 ,
color: Colors . green . shade700 ,
) ,
const SizedBox ( width: 4 ) ,
Text (
' Économies totales: ${ ( totalRemises + totalCadeaux ) . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . green . shade700 ,
fontWeight: FontWeight . bold ,
fontSize: 12 ,
) ,
) ,
] ,
) ,
) ,
] ,
// Détail des économies
if ( totalRemises > 0 & & totalCadeaux > 0 ) . . . [
const SizedBox ( height: 4 ) ,
Text (
' Remises: ${ totalRemises . toStringAsFixed ( 2 ) } MGA • Cadeaux: ${ totalCadeaux . toStringAsFixed ( 2 ) } MGA ' ,
style: TextStyle (
color: Colors . grey . shade600 ,
fontSize: 11 ,
fontStyle: FontStyle . italic ,
) ,
) ,
] ,
] ,
) ;
}
Widget _buildSubmitButton ( ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
return SizedBox (
width: double . infinity ,
child: ElevatedButton (
style: ElevatedButton . styleFrom (
padding: EdgeInsets . symmetric (
vertical: isMobile ? 12 : 16
) ,
backgroundColor: Colors . blue . shade800 ,
foregroundColor: Colors . white ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
elevation: 4 ,
) ,
onPressed: _submitOrder ,
child: _isLoading
? const SizedBox (
width: 20 ,
height: 20 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
color: Colors . white ,
) ,
)
: Text (
isMobile ? ' Valider ' : ' Valider la Commande ' ,
style: TextStyle ( fontSize: isMobile ? 14 : 16 ) ,
) ,
) ,
) ;
}
// 🎯 MODIFIÉ: Validation finale avant soumission
Future < void > _submitOrder ( ) async {
// Vérification panier vide
final itemsInCart = _panierDetails . entries . where ( ( e ) = > e . value . quantite > 0 ) . toList ( ) ;
if ( itemsInCart . isEmpty ) {
Get . snackbar (
' Panier vide ' ,
' Veuillez ajouter des produits à votre commande ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
_showCartBottomSheet ( ) ;
return ;
}
// 🔒 VALIDATION SÉCURITÉ FINALE: Vérifier tous les produits du panier
if ( ! _isUserSuperAdmin ( ) ) {
final produitsNonAutorises = < String > [ ] ;
for ( final entry in itemsInCart ) {
final product = _products . firstWhere ( ( p ) = > p . id = = entry . key ) ;
if ( product . pointDeVenteId ! = _userController . pointDeVenteId ) {
produitsNonAutorises . add ( product . name ) ;
}
}
if ( produitsNonAutorises . isNotEmpty ) {
Get . dialog (
AlertDialog (
title: Row (
children: [
Icon ( Icons . security , color: Colors . red . shade600 ) ,
const SizedBox ( width: 8 ) ,
const Text ( ' Commande non autorisée ' ) ,
] ,
) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
const Text ( ' Les produits suivants ne sont pas autorisés pour votre point de vente: ' ) ,
const SizedBox ( height: 8 ) ,
. . . produitsNonAutorises . map ( ( nom ) = > Padding (
padding: const EdgeInsets . only ( left: 16 , top: 4 ) ,
child: Text ( ' • $ nom ' , style: TextStyle ( color: Colors . red . shade700 ) ) ,
) ) ,
const SizedBox ( height: 12 ) ,
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . orange . shade50 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Row (
children: [
Icon ( Icons . info_outline , color: Colors . orange . shade700 , size: 16 ) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: Text (
' Contactez un administrateur pour commander ces produits. ' ,
style: TextStyle (
fontSize: 12 ,
color: Colors . orange . shade700 ,
) ,
) ,
) ,
] ,
) ,
) ,
] ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Get . back ( ) ,
child: const Text ( ' Compris ' ) ,
) ,
] ,
) ,
) ;
return ;
}
}
// Vérification informations client
if ( _nomController . text . isEmpty | |
_prenomController . text . isEmpty | |
_emailController . text . isEmpty | |
_telephoneController . text . isEmpty | |
_adresseController . text . isEmpty ) {
Get . snackbar (
' Informations manquantes ' ,
' Veuillez remplir les informations client ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
_showClientFormDialog ( ) ;
return ;
}
setState ( ( ) {
_isLoading = true ;
} ) ;
// Créer le client
final client = Client (
nom: _nomController . text ,
prenom: _prenomController . text ,
email: _emailController . text ,
telephone: _telephoneController . text ,
adresse: _adresseController . text ,
dateCreation: DateTime . now ( ) ,
) ;
// Calculer le total final et préparer les détails
double total = 0 ;
final details = < DetailCommande > [ ] ;
for ( final entry in itemsInCart ) {
final detail = entry . value ;
total + = detail . prixFinal ;
details . add ( detail ) ;
}
// Créer la commande avec le total final (après remises)
final commande = Commande (
clientId: 0 ,
dateCommande: DateTime . now ( ) ,
statut: StatutCommande . enAttente ,
montantTotal: total ,
notes: ' Commande passée via l \' application ' ,
commandeurId: _selectedCommercialUser ? . id ,
) ;
try {
await _appDatabase . createCommandeComplete ( client , commande , details ) ;
// Fermer le panier avant d'afficher la confirmation
Get . back ( ) ;
// Afficher le dialogue de confirmation
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
await showDialog (
context: context ,
barrierDismissible: false ,
builder: ( context ) = > AlertDialog (
title: Row (
children: [
Container (
padding: const EdgeInsets . all ( 8 ) ,
decoration: BoxDecoration (
color: Colors . green . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Icon ( Icons . check_circle , color: Colors . green . shade700 ) ,
) ,
const SizedBox ( width: 12 ) ,
Expanded (
child: Text (
' Commande Validée ' ,
style: TextStyle ( fontSize: isMobile ? 16 : 18 ) ,
) ,
) ,
] ,
) ,
content: Text (
' Votre commande a été enregistrée et expédiée avec succès. ' ,
style: TextStyle ( fontSize: isMobile ? 14 : 16 ) ,
) ,
actions: [
SizedBox (
width: double . infinity ,
child: ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: Colors . green . shade700 ,
foregroundColor: Colors . white ,
padding: EdgeInsets . symmetric (
vertical: isMobile ? 12 : 16
) ,
shape: RoundedRectangleBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
) ,
onPressed: ( ) {
Navigator . pop ( context ) ;
_clearFormAndCart ( ) ;
_loadProducts ( ) ;
} ,
child: Text (
' OK ' ,
style: TextStyle ( fontSize: isMobile ? 14 : 16 ) ,
) ,
) ,
) ,
] ,
) ,
) ;
} catch ( e ) {
setState ( ( ) {
_isLoading = false ;
} ) ;
Get . snackbar (
' Erreur ' ,
' Une erreur est survenue: ${ e . toString ( ) } ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
}
}
Future < void > _showCadeauDialog ( Product product ) async {
final detailExistant = _panierDetails [ product . id ! ] ;
if ( detailExistant = = null | | detailExistant . quantite = = 0 ) {
Get . snackbar (
' Produit requis ' ,
' Vous devez d \' abord ajouter ce produit au panier ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . orange ,
colorText: Colors . white ,
) ;
return ;
}
final result = await showDialog < Map < String , dynamic > > (
context: context ,
builder: ( context ) = > CadeauDialog (
product: product ,
quantite: detailExistant . quantite ,
detailExistant: detailExistant ,
) ,
) ;
if ( result ! = null ) {
_ajouterCadeauAuPanier (
result [ ' produit ' ] as Product ,
result [ ' quantite ' ] as int ,
) ;
}
}
void _ajouterCadeauAuPanier ( Product produitCadeau , int quantite ) {
// Vérifier le stock disponible
if ( produitCadeau . stock ! = null & & quantite > produitCadeau . stock ! ) {
Get . snackbar (
' Stock insuffisant ' ,
' Quantité de cadeau demandée non disponible ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . red ,
colorText: Colors . white ,
) ;
return ;
}
setState ( ( ) {
final detailCadeau = DetailCommande . cadeau (
commandeId: 0 , // Sera défini lors de la création
produitId: produitCadeau . id ! ,
quantite: quantite ,
prixUnitaire: produitCadeau . price ,
produitNom: produitCadeau . name ,
produitReference: produitCadeau . reference ,
) ;
_panierDetails [ produitCadeau . id ! ] = detailCadeau ;
} ) ;
Get . snackbar (
' Cadeau ajouté ' ,
' ${ produitCadeau . name } a été ajouté comme cadeau ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 3 ) ,
icon: const Icon ( Icons . card_giftcard , color: Colors . white ) ,
) ;
}
void _basculerStatutCadeau ( int productId ) {
final detailExistant = _panierDetails [ productId ] ;
if ( detailExistant = = null ) return ;
setState ( ( ) {
if ( detailExistant . estCadeau ) {
// Convertir en article normal
_panierDetails [ productId ] = detailExistant . convertirEnArticleNormal ( ) ;
Get . snackbar (
' Statut modifié ' ,
' L \' article n \' est plus un cadeau ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . blue . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
) ;
} else {
// Convertir en cadeau
_panierDetails [ productId ] = detailExistant . convertirEnCadeau ( ) ;
Get . snackbar (
' Cadeau offert ' ,
' L \' article est maintenant un cadeau ' ,
snackPosition: SnackPosition . BOTTOM ,
backgroundColor: Colors . green . shade600 ,
colorText: Colors . white ,
duration: const Duration ( seconds: 2 ) ,
icon: const Icon ( Icons . card_giftcard , color: Colors . white ) ,
) ;
}
} ) ;
}
@ override
void dispose ( ) {
_qrController ? . dispose ( ) ;
// Vos disposals existants...
_hideAllSuggestions ( ) ;
_nomController . dispose ( ) ;
_prenomController . dispose ( ) ;
_emailController . dispose ( ) ;
_telephoneController . dispose ( ) ;
_adresseController . dispose ( ) ;
_searchNameController . dispose ( ) ;
_searchImeiController . dispose ( ) ;
_searchReferenceController . dispose ( ) ;
super . dispose ( ) ;
}
// 10. Modifier le Widget build pour utiliser le nouveau scan automatique
// 8. Modifiez votre méthode build pour inclure les nouvelles cartes d'information
// VERSION OPTIMISÉE DE VOTRE INTERFACE EN-TÊTES ET RECHERCHE
@ override
Widget build ( BuildContext context ) {
final isMobile = MediaQuery . of ( context ) . size . width < 600 ;
return Scaffold (
floatingActionButton: _buildFloatingCartButton ( ) ,
appBar: CustomAppBar ( title: ' Nouvelle commande ' ) ,
drawer: CustomDrawer ( ) ,
body: GestureDetector (
onTap: _hideAllSuggestions ,
child: Column (
children: [
// 🎯 EN-TÊTE OPTIMISÉ
Container (
decoration: BoxDecoration (
color: Colors . white ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.05 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
) ,
] ,
) ,
child: Column (
children: [
// Info utilisateur (toujours visible mais compacte)
_buildCompactUserInfo ( ) ,
// Zone de recherche principale
_buildMainSearchSection ( isMobile ) ,
// Filtres rapides (toujours visibles)
_buildQuickFilters ( isMobile ) ,
] ,
) ,
) ,
// Liste des produits avec indicateur de résultats
Expanded (
child: Column (
children: [
_buildResultsHeader ( ) ,
Expanded ( child: _buildProductList ( ) ) ,
] ,
) ,
) ,
] ,
) ,
) ,
) ;
}
// 🎯 INFORMATION UTILISATEUR COMPACTE
Widget _buildCompactUserInfo ( ) {
if ( _userController . pointDeVenteId < = 0 ) return const SizedBox . shrink ( ) ;
return Container (
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade50 ,
border: Border ( bottom: BorderSide ( color: Colors . blue . shade100 ) ) ,
) ,
child: Row (
children: [
Icon ( Icons . store , size: 16 , color: Colors . blue . shade700 ) ,
const SizedBox ( width: 8 ) ,
Text (
_userController . pointDeVenteDesignation ,
style: TextStyle (
fontSize: 12 ,
fontWeight: FontWeight . w600 ,
color: Colors . blue . shade700 ,
) ,
) ,
const Spacer ( ) ,
Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
child: Text (
' ID: ${ _userController . pointDeVenteId } ' ,
style: TextStyle (
fontSize: 10 ,
color: Colors . blue . shade600 ,
) ,
) ,
) ,
] ,
) ,
) ;
}
// 🎯 ZONE DE RECHERCHE PRINCIPALE OPTIMISÉE
Widget _buildMainSearchSection ( bool isMobile ) {
return Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
children: [
// Recherche principale avec actions intégrées
Row (
children: [
Expanded (
child: Container (
decoration: BoxDecoration (
borderRadius: BorderRadius . circular ( 12 ) ,
boxShadow: [
BoxShadow (
color: Colors . black . withOpacity ( 0.1 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
) ,
] ,
) ,
child: TextField (
controller: _searchNameController ,
decoration: InputDecoration (
hintText: ' Rechercher par nom, IMEI, référence... ' ,
prefixIcon: const Icon ( Icons . search ) ,
suffixIcon: _searchNameController . text . isNotEmpty
? IconButton (
icon: const Icon ( Icons . clear ) ,
onPressed: ( ) {
_searchNameController . clear ( ) ;
_filterProducts ( ) ;
} ,
)
: null ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 12 ) ,
borderSide: BorderSide . none ,
) ,
filled: true ,
fillColor: Colors . white ,
contentPadding: const EdgeInsets . symmetric (
horizontal: 16 ,
vertical: 12
) ,
) ,
) ,
) ,
) ,
const SizedBox ( width: 8 ) ,
// Bouton Scanner toujours visible
Container (
decoration: BoxDecoration (
color: Colors . green . shade700 ,
borderRadius: BorderRadius . circular ( 12 ) ,
boxShadow: [
BoxShadow (
color: Colors . green . withOpacity ( 0.3 ) ,
blurRadius: 4 ,
offset: const Offset ( 0 , 2 ) ,
) ,
] ,
) ,
child: IconButton (
icon: _isScanning
? const SizedBox (
width: 20 ,
height: 20 ,
child: CircularProgressIndicator (
strokeWidth: 2 ,
color: Colors . white ,
) ,
)
: const Icon ( Icons . qr_code_scanner ) ,
color: Colors . white ,
onPressed: _isScanning ? null : _startAutomaticScanning ,
tooltip: ' Scanner un produit ' ,
) ,
) ,
if ( ! isMobile ) . . . [
const SizedBox ( width: 8 ) ,
// Bouton filtres avancés
Container (
decoration: BoxDecoration (
color: Colors . blue . shade700 ,
borderRadius: BorderRadius . circular ( 12 ) ,
) ,
child: IconButton (
icon: const Icon ( Icons . tune ) ,
color: Colors . white ,
onPressed: ( ) = > _showAdvancedFiltersDialog ( ) ,
tooltip: ' Filtres avancés ' ,
) ,
) ,
] ,
] ,
) ,
// Recherche multicritères (desktop uniquement)
if ( ! isMobile ) . . . [
const SizedBox ( height: 12 ) ,
Row (
children: [
Expanded (
child: TextField (
controller: _searchImeiController ,
decoration: InputDecoration (
hintText: ' IMEI ' ,
prefixIcon: const Icon ( Icons . phone_android , size: 20 ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
isDense: true ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: TextField (
controller: _searchReferenceController ,
decoration: InputDecoration (
hintText: ' Référence ' ,
prefixIcon: const Icon ( Icons . qr_code , size: 20 ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
isDense: true ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
) ,
) ,
const SizedBox ( width: 8 ) ,
Expanded (
child: DropdownButtonFormField < String > (
value: _selectedPointDeVente ,
decoration: InputDecoration (
hintText: ' Point de vente ' ,
prefixIcon: const Icon ( Icons . store , size: 20 ) ,
border: OutlineInputBorder (
borderRadius: BorderRadius . circular ( 8 ) ,
) ,
isDense: true ,
filled: true ,
fillColor: Colors . grey . shade50 ,
) ,
items: [
const DropdownMenuItem (
value: null ,
child: Text ( ' Tous les PV ' ) ,
) ,
. . . _pointsDeVente . map ( ( point ) {
return DropdownMenuItem (
value: point [ ' nom ' ] as String ,
child: Text ( point [ ' nom ' ] as String ) ,
) ;
} ) . toList ( ) ,
] ,
onChanged: ( value ) {
setState ( ( ) {
_selectedPointDeVente = value ;
_filterProducts ( ) ;
} ) ;
} ,
) ,
) ,
] ,
) ,
] ,
] ,
) ,
) ;
}
// 🎯 FILTRES RAPIDES OPTIMISÉS
Widget _buildQuickFilters ( bool isMobile ) {
return Container (
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
color: Colors . grey . shade50 ,
border: Border ( top: BorderSide ( color: Colors . grey . shade200 ) ) ,
) ,
child: SingleChildScrollView (
scrollDirection: Axis . horizontal ,
child: Row (
children: [
// Filtre stock
FilterChip (
label: Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon (
_showOnlyInStock ? Icons . inventory : Icons . inventory_2 ,
size: 16 ,
) ,
const SizedBox ( width: 4 ) ,
Text ( _showOnlyInStock ? ' En stock ' : ' Tous ' ) ,
] ,
) ,
selected: _showOnlyInStock ,
onSelected: ( selected ) = > _toggleStockFilter ( ) ,
selectedColor: Colors . green . shade100 ,
checkmarkColor: Colors . green . shade700 ,
) ,
const SizedBox ( width: 8 ) ,
// Filtre mobile pour ouvrir les filtres avancés
if ( isMobile )
ActionChip (
label: const Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . tune , size: 16 ) ,
SizedBox ( width: 4 ) ,
Text ( ' Filtres ' ) ,
] ,
) ,
onPressed: ( ) = > _showMobileFilters ( context ) ,
) ,
const SizedBox ( width: 8 ) ,
// Bouton reset si filtres actifs
if ( _hasActiveFilters ( ) )
ActionChip (
label: const Row (
mainAxisSize: MainAxisSize . min ,
children: [
Icon ( Icons . clear , size: 16 ) ,
SizedBox ( width: 4 ) ,
Text ( ' Reset ' ) ,
] ,
) ,
onPressed: _clearFilters ,
backgroundColor: Colors . orange . shade100 ,
) ,
] ,
) ,
) ,
) ;
}
// 🎯 EN-TÊTE DES RÉSULTATS
Widget _buildResultsHeader ( ) {
return Container (
padding: const EdgeInsets . symmetric ( horizontal: 16 , vertical: 8 ) ,
decoration: BoxDecoration (
color: Colors . white ,
border: Border ( bottom: BorderSide ( color: Colors . grey . shade200 ) ) ,
) ,
child: Row (
children: [
Text (
' ${ _filteredProducts . length } produit(s) ' ,
style: TextStyle (
fontWeight: FontWeight . w600 ,
color: Colors . grey . shade700 ,
) ,
) ,
const Spacer ( ) ,
// Indicateurs de filtres actifs
if ( _hasActiveFilters ( ) ) . . . [
Wrap (
spacing: 4 ,
children: _getActiveFilterChips ( ) ,
) ,
] ,
] ,
) ,
) ;
}
// 🎯 MÉTHODES UTILITAIRES
bool _hasActiveFilters ( ) {
return _selectedPointDeVente ! = null | |
_showOnlyInStock | |
_searchNameController . text . isNotEmpty | |
_searchImeiController . text . isNotEmpty | |
_searchReferenceController . text . isNotEmpty ;
}
List < Widget > _getActiveFilterChips ( ) {
List < Widget > chips = [ ] ;
if ( _selectedPointDeVente ! = null ) {
chips . add ( _buildMiniFilterChip ( ' PV: $ _selectedPointDeVente ' ) ) ;
}
if ( _showOnlyInStock ) {
chips . add ( _buildMiniFilterChip ( ' En stock ' ) ) ;
}
if ( _searchNameController . text . isNotEmpty ) {
chips . add ( _buildMiniFilterChip ( ' Nom: ${ _searchNameController . text } ' ) ) ;
}
if ( _searchImeiController . text . isNotEmpty ) {
chips . add ( _buildMiniFilterChip ( ' IMEI: ${ _searchImeiController . text } ' ) ) ;
}
if ( _searchReferenceController . text . isNotEmpty ) {
chips . add ( _buildMiniFilterChip ( ' Réf: ${ _searchReferenceController . text } ' ) ) ;
}
return chips ;
}
Widget _buildMiniFilterChip ( String label ) {
return Container (
padding: const EdgeInsets . symmetric ( horizontal: 6 , vertical: 2 ) ,
decoration: BoxDecoration (
color: Colors . blue . shade100 ,
borderRadius: BorderRadius . circular ( 8 ) ,
border: Border . all ( color: Colors . blue . shade300 ) ,
) ,
child: Text (
label ,
style: TextStyle (
fontSize: 10 ,
color: Colors . blue . shade700 ,
fontWeight: FontWeight . w500 ,
) ,
) ,
) ;
}
// 🎯 DIALOGUE FILTRES AVANCÉS
void _showAdvancedFiltersDialog ( ) {
showDialog (
context: context ,
builder: ( context ) = > AlertDialog (
title: const Text ( ' Filtres avancés ' ) ,
content: SizedBox (
width: 400 ,
child: Column (
mainAxisSize: MainAxisSize . min ,
children: [
// Tous vos filtres existants ici
_buildPointDeVenteFilter ( ) ,
const SizedBox ( height: 16 ) ,
// Autres filtres selon vos besoins
] ,
) ,
) ,
actions: [
TextButton (
onPressed: ( ) = > Get . back ( ) ,
child: const Text ( ' Fermer ' ) ,
) ,
ElevatedButton (
onPressed: ( ) {
_filterProducts ( ) ;
Get . back ( ) ;
} ,
child: const Text ( ' Appliquer ' ) ,
) ,
] ,
) ,
) ;
}
}