|
|
@ -5,16 +5,18 @@ import 'package:youmazgestion/Components/appDrawer.dart'; |
|
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|
|
import 'package:youmazgestion/controller/userController.dart'; |
|
|
import 'package:youmazgestion/controller/userController.dart'; |
|
|
import '../Models/produit.dart'; |
|
|
import '../Models/produit.dart'; |
|
|
|
|
|
import 'package:mobile_scanner/mobile_scanner.dart'; |
|
|
|
|
|
|
|
|
class DemandeSortiePersonnellePage extends StatefulWidget { |
|
|
class DemandeSortiePersonnellePage extends StatefulWidget { |
|
|
const DemandeSortiePersonnellePage({super.key}); |
|
|
const DemandeSortiePersonnellePage({super.key}); |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
_DemandeSortiePersonnellePageState createState() => _DemandeSortiePersonnellePageState(); |
|
|
_DemandeSortiePersonnellePageState createState() => |
|
|
|
|
|
_DemandeSortiePersonnellePageState(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnellePage> |
|
|
class _DemandeSortiePersonnellePageState |
|
|
with TickerProviderStateMixin { |
|
|
extends State<DemandeSortiePersonnellePage> with TickerProviderStateMixin { |
|
|
final AppDatabase _database = AppDatabase.instance; |
|
|
final AppDatabase _database = AppDatabase.instance; |
|
|
final UserController _userController = Get.find<UserController>(); |
|
|
final UserController _userController = Get.find<UserController>(); |
|
|
|
|
|
|
|
|
@ -44,7 +46,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( |
|
|
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( |
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), |
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), |
|
|
); |
|
|
); |
|
|
_slideAnimation = Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate( |
|
|
_slideAnimation = |
|
|
|
|
|
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate( |
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), |
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
@ -52,6 +55,57 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
_searchController.addListener(_filterProducts); |
|
|
_searchController.addListener(_filterProducts); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _scanQrOrBarcode() async { |
|
|
|
|
|
// Open the mobile scanner as a modal widget |
|
|
|
|
|
await showDialog( |
|
|
|
|
|
context: context, |
|
|
|
|
|
builder: (context) { |
|
|
|
|
|
return AlertDialog( |
|
|
|
|
|
content: Container( |
|
|
|
|
|
width: double.maxFinite, |
|
|
|
|
|
height: 400, // Adjust according to your needs |
|
|
|
|
|
child: MobileScanner( |
|
|
|
|
|
onDetect: (barcodeCapture) { |
|
|
|
|
|
String scanResult = barcodeCapture.rawValue ?? ''; |
|
|
|
|
|
Navigator.of(context).pop(); // Close dialog after scanning |
|
|
|
|
|
|
|
|
|
|
|
if (scanResult.isEmpty) return; |
|
|
|
|
|
|
|
|
|
|
|
setState(() { |
|
|
|
|
|
_searchController.text = scanResult; // Show scanned text |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Assume IMEI is always 15 digits, adjust if necessary |
|
|
|
|
|
Product? found; |
|
|
|
|
|
if (scanResult.length == 15 && |
|
|
|
|
|
int.tryParse(scanResult) != null) { |
|
|
|
|
|
found = |
|
|
|
|
|
_products.firstWhereOrNull((p) => p.imei == scanResult); |
|
|
|
|
|
} else { |
|
|
|
|
|
found = _products |
|
|
|
|
|
.firstWhereOrNull((p) => p.reference == scanResult); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (found != null) { |
|
|
|
|
|
setState(() { |
|
|
|
|
|
_selectedProduct = found; |
|
|
|
|
|
_filteredProducts = [found!]; |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
|
|
|
_showErrorSnackbar('Aucun produit trouvé avec ce code.'); |
|
|
|
|
|
setState(() { |
|
|
|
|
|
_filteredProducts = []; |
|
|
|
|
|
_selectedProduct = null; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
void _filterProducts() { |
|
|
void _filterProducts() { |
|
|
final query = _searchController.text.toLowerCase(); |
|
|
final query = _searchController.text.toLowerCase(); |
|
|
setState(() { |
|
|
setState(() { |
|
|
@ -98,7 +152,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if ((_selectedProduct!.stock ?? 0) < quantite) { |
|
|
if ((_selectedProduct!.stock ?? 0) < quantite) { |
|
|
_showErrorSnackbar('Stock insuffisant (disponible: ${_selectedProduct!.stock})'); |
|
|
_showErrorSnackbar( |
|
|
|
|
|
'Stock insuffisant (disponible: ${_selectedProduct!.stock})'); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -114,11 +169,16 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
adminId: _userController.userId, |
|
|
adminId: _userController.userId, |
|
|
quantite: quantite, |
|
|
quantite: quantite, |
|
|
motif: _motifController.text.trim(), |
|
|
motif: _motifController.text.trim(), |
|
|
pointDeVenteId: _userController.pointDeVenteId > 0 ? _userController.pointDeVenteId : null, |
|
|
pointDeVenteId: _userController.pointDeVenteId > 0 |
|
|
notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null, |
|
|
? _userController.pointDeVenteId |
|
|
|
|
|
: null, |
|
|
|
|
|
notes: _notesController.text.trim().isNotEmpty |
|
|
|
|
|
? _notesController.text.trim() |
|
|
|
|
|
: null, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
_showSuccessSnackbar('Votre demande de sortie personnelle a été soumise pour approbation'); |
|
|
_showSuccessSnackbar( |
|
|
|
|
|
'Votre demande de sortie personnelle a été soumise pour approbation'); |
|
|
|
|
|
|
|
|
// Réinitialiser le formulaire avec animation |
|
|
// Réinitialiser le formulaire avec animation |
|
|
_resetForm(); |
|
|
_resetForm(); |
|
|
@ -146,7 +206,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
return await showDialog<bool>( |
|
|
return await showDialog<bool>( |
|
|
context: context, |
|
|
context: context, |
|
|
builder: (context) => AlertDialog( |
|
|
builder: (context) => AlertDialog( |
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), |
|
|
shape: |
|
|
|
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), |
|
|
title: Row( |
|
|
title: Row( |
|
|
children: [ |
|
|
children: [ |
|
|
Icon(Icons.help_outline, color: Colors.orange.shade700), |
|
|
Icon(Icons.help_outline, color: Colors.orange.shade700), |
|
|
@ -158,7 +219,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
children: [ |
|
|
children: [ |
|
|
const Text('Êtes-vous sûr de vouloir soumettre cette demande ?'), |
|
|
const Text( |
|
|
|
|
|
'Êtes-vous sûr de vouloir soumettre cette demande ?'), |
|
|
const SizedBox(height: 16), |
|
|
const SizedBox(height: 16), |
|
|
Container( |
|
|
Container( |
|
|
padding: const EdgeInsets.all(12), |
|
|
padding: const EdgeInsets.all(12), |
|
|
@ -192,7 +254,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
) ?? false; |
|
|
) ?? |
|
|
|
|
|
false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void _showSuccessSnackbar(String message) { |
|
|
void _showSuccessSnackbar(String message) { |
|
|
@ -203,7 +266,9 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
children: [ |
|
|
children: [ |
|
|
Icon(Icons.check_circle, color: Colors.white), |
|
|
Icon(Icons.check_circle, color: Colors.white), |
|
|
const SizedBox(width: 8), |
|
|
const SizedBox(width: 8), |
|
|
const Text('Succès', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|
|
const Text('Succès', |
|
|
|
|
|
style: |
|
|
|
|
|
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|
|
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|
|
@ -224,7 +289,9 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
children: [ |
|
|
children: [ |
|
|
Icon(Icons.error, color: Colors.white), |
|
|
Icon(Icons.error, color: Colors.white), |
|
|
const SizedBox(width: 8), |
|
|
const SizedBox(width: 8), |
|
|
const Text('Erreur', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|
|
const Text('Erreur', |
|
|
|
|
|
style: |
|
|
|
|
|
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|
|
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|
|
@ -327,31 +394,25 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
const SizedBox(height: 12), |
|
|
const SizedBox(height: 12), |
|
|
|
|
|
|
|
|
// Barre de recherche |
|
|
// Barre de recherche |
|
|
Container( |
|
|
Row( |
|
|
decoration: BoxDecoration( |
|
|
children: [ |
|
|
color: Colors.grey.shade50, |
|
|
Expanded( |
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
|
|
border: Border.all(color: Colors.grey.shade300), |
|
|
|
|
|
), |
|
|
|
|
|
child: TextField( |
|
|
child: TextField( |
|
|
controller: _searchController, |
|
|
controller: _searchController, |
|
|
decoration: InputDecoration( |
|
|
decoration: InputDecoration( |
|
|
hintText: 'Rechercher un produit...', |
|
|
hintText: 'Rechercher un produit...', |
|
|
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600), |
|
|
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600), |
|
|
suffixIcon: _isSearching |
|
|
|
|
|
? IconButton( |
|
|
|
|
|
icon: Icon(Icons.clear, color: Colors.grey.shade600), |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
_searchController.clear(); |
|
|
|
|
|
FocusScope.of(context).unfocus(); |
|
|
|
|
|
}, |
|
|
|
|
|
) |
|
|
|
|
|
: null, |
|
|
|
|
|
border: InputBorder.none, |
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
IconButton( |
|
|
|
|
|
icon: Icon(Icons.qr_code_scanner, color: Colors.blue), |
|
|
|
|
|
onPressed: _scanQrOrBarcode, |
|
|
|
|
|
tooltip: 'Scanner QR ou code-barres', |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
const SizedBox(height: 12), |
|
|
const SizedBox(height: 12), |
|
|
|
|
|
|
|
|
// Liste des produits |
|
|
// Liste des produits |
|
|
@ -366,10 +427,13 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
child: Column( |
|
|
child: Column( |
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
children: [ |
|
|
children: [ |
|
|
Icon(Icons.search_off, size: 48, color: Colors.grey.shade400), |
|
|
Icon(Icons.search_off, |
|
|
|
|
|
size: 48, color: Colors.grey.shade400), |
|
|
const SizedBox(height: 8), |
|
|
const SizedBox(height: 8), |
|
|
Text( |
|
|
Text( |
|
|
_isSearching ? 'Aucun produit trouvé' : 'Aucun produit disponible', |
|
|
_isSearching |
|
|
|
|
|
? 'Aucun produit trouvé' |
|
|
|
|
|
: 'Aucun produit disponible', |
|
|
style: TextStyle(color: Colors.grey.shade600), |
|
|
style: TextStyle(color: Colors.grey.shade600), |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
@ -383,12 +447,17 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
|
|
|
|
|
|
return AnimatedContainer( |
|
|
return AnimatedContainer( |
|
|
duration: const Duration(milliseconds: 200), |
|
|
duration: const Duration(milliseconds: 200), |
|
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |
|
|
margin: const EdgeInsets.symmetric( |
|
|
|
|
|
horizontal: 8, vertical: 4), |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: isSelected ? Colors.orange.shade50 : Colors.transparent, |
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade50 |
|
|
|
|
|
: Colors.transparent, |
|
|
borderRadius: BorderRadius.circular(8), |
|
|
borderRadius: BorderRadius.circular(8), |
|
|
border: Border.all( |
|
|
border: Border.all( |
|
|
color: isSelected ? Colors.orange.shade300 : Colors.transparent, |
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade300 |
|
|
|
|
|
: Colors.transparent, |
|
|
width: 2, |
|
|
width: 2, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
@ -397,30 +466,41 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
width: 48, |
|
|
width: 48, |
|
|
height: 48, |
|
|
height: 48, |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: isSelected ? Colors.orange.shade100 : Colors.grey.shade100, |
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade100 |
|
|
|
|
|
: Colors.grey.shade100, |
|
|
borderRadius: BorderRadius.circular(8), |
|
|
borderRadius: BorderRadius.circular(8), |
|
|
), |
|
|
), |
|
|
child: Icon( |
|
|
child: Icon( |
|
|
Icons.inventory, |
|
|
Icons.inventory, |
|
|
color: isSelected ? Colors.orange.shade700 : Colors.grey.shade600, |
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade700 |
|
|
|
|
|
: Colors.grey.shade600, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
title: Text( |
|
|
title: Text( |
|
|
product.name, |
|
|
product.name, |
|
|
style: TextStyle( |
|
|
style: TextStyle( |
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, |
|
|
fontWeight: |
|
|
color: isSelected ? Colors.orange.shade800 : Colors.grey.shade800, |
|
|
isSelected ? FontWeight.bold : FontWeight.w500, |
|
|
|
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade800 |
|
|
|
|
|
: Colors.grey.shade800, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
subtitle: Text( |
|
|
subtitle: Text( |
|
|
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}', |
|
|
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}', |
|
|
style: TextStyle( |
|
|
style: TextStyle( |
|
|
color: isSelected ? Colors.orange.shade600 : Colors.grey.shade600, |
|
|
color: isSelected |
|
|
|
|
|
? Colors.orange.shade600 |
|
|
|
|
|
: Colors.grey.shade600, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
trailing: isSelected |
|
|
trailing: isSelected |
|
|
? Icon(Icons.check_circle, color: Colors.orange.shade700) |
|
|
? Icon(Icons.check_circle, |
|
|
: Icon(Icons.radio_button_unchecked, color: Colors.grey.shade400), |
|
|
color: Colors.orange.shade700) |
|
|
|
|
|
: Icon(Icons.radio_button_unchecked, |
|
|
|
|
|
color: Colors.grey.shade400), |
|
|
onTap: () { |
|
|
onTap: () { |
|
|
setState(() { |
|
|
setState(() { |
|
|
_selectedProduct = product; |
|
|
_selectedProduct = product; |
|
|
@ -445,7 +525,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
keyboardType: TextInputType.number, |
|
|
keyboardType: TextInputType.number, |
|
|
icon: Icons.format_list_numbered, |
|
|
icon: Icons.format_list_numbered, |
|
|
suffix: _selectedProduct != null |
|
|
suffix: _selectedProduct != null |
|
|
? Text('max: ${_selectedProduct!.stock}', style: TextStyle(color: Colors.grey.shade600)) |
|
|
? Text('max: ${_selectedProduct!.stock}', |
|
|
|
|
|
style: TextStyle(color: Colors.grey.shade600)) |
|
|
: null, |
|
|
: null, |
|
|
validator: (value) { |
|
|
validator: (value) { |
|
|
if (value == null || value.isEmpty) { |
|
|
if (value == null || value.isEmpty) { |
|
|
@ -455,7 +536,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
if (quantite == null || quantite <= 0) { |
|
|
if (quantite == null || quantite <= 0) { |
|
|
return 'Quantité invalide'; |
|
|
return 'Quantité invalide'; |
|
|
} |
|
|
} |
|
|
if (_selectedProduct != null && quantite > (_selectedProduct!.stock ?? 0)) { |
|
|
if (_selectedProduct != null && |
|
|
|
|
|
quantite > (_selectedProduct!.stock ?? 0)) { |
|
|
return 'Quantité supérieure au stock disponible'; |
|
|
return 'Quantité supérieure au stock disponible'; |
|
|
} |
|
|
} |
|
|
return null; |
|
|
return null; |
|
|
@ -538,7 +620,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
), |
|
|
), |
|
|
filled: true, |
|
|
filled: true, |
|
|
fillColor: Colors.grey.shade50, |
|
|
fillColor: Colors.grey.shade50, |
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|
|
contentPadding: |
|
|
|
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
@ -571,10 +654,13 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
const SizedBox(height: 12), |
|
|
const SizedBox(height: 12), |
|
|
_buildInfoRow(Icons.account_circle, 'Demandeur', _userController.name), |
|
|
_buildInfoRow( |
|
|
|
|
|
Icons.account_circle, 'Demandeur', _userController.name), |
|
|
if (_userController.pointDeVenteId > 0) |
|
|
if (_userController.pointDeVenteId > 0) |
|
|
_buildInfoRow(Icons.store, 'Point de vente', _userController.pointDeVenteDesignation), |
|
|
_buildInfoRow(Icons.store, 'Point de vente', |
|
|
_buildInfoRow(Icons.calendar_today, 'Date', DateTime.now().toLocal().toString().split(' ')[0]), |
|
|
_userController.pointDeVenteDesignation), |
|
|
|
|
|
_buildInfoRow(Icons.calendar_today, 'Date', |
|
|
|
|
|
DateTime.now().toLocal().toString().split(' ')[0]), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
); |
|
|
); |
|
|
@ -722,3 +808,7 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP |
|
|
super.dispose(); |
|
|
super.dispose(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
extension on BarcodeCapture { |
|
|
|
|
|
get rawValue => null; |
|
|
|
|
|
} |
|
|
|