commit 24/07/2025-2
This commit is contained in:
commit
a52749c415
@ -8,7 +8,8 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.example.my_app"
|
namespace = "com.example.my_app"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = "26.3.11579264"
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/rabarisonmimistephanethannio/Documents/DEV/guycom_finale/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=7e9f9a517e1b730b3eb5b9aa5a52f2df_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]}
|
||||||
@ -4,7 +4,6 @@ import 'package:youmazgestion/Models/client.dart';
|
|||||||
|
|
||||||
import '../Services/stock_managementDatabase.dart';
|
import '../Services/stock_managementDatabase.dart';
|
||||||
|
|
||||||
|
|
||||||
class ClientFormController extends GetxController {
|
class ClientFormController extends GetxController {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@ -88,7 +87,9 @@ class ClientFormController extends GetxController {
|
|||||||
prenom: _prenomController.text.trim(),
|
prenom: _prenomController.text.trim(),
|
||||||
email: _emailController.text.trim(),
|
email: _emailController.text.trim(),
|
||||||
telephone: _telephoneController.text.trim(),
|
telephone: _telephoneController.text.trim(),
|
||||||
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
|
adresse: _adresseController.text.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: _adresseController.text.trim(),
|
||||||
dateCreation: DateTime.now(),
|
dateCreation: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -98,7 +99,6 @@ class ClientFormController extends GetxController {
|
|||||||
// Procéder avec la commande
|
// Procéder avec la commande
|
||||||
Get.back();
|
Get.back();
|
||||||
_submitOrderWithClient(clientToUse);
|
_submitOrderWithClient(clientToUse);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Erreur',
|
'Erreur',
|
||||||
@ -116,6 +116,7 @@ class ClientFormController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Widget pour le formulaire avec auto-completion
|
// Widget pour le formulaire avec auto-completion
|
||||||
|
// ignore: unused_element
|
||||||
void _showClientFormDialog() {
|
void _showClientFormDialog() {
|
||||||
final controller = Get.put(ClientFormController());
|
final controller = Get.put(ClientFormController());
|
||||||
|
|
||||||
@ -168,7 +169,8 @@ void _showClientFormDialog() {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.check_circle, color: Colors.green.shade600),
|
Icon(Icons.check_circle,
|
||||||
|
color: Colors.green.shade600),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -191,7 +193,8 @@ void _showClientFormDialog() {
|
|||||||
_buildTextFormField(
|
_buildTextFormField(
|
||||||
controller: controller._nomController,
|
controller: controller._nomController,
|
||||||
label: 'Nom',
|
label: 'Nom',
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
validator: (value) =>
|
||||||
|
value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (controller.selectedClient.value != null) {
|
if (controller.selectedClient.value != null) {
|
||||||
controller.selectedClient.value = null;
|
controller.selectedClient.value = null;
|
||||||
@ -203,7 +206,9 @@ void _showClientFormDialog() {
|
|||||||
_buildTextFormField(
|
_buildTextFormField(
|
||||||
controller: controller._prenomController,
|
controller: controller._prenomController,
|
||||||
label: 'Prénom',
|
label: 'Prénom',
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
validator: (value) => value?.isEmpty ?? true
|
||||||
|
? 'Veuillez entrer un prénom'
|
||||||
|
: null,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (controller.selectedClient.value != null) {
|
if (controller.selectedClient.value != null) {
|
||||||
controller.selectedClient.value = null;
|
controller.selectedClient.value = null;
|
||||||
@ -217,8 +222,10 @@ void _showClientFormDialog() {
|
|||||||
label: 'Email',
|
label: 'Email',
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
// if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
||||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
if (value?.isEmpty ?? true) return null;
|
||||||
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||||
|
.hasMatch(value!)) {
|
||||||
return 'Email invalide';
|
return 'Email invalide';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -237,7 +244,9 @@ void _showClientFormDialog() {
|
|||||||
controller: controller._telephoneController,
|
controller: controller._telephoneController,
|
||||||
label: 'Téléphone',
|
label: 'Téléphone',
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
validator: (value) => value?.isEmpty ?? true
|
||||||
|
? 'Veuillez entrer un téléphone'
|
||||||
|
: null,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (controller.selectedClient.value != null) {
|
if (controller.selectedClient.value != null) {
|
||||||
controller.selectedClient.value = null;
|
controller.selectedClient.value = null;
|
||||||
@ -252,7 +261,9 @@ void _showClientFormDialog() {
|
|||||||
controller: controller._adresseController,
|
controller: controller._adresseController,
|
||||||
label: 'Adresse',
|
label: 'Adresse',
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
validator: (value) => value?.isEmpty ?? true
|
||||||
|
? 'Veuillez entrer une adresse'
|
||||||
|
: null,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (controller.selectedClient.value != null) {
|
if (controller.selectedClient.value != null) {
|
||||||
controller.selectedClient.value = null;
|
controller.selectedClient.value = null;
|
||||||
@ -288,8 +299,9 @@ void _showClientFormDialog() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
...controller.suggestedClients.map((client) =>
|
...controller.suggestedClients.map(
|
||||||
_buildClientSuggestionTile(client, controller),
|
(client) =>
|
||||||
|
_buildClientSuggestionTile(client, controller),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -349,7 +361,8 @@ Widget _buildSearchSection(ClientFormController controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Widget pour afficher une suggestion de client
|
// Widget pour afficher une suggestion de client
|
||||||
Widget _buildClientSuggestionTile(Client client, ClientFormController controller) {
|
Widget _buildClientSuggestionTile(
|
||||||
|
Client client, ClientFormController controller) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
|||||||
@ -25,13 +25,14 @@ class _ScanQRPageState extends State<ScanQRPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _initializeController() {
|
void _initializeController() {
|
||||||
if (!isMobile) {
|
if (!isMobile) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_hasError = true;
|
_hasError = true;
|
||||||
_errorMessage = "Le scanner QR n'est pas disponible sur cette plateforme.";
|
_errorMessage =
|
||||||
});
|
"Le scanner QR n'est pas disponible sur cette plateforme.";
|
||||||
return;
|
});
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
cameraController = MobileScannerController(
|
cameraController = MobileScannerController(
|
||||||
detectionSpeed: DetectionSpeed.noDuplicates,
|
detectionSpeed: DetectionSpeed.noDuplicates,
|
||||||
@ -61,22 +62,25 @@ class _ScanQRPageState extends State<ScanQRPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Scanner QR Code'),
|
title: const Text('Scanner QR Code'),
|
||||||
actions: _hasError ? [] : [
|
actions: _hasError
|
||||||
if (cameraController != null) ...[
|
? []
|
||||||
IconButton(
|
: [
|
||||||
color: Colors.white,
|
if (cameraController != null) ...[
|
||||||
icon: const Icon(Icons.flash_on, color: Colors.white),
|
IconButton(
|
||||||
iconSize: 32.0,
|
color: Colors.white,
|
||||||
onPressed: () => cameraController!.toggleTorch(),
|
icon: const Icon(Icons.flash_on, color: Colors.white),
|
||||||
),
|
iconSize: 32.0,
|
||||||
IconButton(
|
onPressed: () => cameraController!.toggleTorch(),
|
||||||
color: Colors.white,
|
),
|
||||||
icon: const Icon(Icons.flip_camera_ios, color: Colors.white),
|
IconButton(
|
||||||
iconSize: 32.0,
|
color: Colors.white,
|
||||||
onPressed: () => cameraController!.switchCamera(),
|
icon:
|
||||||
),
|
const Icon(Icons.flip_camera_ios, color: Colors.white),
|
||||||
],
|
iconSize: 32.0,
|
||||||
],
|
onPressed: () => cameraController!.switchCamera(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: _hasError ? _buildErrorWidget() : _buildScannerWidget(),
|
body: _hasError ? _buildErrorWidget() : _buildScannerWidget(),
|
||||||
);
|
);
|
||||||
@ -150,7 +154,8 @@ class _ScanQRPageState extends State<ScanQRPage> {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error, size: 64, color: Colors.red),
|
const Icon(Icons.error, size: 64, color: Colors.red),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text('Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
|
Text(
|
||||||
|
'Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _initializeController(),
|
onPressed: () => _initializeController(),
|
||||||
@ -236,19 +241,25 @@ class QrScannerOverlay extends CustomPainter {
|
|||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
// Coins du scanner
|
// Coins du scanner
|
||||||
_drawCorner(canvas, borderPaint, areaLeft, areaTop, borderLength, true, true);
|
_drawCorner(
|
||||||
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength, false, true);
|
canvas, borderPaint, areaLeft, areaTop, borderLength, true, true);
|
||||||
_drawCorner(canvas, borderPaint, areaLeft, areaTop + areaSize, borderLength, true, false);
|
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop, borderLength,
|
||||||
_drawCorner(canvas, borderPaint, areaLeft + areaSize, areaTop + areaSize, borderLength, false, false);
|
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 horizontalStart = isLeft ? x : x - length;
|
||||||
final double horizontalEnd = isLeft ? x + length : x;
|
final double horizontalEnd = isLeft ? x + length : x;
|
||||||
final double verticalStart = isTop ? y : y - length;
|
final double verticalStart = isTop ? y : y - length;
|
||||||
final double verticalEnd = isTop ? y + length : y;
|
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);
|
canvas.drawLine(Offset(x, verticalStart), Offset(x, verticalEnd), paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(() {
|
||||||
@ -62,7 +116,7 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
|
|||||||
_isSearching = true;
|
_isSearching = true;
|
||||||
_filteredProducts = _products.where((product) {
|
_filteredProducts = _products.where((product) {
|
||||||
return product.name.toLowerCase().contains(query) ||
|
return product.name.toLowerCase().contains(query) ||
|
||||||
(product.reference?.toLowerCase().contains(query) ?? false);
|
(product.reference?.toLowerCase().contains(query) ?? false);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -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();
|
||||||
@ -144,55 +204,58 @@ class _DemandeSortiePersonnellePageState extends State<DemandeSortiePersonnelleP
|
|||||||
|
|
||||||
Future<bool> _showConfirmationDialog() async {
|
Future<bool> _showConfirmationDialog() async {
|
||||||
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:
|
||||||
title: Row(
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
children: [
|
title: Row(
|
||||||
Icon(Icons.help_outline, color: Colors.orange.shade700),
|
children: [
|
||||||
const SizedBox(width: 8),
|
Icon(Icons.help_outline, color: Colors.orange.shade700),
|
||||||
const Text('Confirmer la demande'),
|
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}'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
content: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
actions: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
TextButton(
|
children: [
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
const Text(
|
||||||
child: const Text('Annuler'),
|
'Êtes-vous sûr de vouloir soumettre cette demande ?'),
|
||||||
),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
Container(
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
padding: const EdgeInsets.all(12),
|
||||||
style: ElevatedButton.styleFrom(
|
decoration: BoxDecoration(
|
||||||
backgroundColor: Colors.orange.shade700,
|
color: Colors.grey.shade50,
|
||||||
foregroundColor: Colors.white,
|
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) {
|
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),
|
child: TextField(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
controller: _searchController,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
child: TextField(
|
hintText: 'Rechercher un produit...',
|
||||||
controller: _searchController,
|
prefixIcon: Icon(Icons.search, color: Colors.grey.shade600),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
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;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user