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

4650 lines
170 KiB

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) {
final isProduitCommandable = _isProduitCommandable(product);
final canRequestTransfer = product.stock != null && product.stock! >= 1;
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header avec icône de succès
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.shade200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
shape: BoxShape.circle,
),
child: Icon(
Icons.check_circle,
color: Colors.green.shade700,
size: 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Produit identifié !',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
Text(
'Ajouté au panier avec succès',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// 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: [
Text(
product.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 12),
// Détails du produit en grille
_buildProductDetailRow('Prix', '${product.price.toStringAsFixed(2)} MGA'),
_buildProductDetailRow('Quantité ajoutée', '$newQuantity'),
if (product.imei != null && product.imei!.isNotEmpty)
_buildProductDetailRow('IMEI', product.imei!),
if (product.reference != null && product.reference!.isNotEmpty)
_buildProductDetailRow('Référence', product.reference!),
if (product.stock != null)
_buildProductDetailRow('Stock restant', '${product.stock! - newQuantity}'),
],
),
),
const SizedBox(height: 20),
// Badge identification automatique
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.auto_awesome,
color: Colors.blue.shade700, size: 16),
const SizedBox(width: 6),
Text(
'Identifié automatiquement',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
],
),
),
const SizedBox(height: 24),
// Boutons d'action redessinés
Column(
children: [
// Bouton principal selon les permissions
SizedBox(
width: double.infinity,
height: 48,
child: (!isProduitCommandable && !_isUserSuperAdmin())
? ElevatedButton.icon(
onPressed: canRequestTransfer
? () {
Get.back();
_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,
margin: const EdgeInsets.all(16),
);
},
icon: const Icon(Icons.swap_horiz, size: 20),
label: const Text(
'Demander un transfert',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: canRequestTransfer
? Colors.orange.shade600
: Colors.grey.shade400,
foregroundColor: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)
: ElevatedButton.icon(
onPressed: () {
_ajouterAuPanier(product, 1);
Get.back();
_showCartBottomSheet();
},
icon: const Icon(Icons.shopping_cart, size: 20),
label: const Text(
'Voir le panier',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(height: 12),
// Boutons secondaires
Row(
children: [
// Continuer
Expanded(
child: SizedBox(
height: 44,
child: OutlinedButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.close, size: 18),
label: const Text('Continuer'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey.shade700,
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
const SizedBox(width: 12),
// Scanner encore
Expanded(
child: SizedBox(
height: 44,
child: ElevatedButton.icon(
onPressed: () {
Get.back();
_startAutomaticScanning();
},
icon: const Icon(Icons.qr_code_scanner, size: 18),
label: const Text('Scanner'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600,
foregroundColor: Colors.white,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
],
),
],
),
],
),
),
),
);
}
// Widget helper pour les détails du produit
Widget _buildProductDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
],
),
);
}
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 != null && value.isNotEmpty) {
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Email invalide';
}
}
return null; // valide même si vide
},
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:!isMobile ? const Text('Demander transfertt'):const SizedBox.shrink(),
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 ||
_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'),
),
],
),
);
}
}