Browse Source

scan qr

28062025_02
Stephane 5 months ago
parent
commit
01e9cabeba
  1. 73
      lib/Components/QrScan.dart
  2. 302
      lib/Views/demande_sortie_personnelle_page.dart

73
lib/Components/QrScan.dart

@ -25,13 +25,14 @@ class _ScanQRPageState extends State<ScanQRPage> {
}
void _initializeController() {
if (!isMobile) {
setState(() {
_hasError = true;
_errorMessage = "Le scanner QR n'est pas disponible sur cette plateforme.";
});
return;
}
if (!isMobile) {
setState(() {
_hasError = true;
_errorMessage =
"Le scanner QR n'est pas disponible sur cette plateforme.";
});
return;
}
try {
cameraController = MobileScannerController(
detectionSpeed: DetectionSpeed.noDuplicates,
@ -61,22 +62,25 @@ class _ScanQRPageState extends State<ScanQRPage> {
return Scaffold(
appBar: AppBar(
title: const Text('Scanner QR Code'),
actions: _hasError ? [] : [
if (cameraController != null) ...[
IconButton(
color: Colors.white,
icon: const Icon(Icons.flash_on, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.flip_camera_ios, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.switchCamera(),
),
],
],
actions: _hasError
? []
: [
if (cameraController != null) ...[
IconButton(
color: Colors.white,
icon: const Icon(Icons.flash_on, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.toggleTorch(),
),
IconButton(
color: Colors.white,
icon:
const Icon(Icons.flip_camera_ios, color: Colors.white),
iconSize: 32.0,
onPressed: () => cameraController!.switchCamera(),
),
],
],
),
body: _hasError ? _buildErrorWidget() : _buildScannerWidget(),
);
@ -150,7 +154,8 @@ class _ScanQRPageState extends State<ScanQRPage> {
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
Text(
'Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _initializeController(),
@ -236,19 +241,25 @@ class QrScannerOverlay extends CustomPainter {
..style = PaintingStyle.stroke;
// Coins du scanner
_drawCorner(canvas, borderPaint, areaLeft, areaTop, borderLength, true, true);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength, false, true);
_drawCorner(canvas, borderPaint, areaLeft, areaTop + areaSize, borderLength, true, false);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop + areaSize, borderLength, false, false);
_drawCorner(
canvas, borderPaint, areaLeft, areaTop, borderLength, true, true);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength,
false, true);
_drawCorner(canvas, borderPaint, areaLeft, areaTop + areaSize, borderLength,
true, false);
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop + areaSize,
borderLength, false, false);
}
void _drawCorner(Canvas canvas, Paint paint, double x, double y, double length, bool isLeft, bool isTop) {
void _drawCorner(Canvas canvas, Paint paint, double x, double y,
double length, bool isLeft, bool isTop) {
final double horizontalStart = isLeft ? x : x - length;
final double horizontalEnd = isLeft ? x + length : x;
final double verticalStart = isTop ? y : y - length;
final double verticalEnd = isTop ? y + length : y;
canvas.drawLine(Offset(horizontalStart, y), Offset(horizontalEnd, y), paint);
canvas.drawLine(
Offset(horizontalStart, y), Offset(horizontalEnd, y), paint);
canvas.drawLine(Offset(x, verticalStart), Offset(x, verticalEnd), paint);
}
@ -256,4 +267,4 @@ class QrScannerOverlay extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
}

302
lib/Views/demande_sortie_personnelle_page.dart

@ -5,31 +5,33 @@ 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();
_DemandeSortiePersonnellePageState createState() =>
_DemandeSortiePersonnellePageState();
}
class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnellePage>
with TickerProviderStateMixin {
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;
@ -44,14 +46,66 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
_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(
_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 {
// 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() {
final query = _searchController.text.toLowerCase();
setState(() {
@ -62,7 +116,7 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
_isSearching = true;
_filteredProducts = _products.where((product) {
return product.name.toLowerCase().contains(query) ||
(product.reference?.toLowerCase().contains(query) ?? false);
(product.reference?.toLowerCase().contains(query) ?? false);
}).toList();
}
});
@ -91,14 +145,15 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
}
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})');
_showErrorSnackbar(
'Stock insuffisant (disponible: ${_selectedProduct!.stock})');
return;
}
@ -114,11 +169,16 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
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,
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');
_showSuccessSnackbar(
'Votre demande de sortie personnelle a été soumise pour approbation');
// Réinitialiser le formulaire avec animation
_resetForm();
@ -144,55 +204,58 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
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}'),
],
),
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'),
],
),
],
),
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,
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}'),
],
),
),
],
),
child: const Text('Confirmer'),
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;
) ??
false;
}
void _showSuccessSnackbar(String message) {
@ -203,7 +266,9 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
children: [
Icon(Icons.check_circle, color: Colors.white),
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)),
@ -224,7 +289,9 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
children: [
Icon(Icons.error, color: Colors.white),
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)),
@ -325,33 +392,27 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
),
),
const SizedBox(height: 12),
// Barre de recherche
Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher un produit...',
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),
Row(
children: [
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher un produit...',
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600),
),
),
),
),
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
@ -366,10 +427,13 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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),
Text(
_isSearching ? 'Aucun produit trouvé' : 'Aucun produit disponible',
_isSearching
? 'Aucun produit trouvé'
: 'Aucun produit disponible',
style: TextStyle(color: Colors.grey.shade600),
),
],
@ -380,15 +444,20 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
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),
margin: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isSelected ? Colors.orange.shade50 : Colors.transparent,
color: isSelected
? Colors.orange.shade50
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? Colors.orange.shade300 : Colors.transparent,
color: isSelected
? Colors.orange.shade300
: Colors.transparent,
width: 2,
),
),
@ -397,30 +466,41 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
width: 48,
height: 48,
decoration: BoxDecoration(
color: isSelected ? Colors.orange.shade100 : Colors.grey.shade100,
color: isSelected
? Colors.orange.shade100
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.inventory,
color: isSelected ? Colors.orange.shade700 : Colors.grey.shade600,
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,
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,
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),
? Icon(Icons.check_circle,
color: Colors.orange.shade700)
: Icon(Icons.radio_button_unchecked,
color: Colors.grey.shade400),
onTap: () {
setState(() {
_selectedProduct = product;
@ -445,7 +525,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
keyboardType: TextInputType.number,
icon: Icons.format_list_numbered,
suffix: _selectedProduct != null
? Text('max: ${_selectedProduct!.stock}', style: TextStyle(color: Colors.grey.shade600))
? Text('max: ${_selectedProduct!.stock}',
style: TextStyle(color: Colors.grey.shade600))
: null,
validator: (value) {
if (value == null || value.isEmpty) {
@ -455,7 +536,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
if (quantite == null || quantite <= 0) {
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 null;
@ -538,7 +620,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
),
filled: true,
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),
_buildInfoRow(Icons.account_circle, 'Demandeur', _userController.name),
_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]),
_buildInfoRow(Icons.store, 'Point de vente',
_userController.pointDeVenteDesignation),
_buildInfoRow(Icons.calendar_today, 'Date',
DateTime.now().toLocal().toString().split(' ')[0]),
],
),
);
@ -721,4 +807,8 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
_searchController.dispose();
super.dispose();
}
}
}
extension on BarcodeCapture {
get rawValue => null;
}

Loading…
Cancel
Save