push 09112025
This commit is contained in:
parent
c757bdb701
commit
13554ee49c
@ -147,7 +147,7 @@ class _ScanQRPageState extends State<ScanQRPage> {
|
||||
}
|
||||
}
|
||||
},
|
||||
errorBuilder: (context, error, child) {
|
||||
errorBuilder: (context, error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -155,7 +155,8 @@ class _ScanQRPageState extends State<ScanQRPage> {
|
||||
const Icon(Icons.error, size: 64, color: Colors.red),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}'),
|
||||
'Erreur: ${error.errorDetails?.message ?? 'Erreur inconnue'}',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _initializeController(),
|
||||
|
||||
@ -1,37 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:youmazgestion/Models/client.dart';
|
||||
|
||||
|
||||
//Classe suplementaire
|
||||
|
||||
// Classe supplémentaire
|
||||
class CommandeActions extends StatelessWidget {
|
||||
final Commande commande;
|
||||
final Function(int, StatutCommande) onStatutChanged;
|
||||
final Function(Commande) onGenerateBonLivraison;
|
||||
|
||||
|
||||
const CommandeActions({
|
||||
required this.commande,
|
||||
required this.onStatutChanged,
|
||||
required this.onGenerateBonLivraison,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
List<Widget> _buildActionButtons(BuildContext context) {
|
||||
List<Widget> buttons = [];
|
||||
|
||||
switch (commande.statut) {
|
||||
case StatutCommande.enAttente:
|
||||
buttons.addAll([
|
||||
|
||||
// Bouton confirmer
|
||||
_buildActionButton(
|
||||
label: 'Confirmer',
|
||||
icon: Icons.check_circle,
|
||||
color: Colors.blue,
|
||||
onPressed: () => onGenerateBonLivraison(commande),
|
||||
onPressed: () => _showConfirmDialog(
|
||||
context,
|
||||
'Confirmer la commande',
|
||||
'Êtes-vous sûr de vouloir confirmer cette commande ?',
|
||||
() {
|
||||
// Change le statut à "confirmée"
|
||||
onStatutChanged(commande.id!, StatutCommande.confirmee);
|
||||
// Et génère le bon de livraison après confirmation
|
||||
onGenerateBonLivraison(commande);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton annuler
|
||||
_buildActionButton(
|
||||
label: 'Annuler',
|
||||
icon: Icons.cancel,
|
||||
@ -39,7 +45,7 @@ class CommandeActions extends StatelessWidget {
|
||||
onPressed: () => _showConfirmDialog(
|
||||
context,
|
||||
'Annuler la commande',
|
||||
'Êtes-vous sûr de vouloir annuler cette commande?',
|
||||
'Êtes-vous sûr de vouloir annuler cette commande ?',
|
||||
() => onStatutChanged(commande.id!, StatutCommande.annulee),
|
||||
),
|
||||
),
|
||||
@ -181,6 +187,7 @@ class CommandeActions extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
831
lib/Components/windows_qr_scanner.dart
Normal file
831
lib/Components/windows_qr_scanner.dart
Normal file
@ -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;
|
||||
}
|
||||
1090
lib/Services/qrService.dart
Normal file
1090
lib/Services/qrService.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -1976,14 +1976,48 @@ List<String> parseHeaderInfo(dynamic blobData) {
|
||||
}
|
||||
|
||||
// 3. Méthodes pour les commandes
|
||||
Future<int> updateStatutCommande(
|
||||
Future<int> updateStatutCommande(
|
||||
int commandeId, StatutCommande statut) async {
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
// 🔹 Si le statut devient "annulée"
|
||||
if (statut == StatutCommande.annulee) {
|
||||
// 1. Récupérer les détails de la commande
|
||||
final details = await db.query(
|
||||
'SELECT produitId, quantite FROM details_commandes WHERE commandeId = ?',
|
||||
[commandeId],
|
||||
);
|
||||
|
||||
// 2. Remettre le stock pour chaque produit
|
||||
for (final row in details) {
|
||||
final produitId = row['produitId'];
|
||||
final quantite = row['quantite'];
|
||||
|
||||
await db.query(
|
||||
'UPDATE products SET stock = stock + ? WHERE id = ?',
|
||||
[quantite, produitId],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Mettre à jour le statut de la commande
|
||||
final result = await db.query(
|
||||
'UPDATE commandes SET statut = ? WHERE id = ?',
|
||||
[statut.index, commandeId]);
|
||||
[statut.index, commandeId],
|
||||
);
|
||||
|
||||
await db.query('COMMIT');
|
||||
return result.affectedRows!;
|
||||
} catch (e) {
|
||||
await db.query('ROLLBACK');
|
||||
print("Erreur lors de la mise à jour du statut de la commande: $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<List<Commande>> getCommandesByClient(int clientId) async {
|
||||
final db = await database;
|
||||
@ -2532,7 +2566,8 @@ Future<List<Map<String, dynamic>>> getVentesParPointDeVente({
|
||||
final db = await database;
|
||||
|
||||
try {
|
||||
String whereClause = 'WHERE c.statut != 5';
|
||||
// 🔹 On ne garde que les commandes confirmées (statut = 1)
|
||||
String whereClause = "WHERE c.statut = 1";
|
||||
List<dynamic> whereArgs = [];
|
||||
|
||||
if (aujourdHuiSeulement == true) {
|
||||
@ -2546,7 +2581,8 @@ Future<List<Map<String, dynamic>>> getVentesParPointDeVente({
|
||||
_formatDate(endOfDay),
|
||||
]);
|
||||
} else if (dateDebut != null && dateFin != null) {
|
||||
final adjustedEndDate = DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
|
||||
final adjustedEndDate =
|
||||
DateTime(dateFin.year, dateFin.month, dateFin.day, 23, 59, 59);
|
||||
whereClause += ' AND c.dateCommande >= ? AND c.dateCommande <= ?';
|
||||
whereArgs.addAll([
|
||||
_formatDate(dateDebut),
|
||||
@ -2580,6 +2616,7 @@ Future<List<Map<String, dynamic>>> getVentesParPointDeVente({
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getTopProduitsParPointDeVente(
|
||||
int pointDeVenteId, {
|
||||
int limit = 5,
|
||||
|
||||
@ -11,6 +11,7 @@ import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:excel/excel.dart' hide Border;
|
||||
import 'package:youmazgestion/Services/qrService.dart';
|
||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
import '../Components/appDrawer.dart';
|
||||
@ -28,6 +29,7 @@ class _ProductManagementPageState extends State<ProductManagementPage> {
|
||||
final AppDatabase _productDatabase = AppDatabase.instance;
|
||||
final AppDatabase _appDatabase = AppDatabase.instance;
|
||||
final UserController _userController = Get.find<UserController>();
|
||||
final pdfService = PdfPrintService();
|
||||
|
||||
List<Product> _products = [];
|
||||
List<Product> _filteredProducts = [];
|
||||
@ -44,16 +46,20 @@ class _ProductManagementPageState extends State<ProductManagementPage> {
|
||||
bool _isAssigning = false;
|
||||
final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR');
|
||||
|
||||
Future<void> _loadAvailableCategories() async {
|
||||
try {
|
||||
final categories = await _productDatabase.getCategories();
|
||||
setState(() {
|
||||
_availableCategories = ['Non catégorisé', ...categories];
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des catégories: $e');
|
||||
// Garder la catégorie par défaut en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
// Catégories prédéfinies pour l'ajout de produits
|
||||
final List<String> _predefinedCategories = [
|
||||
'Smartphone',
|
||||
'Tablette',
|
||||
'Accessoires',
|
||||
'Multimedia',
|
||||
'Informatique',
|
||||
'Laptop',
|
||||
'Non catégorisé'
|
||||
];
|
||||
List<String> _availableCategories = ['Non catégorisé'];
|
||||
bool _isUserSuperAdmin() {
|
||||
return _userController.role == 'Super Admin';
|
||||
}
|
||||
@ -67,6 +73,7 @@ bool _isUserSuperAdmin() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
_loadPointsDeVente();
|
||||
_loadAvailableCategories();
|
||||
_searchController.addListener(_filterProducts);
|
||||
}
|
||||
|
||||
@ -94,7 +101,7 @@ bool _isUserSuperAdmin() {
|
||||
List<Map<String, dynamic>> pointsDeVente = [];
|
||||
bool isLoadingPoints = true;
|
||||
String selectedCategory =
|
||||
_predefinedCategories.last; // 'Non catégorisé' par défaut
|
||||
_availableCategories.last; // 'Non catégorisé' par défaut
|
||||
File? pickedImage;
|
||||
String? qrPreviewData;
|
||||
bool autoGenerateReference = true;
|
||||
@ -923,7 +930,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
// Catégorie
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedCategory,
|
||||
items: _predefinedCategories
|
||||
items: _availableCategories
|
||||
.map((category) => DropdownMenuItem(
|
||||
value: category, child: Text(category)))
|
||||
.toList(),
|
||||
@ -1360,9 +1367,45 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
if (columnIndex >= row.length || row[columnIndex]?.value == null)
|
||||
return null;
|
||||
|
||||
return row[columnIndex]!.value.toString().trim();
|
||||
var cellValue = row[columnIndex]!.value;
|
||||
|
||||
// 🔥 TRAITEMENT SPÉCIAL POUR IMEI
|
||||
if (field == 'imei') {
|
||||
print('🔍 IMEI brut depuis Excel: $cellValue (${cellValue.runtimeType})');
|
||||
|
||||
// Si c'est un nombre (notation scientifique)
|
||||
if (cellValue is num) {
|
||||
// Convertir directement en entier pour éviter les décimales
|
||||
String imeiStr = cellValue.toInt().toString();
|
||||
print('🔄 IMEI converti depuis num: $imeiStr');
|
||||
return imeiStr;
|
||||
}
|
||||
|
||||
// Si c'est déjà un String
|
||||
String strValue = cellValue.toString().trim();
|
||||
|
||||
// Gérer la notation scientifique dans les strings
|
||||
if (strValue.contains('E') || strValue.contains('e')) {
|
||||
try {
|
||||
// Remplacer virgule par point et parser
|
||||
String normalized = strValue.replaceAll(',', '.');
|
||||
double numValue = double.parse(normalized);
|
||||
String imeiStr = numValue.toInt().toString();
|
||||
print('🔄 IMEI converti depuis notation scientifique: $strValue → $imeiStr');
|
||||
return imeiStr;
|
||||
} catch (e) {
|
||||
print('❌ Erreur conversion IMEI: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return strValue;
|
||||
}
|
||||
|
||||
// Pour les autres champs
|
||||
return cellValue.toString().trim();
|
||||
}
|
||||
|
||||
void _startPointDeVenteAssignmentScanning() {
|
||||
if (_isScanning) return;
|
||||
|
||||
@ -1390,8 +1433,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, dynamic> _normalizeRowData(
|
||||
Map<String, dynamic> _normalizeRowData(
|
||||
List<Data?> row, Map<String, int> mapping, int rowIndex) {
|
||||
final normalizedData = <String, dynamic>{};
|
||||
|
||||
@ -1401,16 +1443,11 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
return value.toString().trim();
|
||||
}
|
||||
|
||||
// Fonction simple pour les nombres (maintenant ils sont corrects)
|
||||
// Fonction simple pour les nombres
|
||||
double? _normalizeNumber(String? value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
|
||||
// Remplacer les virgules par des points et supprimer les espaces
|
||||
final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), '');
|
||||
|
||||
// Supprimer les caractères non numériques sauf le point
|
||||
final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), '');
|
||||
|
||||
return double.tryParse(numericString);
|
||||
}
|
||||
|
||||
@ -1422,7 +1459,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
}
|
||||
}
|
||||
|
||||
// Normalisation du prix (maintenant simple car corrigé en amont)
|
||||
// Normalisation du prix
|
||||
if (mapping.containsKey('price')) {
|
||||
final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price'));
|
||||
final price = _normalizeNumber(priceValue);
|
||||
@ -1438,7 +1475,6 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
if (reference != null && reference.isNotEmpty) {
|
||||
normalizedData['reference'] = reference;
|
||||
} else {
|
||||
// Génération automatique si non fournie
|
||||
normalizedData['reference'] = _generateUniqueReference();
|
||||
}
|
||||
}
|
||||
@ -1463,7 +1499,6 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
if (mapping.containsKey('ram')) {
|
||||
final ram = _cleanValue(_getColumnValue(row, mapping, 'ram'));
|
||||
if (ram != null && ram.isNotEmpty) {
|
||||
// Standardisation du format (ex: "8 Go", "16GB" -> "8 Go", "16 Go")
|
||||
final ramValue = ram.replaceAll('GB', 'Go').replaceAll('go', 'Go');
|
||||
normalizedData['ram'] = ramValue;
|
||||
}
|
||||
@ -1471,48 +1506,88 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
|
||||
// Normalisation de la mémoire interne
|
||||
if (mapping.containsKey('memoire_interne')) {
|
||||
final memoire =
|
||||
_cleanValue(_getColumnValue(row, mapping, 'memoire_interne'));
|
||||
final memoire = _cleanValue(_getColumnValue(row, mapping, 'memoire_interne'));
|
||||
if (memoire != null && memoire.isNotEmpty) {
|
||||
// Standardisation du format (ex: "256GB" -> "256 Go")
|
||||
final memoireValue =
|
||||
memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go');
|
||||
final memoireValue = memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go');
|
||||
normalizedData['memoire_interne'] = memoireValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalisation de l'IMEI
|
||||
if (mapping.containsKey('imei')) {
|
||||
// 🔥 IMPORTANT: Normaliser l'IMEI EN PREMIER avant de gérer le stock
|
||||
// 🔥 Normalisation de l'IMEI (simplifié car _getColumnValue le gère maintenant)
|
||||
String? imeiValue;
|
||||
if (mapping.containsKey('imei')) {
|
||||
final imei = _cleanValue(_getColumnValue(row, mapping, 'imei'));
|
||||
|
||||
if (imei != null && imei.isNotEmpty) {
|
||||
// Suppression des espaces et tirets dans l'IMEI
|
||||
final imeiValue = imei.replaceAll(RegExp(r'[\s-]'), '');
|
||||
if (imeiValue.length >= 15) {
|
||||
normalizedData['imei'] = imeiValue.substring(0, 15);
|
||||
} else {
|
||||
// Nettoyer les espaces et tirets
|
||||
String cleanedImei = imei.replaceAll(RegExp(r'[\s-]'), '');
|
||||
|
||||
// Vérifier que c'est bien un IMEI valide (10-15 chiffres)
|
||||
if (cleanedImei.length >= 10 &&
|
||||
cleanedImei.length <= 15 &&
|
||||
RegExp(r'^\d+$').hasMatch(cleanedImei)) {
|
||||
|
||||
imeiValue = cleanedImei.length > 15
|
||||
? cleanedImei.substring(0, 15)
|
||||
: cleanedImei;
|
||||
|
||||
normalizedData['imei'] = imeiValue;
|
||||
print('✅ IMEI valide enregistré: $imeiValue');
|
||||
} else {
|
||||
print('⚠️ IMEI invalide ignoré: "$cleanedImei" (longueur: ${cleanedImei.length})');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Le reste du code reste identique...
|
||||
|
||||
// Normalisation du point de vente
|
||||
if (mapping.containsKey('point_de_vente')) {
|
||||
final pv = _cleanValue(_getColumnValue(row, mapping, 'point_de_vente'));
|
||||
if (pv != null && pv.isNotEmpty) {
|
||||
// Suppression des espaces superflus
|
||||
normalizedData['point_de_vente'] =
|
||||
pv.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
normalizedData['point_de_vente'] = pv.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Valeurs par défaut
|
||||
normalizedData['description'] = ''; // Description toujours vide
|
||||
normalizedData['description'] = '';
|
||||
|
||||
// 🎯 LOGIQUE CRITIQUE: Gestion du stock selon la présence d'IMEI
|
||||
// On vérifie si normalizedData['imei'] existe ET n'est pas vide
|
||||
if (normalizedData.containsKey('imei') &&
|
||||
normalizedData['imei'] != null &&
|
||||
normalizedData['imei'].toString().isNotEmpty) {
|
||||
// Produit avec IMEI → stock forcé à 1
|
||||
normalizedData['stock'] = 1;
|
||||
print('🔒 Stock forcé à 1 car IMEI présent: ${normalizedData['imei']}');
|
||||
} else {
|
||||
// Produit sans IMEI → utiliser le stock du fichier
|
||||
if (mapping.containsKey('stock')) {
|
||||
final stockValue = _cleanValue(_getColumnValue(row, mapping, 'stock'));
|
||||
final stock = int.tryParse(stockValue ?? '0') ?? 1;
|
||||
normalizedData['stock'] = stock > 0 ? stock : 1;
|
||||
|
||||
// Try parsing as int first
|
||||
int? stock = int.tryParse(stockValue ?? '');
|
||||
|
||||
// If parsing as int fails, try parsing as double and convert to int
|
||||
if (stock == null && stockValue != null && stockValue.isNotEmpty) {
|
||||
final doubleValue = double.tryParse(stockValue);
|
||||
if (doubleValue != null) {
|
||||
stock = doubleValue.toInt();
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: ensure at least 1
|
||||
stock ??= 1;
|
||||
|
||||
// Never allow 0 or negative values
|
||||
normalizedData['stock'] = stock > 0 ? stock : 1;
|
||||
|
||||
print('📦 Stock depuis Excel: $stock (pas d\'IMEI)');
|
||||
} else {
|
||||
normalizedData['stock'] = 1; // Valeur par défaut
|
||||
normalizedData['stock'] = 1;
|
||||
print('📦 Stock par défaut: 1 (pas d\'IMEI, pas de colonne stock)');
|
||||
}
|
||||
}
|
||||
|
||||
// Validation des données obligatoires
|
||||
@ -1522,10 +1597,8 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
}
|
||||
|
||||
return normalizedData;
|
||||
}
|
||||
|
||||
// Méthode pour mapper les en-têtes aux colonnes (CORRIGÉE)
|
||||
Map<String, int> _mapHeaders(List<Data?> headerRow) {
|
||||
}
|
||||
Map<String, int> _mapHeaders(List<Data?> headerRow) {
|
||||
Map<String, int> columnMapping = {};
|
||||
|
||||
for (int i = 0; i < headerRow.length; i++) {
|
||||
@ -1533,64 +1606,66 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
|
||||
String header = headerRow[i]!.value.toString().trim().toUpperCase();
|
||||
|
||||
// Debug : afficher chaque en-tête trouvé
|
||||
print('En-tête trouvé: "$header" à la colonne $i');
|
||||
print('📋 En-tête colonne $i: "$header"');
|
||||
|
||||
// Mapping amélioré pour gérer les variations
|
||||
if ((header.contains('NOM') &&
|
||||
(header.contains('PRODUIT') || header.contains('DU'))) ||
|
||||
header == 'NOM DU PRODUITS' ||
|
||||
header == 'NOM') {
|
||||
// Nom du produit
|
||||
if ((header.contains('NOM') && header.contains('PRODUIT')) || header == 'NOM') {
|
||||
columnMapping['name'] = i;
|
||||
print('→ Mappé vers name');
|
||||
} else if ((header.contains('REFERENCE') &&
|
||||
(header.contains('PRODUIT') || header.contains('PRODUITS'))) ||
|
||||
header == 'REFERENCE PRODUITS' ||
|
||||
header == 'REFERENCE') {
|
||||
print(' ✅ Mappé vers name');
|
||||
}
|
||||
// Référence
|
||||
else if (header.contains('REFERENCE')) {
|
||||
columnMapping['reference'] = i;
|
||||
print('→ Mappé vers reference');
|
||||
} else if ((header.contains('CATEGORIES') &&
|
||||
(header.contains('PRODUIT') || header.contains('PRODUITS'))) ||
|
||||
header == 'CATEGORIES PRODUITS' ||
|
||||
header == 'CATEGORIE' ||
|
||||
header == 'CATEGORY') {
|
||||
print(' ✅ Mappé vers reference');
|
||||
}
|
||||
// Catégorie - VERSION TOLÉRANTE ⭐
|
||||
else if (header.contains('CATEG') && header.contains('PRODUIT')) {
|
||||
columnMapping['category'] = i;
|
||||
print('→ Mappé vers category');
|
||||
} else if (header == 'MARQUE' || header == 'BRAND') {
|
||||
print(' ✅ Mappé vers category');
|
||||
}
|
||||
// Marque
|
||||
else if (header == 'MARQUE' || header == 'BRAND') {
|
||||
columnMapping['marque'] = i;
|
||||
print('→ Mappé vers marque');
|
||||
} else if (header == 'RAM' || header.contains('MEMOIRE RAM')) {
|
||||
print(' ✅ Mappé vers marque');
|
||||
}
|
||||
// RAM
|
||||
else if (header == 'RAM' || header.contains('MEMOIRE RAM')) {
|
||||
columnMapping['ram'] = i;
|
||||
print('→ Mappé vers ram');
|
||||
} else if (header == 'INTERNE' ||
|
||||
header.contains('MEMOIRE INTERNE') ||
|
||||
header.contains('STOCKAGE')) {
|
||||
print(' ✅ Mappé vers ram');
|
||||
}
|
||||
// Mémoire interne
|
||||
else if (header == 'INTERNE' || header.contains('MEMOIRE INTERNE') || header.contains('STOCKAGE')) {
|
||||
columnMapping['memoire_interne'] = i;
|
||||
print('→ Mappé vers memoire_interne');
|
||||
} else if (header == 'IMEI' || header.contains('NUMERO IMEI')) {
|
||||
print(' ✅ Mappé vers memoire_interne');
|
||||
}
|
||||
// IMEI
|
||||
else if (header == 'IMEI' || header.contains('NUMERO IMEI')) {
|
||||
columnMapping['imei'] = i;
|
||||
print('→ Mappé vers imei');
|
||||
} else if (header == 'PRIX' || header == 'PRICE') {
|
||||
print(' ✅ Mappé vers imei');
|
||||
}
|
||||
// Prix
|
||||
else if (header == 'PRIX' || header == 'PRICE') {
|
||||
columnMapping['price'] = i;
|
||||
print('→ Mappé vers price');
|
||||
} else if (header == 'STOCK' || header == 'QUANTITY' || header == 'QTE') {
|
||||
print(' ✅ Mappé vers price');
|
||||
}
|
||||
// Stock
|
||||
else if (header == 'STOCK' || header == 'QUANTITY' || header == 'QTE') {
|
||||
columnMapping['stock'] = i;
|
||||
print('→ Mappé vers stock');
|
||||
} else if (header == 'BOUTIQUE' ||
|
||||
header.contains('POINT DE VENTE') ||
|
||||
header == 'MAGASIN') {
|
||||
print(' ✅ Mappé vers stock');
|
||||
}
|
||||
// Point de vente
|
||||
else if (header.contains('BOUTIQUE') || header.contains('POINT') || header == 'MAGASIN') {
|
||||
columnMapping['point_de_vente'] = i;
|
||||
print('→ Mappé vers point_de_vente');
|
||||
} else {
|
||||
print('→ Non reconnu');
|
||||
print(' ✅ Mappé vers point_de_vente');
|
||||
}
|
||||
else {
|
||||
print(' ⚠️ Non reconnu');
|
||||
}
|
||||
}
|
||||
|
||||
// Debug : afficher le mapping final
|
||||
print('Mapping final: $columnMapping');
|
||||
|
||||
print('\n🎯 MAPPING FINAL: $columnMapping\n');
|
||||
return columnMapping;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAssignmentScannerPage() {
|
||||
return Scaffold(
|
||||
@ -2184,7 +2259,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
'RAM', // ram
|
||||
'INTERNE', // memoire_interne
|
||||
'IMEI', // imei
|
||||
'STOCK'
|
||||
'STOCK',
|
||||
'PRIX', // price
|
||||
'BOUTIQUE', // point_de_vente
|
||||
];
|
||||
@ -3255,15 +3330,12 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes placeholder pour les fonctions manquantes
|
||||
void _showQRCode(Product product) {
|
||||
// État pour contrôler le type d'affichage (true = URL complète, false = référence seulement)
|
||||
RxBool showFullUrl = true.obs;
|
||||
RxBool showFullUrl = false.obs;
|
||||
RxInt nombreAImprimer = (product.stock ?? 1).obs; // 🔹 Valeur modifiable
|
||||
|
||||
Get.dialog(
|
||||
Obx(() {
|
||||
// Données du QR code selon l'état
|
||||
final qrData = showFullUrl.value
|
||||
? 'https://stock.guycom.mg/${product.reference}'
|
||||
: product.reference!;
|
||||
@ -3282,11 +3354,11 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
],
|
||||
),
|
||||
content: Container(
|
||||
width: 300,
|
||||
width: 350,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Bouton pour basculer entre URL et référence
|
||||
// Bouton bascule URL / Référence
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
showFullUrl.value = !showFullUrl.value;
|
||||
@ -3296,19 +3368,18 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
size: 16,
|
||||
),
|
||||
label: Text(
|
||||
showFullUrl.value ? 'URL/Référence' : 'Référence',
|
||||
showFullUrl.value ? 'URL Complète' : 'Référence Seulement',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
showFullUrl.value ? Colors.blue : Colors.green,
|
||||
backgroundColor: showFullUrl.value ? Colors.blue : Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 36),
|
||||
minimumSize: const Size(double.infinity, 40),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Container du QR Code
|
||||
// QR Code affiché
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@ -3321,11 +3392,12 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
version: QrVersions.auto,
|
||||
size: 200,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Affichage des données actuelles
|
||||
// Informations du produit
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@ -3334,18 +3406,60 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
showFullUrl.value
|
||||
? 'URL Complète'
|
||||
: 'Référence Seulement',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.inventory_2, size: 16, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
product.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 🔹 Nouveau champ : nombre à imprimer
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.format_list_numbered, size: 16, color: Colors.deepPurple),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nombre d\'étiquettes à imprimer',
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
),
|
||||
controller: TextEditingController(text: nombreAImprimer.value.toString()),
|
||||
onChanged: (val) {
|
||||
final parsed = int.tryParse(val);
|
||||
if (parsed != null && parsed > 0) {
|
||||
nombreAImprimer.value = parsed;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.label, size: 16, color: Colors.orange.shade600),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
qrData,
|
||||
style:
|
||||
const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
'Format: Étiquette Niimbot B1 (50x15mm)',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -3354,23 +3468,72 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
// Copier
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: qrData));
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Copié',
|
||||
'${showFullUrl.value ? "URL" : "Référence"} copiée dans le presse-papiers',
|
||||
'${showFullUrl.value ? "URL" : "Référence"} copiée',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 2),
|
||||
icon: const Icon(Icons.check_circle, color: Colors.white),
|
||||
);
|
||||
},
|
||||
child: Text('Copier ${showFullUrl.value ? "URL" : "Référence"}'),
|
||||
icon: const Icon(Icons.copy, size: 18),
|
||||
label: const Text('Copier'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _generatePDF(product, qrData),
|
||||
child: const Text('Imprimer en PDF'),
|
||||
|
||||
// Paramètres
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
pdfService.showNiimbotSettingsDialog();
|
||||
},
|
||||
icon: const Icon(Icons.settings, size: 18),
|
||||
label: const Text('Paramètres'),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.blue.shade700),
|
||||
),
|
||||
|
||||
// 🔹 Imprimer selon le nombre choisi
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
|
||||
final int n = nombreAImprimer.value;
|
||||
for (int i = 0; i < n; i++) {
|
||||
await pdfService.printQrNiimbotOptimized(
|
||||
qrData,
|
||||
productName: null,
|
||||
reference: product.reference ?? '',
|
||||
leftPadding: 1.0,
|
||||
topPadding: 0.5,
|
||||
qrSize: 12.0,
|
||||
fontSize: 5.0,
|
||||
labelSize: NiimbotLabelSize.small,
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
'Impression terminée',
|
||||
'$n étiquette${n > 1 ? "s" : ""} imprimée${n > 1 ? "s" : ""}',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.print, size: 18),
|
||||
label: const Text('Imprimer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
|
||||
// Fermer
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Fermer'),
|
||||
@ -3379,8 +3542,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Future<void> _generatePDF(Product product, String qrUrl) async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
@ -3439,9 +3601,9 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
List<Map<String, dynamic>> pointsDeVente = [];
|
||||
bool isLoadingPoints = true;
|
||||
// Initialiser la catégorie sélectionnée de manière sécurisée
|
||||
String selectedCategory = _predefinedCategories.contains(product.category)
|
||||
String selectedCategory = _availableCategories.contains(product.category)
|
||||
? product.category
|
||||
: _predefinedCategories.last; // 'Non catégorisé' par défaut
|
||||
: _availableCategories.last; // 'Non catégorisé' par défaut
|
||||
File? pickedImage;
|
||||
String? qrPreviewData;
|
||||
bool showAddNewPoint = false;
|
||||
@ -3809,7 +3971,7 @@ Future<void> _showDemandeTransfertDialog(Product product) async {
|
||||
// Catégorie avec gestion des valeurs non présentes
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedCategory,
|
||||
items: _predefinedCategories
|
||||
items: _availableCategories
|
||||
.map((category) => DropdownMenuItem(
|
||||
value: category, child: Text(category)))
|
||||
.toList(),
|
||||
|
||||
@ -163,11 +163,11 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
||||
await _showCashPaymentDialog(commande, selectedPayment.amountGiven);
|
||||
}
|
||||
|
||||
await _updateStatut(
|
||||
commande.id!,
|
||||
StatutCommande.confirmee,
|
||||
validateurId: userController.userId,
|
||||
);
|
||||
// await _updateStatut(
|
||||
// commande.id!,
|
||||
// StatutCommande.confirmee,
|
||||
// validateurId: userController.userId,
|
||||
// );
|
||||
|
||||
await _generateReceipt(commande, selectedPayment);
|
||||
}
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
// Importations nécessaires
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:zxing2/qrcode.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.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});
|
||||
@ -54,55 +61,200 @@ class _DemandeSortiePersonnellePageState
|
||||
_loadProducts();
|
||||
_searchController.addListener(_filterProducts);
|
||||
}
|
||||
/// ----------------- SCAN QR CODE -----------------
|
||||
CameraController? _cameraController;
|
||||
bool _isScanning = false;
|
||||
|
||||
Future<void> _scanQrOrBarcode() async {
|
||||
if (defaultTargetPlatform == TargetPlatform.windows) {
|
||||
final cameras = await availableCameras();
|
||||
if (cameras.isEmpty) {
|
||||
_showErrorSnackbar("Aucune caméra détectée");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disposer l'ancien contrôleur
|
||||
await _cameraController?.dispose();
|
||||
|
||||
_cameraController = CameraController(
|
||||
cameras.first,
|
||||
ResolutionPreset.high, // ✅ Meilleure résolution pour QR
|
||||
enableAudio: false,
|
||||
);
|
||||
|
||||
try {
|
||||
await _cameraController!.initialize();
|
||||
} catch (e) {
|
||||
_showErrorSnackbar("Erreur initialisation caméra: $e");
|
||||
return;
|
||||
}
|
||||
|
||||
_isScanning = true;
|
||||
|
||||
Future<void> scanLoop() async {
|
||||
// ✅ Attendre que le dialog soit affiché
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
while (_isScanning && _cameraController != null) {
|
||||
try {
|
||||
final XFile file = await _cameraController!.takePicture();
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
// ✅ Décoder l'image
|
||||
final imageDecoded = img.decodeImage(bytes);
|
||||
if (imageDecoded == null) {
|
||||
print("❌ Image non décodée");
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
continue;
|
||||
}
|
||||
|
||||
print("✅ Image décodée: ${imageDecoded.width}x${imageDecoded.height}");
|
||||
|
||||
// ✅ CORRECTION CRITIQUE: Convertir en format RGB correct
|
||||
final rgbBytes = <int>[];
|
||||
for (int y = 0; y < imageDecoded.height; y++) {
|
||||
for (int x = 0; x < imageDecoded.width; x++) {
|
||||
final pixel = imageDecoded.getPixel(x, y);
|
||||
rgbBytes.add(pixel.r.toInt());
|
||||
rgbBytes.add(pixel.g.toInt());
|
||||
rgbBytes.add(pixel.b.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
// Créer Int32List pour ZXing
|
||||
final int32Data = Int32List(imageDecoded.width * imageDecoded.height);
|
||||
for (int i = 0; i < imageDecoded.height; i++) {
|
||||
for (int j = 0; j < imageDecoded.width; j++) {
|
||||
final idx = i * imageDecoded.width + j;
|
||||
final rgbIdx = idx * 3;
|
||||
final r = rgbBytes[rgbIdx];
|
||||
final g = rgbBytes[rgbIdx + 1];
|
||||
final b = rgbBytes[rgbIdx + 2];
|
||||
// Format ARGB
|
||||
int32Data[idx] = (0xFF << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
|
||||
final luminanceSource = RGBLuminanceSource(
|
||||
imageDecoded.width,
|
||||
imageDecoded.height,
|
||||
int32Data,
|
||||
);
|
||||
|
||||
final bitmap = BinaryBitmap(HybridBinarizer(luminanceSource));
|
||||
final reader = QRCodeReader();
|
||||
|
||||
try {
|
||||
final result = reader.decode(bitmap);
|
||||
if (result.text.isNotEmpty) {
|
||||
print("✅ QR Code détecté: ${result.text}");
|
||||
_isScanning = false;
|
||||
|
||||
if (mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_searchController.text = result.text;
|
||||
});
|
||||
_filterProducts();
|
||||
_showSuccessSnackbar("QR Code détecté : ${result.text}");
|
||||
break;
|
||||
}
|
||||
} on NotFoundException catch (_) {
|
||||
// Pas de QR trouvé dans cette frame
|
||||
print("⚠️ Pas de QR trouvé");
|
||||
} catch (e) {
|
||||
print("❌ Erreur décodage QR: $e");
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
print("❌ Erreur capture: $e");
|
||||
}
|
||||
|
||||
// ✅ Délai plus long pour éviter la surcharge
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Lancer la boucle AVANT d'afficher le dialog
|
||||
final scanFuture = scanLoop();
|
||||
|
||||
void _scanQrOrBarcode() async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
content: Container(
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
_isScanning = false;
|
||||
return true;
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('Scanner le QR Code'),
|
||||
content: SizedBox(
|
||||
width: 640,
|
||||
height: 480,
|
||||
child: _cameraController != null &&
|
||||
_cameraController!.value.isInitialized
|
||||
? CameraPreview(_cameraController!)
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_isScanning = false;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
_isScanning = false;
|
||||
await scanFuture; // Attendre la fin de la boucle
|
||||
await _cameraController?.dispose();
|
||||
_cameraController = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 📱 Mobile et macOS → mobile_scanner
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Scanner le QR Code'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
height: 400,
|
||||
child: MobileScanner(
|
||||
onDetect: (BarcodeCapture barcodeCap) {
|
||||
print("BarcodeCapture: $barcodeCap");
|
||||
// Now accessing the barcodes attribute
|
||||
final List<Barcode> barcodes = barcodeCap.barcodes;
|
||||
|
||||
onDetect: (capture) {
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
if (barcodes.isNotEmpty) {
|
||||
// Get the first detected barcode value
|
||||
String? scanResult = barcodes.first.rawValue;
|
||||
|
||||
print("Scanned Result: $scanResult");
|
||||
|
||||
final scanResult = barcodes.first.rawValue;
|
||||
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
|
||||
setState(() => _searchController.text = scanResult);
|
||||
_filterProducts();
|
||||
} else {
|
||||
print("Scan result was empty or null.");
|
||||
Navigator.of(context).pop();
|
||||
_showSuccessSnackbar("QR Code détecté : $scanResult");
|
||||
}
|
||||
} else {
|
||||
print("No barcodes detected.");
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// ----------------- FILTRAGE PRODUITS -----------------
|
||||
void _filterProducts() {
|
||||
final query = _searchController.text.toLowerCase();
|
||||
setState(() {
|
||||
@ -825,7 +977,3 @@ class _DemandeSortiePersonnellePageState
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
extension on BarcodeCapture {
|
||||
get rawValue => null;
|
||||
}
|
||||
|
||||
@ -5,13 +5,18 @@ import 'dart:async';
|
||||
|
||||
class DatabaseConfig {
|
||||
// Local MySQL settings
|
||||
static const String localHost = '192.168.88.73';
|
||||
static const String localHost = '192.168.88.3';
|
||||
static const String localUsername = 'guycom';
|
||||
static const String? localPassword = '3iV59wjRdbuXAPR';
|
||||
static const String localDatabase = 'guycom';
|
||||
// static const String localHost = 'localhost';
|
||||
// static const String localUsername = 'root';
|
||||
// static const String? localPassword = null;
|
||||
// static const String localDatabase = 'guycom';
|
||||
|
||||
// Production (public) MySQL settings
|
||||
static const String prodHost = '102.17.52.31';
|
||||
static const String prodHost = '102.16.56.177';
|
||||
// static const String prodHost = '185.70.105.157';
|
||||
static const String prodUsername = 'guycom';
|
||||
static const String prodPassword = '3iV59wjRdbuXAPR';
|
||||
static const String prodDatabase = 'guycom';
|
||||
@ -23,7 +28,7 @@ class DatabaseConfig {
|
||||
static const int maxConnections = 10;
|
||||
static const int minConnections = 2;
|
||||
|
||||
static bool get isDevelopment => false;
|
||||
static bool get isDevelopment => true;
|
||||
|
||||
/// Build config map for connection
|
||||
static Map<String, dynamic> _buildConfig({
|
||||
@ -80,7 +85,7 @@ class DatabaseConfig {
|
||||
config['database']?.toString().isNotEmpty == true &&
|
||||
config['user'] != null;
|
||||
} catch (e) {
|
||||
// print("Erreur de validation de la configuration: $e");
|
||||
print("Erreur de validation de la configuration: $e");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,23 +6,23 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <charset_converter/charset_converter_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <open_file_linux/open_file_linux_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) charset_converter_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "CharsetConverterPlugin");
|
||||
charset_converter_plugin_register_with_registrar(charset_converter_registrar);
|
||||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
||||
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||
printing_plugin_register_with_registrar(printing_registrar);
|
||||
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
|
||||
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
charset_converter
|
||||
file_selector_linux
|
||||
open_file_linux
|
||||
printing
|
||||
screen_retriever
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
|
||||
@ -10,6 +10,7 @@ import file_selector_macos
|
||||
import mobile_scanner
|
||||
import open_file_mac
|
||||
import path_provider_foundation
|
||||
import printing
|
||||
import screen_retriever
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
||||
132
pubspec.lock
132
pubspec.lock
@ -49,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.9"
|
||||
bidi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: bidi
|
||||
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -97,6 +105,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
camera_windows:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: camera_windows
|
||||
sha256: c4339d71bc4256993f5c8ae2f3355463d830a5cb52851409ab1c627401c69811
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6+2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -105,22 +121,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
charset_converter:
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charset_converter
|
||||
sha256: a601f27b78ca86c3d88899d53059786d9c3f3c485b64974e9105c06c2569aef5
|
||||
name: charcode
|
||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "1.4.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c"
|
||||
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.4.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -161,14 +177,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -193,22 +201,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
esc_pos_printer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: esc_pos_printer
|
||||
sha256: "312b05f909f3f7dd1e6a3332cf384dcee2c3a635138823654cd9c0133d8b5c45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
esc_pos_utils:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: esc_pos_utils
|
||||
sha256: "8ec0013d7a7f1e790ced6b09b95ce3bf2c6f9468a3e2bc49ece000761d86c6f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
excel:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -281,6 +273,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
fl_chart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -376,14 +376,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.8.0"
|
||||
gbk_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gbk_codec
|
||||
sha256: "3af5311fc9393115e3650ae6023862adf998051a804a08fb804f042724999f61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -396,10 +388,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: get_it
|
||||
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
|
||||
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.7.0"
|
||||
version: "8.2.0"
|
||||
google_fonts:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -416,22 +408,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.2"
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.5+1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -460,10 +436,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6"
|
||||
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "4.3.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -660,18 +636,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mobile_scanner
|
||||
sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760
|
||||
sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.3"
|
||||
version: "7.0.1"
|
||||
msix:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: msix
|
||||
sha256: e3de4d9f52543ad6e4b0f534991e1303cbd379d24be28dd241ac60bd9439a201
|
||||
sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.0"
|
||||
version: "3.16.12"
|
||||
mysql1:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -844,10 +820,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pdf
|
||||
sha256: "10659b915e65832b106f6d1d213e09b789cc1f24bf282ee911e49db35b96be4d"
|
||||
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.4"
|
||||
version: "3.11.3"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf_widget_wrapper
|
||||
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -888,6 +872,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
printing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: printing
|
||||
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.14.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1254,7 +1246,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: win32
|
||||
sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f
|
||||
@ -1293,6 +1285,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
zxing2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: zxing2
|
||||
sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
sdks:
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
flutter: ">=3.29.0"
|
||||
|
||||
13
pubspec.yaml
13
pubspec.yaml
@ -44,10 +44,12 @@ dependencies:
|
||||
sqflite_common_ffi: ^2.2.5
|
||||
quantity_input: ^1.0.2
|
||||
grouped_list: ^5.1.2
|
||||
esc_pos_printer: ^4.0.1
|
||||
esc_pos_utils: ^1.1.0
|
||||
# esc_pos_printer: ^4.0.1
|
||||
win32: ^5.12.0
|
||||
# esc_pos_utils: ^1.1.0
|
||||
printing: ^5.10.0
|
||||
flutter_login: ^4.1.1
|
||||
image: ^3.0.2
|
||||
image: ^4.3.0
|
||||
logging: ^1.2.0
|
||||
msix: ^3.7.0
|
||||
flutter_charts: ^0.5.1
|
||||
@ -63,13 +65,14 @@ dependencies:
|
||||
path_provider: ^2.0.15
|
||||
shared_preferences: ^2.2.2
|
||||
excel: ^2.0.1
|
||||
mobile_scanner: ^5.0.0
|
||||
mobile_scanner: ^7.0.1
|
||||
fl_chart: ^0.65.0
|
||||
numbers_to_letters: ^1.0.0
|
||||
qr_code_scanner_plus: ^2.0.10+1
|
||||
window_manager: ^0.3.7
|
||||
camera: ^0.10.5+9
|
||||
|
||||
zxing2: ^0.2.1
|
||||
camera_windows: ^0.2.6+2
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@ -6,17 +6,20 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <charset_converter/charset_converter_plugin.h>
|
||||
#include <camera_windows/camera_windows.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
CharsetConverterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("CharsetConverterPlugin"));
|
||||
CameraWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("CameraWindows"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
||||
@ -3,8 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
charset_converter
|
||||
camera_windows
|
||||
file_selector_windows
|
||||
printing
|
||||
screen_retriever
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
|
||||
Loading…
Reference in New Issue
Block a user