16 changed files with 2747 additions and 457 deletions
@ -0,0 +1,831 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Components/appDrawer.dart'; |
|||
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; |
|||
import 'package:youmazgestion/controller/userController.dart'; |
|||
import '../Models/produit.dart'; |
|||
import 'package:mobile_scanner/mobile_scanner.dart'; |
|||
|
|||
class DemandeSortiePersonnellePage extends StatefulWidget { |
|||
const DemandeSortiePersonnellePage({super.key}); |
|||
|
|||
@override |
|||
_DemandeSortiePersonnellePageState createState() => |
|||
_DemandeSortiePersonnellePageState(); |
|||
} |
|||
|
|||
class _DemandeSortiePersonnellePageState |
|||
extends State<DemandeSortiePersonnellePage> with TickerProviderStateMixin { |
|||
final AppDatabase _database = AppDatabase.instance; |
|||
final UserController _userController = Get.find<UserController>(); |
|||
|
|||
final _formKey = GlobalKey<FormState>(); |
|||
final _quantiteController = TextEditingController(text: '1'); |
|||
final _motifController = TextEditingController(); |
|||
final _notesController = TextEditingController(); |
|||
final _searchController = TextEditingController(); |
|||
|
|||
Product? _selectedProduct; |
|||
List<Product> _products = []; |
|||
List<Product> _filteredProducts = []; |
|||
bool _isLoading = false; |
|||
bool _isSearching = false; |
|||
|
|||
late AnimationController _animationController; |
|||
late Animation<double> _fadeAnimation; |
|||
late Animation<Offset> _slideAnimation; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_animationController = AnimationController( |
|||
duration: const Duration(milliseconds: 800), |
|||
vsync: this, |
|||
); |
|||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( |
|||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), |
|||
); |
|||
_slideAnimation = |
|||
Tween<Offset>(begin: const Offset(0, 0.3), end: Offset.zero).animate( |
|||
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic), |
|||
); |
|||
|
|||
_loadProducts(); |
|||
_searchController.addListener(_filterProducts); |
|||
} |
|||
|
|||
void _scanQrOrBarcode() async { |
|||
await showDialog( |
|||
context: context, |
|||
builder: (context) { |
|||
return AlertDialog( |
|||
content: Container( |
|||
width: double.maxFinite, |
|||
height: 400, |
|||
child: MobileScanner( |
|||
onDetect: (BarcodeCapture barcodeCap) { |
|||
print("BarcodeCapture: $barcodeCap"); |
|||
// Now accessing the barcodes attribute |
|||
final List<Barcode> barcodes = barcodeCap.barcodes; |
|||
|
|||
if (barcodes.isNotEmpty) { |
|||
// Get the first detected barcode value |
|||
String? scanResult = barcodes.first.rawValue; |
|||
|
|||
print("Scanned Result: $scanResult"); |
|||
|
|||
if (scanResult != null && scanResult.isNotEmpty) { |
|||
setState(() { |
|||
_searchController.text = scanResult; |
|||
print( |
|||
"Updated Search Controller: ${_searchController.text}"); |
|||
}); |
|||
|
|||
// Close dialog after scanning |
|||
Navigator.of(context).pop(); |
|||
|
|||
// Refresh product list based on new search input |
|||
_filterProducts(); |
|||
} else { |
|||
print("Scan result was empty or null."); |
|||
Navigator.of(context).pop(); |
|||
} |
|||
} else { |
|||
print("No barcodes detected."); |
|||
Navigator.of(context).pop(); |
|||
} |
|||
}, |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
void _filterProducts() { |
|||
final query = _searchController.text.toLowerCase(); |
|||
setState(() { |
|||
if (query.isEmpty) { |
|||
_filteredProducts = _products; |
|||
_isSearching = false; |
|||
} else { |
|||
_isSearching = true; |
|||
_filteredProducts = _products.where((product) { |
|||
return product.name.toLowerCase().contains(query) || |
|||
(product.reference?.toLowerCase().contains(query) ?? false); |
|||
}).toList(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
setState(() => _isLoading = true); |
|||
try { |
|||
final products = await _database.getProducts(); |
|||
setState(() { |
|||
_products = products.where((p) { |
|||
// Check stock availability |
|||
print("point de vente id: ${_userController.pointDeVenteId}"); |
|||
bool hasStock = _userController.pointDeVenteId == 0 |
|||
? (p.stock ?? 0) > 0 |
|||
: (p.stock ?? 0) > 0 && |
|||
p.pointDeVenteId == _userController.pointDeVenteId; |
|||
return hasStock; |
|||
}).toList(); |
|||
|
|||
// Setting filtered products |
|||
_filteredProducts = _products; |
|||
|
|||
// End loading state |
|||
_isLoading = false; |
|||
}); |
|||
|
|||
// Start the animation |
|||
_animationController.forward(); |
|||
} catch (e) { |
|||
// Handle any errors |
|||
setState(() { |
|||
_isLoading = false; |
|||
}); |
|||
_showErrorSnackbar('Impossible de charger les produits: $e'); |
|||
} |
|||
} |
|||
|
|||
Future<void> _soumettreDemandePersonnelle() async { |
|||
if (!_formKey.currentState!.validate() || _selectedProduct == null) { |
|||
_showErrorSnackbar('Veuillez remplir tous les champs obligatoires'); |
|||
return; |
|||
} |
|||
|
|||
final quantite = int.tryParse(_quantiteController.text) ?? 0; |
|||
|
|||
if (quantite <= 0) { |
|||
_showErrorSnackbar('La quantité doit être supérieure à 0'); |
|||
return; |
|||
} |
|||
|
|||
if ((_selectedProduct!.stock ?? 0) < quantite) { |
|||
_showErrorSnackbar( |
|||
'Stock insuffisant (disponible: ${_selectedProduct!.stock})'); |
|||
return; |
|||
} |
|||
|
|||
// Confirmation dialog |
|||
final confirmed = await _showConfirmationDialog(); |
|||
if (!confirmed) return; |
|||
|
|||
setState(() => _isLoading = true); |
|||
|
|||
try { |
|||
await _database.createSortieStockPersonnelle( |
|||
produitId: _selectedProduct!.id!, |
|||
adminId: _userController.userId, |
|||
quantite: quantite, |
|||
motif: _motifController.text.trim(), |
|||
pointDeVenteId: _userController.pointDeVenteId > 0 |
|||
? _userController.pointDeVenteId |
|||
: null, |
|||
notes: _notesController.text.trim().isNotEmpty |
|||
? _notesController.text.trim() |
|||
: null, |
|||
); |
|||
|
|||
_showSuccessSnackbar( |
|||
'Votre demande de sortie personnelle a été soumise pour approbation'); |
|||
|
|||
// Réinitialiser le formulaire avec animation |
|||
_resetForm(); |
|||
_loadProducts(); |
|||
} catch (e) { |
|||
_showErrorSnackbar('Impossible de soumettre la demande: $e'); |
|||
} finally { |
|||
setState(() => _isLoading = false); |
|||
} |
|||
} |
|||
|
|||
void _resetForm() { |
|||
_formKey.currentState!.reset(); |
|||
_quantiteController.text = '1'; |
|||
_motifController.clear(); |
|||
_notesController.clear(); |
|||
_searchController.clear(); |
|||
setState(() { |
|||
_selectedProduct = null; |
|||
_isSearching = false; |
|||
}); |
|||
} |
|||
|
|||
Future<bool> _showConfirmationDialog() async { |
|||
return await showDialog<bool>( |
|||
context: context, |
|||
builder: (context) => AlertDialog( |
|||
shape: |
|||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), |
|||
title: Row( |
|||
children: [ |
|||
Icon(Icons.help_outline, color: Colors.orange.shade700), |
|||
const SizedBox(width: 8), |
|||
const Text('Confirmer la demande'), |
|||
], |
|||
), |
|||
content: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Êtes-vous sûr de vouloir soumettre cette demande ?'), |
|||
const SizedBox(height: 16), |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text('Produit: ${_selectedProduct?.name}'), |
|||
Text('Quantité: ${_quantiteController.text}'), |
|||
Text('Motif: ${_motifController.text}'), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(false), |
|||
child: const Text('Annuler'), |
|||
), |
|||
ElevatedButton( |
|||
onPressed: () => Navigator.of(context).pop(true), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.orange.shade700, |
|||
foregroundColor: Colors.white, |
|||
), |
|||
child: const Text('Confirmer'), |
|||
), |
|||
], |
|||
), |
|||
) ?? |
|||
false; |
|||
} |
|||
|
|||
void _showSuccessSnackbar(String message) { |
|||
Get.snackbar( |
|||
'', |
|||
'', |
|||
titleText: Row( |
|||
children: [ |
|||
Icon(Icons.check_circle, color: Colors.white), |
|||
const SizedBox(width: 8), |
|||
const Text('Succès', |
|||
style: |
|||
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|||
], |
|||
), |
|||
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|||
backgroundColor: Colors.green.shade600, |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 4), |
|||
margin: const EdgeInsets.all(16), |
|||
borderRadius: 12, |
|||
icon: Icon(Icons.check_circle_outline, color: Colors.white), |
|||
); |
|||
} |
|||
|
|||
void _showErrorSnackbar(String message) { |
|||
Get.snackbar( |
|||
'', |
|||
'', |
|||
titleText: Row( |
|||
children: [ |
|||
Icon(Icons.error, color: Colors.white), |
|||
const SizedBox(width: 8), |
|||
const Text('Erreur', |
|||
style: |
|||
TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), |
|||
], |
|||
), |
|||
messageText: Text(message, style: const TextStyle(color: Colors.white)), |
|||
backgroundColor: Colors.red.shade600, |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 4), |
|||
margin: const EdgeInsets.all(16), |
|||
borderRadius: 12, |
|||
); |
|||
} |
|||
|
|||
Widget _buildHeaderCard() { |
|||
return Container( |
|||
padding: const EdgeInsets.all(20), |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
colors: [Colors.blue.shade600, Colors.blue.shade400], |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
), |
|||
borderRadius: BorderRadius.circular(16), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.blue.shade200, |
|||
blurRadius: 12, |
|||
offset: const Offset(0, 4), |
|||
), |
|||
], |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.white.withOpacity(0.2), |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Icon(Icons.inventory_2, color: Colors.white, size: 28), |
|||
), |
|||
const SizedBox(width: 16), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Sortie personnelle de stock', |
|||
style: TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
'Demande d\'approbation requise', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.white.withOpacity(0.8), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 16), |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.white.withOpacity(0.1), |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Text( |
|||
'Cette fonctionnalité permet aux administrateurs de demander ' |
|||
'la sortie d\'un produit du stock pour usage personnel. ' |
|||
'Toute demande nécessite une approbation avant traitement.', |
|||
style: TextStyle(fontSize: 14, color: Colors.white), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildProductSelector() { |
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Sélection du produit *', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.grey.shade800, |
|||
), |
|||
), |
|||
const SizedBox(height: 12), |
|||
|
|||
// Barre de recherche |
|||
Row( |
|||
children: [ |
|||
Expanded( |
|||
child: TextField( |
|||
controller: _searchController, |
|||
decoration: InputDecoration( |
|||
hintText: 'Rechercher un produit...', |
|||
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600), |
|||
), |
|||
onChanged: (value) { |
|||
_filterProducts(); // Call to filter products |
|||
}, |
|||
), |
|||
), |
|||
IconButton( |
|||
icon: Icon(Icons.qr_code_scanner, color: Colors.blue), |
|||
onPressed: _scanQrOrBarcode, |
|||
tooltip: 'Scanner QR ou code-barres', |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 12), |
|||
|
|||
// Liste des produits |
|||
Container( |
|||
height: 200, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(12), |
|||
border: Border.all(color: Colors.grey.shade300), |
|||
), |
|||
child: _filteredProducts.isEmpty |
|||
? Center( |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon(Icons.search_off, |
|||
size: 48, color: Colors.grey.shade400), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
_isSearching |
|||
? 'Aucun produit trouvé' |
|||
: 'Aucun produit disponible', |
|||
style: TextStyle(color: Colors.grey.shade600), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: ListView.builder( |
|||
itemCount: _filteredProducts.length, |
|||
itemBuilder: (context, index) { |
|||
final product = _filteredProducts[index]; |
|||
final isSelected = _selectedProduct?.id == product.id; |
|||
|
|||
return AnimatedContainer( |
|||
duration: const Duration(milliseconds: 200), |
|||
margin: const EdgeInsets.symmetric( |
|||
horizontal: 8, vertical: 4), |
|||
decoration: BoxDecoration( |
|||
color: isSelected |
|||
? Colors.orange.shade50 |
|||
: Colors.transparent, |
|||
borderRadius: BorderRadius.circular(8), |
|||
border: Border.all( |
|||
color: isSelected |
|||
? Colors.orange.shade300 |
|||
: Colors.transparent, |
|||
width: 2, |
|||
), |
|||
), |
|||
child: ListTile( |
|||
leading: Container( |
|||
width: 48, |
|||
height: 48, |
|||
decoration: BoxDecoration( |
|||
color: isSelected |
|||
? Colors.orange.shade100 |
|||
: Colors.grey.shade100, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Icon( |
|||
Icons.inventory, |
|||
color: isSelected |
|||
? Colors.orange.shade700 |
|||
: Colors.grey.shade600, |
|||
), |
|||
), |
|||
title: Text( |
|||
product.name, |
|||
style: TextStyle( |
|||
fontWeight: |
|||
isSelected ? FontWeight.bold : FontWeight.w500, |
|||
color: isSelected |
|||
? Colors.orange.shade800 |
|||
: Colors.grey.shade800, |
|||
), |
|||
), |
|||
subtitle: Text( |
|||
'Stock: ${product.stock} • Réf: ${product.reference ?? 'N/A'}', |
|||
style: TextStyle( |
|||
color: isSelected |
|||
? Colors.orange.shade600 |
|||
: Colors.grey.shade600, |
|||
), |
|||
), |
|||
trailing: isSelected |
|||
? Icon(Icons.check_circle, |
|||
color: Colors.orange.shade700) |
|||
: Icon(Icons.radio_button_unchecked, |
|||
color: Colors.grey.shade400), |
|||
onTap: () { |
|||
setState(() { |
|||
_selectedProduct = product; |
|||
}); |
|||
}, |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildFormSection() { |
|||
return Column( |
|||
children: [ |
|||
// Quantité |
|||
_buildInputField( |
|||
label: 'Quantité *', |
|||
controller: _quantiteController, |
|||
keyboardType: TextInputType.number, |
|||
icon: Icons.format_list_numbered, |
|||
suffix: _selectedProduct != null |
|||
? Text('max: ${_selectedProduct!.stock}', |
|||
style: TextStyle(color: Colors.grey.shade600)) |
|||
: null, |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer une quantité'; |
|||
} |
|||
final quantite = int.tryParse(value); |
|||
if (quantite == null || quantite <= 0) { |
|||
return 'Quantité invalide'; |
|||
} |
|||
if (_selectedProduct != null && |
|||
quantite > (_selectedProduct!.stock ?? 0)) { |
|||
return 'Quantité supérieure au stock disponible'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 20), |
|||
|
|||
// Motif |
|||
_buildInputField( |
|||
label: 'Motif *', |
|||
controller: _motifController, |
|||
icon: Icons.description, |
|||
hintText: 'Raison de cette sortie personnelle', |
|||
validator: (value) { |
|||
if (value == null || value.trim().isEmpty) { |
|||
return 'Veuillez indiquer le motif'; |
|||
} |
|||
if (value.trim().length < 5) { |
|||
return 'Le motif doit contenir au moins 5 caractères'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 20), |
|||
|
|||
// Notes |
|||
_buildInputField( |
|||
label: 'Notes complémentaires', |
|||
controller: _notesController, |
|||
icon: Icons.note_add, |
|||
hintText: 'Informations complémentaires (optionnel)', |
|||
maxLines: 3, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildInputField({ |
|||
required String label, |
|||
required TextEditingController controller, |
|||
required IconData icon, |
|||
String? hintText, |
|||
TextInputType? keyboardType, |
|||
int maxLines = 1, |
|||
Widget? suffix, |
|||
String? Function(String?)? validator, |
|||
}) { |
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
label, |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.grey.shade800, |
|||
), |
|||
), |
|||
const SizedBox(height: 8), |
|||
TextFormField( |
|||
controller: controller, |
|||
keyboardType: keyboardType, |
|||
maxLines: maxLines, |
|||
validator: validator, |
|||
decoration: InputDecoration( |
|||
hintText: hintText, |
|||
prefixIcon: Icon(icon, color: Colors.grey.shade600), |
|||
suffix: suffix, |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
borderSide: BorderSide(color: Colors.grey.shade300), |
|||
), |
|||
enabledBorder: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
borderSide: BorderSide(color: Colors.grey.shade300), |
|||
), |
|||
focusedBorder: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
borderSide: BorderSide(color: Colors.orange.shade400, width: 2), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.grey.shade50, |
|||
contentPadding: |
|||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildUserInfoCard() { |
|||
return Container( |
|||
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: [ |
|||
Icon(Icons.person, color: Colors.grey.shade700), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'Informations de la demande', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.grey.shade800, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(height: 12), |
|||
_buildInfoRow( |
|||
Icons.account_circle, 'Demandeur', _userController.name), |
|||
if (_userController.pointDeVenteId > 0) |
|||
_buildInfoRow(Icons.store, 'Point de vente', |
|||
_userController.pointDeVenteDesignation), |
|||
_buildInfoRow(Icons.calendar_today, 'Date', |
|||
DateTime.now().toLocal().toString().split(' ')[0]), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildInfoRow(IconData icon, String label, String value) { |
|||
return Padding( |
|||
padding: const EdgeInsets.symmetric(vertical: 4), |
|||
child: Row( |
|||
children: [ |
|||
Icon(icon, size: 16, color: Colors.grey.shade600), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
'$label: ', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.w500, |
|||
color: Colors.grey.shade700, |
|||
), |
|||
), |
|||
Expanded( |
|||
child: Text( |
|||
value, |
|||
style: TextStyle(color: Colors.grey.shade800), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildSubmitButton() { |
|||
return Container( |
|||
width: double.infinity, |
|||
height: 56, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(16), |
|||
gradient: LinearGradient( |
|||
colors: [Colors.orange.shade700, Colors.orange.shade500], |
|||
begin: Alignment.topLeft, |
|||
end: Alignment.bottomRight, |
|||
), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.orange.shade300, |
|||
blurRadius: 12, |
|||
offset: const Offset(0, 4), |
|||
), |
|||
], |
|||
), |
|||
child: ElevatedButton( |
|||
onPressed: _isLoading ? null : _soumettreDemandePersonnelle, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.transparent, |
|||
shadowColor: Colors.transparent, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(16), |
|||
), |
|||
), |
|||
child: _isLoading |
|||
? const Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
SizedBox( |
|||
width: 24, |
|||
height: 24, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
SizedBox(width: 12), |
|||
Text( |
|||
'Traitement...', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
], |
|||
) |
|||
: const Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon(Icons.send, color: Colors.white), |
|||
SizedBox(width: 8), |
|||
Text( |
|||
'Soumettre la demande', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: CustomAppBar(title: 'Demande sortie personnelle'), |
|||
drawer: CustomDrawer(), |
|||
body: _isLoading && _products.isEmpty |
|||
? const Center(child: CircularProgressIndicator()) |
|||
: FadeTransition( |
|||
opacity: _fadeAnimation, |
|||
child: SlideTransition( |
|||
position: _slideAnimation, |
|||
child: SingleChildScrollView( |
|||
padding: const EdgeInsets.all(16), |
|||
child: Form( |
|||
key: _formKey, |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
_buildHeaderCard(), |
|||
const SizedBox(height: 24), |
|||
_buildProductSelector(), |
|||
const SizedBox(height: 24), |
|||
_buildFormSection(), |
|||
const SizedBox(height: 24), |
|||
_buildUserInfoCard(), |
|||
const SizedBox(height: 32), |
|||
_buildSubmitButton(), |
|||
const SizedBox(height: 16), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_animationController.dispose(); |
|||
_quantiteController.dispose(); |
|||
_motifController.dispose(); |
|||
_notesController.dispose(); |
|||
_searchController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
|
|||
extension on BarcodeCapture { |
|||
get rawValue => null; |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue