|
|
|
@ -26,6 +26,7 @@ class ProductManagementPage extends StatefulWidget { |
|
|
|
|
|
|
|
class _ProductManagementPageState extends State<ProductManagementPage> { |
|
|
|
final AppDatabase _productDatabase = AppDatabase.instance; |
|
|
|
final AppDatabase _appDatabase = AppDatabase.instance; |
|
|
|
final UserController _userController = Get.find<UserController>(); |
|
|
|
|
|
|
|
List<Product> _products = []; |
|
|
|
@ -99,6 +100,494 @@ bool _isUserSuperAdmin() { |
|
|
|
bool autoGenerateReference = true; |
|
|
|
bool showAddNewPoint = false; |
|
|
|
|
|
|
|
// 🎨 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, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
// 🎨 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, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fonction pour mettre à jour le QR preview |
|
|
|
void updateQrPreview() { |
|
|
|
if (nameController.text.isNotEmpty) { |
|
|
|
@ -1253,25 +1742,32 @@ bool _isUserSuperAdmin() { |
|
|
|
} |
|
|
|
|
|
|
|
// Assigner le point de vente de l'utilisateur au produit |
|
|
|
final updatedProduct = Product( |
|
|
|
id: foundProduct.id, |
|
|
|
name: foundProduct.name, |
|
|
|
price: foundProduct.price, |
|
|
|
image: foundProduct.image, |
|
|
|
category: foundProduct.category, |
|
|
|
description: foundProduct.description, |
|
|
|
stock: foundProduct.stock, |
|
|
|
qrCode: foundProduct.qrCode, |
|
|
|
reference: foundProduct.reference, |
|
|
|
marque: foundProduct.marque, |
|
|
|
ram: foundProduct.ram, |
|
|
|
memoireInterne: foundProduct.memoireInterne, |
|
|
|
imei: foundProduct.imei, |
|
|
|
pointDeVenteId: |
|
|
|
_userController.pointDeVenteId, // Nouveau point de vente |
|
|
|
); |
|
|
|
|
|
|
|
await _productDatabase.updateProduct(updatedProduct); |
|
|
|
// final updatedProduct = Product( |
|
|
|
// id: foundProduct.id, |
|
|
|
// name: foundProduct.name, |
|
|
|
// price: foundProduct.price, |
|
|
|
// image: foundProduct.image, |
|
|
|
// category: foundProduct.category, |
|
|
|
// description: foundProduct.description, |
|
|
|
// stock: foundProduct.stock, |
|
|
|
// qrCode: foundProduct.qrCode, |
|
|
|
// reference: foundProduct.reference, |
|
|
|
// marque: foundProduct.marque, |
|
|
|
// ram: foundProduct.ram, |
|
|
|
// memoireInterne: foundProduct.memoireInterne, |
|
|
|
// imei: foundProduct.imei, |
|
|
|
// pointDeVenteId: |
|
|
|
// _userController.pointDeVenteId, // Nouveau point de vente |
|
|
|
// ); |
|
|
|
await _appDatabase.createDemandeTransfert( |
|
|
|
produitId: foundProduct.id!, |
|
|
|
pointDeVenteSourceId: _userController.pointDeVenteId, |
|
|
|
pointDeVenteDestinationId: _userController.pointDeVenteId, |
|
|
|
demandeurId: _userController.userId, |
|
|
|
quantite: foundProduct.stock, |
|
|
|
notes: 'produit non assigner', |
|
|
|
); |
|
|
|
// await _productDatabase.updateProduct(updatedProduct); |
|
|
|
|
|
|
|
// Recharger les produits pour refléter les changements |
|
|
|
_loadProducts(); |
|
|
|
@ -1311,7 +1807,7 @@ bool _isUserSuperAdmin() { |
|
|
|
child: Icon(Icons.check_circle, color: Colors.green.shade700), |
|
|
|
), |
|
|
|
const SizedBox(width: 12), |
|
|
|
const Expanded(child: Text('Attribution réussie !')), |
|
|
|
const Expanded(child: Text( 'demande attribution réussie en attente de validation!')), |
|
|
|
], |
|
|
|
), |
|
|
|
content: Column( |
|
|
|
|