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.
4768 lines
180 KiB
4768 lines
180 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?.isEmpty ?? true) return 'Veuillez entrer un email';
|
|
if (value?.isEmpty ?? true) return null;
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
|
.hasMatch(value!)) {
|
|
return 'Email invalide';
|
|
}
|
|
return null;
|
|
},
|
|
onChanged: (value) async {
|
|
if (value.length >= 3) {
|
|
final suggestions =
|
|
await _appDatabase.suggestClients(value);
|
|
setDialogState(() {
|
|
localClientSuggestions = suggestions;
|
|
showEmailSuggestions = suggestions.isNotEmpty;
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
});
|
|
} else {
|
|
setDialogState(() {
|
|
showEmailSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Champ Téléphone avec suggestions (SANS bouton recherche)
|
|
_buildTextFormFieldWithKey(
|
|
key: telephoneFieldKey,
|
|
controller: _telephoneController,
|
|
label: 'Téléphone',
|
|
keyboardType: TextInputType.phone,
|
|
validator: (value) => value?.isEmpty ?? true
|
|
? 'Veuillez entrer un téléphone'
|
|
: null,
|
|
onChanged: (value) async {
|
|
if (value.length >= 3) {
|
|
final suggestions =
|
|
await _appDatabase.suggestClients(value);
|
|
setDialogState(() {
|
|
localClientSuggestions = suggestions;
|
|
showTelephoneSuggestions =
|
|
suggestions.isNotEmpty;
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
});
|
|
} else {
|
|
setDialogState(() {
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
_buildTextFormField(
|
|
controller: _adresseController,
|
|
label: 'Adresse',
|
|
maxLines: 2,
|
|
validator: (value) => value?.isEmpty ?? true
|
|
? 'Veuillez entrer une adresse'
|
|
: null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildCommercialDropdown(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isMobile ? 16 : 20,
|
|
vertical: isMobile ? 10 : 12),
|
|
),
|
|
onPressed: () {
|
|
if (_formKey.currentState!.validate()) {
|
|
// Fermer toutes les suggestions avant de soumettre
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
Get.back();
|
|
_submitOrder();
|
|
}
|
|
},
|
|
child: Text(
|
|
isMobile ? 'Valider' : 'Valider la commande',
|
|
style: TextStyle(fontSize: isMobile ? 12 : 14),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Overlay pour les suggestions du nom
|
|
if (showNomSuggestions)
|
|
_buildSuggestionOverlay(
|
|
fieldKey: nomFieldKey,
|
|
suggestions: localClientSuggestions,
|
|
onClientSelected: (client) {
|
|
_fillFormWithClient(client);
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
onDismiss: () {
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
),
|
|
|
|
// Overlay pour les suggestions du prénom
|
|
if (showPrenomSuggestions)
|
|
_buildSuggestionOverlay(
|
|
fieldKey: prenomFieldKey,
|
|
suggestions: localClientSuggestions,
|
|
onClientSelected: (client) {
|
|
_fillFormWithClient(client);
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
onDismiss: () {
|
|
setDialogState(() {
|
|
showPrenomSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
),
|
|
|
|
// Overlay pour les suggestions de l'email
|
|
if (showEmailSuggestions)
|
|
_buildSuggestionOverlay(
|
|
fieldKey: emailFieldKey,
|
|
suggestions: localClientSuggestions,
|
|
onClientSelected: (client) {
|
|
_fillFormWithClient(client);
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
onDismiss: () {
|
|
setDialogState(() {
|
|
showEmailSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
),
|
|
|
|
// Overlay pour les suggestions du téléphone
|
|
if (showTelephoneSuggestions)
|
|
_buildSuggestionOverlay(
|
|
fieldKey: telephoneFieldKey,
|
|
suggestions: localClientSuggestions,
|
|
onClientSelected: (client) {
|
|
_fillFormWithClient(client);
|
|
setDialogState(() {
|
|
showNomSuggestions = false;
|
|
showPrenomSuggestions = false;
|
|
showEmailSuggestions = false;
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
onDismiss: () {
|
|
setDialogState(() {
|
|
showTelephoneSuggestions = false;
|
|
localClientSuggestions = [];
|
|
});
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget pour créer un TextFormField avec une clé
|
|
Widget _buildTextFormFieldWithKey({
|
|
required GlobalKey key,
|
|
required TextEditingController controller,
|
|
required String label,
|
|
TextInputType? keyboardType,
|
|
int maxLines = 1,
|
|
String? Function(String?)? validator,
|
|
void Function(String)? onChanged,
|
|
}) {
|
|
return Container(
|
|
key: key,
|
|
child: _buildTextFormField(
|
|
controller: controller,
|
|
label: label,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines,
|
|
validator: validator,
|
|
onChanged: onChanged,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget pour l'overlay des suggestions
|
|
// Widget pour l'overlay des suggestions
|
|
Widget _buildSuggestionOverlay({
|
|
required GlobalKey fieldKey,
|
|
required List<Client> suggestions,
|
|
required Function(Client) onClientSelected,
|
|
required VoidCallback onDismiss,
|
|
}) {
|
|
return Positioned.fill(
|
|
child: GestureDetector(
|
|
onTap: onDismiss,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Builder(
|
|
builder: (context) {
|
|
// Obtenir la position du champ
|
|
final RenderBox? renderBox =
|
|
fieldKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (renderBox == null) return const SizedBox();
|
|
|
|
final position = renderBox.localToGlobal(Offset.zero);
|
|
final size = renderBox.size;
|
|
|
|
return Stack(
|
|
children: [
|
|
Positioned(
|
|
left: position.dx,
|
|
top: position.dy + size.height + 4,
|
|
width: size.width,
|
|
child: GestureDetector(
|
|
onTap: () {}, // Empêcher la fermeture au tap sur la liste
|
|
child: Container(
|
|
constraints: const BoxConstraints(
|
|
maxHeight:
|
|
200, // Hauteur maximum pour la scrollabilité
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.15),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Scrollbar(
|
|
thumbVisibility: suggestions.length > 3,
|
|
child: ListView.separated(
|
|
padding: EdgeInsets.zero,
|
|
shrinkWrap: true,
|
|
itemCount: suggestions.length,
|
|
separatorBuilder: (context, index) => Divider(
|
|
height: 1,
|
|
color: Colors.grey.shade200,
|
|
),
|
|
itemBuilder: (context, index) {
|
|
final client = suggestions[index];
|
|
return ListTile(
|
|
dense: true,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 4,
|
|
),
|
|
leading: CircleAvatar(
|
|
radius: 16,
|
|
backgroundColor: Colors.blue.shade100,
|
|
child: Icon(
|
|
Icons.person,
|
|
size: 16,
|
|
color: Colors.blue.shade700,
|
|
),
|
|
),
|
|
title: Text(
|
|
'${client.nom} ${client.prenom}',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
'${client.telephone} • ${client.email}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
onTap: () => onClientSelected(client),
|
|
hoverColor: Colors.blue.shade50,
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode pour remplir le formulaire avec les données du client
|
|
void _fillFormWithClient(Client client) {
|
|
_nomController.text = client.nom;
|
|
_prenomController.text = client.prenom;
|
|
_emailController.text = client.email;
|
|
_telephoneController.text = client.telephone;
|
|
_adresseController.text = client.adresse ?? '';
|
|
|
|
Get.snackbar(
|
|
'Client trouvé',
|
|
'Les informations ont été remplies automatiquement',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextFormField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
TextInputType? keyboardType,
|
|
String? Function(String?)? validator,
|
|
int? maxLines,
|
|
void Function(String)? onChanged,
|
|
}) {
|
|
return TextFormField(
|
|
controller: controller,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
),
|
|
keyboardType: keyboardType,
|
|
validator: validator,
|
|
maxLines: maxLines,
|
|
onChanged: onChanged,
|
|
);
|
|
}
|
|
|
|
Widget _buildCommercialDropdown() {
|
|
return DropdownButtonFormField<Users>(
|
|
value: _selectedCommercialUser,
|
|
decoration: InputDecoration(
|
|
labelText: 'Commercial',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
),
|
|
items: _commercialUsers.map((Users user) {
|
|
return DropdownMenuItem<Users>(
|
|
value: user,
|
|
child: Text('${user.name} ${user.lastName}'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (Users? newValue) {
|
|
setState(() {
|
|
_selectedCommercialUser = newValue;
|
|
});
|
|
},
|
|
validator: (value) =>
|
|
value == null ? 'Veuillez sélectionner un commercial' : null,
|
|
);
|
|
}
|
|
|
|
Widget _buildUserPointDeVenteInfo() {
|
|
if (_userController.pointDeVenteId <= 0) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
return Card(
|
|
elevation: 2,
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.store,
|
|
color: Colors.blue.shade700,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Votre point de vente',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color.fromARGB(255, 9, 56, 95),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_userController.pointDeVenteDesignation,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.blue.shade700,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.blue.shade200),
|
|
),
|
|
child: Text(
|
|
'ID: ${_userController.pointDeVenteId}',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.blue.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 6. Ajoutez cette méthode pour filtrer les produits par point de vente
|
|
// 🎯 MODIFIÉ: Dropdown avec gestion améliorée
|
|
Widget _buildPointDeVenteFilter() {
|
|
// if (!_isUserSuperAdmin()) {
|
|
// return const SizedBox.shrink(); // Cacher pour les non-admins
|
|
// }
|
|
|
|
return Card(
|
|
elevation: 2,
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.filter_list, color: Colors.green.shade700),
|
|
const SizedBox(width: 8),
|
|
const Text('Filtrer par point de vente (Admin)',
|
|
style:
|
|
TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
DropdownButtonFormField<String>(
|
|
value: _selectedPointDeVente,
|
|
decoration: InputDecoration(labelText: 'Point de vente'),
|
|
items: [
|
|
const DropdownMenuItem(
|
|
value: null, child: Text('Tous les points de vente')),
|
|
..._pointsDeVente.map((point) {
|
|
return DropdownMenuItem(
|
|
value: point['nom'] as String,
|
|
child: Text(point['nom'] as String),
|
|
);
|
|
}).toList(),
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedPointDeVente = value;
|
|
_filterProducts();
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 MODIFIÉ: Interface utilisateur adaptée selon le rôle
|
|
// 🎯 NOUVEAU: Header d'information adapté
|
|
Widget _buildRoleBasedHeader() {
|
|
final commandableCount =
|
|
_products.where((p) => _isProduitCommandable(p)).length;
|
|
final totalCount = _products.length;
|
|
|
|
return Card(
|
|
elevation: 2,
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade100
|
|
: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
_isUserSuperAdmin()
|
|
? Icons.admin_panel_settings
|
|
: Icons.visibility,
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade700
|
|
: Colors.blue.shade700,
|
|
size: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
_isUserSuperAdmin()
|
|
? 'Mode Administrateur'
|
|
: 'Mode Consultation étendue',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade700
|
|
: Colors.blue.shade700,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
_isUserSuperAdmin()
|
|
? 'Tous les produits sont visibles et commandables'
|
|
: 'Tous les produits sont visibles • Commandes limitées à votre PV',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade50
|
|
: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade200
|
|
: Colors.blue.shade200),
|
|
),
|
|
child: Text(
|
|
_userController.role.toUpperCase(),
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: _isUserSuperAdmin()
|
|
? Colors.purple.shade600
|
|
: Colors.blue.shade600,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
// Statistiques de produits
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
// Produits visibles
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.blue.shade200),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.visibility,
|
|
size: 16, color: Colors.blue.shade600),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'$totalCount produit(s) visibles',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.blue.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
if (!_isUserSuperAdmin()) ...[
|
|
const SizedBox(width: 8),
|
|
// Produits commandables
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.green.shade200),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.shopping_cart,
|
|
size: 16, color: Colors.green.shade600),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'$commandableCount commandables',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.green.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductList() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return _filteredProducts.isEmpty
|
|
? _buildEmptyState()
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16.0),
|
|
itemCount: _filteredProducts.length,
|
|
itemBuilder: (context, index) {
|
|
final product = _filteredProducts[index];
|
|
final quantity = _quantites[product.id] ?? 0;
|
|
|
|
return _buildProductListItem(product, quantity, isMobile);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32.0),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.search_off,
|
|
size: 64,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Aucun produit trouvé',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Modifiez vos critères de recherche',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 MODIFIÉ: Interface produit avec indication visuelle de la commandabilité
|
|
Widget _buildProductListItem(Product product, int quantity, bool isMobile) {
|
|
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
|
|
final detailPanier = _panierDetails[product.id!];
|
|
final int currentQuantity = detailPanier?.quantite ?? 0;
|
|
final isCurrentUserPointDeVente =
|
|
product.pointDeVenteId == _userController.pointDeVenteId;
|
|
final isProduitCommandable = _isProduitCommandable(product);
|
|
|
|
return FutureBuilder<String?>(
|
|
future: _appDatabase.getPointDeVenteNomById(product.pointDeVenteId ?? 0),
|
|
builder: (context, snapshot) {
|
|
String pointDeVenteText = 'Chargement...';
|
|
if (snapshot.connectionState == ConnectionState.done) {
|
|
if (snapshot.hasError) {
|
|
pointDeVenteText = 'Erreur de chargement';
|
|
} else {
|
|
pointDeVenteText = snapshot.data ?? 'Non spécifié';
|
|
}
|
|
}
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
side: isCurrentUserPointDeVente
|
|
? BorderSide(color: Colors.orange.shade300, width: 2)
|
|
: !isProduitCommandable
|
|
? BorderSide(color: Colors.grey.shade300, width: 1.5)
|
|
: BorderSide.none,
|
|
),
|
|
child: Opacity(
|
|
opacity: isProduitCommandable ? 1.0 : 0.7,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: isOutOfStock
|
|
? Border.all(color: Colors.red.shade200, width: 1.5)
|
|
: detailPanier?.estCadeau == true
|
|
? Border.all(color: Colors.green.shade300, width: 2)
|
|
: detailPanier?.aRemise == true
|
|
? Border.all(
|
|
color: Colors.orange.shade300, width: 2)
|
|
: isCurrentUserPointDeVente
|
|
? Border.all(
|
|
color: Colors.orange.shade300, width: 2)
|
|
: !isProduitCommandable
|
|
? Border.all(
|
|
color: Colors.grey.shade200, width: 1)
|
|
: null,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: isMobile ? 40 : 50,
|
|
height: isMobile ? 40 : 50,
|
|
decoration: BoxDecoration(
|
|
color: !isProduitCommandable
|
|
? Colors.grey.shade100
|
|
: isOutOfStock
|
|
? Colors.red.shade50
|
|
: detailPanier?.estCadeau == true
|
|
? Colors.green.shade50
|
|
: detailPanier?.aRemise == true
|
|
? Colors.orange.shade50
|
|
: isCurrentUserPointDeVente
|
|
? Colors.orange.shade50
|
|
: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
!isProduitCommandable
|
|
? Icons.lock_outline
|
|
: detailPanier?.estCadeau == true
|
|
? Icons.card_giftcard
|
|
: detailPanier?.aRemise == true
|
|
? Icons.discount
|
|
: isCurrentUserPointDeVente
|
|
? Icons.store
|
|
: Icons.shopping_bag,
|
|
size: isMobile ? 20 : 24,
|
|
color: !isProduitCommandable
|
|
? Colors.grey.shade500
|
|
: isOutOfStock
|
|
? Colors.red
|
|
: detailPanier?.estCadeau == true
|
|
? Colors.green.shade700
|
|
: detailPanier?.aRemise == true
|
|
? Colors.orange.shade700
|
|
: isCurrentUserPointDeVente
|
|
? Colors.orange.shade700
|
|
: Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
product.name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 14 : 16,
|
|
color: !isProduitCommandable
|
|
? Colors.grey.shade600
|
|
: isOutOfStock
|
|
? Colors.red.shade700
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
// Indicateurs de statut
|
|
if (!isProduitCommandable &&
|
|
!_isUserSuperAdmin())
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.lock_outline,
|
|
size: 10,
|
|
color: Colors.grey.shade600),
|
|
const SizedBox(width: 2),
|
|
Text(
|
|
'AUTRE PV',
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (detailPanier?.estCadeau == true)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
'CADEAU',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
),
|
|
if (isCurrentUserPointDeVente &&
|
|
detailPanier?.estCadeau != true &&
|
|
isProduitCommandable)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
'MON PV',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange.shade700,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
|
|
// ===== PRIX AVEC GESTION CADEAUX/REMISES =====
|
|
Row(
|
|
children: [
|
|
if (detailPanier?.estCadeau == true) ...[
|
|
Text(
|
|
'Gratuit',
|
|
style: TextStyle(
|
|
color: Colors.green.shade700,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${product.price.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
color: Colors.grey.shade500,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: isMobile ? 11 : 13,
|
|
decoration: TextDecoration.lineThrough,
|
|
),
|
|
),
|
|
] else ...[
|
|
Text(
|
|
'${product.price.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
color: Colors.green.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: isMobile ? 12 : 14,
|
|
decoration:
|
|
detailPanier?.aRemise == true
|
|
? TextDecoration.lineThrough
|
|
: null,
|
|
),
|
|
),
|
|
if (detailPanier?.aRemise == true) ...[
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'${(detailPanier!.prixFinal / detailPanier.quantite).toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
color: Colors.orange.shade700,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
],
|
|
),
|
|
|
|
// Affichage remise
|
|
if (detailPanier?.aRemise == true &&
|
|
!detailPanier!.estCadeau)
|
|
Text(
|
|
'Remise: ${detailPanier!.remiseDescription}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 10 : 12,
|
|
color: Colors.orange.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
|
|
// Stock
|
|
if (product.stock != null)
|
|
Text(
|
|
'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 10 : 12,
|
|
color: isOutOfStock
|
|
? Colors.red.shade600
|
|
: Colors.grey.shade600,
|
|
fontWeight: isOutOfStock
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
|
|
// ===== AFFICHAGE IMEI ET RÉFÉRENCE =====
|
|
if (product.imei != null &&
|
|
product.imei!.isNotEmpty)
|
|
Text(
|
|
'IMEI: ${product.imei}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 9 : 11,
|
|
color: Colors.grey.shade600,
|
|
fontFamily: 'monospace',
|
|
),
|
|
),
|
|
if (product.reference != null &&
|
|
product.reference!.isNotEmpty)
|
|
Text(
|
|
'Réf: ${product.reference}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 9 : 11,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
|
|
// Point de vente
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.store,
|
|
size: 12,
|
|
color: isCurrentUserPointDeVente
|
|
? Colors.orange.shade700
|
|
: !isProduitCommandable
|
|
? Colors.grey.shade500
|
|
: Colors.grey.shade600),
|
|
const SizedBox(width: 4),
|
|
Expanded(
|
|
child: Text(
|
|
'PV: $pointDeVenteText',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 9 : 11,
|
|
color: isCurrentUserPointDeVente
|
|
? Colors.orange.shade700
|
|
: !isProduitCommandable
|
|
? Colors.grey.shade500
|
|
: Colors.grey.shade600,
|
|
fontWeight: isCurrentUserPointDeVente
|
|
? FontWeight.w600
|
|
: FontWeight.normal,
|
|
),
|
|
),
|
|
),
|
|
if (!isProduitCommandable &&
|
|
!_isUserSuperAdmin())
|
|
Icon(
|
|
Icons.lock_outline,
|
|
size: 12,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ===== CONTRÔLES QUANTITÉ ET ACTIONS =====
|
|
Column(
|
|
children: [
|
|
// Boutons d'actions (seulement si commandable ET dans le panier)
|
|
if (isProduitCommandable &&
|
|
currentQuantity > 0) ...[
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Bouton cadeau
|
|
Container(
|
|
margin: const EdgeInsets.only(right: 4),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
detailPanier?.estCadeau == true
|
|
? Icons.card_giftcard
|
|
: Icons.card_giftcard_outlined,
|
|
size: isMobile ? 16 : 18,
|
|
color: detailPanier?.estCadeau == true
|
|
? Colors.green.shade700
|
|
: Colors.grey.shade600,
|
|
),
|
|
onPressed: isOutOfStock
|
|
? null
|
|
: () => _basculerStatutCadeau(
|
|
product.id!),
|
|
tooltip: detailPanier?.estCadeau == true
|
|
? 'Retirer le statut cadeau'
|
|
: 'Marquer comme cadeau',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor:
|
|
detailPanier?.estCadeau == true
|
|
? Colors.green.shade100
|
|
: Colors.grey.shade100,
|
|
minimumSize: Size(isMobile ? 32 : 36,
|
|
isMobile ? 32 : 36),
|
|
),
|
|
),
|
|
),
|
|
// Bouton remise (seulement pour les articles non-cadeaux)
|
|
if (!detailPanier!.estCadeau)
|
|
Container(
|
|
margin: const EdgeInsets.only(right: 4),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
detailPanier.aRemise
|
|
? Icons.discount
|
|
: Icons.local_offer,
|
|
size: isMobile ? 16 : 18,
|
|
color: detailPanier.aRemise
|
|
? Colors.orange.shade700
|
|
: Colors.grey.shade600,
|
|
),
|
|
onPressed: isOutOfStock
|
|
? null
|
|
: () => _showRemiseDialog(product),
|
|
tooltip: detailPanier.aRemise
|
|
? 'Modifier la remise'
|
|
: 'Ajouter une remise',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: detailPanier.aRemise
|
|
? Colors.orange.shade100
|
|
: Colors.grey.shade100,
|
|
minimumSize: Size(isMobile ? 32 : 36,
|
|
isMobile ? 32 : 36),
|
|
),
|
|
),
|
|
),
|
|
// Bouton pour ajouter un cadeau à un autre produit
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 4),
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.add_circle_outline,
|
|
size: isMobile ? 16 : 18,
|
|
color: Colors.green.shade600,
|
|
),
|
|
onPressed: isOutOfStock
|
|
? null
|
|
: () => _showCadeauDialog(product),
|
|
tooltip: 'Ajouter un cadeau',
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.green.shade50,
|
|
minimumSize: Size(isMobile ? 32 : 36,
|
|
isMobile ? 32 : 36),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
|
|
// Contrôles de quantité (seulement si commandable)
|
|
if (isProduitCommandable)
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: isOutOfStock
|
|
? Colors.grey.shade100
|
|
: detailPanier?.estCadeau == true
|
|
? Colors.green.shade50
|
|
: isCurrentUserPointDeVente
|
|
? Colors.orange.shade50
|
|
: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(Icons.remove,
|
|
size: isMobile ? 16 : 18),
|
|
onPressed: isOutOfStock
|
|
? null
|
|
: () {
|
|
if (currentQuantity > 0) {
|
|
_modifierQuantite(product.id!,
|
|
currentQuantity - 1);
|
|
}
|
|
},
|
|
),
|
|
Text(
|
|
currentQuantity.toString(),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(Icons.add,
|
|
size: isMobile ? 16 : 18),
|
|
onPressed: isOutOfStock
|
|
? null
|
|
: () {
|
|
if (product.stock == null ||
|
|
currentQuantity <
|
|
product.stock!) {
|
|
if (currentQuantity == 0) {
|
|
_ajouterAuPanier(product, 1);
|
|
} else {
|
|
_modifierQuantite(product.id!,
|
|
currentQuantity + 1);
|
|
}
|
|
} else {
|
|
Get.snackbar(
|
|
'Stock insuffisant',
|
|
'Quantité demandée non disponible',
|
|
snackPosition:
|
|
SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
// Message informatif pour produits non-commandables
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border:
|
|
Border.all(color: Colors.grey.shade300),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.info_outline,
|
|
size: 14,
|
|
color: Colors.grey.shade600),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'Consultation',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
ElevatedButton.icon(
|
|
icon: const Icon(Icons.swap_horiz,
|
|
size: 14),
|
|
label: !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 ||
|
|
_emailController.text.isEmpty ||
|
|
_telephoneController.text.isEmpty ||
|
|
_adresseController.text.isEmpty) {
|
|
Get.snackbar(
|
|
'Informations manquantes',
|
|
'Veuillez remplir les informations client',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
_showClientFormDialog();
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
// Créer le client
|
|
final client = Client(
|
|
nom: _nomController.text,
|
|
prenom: _prenomController.text,
|
|
email: _emailController.text,
|
|
telephone: _telephoneController.text,
|
|
adresse: _adresseController.text,
|
|
dateCreation: DateTime.now(),
|
|
);
|
|
|
|
// Calculer le total final et préparer les détails
|
|
double total = 0;
|
|
final details = <DetailCommande>[];
|
|
|
|
for (final entry in itemsInCart) {
|
|
final detail = entry.value;
|
|
total += detail.prixFinal;
|
|
details.add(detail);
|
|
}
|
|
|
|
// Créer la commande avec le total final (après remises)
|
|
final commande = Commande(
|
|
clientId: 0,
|
|
dateCommande: DateTime.now(),
|
|
statut: StatutCommande.enAttente,
|
|
montantTotal: total,
|
|
notes: 'Commande passée via l\'application',
|
|
commandeurId: _selectedCommercialUser?.id,
|
|
);
|
|
|
|
try {
|
|
await _appDatabase.createCommandeComplete(client, commande, details);
|
|
|
|
// Fermer le panier avant d'afficher la confirmation
|
|
Get.back();
|
|
|
|
// Afficher le dialogue de confirmation
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
await showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.check_circle, color: Colors.green.shade700),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Commande Validée',
|
|
style: TextStyle(fontSize: isMobile ? 16 : 18),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Text(
|
|
'Votre commande a été enregistrée et expédiée avec succès.',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
actions: [
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green.shade700,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: isMobile ? 12 : 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
_clearFormAndCart();
|
|
_loadProducts();
|
|
},
|
|
child: Text(
|
|
'OK',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Une erreur est survenue: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> _showCadeauDialog(Product product) async {
|
|
final detailExistant = _panierDetails[product.id!];
|
|
|
|
if (detailExistant == null || detailExistant.quantite == 0) {
|
|
Get.snackbar(
|
|
'Produit requis',
|
|
'Vous devez d\'abord ajouter ce produit au panier',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
);
|
|
return;
|
|
}
|
|
|
|
final result = await showDialog<Map<String, dynamic>>(
|
|
context: context,
|
|
builder: (context) => CadeauDialog(
|
|
product: product,
|
|
quantite: detailExistant.quantite,
|
|
detailExistant: detailExistant,
|
|
),
|
|
);
|
|
|
|
if (result != null) {
|
|
_ajouterCadeauAuPanier(
|
|
result['produit'] as Product,
|
|
result['quantite'] as int,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _ajouterCadeauAuPanier(Product produitCadeau, int quantite) {
|
|
// Vérifier le stock disponible
|
|
if (produitCadeau.stock != null && quantite > produitCadeau.stock!) {
|
|
Get.snackbar(
|
|
'Stock insuffisant',
|
|
'Quantité de cadeau demandée non disponible',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
final detailCadeau = DetailCommande.cadeau(
|
|
commandeId: 0, // Sera défini lors de la création
|
|
produitId: produitCadeau.id!,
|
|
quantite: quantite,
|
|
prixUnitaire: produitCadeau.price,
|
|
produitNom: produitCadeau.name,
|
|
produitReference: produitCadeau.reference,
|
|
);
|
|
_panierDetails[produitCadeau.id!] = detailCadeau;
|
|
});
|
|
|
|
Get.snackbar(
|
|
'Cadeau ajouté',
|
|
'${produitCadeau.name} a été ajouté comme cadeau',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.green.shade600,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 3),
|
|
icon: const Icon(Icons.card_giftcard, color: Colors.white),
|
|
);
|
|
}
|
|
|
|
void _basculerStatutCadeau(int productId) {
|
|
final detailExistant = _panierDetails[productId];
|
|
if (detailExistant == null) return;
|
|
|
|
setState(() {
|
|
if (detailExistant.estCadeau) {
|
|
// Convertir en article normal
|
|
_panierDetails[productId] = detailExistant.convertirEnArticleNormal();
|
|
|
|
Get.snackbar(
|
|
'Statut modifié',
|
|
'L\'article n\'est plus un cadeau',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.blue.shade600,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
} else {
|
|
// Convertir en cadeau
|
|
_panierDetails[productId] = detailExistant.convertirEnCadeau();
|
|
|
|
Get.snackbar(
|
|
'Cadeau offert',
|
|
'L\'article est maintenant un cadeau',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.green.shade600,
|
|
colorText: Colors.white,
|
|
duration: const Duration(seconds: 2),
|
|
icon: const Icon(Icons.card_giftcard, color: Colors.white),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_qrController?.dispose();
|
|
|
|
// Vos disposals existants...
|
|
_hideAllSuggestions();
|
|
_nomController.dispose();
|
|
_prenomController.dispose();
|
|
_emailController.dispose();
|
|
_telephoneController.dispose();
|
|
_adresseController.dispose();
|
|
_searchNameController.dispose();
|
|
_searchImeiController.dispose();
|
|
_searchReferenceController.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
// 10. Modifier le Widget build pour utiliser le nouveau scan automatique
|
|
// 8. Modifiez votre méthode build pour inclure les nouvelles cartes d'information
|
|
// VERSION OPTIMISÉE DE VOTRE INTERFACE EN-TÊTES ET RECHERCHE
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return Scaffold(
|
|
floatingActionButton: _buildFloatingCartButton(),
|
|
appBar: CustomAppBar(title: 'Nouvelle commande'),
|
|
drawer: CustomDrawer(),
|
|
body: GestureDetector(
|
|
onTap: _hideAllSuggestions,
|
|
child: Column(
|
|
children: [
|
|
// 🎯 EN-TÊTE OPTIMISÉ
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Info utilisateur (toujours visible mais compacte)
|
|
_buildCompactUserInfo(),
|
|
|
|
// Zone de recherche principale
|
|
_buildMainSearchSection(isMobile),
|
|
|
|
// Filtres rapides (toujours visibles)
|
|
_buildQuickFilters(isMobile),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Liste des produits avec indicateur de résultats
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
_buildResultsHeader(),
|
|
Expanded(child: _buildProductList()),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 INFORMATION UTILISATEUR COMPACTE
|
|
Widget _buildCompactUserInfo() {
|
|
if (_userController.pointDeVenteId <= 0) return const SizedBox.shrink();
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
border: Border(bottom: BorderSide(color: Colors.blue.shade100)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.store, size: 16, color: Colors.blue.shade700),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_userController.pointDeVenteDesignation,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.blue.shade700,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'ID: ${_userController.pointDeVenteId}',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.blue.shade600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 ZONE DE RECHERCHE PRINCIPALE OPTIMISÉE
|
|
Widget _buildMainSearchSection(bool isMobile) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// Recherche principale avec actions intégrées
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: TextField(
|
|
controller: _searchNameController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Rechercher par nom, IMEI, référence...',
|
|
prefixIcon: const Icon(Icons.search),
|
|
suffixIcon: _searchNameController.text.isNotEmpty
|
|
? IconButton(
|
|
icon: const Icon(Icons.clear),
|
|
onPressed: () {
|
|
_searchNameController.clear();
|
|
_filterProducts();
|
|
},
|
|
)
|
|
: null,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
|
|
// Bouton Scanner toujours visible
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade700,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.green.withOpacity(0.3),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: IconButton(
|
|
icon: _isScanning
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Icon(Icons.qr_code_scanner),
|
|
color: Colors.white,
|
|
onPressed: _isScanning ? null : _startAutomaticScanning,
|
|
tooltip: 'Scanner un produit',
|
|
),
|
|
),
|
|
|
|
if (!isMobile) ...[
|
|
const SizedBox(width: 8),
|
|
// Bouton filtres avancés
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade700,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: IconButton(
|
|
icon: const Icon(Icons.tune),
|
|
color: Colors.white,
|
|
onPressed: () => _showAdvancedFiltersDialog(),
|
|
tooltip: 'Filtres avancés',
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
|
|
// Recherche multicritères (desktop uniquement)
|
|
if (!isMobile) ...[
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchImeiController,
|
|
decoration: InputDecoration(
|
|
hintText: 'IMEI',
|
|
prefixIcon: const Icon(Icons.phone_android, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
isDense: true,
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchReferenceController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Référence',
|
|
prefixIcon: const Icon(Icons.qr_code, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
isDense: true,
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: DropdownButtonFormField<String>(
|
|
value: _selectedPointDeVente,
|
|
decoration: InputDecoration(
|
|
hintText: 'Point de vente',
|
|
prefixIcon: const Icon(Icons.store, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
isDense: true,
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
items: [
|
|
const DropdownMenuItem(
|
|
value: null,
|
|
child: Text('Tous les PV'),
|
|
),
|
|
..._pointsDeVente.map((point) {
|
|
return DropdownMenuItem(
|
|
value: point['nom'] as String,
|
|
child: Text(point['nom'] as String),
|
|
);
|
|
}).toList(),
|
|
],
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_selectedPointDeVente = value;
|
|
_filterProducts();
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 FILTRES RAPIDES OPTIMISÉS
|
|
Widget _buildQuickFilters(bool isMobile) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
border: Border(top: BorderSide(color: Colors.grey.shade200)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
// Filtre stock
|
|
FilterChip(
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
_showOnlyInStock ? Icons.inventory : Icons.inventory_2,
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(_showOnlyInStock ? 'En stock' : 'Tous'),
|
|
],
|
|
),
|
|
selected: _showOnlyInStock,
|
|
onSelected: (selected) => _toggleStockFilter(),
|
|
selectedColor: Colors.green.shade100,
|
|
checkmarkColor: Colors.green.shade700,
|
|
),
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
// Filtre mobile pour ouvrir les filtres avancés
|
|
if (isMobile)
|
|
ActionChip(
|
|
label: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.tune, size: 16),
|
|
SizedBox(width: 4),
|
|
Text('Filtres'),
|
|
],
|
|
),
|
|
onPressed: () => _showMobileFilters(context),
|
|
),
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
// Bouton reset si filtres actifs
|
|
if (_hasActiveFilters())
|
|
ActionChip(
|
|
label: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.clear, size: 16),
|
|
SizedBox(width: 4),
|
|
Text('Reset'),
|
|
],
|
|
),
|
|
onPressed: _clearFilters,
|
|
backgroundColor: Colors.orange.shade100,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 EN-TÊTE DES RÉSULTATS
|
|
Widget _buildResultsHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'${_filteredProducts.length} produit(s)',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
|
|
// Indicateurs de filtres actifs
|
|
if (_hasActiveFilters()) ...[
|
|
Wrap(
|
|
spacing: 4,
|
|
children: _getActiveFilterChips(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 MÉTHODES UTILITAIRES
|
|
bool _hasActiveFilters() {
|
|
return _selectedPointDeVente != null ||
|
|
_showOnlyInStock ||
|
|
_searchNameController.text.isNotEmpty ||
|
|
_searchImeiController.text.isNotEmpty ||
|
|
_searchReferenceController.text.isNotEmpty;
|
|
}
|
|
|
|
List<Widget> _getActiveFilterChips() {
|
|
List<Widget> chips = [];
|
|
|
|
if (_selectedPointDeVente != null) {
|
|
chips.add(_buildMiniFilterChip('PV: $_selectedPointDeVente'));
|
|
}
|
|
if (_showOnlyInStock) {
|
|
chips.add(_buildMiniFilterChip('En stock'));
|
|
}
|
|
if (_searchNameController.text.isNotEmpty) {
|
|
chips.add(_buildMiniFilterChip('Nom: ${_searchNameController.text}'));
|
|
}
|
|
if (_searchImeiController.text.isNotEmpty) {
|
|
chips.add(_buildMiniFilterChip('IMEI: ${_searchImeiController.text}'));
|
|
}
|
|
if (_searchReferenceController.text.isNotEmpty) {
|
|
chips
|
|
.add(_buildMiniFilterChip('Réf: ${_searchReferenceController.text}'));
|
|
}
|
|
|
|
return chips;
|
|
}
|
|
|
|
Widget _buildMiniFilterChip(String label) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.blue.shade300),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.blue.shade700,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 🎯 DIALOGUE FILTRES AVANCÉS
|
|
void _showAdvancedFiltersDialog() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Filtres avancés'),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Tous vos filtres existants ici
|
|
_buildPointDeVenteFilter(),
|
|
const SizedBox(height: 16),
|
|
// Autres filtres selon vos besoins
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('Fermer'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_filterProducts();
|
|
Get.back();
|
|
},
|
|
child: const Text('Appliquer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|