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