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.
831 lines
27 KiB
831 lines
27 KiB
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;
|
|
}
|
|
|