From 13554ee49c25a2815c8e589976b78584f0b3f0dc Mon Sep 17 00:00:00 2001 From: andrymodeste Date: Sun, 9 Nov 2025 12:33:37 +0100 Subject: [PATCH] push 09112025 --- lib/Components/QrScan.dart | 5 +- .../CommandeActions.dart | 31 +- lib/Components/windows_qr_scanner.dart | 831 +++++++++++++ lib/Services/qrService.dart | 1090 +++++++++++++++++ lib/Services/stock_managementDatabase.dart | 51 +- lib/Views/HandleProduct.dart | 770 +++++++----- lib/Views/commandManagement.dart | 10 +- .../demande_sortie_personnelle_page.dart | 234 +++- lib/config/DatabaseConfig.dart | 13 +- linux/flutter/generated_plugin_registrant.cc | 8 +- linux/flutter/generated_plugins.cmake | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 132 +- pubspec.yaml | 13 +- .../flutter/generated_plugin_registrant.cc | 9 +- windows/flutter/generated_plugins.cmake | 3 +- 16 files changed, 2747 insertions(+), 457 deletions(-) create mode 100644 lib/Components/windows_qr_scanner.dart create mode 100644 lib/Services/qrService.dart diff --git a/lib/Components/QrScan.dart b/lib/Components/QrScan.dart index bcaf4b1..0582ee7 100644 --- a/lib/Components/QrScan.dart +++ b/lib/Components/QrScan.dart @@ -147,7 +147,7 @@ class _ScanQRPageState extends State { } } }, - errorBuilder: (context, error, child) { + errorBuilder: (context, error) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -155,7 +155,8 @@ class _ScanQRPageState extends State { 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(), diff --git a/lib/Components/commandManagementComponents/CommandeActions.dart b/lib/Components/commandManagementComponents/CommandeActions.dart index 47566b2..cbcc88a 100644 --- a/lib/Components/commandManagementComponents/CommandeActions.dart +++ b/lib/Components/commandManagementComponents/CommandeActions.dart @@ -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 _buildActionButtons(BuildContext context) { List 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,7 +187,8 @@ class CommandeActions extends StatelessWidget { }, ); } - @override + + @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), @@ -210,4 +217,4 @@ class CommandeActions extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/Components/windows_qr_scanner.dart b/lib/Components/windows_qr_scanner.dart new file mode 100644 index 0000000..65d5688 --- /dev/null +++ b/lib/Components/windows_qr_scanner.dart @@ -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 with TickerProviderStateMixin { + final AppDatabase _database = AppDatabase.instance; + final UserController _userController = Get.find(); + + final _formKey = GlobalKey(); + final _quantiteController = TextEditingController(text: '1'); + final _motifController = TextEditingController(); + final _notesController = TextEditingController(); + final _searchController = TextEditingController(); + + Product? _selectedProduct; + List _products = []; + List _filteredProducts = []; + bool _isLoading = false; + bool _isSearching = false; + + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + _slideAnimation = + Tween(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 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 _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 _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 _showConfirmationDialog() async { + return await showDialog( + 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; +} diff --git a/lib/Services/qrService.dart b/lib/Services/qrService.dart new file mode 100644 index 0000000..57c4606 --- /dev/null +++ b/lib/Services/qrService.dart @@ -0,0 +1,1090 @@ +import 'dart:typed_data'; +import 'dart:io' show Platform, File; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:printing/printing.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +// Classe pour gérer les formats d'étiquettes Niimbot +class NiimbotLabelSize { + final double width; + final double height; + final String name; + + const NiimbotLabelSize(this.width, this.height, this.name); + + // Formats courants Niimbot B1 + static const small = NiimbotLabelSize(50, 15, "Petit (50x15mm)"); + static const medium = NiimbotLabelSize(40, 30, "Moyen (40x30mm)"); + static const large = NiimbotLabelSize(50, 30, "Grand (50x30mm)"); + + PdfPageFormat get pageFormat => PdfPageFormat( + width * PdfPageFormat.mm, + height * PdfPageFormat.mm, + marginAll: 0, + ); +} + +class PdfPrintService { + pw.Font? robotoRegular; + pw.Font? robotoBold; + + PdfPrintService(); + + /// Charge les polices Unicode + Future loadFonts() async { + if (robotoRegular != null && robotoBold != null) return; + + try { + // Charger les polices depuis les assets ou utiliser les polices système + final regularData = await rootBundle.load('assets/fonts/Roboto-Regular.ttf'); + final boldData = await rootBundle.load('assets/fonts/Roboto-Bold.ttf'); + + robotoRegular = pw.Font.ttf(regularData); + robotoBold = pw.Font.ttf(boldData); + } catch (e) { + print('Erreur chargement polices personnalisées: $e'); + // Fallback vers les polices par défaut + try { + robotoRegular = await PdfGoogleFonts.robotoRegular(); + robotoBold = await PdfGoogleFonts.robotoBold(); + } catch (e2) { + print('Erreur chargement polices Google: $e2'); + // Utiliser les polices système par défaut + robotoRegular = null; + robotoBold = null; + } + } + } + + /// Génère une image QR optimisée + Future _generateQrImage(String data, {double size = 512}) async { + final qrValidation = QrValidator.validate( + data: data, + version: QrVersions.auto, + errorCorrectionLevel: QrErrorCorrectLevel.M, + ); + + if (qrValidation.status != QrValidationStatus.valid) { + throw Exception('QR code invalide: ${qrValidation.error}'); + } + + final qrCode = qrValidation.qrCode!; + final painter = QrPainter.withQr( + qr: qrCode, + color: Colors.black, + emptyColor: Colors.white, + gapless: true, + embeddedImageStyle: null, + eyeStyle: const QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Colors.black, + ), + dataModuleStyle: const QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Colors.black, + ), + ); + + final picData = await painter.toImageData( + size, + format: ImageByteFormat.png + ); + return picData!.buffer.asUint8List(); + } + + /// Génère le PDF optimisé pour l'impression d'étiquettes + Future generateQrPdf(String data, { + String? productName, + String? reference, + bool isLabelFormat = true, + }) async { + await loadFonts(); + + final qrImageData = await _generateQrImage(data, size: 300); + final qrImage = pw.MemoryImage(qrImageData); + + final pdf = pw.Document(); + + if (isLabelFormat) { + // Format étiquette (petit format) + pdf.addPage( + pw.Page( + pageFormat: const PdfPageFormat( + 2.5 * PdfPageFormat.inch, + 1.5 * PdfPageFormat.inch, + marginAll: 0, + ), + build: (context) => _buildLabelContent(qrImage, data, productName, reference), + ), + ); + } else { + // Format A4 standard + pdf.addPage( + pw.Page( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(20), + build: (context) => _buildA4Content(qrImage, data, productName, reference), + ), + ); + } + + return pdf.save(); + } + + /// Contenu pour format étiquette + pw.Widget _buildLabelContent( + pw.MemoryImage qrImage, + String data, + String? productName, + String? reference, + ) { + return pw.Container( + width: double.infinity, + height: double.infinity, + padding: const pw.EdgeInsets.all(4), + child: pw.Center( + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // QR Code principal + pw.Container( + width: 70, + height: 70, + child: pw.Center( + child: pw.Image( + qrImage, + width: 70, + height: 70, + fit: pw.BoxFit.contain, + ), + ), + ), + + pw.SizedBox(height: 4), + + // Informations textuelles centrées + if (reference != null) ...[ + pw.Container( + width: double.infinity, + child: pw.Text( + reference, + style: _getTextStyle(fontSize: 8, bold: true), + textAlign: pw.TextAlign.center, + maxLines: 1, + ), + ), + pw.SizedBox(height: 2), + ], + if (productName != null && productName.isNotEmpty) ...[ + pw.Container( + width: double.infinity, + child: pw.Text( + productName.length > 20 ? '${productName.substring(0, 17)}...' : productName, + style: _getTextStyle(fontSize: 6), + textAlign: pw.TextAlign.center, + maxLines: 1, + ), + ), + ], + ], + ), + ), + ); + } + + /// Contenu pour format A4 + pw.Widget _buildA4Content( + pw.MemoryImage qrImage, + String data, + String? productName, + String? reference, + ) { + return pw.Center( + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + children: [ + if (productName != null) ...[ + pw.Text( + productName, + style: _getTextStyle(fontSize: 18, bold: true), + textAlign: pw.TextAlign.center, + ), + pw.SizedBox(height: 20), + ], + + pw.Image( + qrImage, + width: 200, + height: 200, + fit: pw.BoxFit.contain, + ), + + pw.SizedBox(height: 20), + + if (reference != null) ...[ + pw.Text( + 'Référence: $reference', + style: _getTextStyle(fontSize: 14, bold: true), + ), + pw.SizedBox(height: 10), + ], + + pw.Text( + 'URL: $data', + style: _getTextStyle(fontSize: 12), + textAlign: pw.TextAlign.center, + ), + ], + ), + ); + } + + /// Obtient le style de texte approprié + pw.TextStyle _getTextStyle({double fontSize = 12, bool bold = false}) { + if (bold && robotoBold != null) { + return pw.TextStyle(font: robotoBold!, fontSize: fontSize); + } else if (!bold && robotoRegular != null) { + return pw.TextStyle(font: robotoRegular!, fontSize: fontSize); + } else { + return pw.TextStyle(fontSize: fontSize, fontWeight: bold ? pw.FontWeight.bold : pw.FontWeight.normal); + } + } + + /// Imprime le QR code avec options + Future printQr( + String data, { + String? productName, + String? reference, + bool isLabelFormat = true, + bool showPreview = true, + }) async { + try { + final pdfBytes = await generateQrPdf( + data, + productName: productName, + reference: reference, + isLabelFormat: isLabelFormat, + ); + + if (Platform.isWindows || Platform.isAndroid || Platform.isIOS || Platform.isMacOS) { + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'QR_Code_${reference ?? 'etiquette'}', + format: isLabelFormat ? + const PdfPageFormat( + 2.5 * PdfPageFormat.inch, + 1.5 * PdfPageFormat.inch, + marginAll: 0, + ) : PdfPageFormat.a4, + usePrinterSettings: false, + ); + + Get.snackbar( + 'Succès', + 'QR Code envoyé à l\'imprimante', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } else { + Get.snackbar( + 'Info', + 'Impression non supportée sur cette plateforme', + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + } + } catch (e) { + print('Erreur impression: $e'); + Get.snackbar( + 'Erreur', + 'Impossible d\'imprimer: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + // ===== NOUVELLES MÉTHODES OPTIMISÉES POUR NIIMBOT B1 ===== +Future printQrNiimbotOptimized( + String data, { + String? productName, + String? reference, + double leftPadding = 1.0, + double topPadding = 0.5, + double qrSize = 11.5, // légèrement plus petit pour laisser la place au texte + double fontSize = 5.0, + NiimbotLabelSize labelSize = NiimbotLabelSize.small, +}) async { + try { + await loadFonts(); + + // Générer le QR code en image mémoire + final qrImageData = await _generateQrImage(data, size: 110); + final qrImage = pw.MemoryImage(qrImageData); + + final pdf = pw.Document(); + + pdf.addPage( + pw.Page( + pageFormat: labelSize.pageFormat, + margin: pw.EdgeInsets.zero, + build: (context) { + return pw.Container( + width: double.infinity, + height: double.infinity, + padding: pw.EdgeInsets.only( + left: leftPadding * PdfPageFormat.mm, + top: topPadding * PdfPageFormat.mm, + right: 1 * PdfPageFormat.mm, + bottom: 0.3 * PdfPageFormat.mm, // bordure minimale + ), + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.start, + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // --- QR code --- + pw.Container( + width: qrSize * PdfPageFormat.mm, + height: qrSize * PdfPageFormat.mm, + child: pw.Image(qrImage, fit: pw.BoxFit.contain), + ), + + // --- Référence directement sous le QR --- + if (reference != null && reference.isNotEmpty) + pw.Padding( + padding: pw.EdgeInsets.only(top: 0.3 * PdfPageFormat.mm), + child: pw.Text( + reference, + style: pw.TextStyle( + fontSize: fontSize, + fontWeight: pw.FontWeight.bold, + ), + textAlign: pw.TextAlign.center, + maxLines: 1, + overflow: pw.TextOverflow.clip, + ), + ), + ], + ), + ); + }, + ), + ); + + final pdfBytes = await pdf.save(); + + // Impression via Printing + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'QR_${reference ?? DateTime.now().millisecondsSinceEpoch}', + format: labelSize.pageFormat, + ); + + Get.snackbar( + 'Succès', + 'QR imprimé (${labelSize.name}) avec référence visible', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } catch (e) { + print('Erreur impression Niimbot: $e'); + Get.snackbar( + 'Erreur', + 'Impression échouée: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } +} + + + /// Méthode de calibrage pour trouver le décalage optimal + Future printNiimbotCalibration({ + NiimbotLabelSize labelSize = NiimbotLabelSize.small, + }) async { + try { + final pdf = pw.Document(); + + pdf.addPage( + pw.Page( + pageFormat: labelSize.pageFormat, + build: (context) => pw.Container( + width: double.infinity, + height: double.infinity, + decoration: pw.BoxDecoration( + border: pw.Border.all(width: 0.5, color: PdfColors.black), + ), + child: pw.Stack( + children: [ + // Grille de référence + pw.Positioned( + left: 0, + top: 0, + child: pw.Container( + width: labelSize.width * PdfPageFormat.mm, + height: labelSize.height * PdfPageFormat.mm, + child: pw.CustomPaint( + painter: (PdfGraphics canvas, PdfPoint size) { + // Lignes verticales tous les 5mm + for (int i = 5; i < labelSize.width; i += 5) { + canvas.drawLine( + i * PdfPageFormat.mm, + 0, + i * PdfPageFormat.mm, + labelSize.height * PdfPageFormat.mm, + ); + } + // Lignes horizontales tous les 5mm + for (int i = 5; i < labelSize.height; i += 5) { + canvas.drawLine( + 0, + i * PdfPageFormat.mm, + labelSize.width * PdfPageFormat.mm, + i * PdfPageFormat.mm, + ); + } + }, + ), + ), + ), + + // Marqueur centre + pw.Positioned( + left: (labelSize.width / 2 - 3) * PdfPageFormat.mm, + top: (labelSize.height / 2 - 3) * PdfPageFormat.mm, + child: pw.Container( + width: 6 * PdfPageFormat.mm, + height: 6 * PdfPageFormat.mm, + color: PdfColors.black, + child: pw.Center( + child: pw.Text( + 'C', + style: pw.TextStyle( + color: PdfColors.white, + fontSize: 5, + fontWeight: pw.FontWeight.bold, + ), + ), + ), + ), + ), + + // Coin haut-gauche + pw.Positioned( + left: 1 * PdfPageFormat.mm, + top: 1 * PdfPageFormat.mm, + child: pw.Container( + width: 4 * PdfPageFormat.mm, + height: 4 * PdfPageFormat.mm, + color: PdfColors.black, + child: pw.Center( + child: pw.Text( + '1', + style: pw.TextStyle( + color: PdfColors.white, + fontSize: 3, + fontWeight: pw.FontWeight.bold, + ), + ), + ), + ), + ), + + // Titre + pw.Positioned( + left: 1 * PdfPageFormat.mm, + bottom: 1 * PdfPageFormat.mm, + child: pw.Text( + 'CALIBRAGE ${labelSize.name}', + style: _getTextStyle(fontSize: 3, bold: true), + ), + ), + ], + ), + ), + ), + ); + + final pdfBytes = await pdf.save(); + + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'Calibrage_Niimbot_${DateTime.now().millisecondsSinceEpoch}', + format: labelSize.pageFormat, + usePrinterSettings: false, + ); + + Get.snackbar( + 'Calibrage', + 'Page de calibrage envoyée (${labelSize.name})', + backgroundColor: Colors.blue, + colorText: Colors.white, + duration: const Duration(seconds: 4), + ); + } catch (e) { + Get.snackbar( + 'Erreur calibrage', + 'Impossible d\'imprimer: $e', + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + /// Méthode avec paramètres ajustables pour corriger le décalage + Future printQrNiimbotWithOffset( + String data, { + String? productName, + String? reference, + double offsetX = 0.0, + double offsetY = 0.0, + double scale = 1.0, + NiimbotLabelSize labelSize = NiimbotLabelSize.small, + }) async { + try { + await loadFonts(); + + final qrImageData = await _generateQrImage(data, size: 120); + final qrImage = pw.MemoryImage(qrImageData); + + final pdf = pw.Document(); + + pdf.addPage( + pw.Page( + pageFormat: labelSize.pageFormat, + build: (context) => pw.Container( + width: double.infinity, + height: double.infinity, + child: pw.Stack( + children: [ + pw.Positioned( + left: (labelSize.width / 2 + offsetX) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2, + top: (labelSize.height / 2 + offsetY) * PdfPageFormat.mm - (15 * scale) * PdfPageFormat.mm / 2, + child: pw.Transform( + transform: Matrix4.identity()..scale(scale), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.Container( + width: 12 * PdfPageFormat.mm, + height: 12 * PdfPageFormat.mm, + child: pw.Image( + qrImage, + fit: pw.BoxFit.contain, + ), + ), + + if (reference != null && reference.isNotEmpty) ...[ + pw.SizedBox(height: 0.5 * PdfPageFormat.mm), + pw.Container( + width: 20 * PdfPageFormat.mm, + child: pw.Text( + reference.length > 12 ? '${reference.substring(0, 12)}...' : reference, + style: _getTextStyle(fontSize: 4, bold: true), + textAlign: pw.TextAlign.center, + maxLines: 1, + ), + ), + ], + ], + ), + ), + ), + ], + ), + ), + ), + ); + + final pdfBytes = await pdf.save(); + + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'QR_Offset_X${offsetX}_Y${offsetY}_S${scale}', + format: labelSize.pageFormat, + usePrinterSettings: false, + ); + + Get.snackbar( + 'Test décalage', + 'QR imprimé avec décalage X:${offsetX}mm Y:${offsetY}mm Scale:${scale}', + backgroundColor: Colors.orange, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } catch (e) { + Get.snackbar( + 'Erreur test', + 'Impossible d\'imprimer: $e', + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + /// Widget de dialogue pour sélectionner le format d'étiquette + void showNiimbotFormatDialog(Function(NiimbotLabelSize) onFormatSelected) { + Get.dialog( + AlertDialog( + title: Row( + children: [ + Icon(Icons.straighten, color: Colors.orange[700]), + const SizedBox(width: 8), + const Text('Taille d\'étiquette'), + ], + ), + content: Container( + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Sélectionnez la taille de vos étiquettes Niimbot B1 :', + style: TextStyle(fontSize: 14, color: Colors.grey.shade700), + ), + const SizedBox(height: 16), + + // Option Petit + _buildFormatOption( + size: NiimbotLabelSize.small, + icon: Icons.label_outline, + color: Colors.green, + onTap: () { + Get.back(); + onFormatSelected(NiimbotLabelSize.small); + }, + ), + const SizedBox(height: 8), + + // Option Moyen + _buildFormatOption( + size: NiimbotLabelSize.medium, + icon: Icons.label, + color: Colors.orange, + onTap: () { + Get.back(); + onFormatSelected(NiimbotLabelSize.medium); + }, + ), + const SizedBox(height: 8), + + // Option Grand + _buildFormatOption( + size: NiimbotLabelSize.large, + icon: Icons.label_important, + color: Colors.red, + onTap: () { + Get.back(); + onFormatSelected(NiimbotLabelSize.large); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ], + ), + ); + } + + Widget _buildFormatOption({ + required NiimbotLabelSize size, + required IconData icon, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: color.withOpacity(0.5)), + borderRadius: BorderRadius.circular(8), + color: color.withOpacity(0.1), + ), + child: Row( + children: [ + Icon(icon, color: color, size: 32), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + size.name, + style: TextStyle( + fontWeight: FontWeight.bold, + color: color.withOpacity(0.7), + fontSize: 14, + ), + ), + Text( + '${size.width.toInt()}mm x ${size.height.toInt()}mm', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Icon(Icons.arrow_forward_ios, size: 16, color: color), + ], + ), + ), + ); + } + + /// Widget de dialogue pour ajuster les paramètres d'impression + void showNiimbotSettingsDialog() { + final offsetXController = TextEditingController(text: '0.0'); + final offsetYController = TextEditingController(text: '0.0'); + final scaleController = TextEditingController(text: '1.0'); + final qrSizeController = TextEditingController(text: '12.0'); + + // Variable pour stocker la taille sélectionnée + Rx selectedSize = NiimbotLabelSize.small.obs; + + Get.dialog( + Obx(() => AlertDialog( + title: Row( + children: [ + Icon(Icons.settings, color: Colors.blue.shade700), + const SizedBox(width: 8), + const Text('Paramètres Niimbot B1'), + ], + ), + content: Container( + width: 400, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Sélection de format + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Format d\'étiquette :', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange.shade700), + ), + const SizedBox(height: 8), + DropdownButton( + value: selectedSize.value, + isExpanded: true, + items: [ + DropdownMenuItem(value: NiimbotLabelSize.small, child: Text(NiimbotLabelSize.small.name)), + DropdownMenuItem(value: NiimbotLabelSize.medium, child: Text(NiimbotLabelSize.medium.name)), + DropdownMenuItem(value: NiimbotLabelSize.large, child: Text(NiimbotLabelSize.large.name)), + ], + onChanged: (value) { + if (value != null) { + selectedSize.value = value; + } + }, + ), + ], + ), + ), + const SizedBox(height: 16), + + // Instructions + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Conseils pour corriger le décalage :', + style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue.shade700), + ), + const SizedBox(height: 4), + Text('• Décalage X : + = droite, - = gauche', style: TextStyle(fontSize: 12)), + Text('• Décalage Y : + = bas, - = haut', style: TextStyle(fontSize: 12)), + Text('• Échelle : 0.9 = plus petit, 1.1 = plus grand', style: TextStyle(fontSize: 12)), + ], + ), + ), + const SizedBox(height: 16), + + // Champs de saisie + Row( + children: [ + Expanded( + child: TextField( + controller: offsetXController, + decoration: const InputDecoration( + labelText: 'Décalage X (mm)', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: offsetYController, + decoration: const InputDecoration( + labelText: 'Décalage Y (mm)', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + ), + ), + ], + ), + const SizedBox(height: 12), + + Row( + children: [ + Expanded( + child: TextField( + controller: scaleController, + decoration: const InputDecoration( + labelText: 'Échelle', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: qrSizeController, + decoration: const InputDecoration( + labelText: 'Taille QR (mm)', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.numberWithOptions(decimal: true), + ), + ), + ], + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + TextButton( + onPressed: () async { + await printNiimbotCalibration(labelSize: selectedSize.value); + }, + child: const Text('Calibrage'), + style: TextButton.styleFrom(foregroundColor: Colors.blue), + ), + ElevatedButton( + onPressed: () async { + final offsetX = double.tryParse(offsetXController.text) ?? 0.0; + final offsetY = double.tryParse(offsetYController.text) ?? 0.0; + final scale = double.tryParse(scaleController.text) ?? 1.0; + + Get.back(); + await printQrNiimbotWithOffset( + 'https://stock.guycom.mg/TEST_${DateTime.now().millisecondsSinceEpoch}', + reference: 'TEST-${DateTime.now().millisecond}', + offsetX: offsetX, + offsetY: offsetY, + scale: scale, + labelSize: selectedSize.value, + ); + }, + child: const Text('Test'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + ), + ], + )), + ); + } + + // ===== MÉTHODES ORIGINALES (CONSERVÉES POUR COMPATIBILITÉ) ===== + + /// Méthode spécialisée pour imprimantes Niimbot (ancienne version) + Future printQrNiimbot( + String data, { + String? productName, + String? reference, + }) async { + try { + await loadFonts(); + + final qrImageData = await _generateQrImage(data, size: 200); + final qrImage = pw.MemoryImage(qrImageData); + + final pdf = pw.Document(); + + // Format spécifique Niimbot : 50mm x 30mm + pdf.addPage( + pw.Page( + pageFormat: const PdfPageFormat( + 50 * PdfPageFormat.mm, + 30 * PdfPageFormat.mm, + marginAll: 0, + ), + build: (context) => pw.Container( + width: double.infinity, + height: double.infinity, + child: pw.Center( + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // QR Code centré + pw.Container( + width: 20 * PdfPageFormat.mm, + height: 20 * PdfPageFormat.mm, + child: pw.Image( + qrImage, + fit: pw.BoxFit.contain, + ), + ), + + if (reference != null) ...[ + pw.SizedBox(height: 2 * PdfPageFormat.mm), + pw.Text( + reference, + style: _getTextStyle(fontSize: 8, bold: true), + textAlign: pw.TextAlign.center, + ), + ], + ], + ), + ), + ), + ), + ); + + final pdfBytes = await pdf.save(); + + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'QR_Niimbot_${reference ?? 'etiquette'}', + format: const PdfPageFormat( + 50 * PdfPageFormat.mm, + 30 * PdfPageFormat.mm, + marginAll: 0, + ), + usePrinterSettings: false, + ); + + Get.snackbar( + 'Succès', + 'QR Code Niimbot envoyé à l\'imprimante', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } catch (e) { + print('Erreur impression Niimbot: $e'); + Get.snackbar( + 'Erreur', + 'Impossible d\'imprimer sur Niimbot: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } + } + + /// Prévisualise le PDF avant impression + Future previewQr( + String data, { + String? productName, + String? reference, + bool isLabelFormat = true, + }) async { + try { + final pdfBytes = await generateQrPdf( + data, + productName: productName, + reference: reference, + isLabelFormat: isLabelFormat, + ); + + await Printing.layoutPdf( + onLayout: (format) async => pdfBytes, + name: 'Aperçu_QR_Code', + ); + } catch (e) { + print('Erreur aperçu: $e'); + Get.snackbar( + 'Erreur', + 'Impossible d\'afficher l\'aperçu: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + /// Sauvegarde le PDF + Future saveQrPdf( + String data, { + String? productName, + String? reference, + bool isLabelFormat = true, + String? fileName, + }) async { + try { + final pdfBytes = await generateQrPdf( + data, + productName: productName, + reference: reference, + isLabelFormat: isLabelFormat, + ); + + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/${fileName ?? 'qr_code_${reference ?? DateTime.now().millisecondsSinceEpoch}'}.pdf'); + + await file.writeAsBytes(pdfBytes); + + Get.snackbar( + 'Succès', + 'PDF sauvegardé: ${file.path}', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 3), + ); + } catch (e) { + print('Erreur sauvegarde: $e'); + Get.snackbar( + 'Erreur', + 'Impossible de sauvegarder: ${e.toString()}', + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } +} \ No newline at end of file diff --git a/lib/Services/stock_managementDatabase.dart b/lib/Services/stock_managementDatabase.dart index 8181810..f806e39 100644 --- a/lib/Services/stock_managementDatabase.dart +++ b/lib/Services/stock_managementDatabase.dart @@ -1976,14 +1976,48 @@ List parseHeaderInfo(dynamic blobData) { } // 3. Méthodes pour les commandes - Future updateStatutCommande( - int commandeId, StatutCommande statut) async { - final db = await database; +Future 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]); + 'UPDATE commandes SET statut = ? WHERE id = ?', + [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> getCommandesByClient(int clientId) async { final db = await database; @@ -2532,7 +2566,8 @@ Future>> 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 whereArgs = []; if (aujourdHuiSeulement == true) { @@ -2546,7 +2581,8 @@ Future>> 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>> getVentesParPointDeVente({ return []; } } + Future>> getTopProduitsParPointDeVente( int pointDeVenteId, { int limit = 5, diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart index 82cb1a3..bedcebf 100644 --- a/lib/Views/HandleProduct.dart +++ b/lib/Views/HandleProduct.dart @@ -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 { final AppDatabase _productDatabase = AppDatabase.instance; final AppDatabase _appDatabase = AppDatabase.instance; final UserController _userController = Get.find(); + final pdfService = PdfPrintService(); List _products = []; List _filteredProducts = []; @@ -44,16 +46,20 @@ class _ProductManagementPageState extends State { bool _isAssigning = false; final GlobalKey _qrKey = GlobalKey(debugLabel: 'QR'); + Future _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 _predefinedCategories = [ - 'Smartphone', - 'Tablette', - 'Accessoires', - 'Multimedia', - 'Informatique', - 'Laptop', - 'Non catégorisé' - ]; +List _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> 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 _showDemandeTransfertDialog(Product product) async { // Catégorie DropdownButtonFormField( value: selectedCategory, - items: _predefinedCategories + items: _availableCategories .map((category) => DropdownMenuItem( value: category, child: Text(category))) .toList(), @@ -1353,15 +1360,51 @@ Future _showDemandeTransfertDialog(Product product) async { // === FONCTIONS DE SCAN POUR ATTRIBUTION POINT DE VENTE === String? _getColumnValue( - List row, Map mapping, String field) { - if (!mapping.containsKey(field)) return null; + List row, Map mapping, String field) { + if (!mapping.containsKey(field)) return null; - int columnIndex = mapping[field]!; - if (columnIndex >= row.length || row[columnIndex]?.value == null) - return null; + int columnIndex = mapping[field]!; + 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,208 +1433,240 @@ Future _showDemandeTransfertDialog(Product product) async { }); }); } +Map _normalizeRowData( + List row, Map mapping, int rowIndex) { + final normalizedData = {}; + + // Fonction interne pour nettoyer et normaliser les valeurs + String? _cleanValue(String? value) { + if (value == null) return null; + return value.toString().trim(); + } - Map _normalizeRowData( - List row, Map mapping, int rowIndex) { - final normalizedData = {}; - - // Fonction interne pour nettoyer et normaliser les valeurs - String? _cleanValue(String? value) { - if (value == null) return null; - return value.toString().trim(); - } - - // Fonction simple pour les nombres (maintenant ils sont corrects) - 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.]'), ''); + // Fonction simple pour les nombres + double? _normalizeNumber(String? value) { + if (value == null || value.isEmpty) return null; + final cleaned = value.replaceAll(',', '.').replaceAll(RegExp(r'\s+'), ''); + final numericString = cleaned.replaceAll(RegExp(r'[^0-9.]'), ''); + return double.tryParse(numericString); + } - return double.tryParse(numericString); + // Normalisation du nom + if (mapping.containsKey('name')) { + final name = _cleanValue(_getColumnValue(row, mapping, 'name')); + if (name != null && name.isNotEmpty) { + normalizedData['name'] = name; } + } - // Normalisation du nom - if (mapping.containsKey('name')) { - final name = _cleanValue(_getColumnValue(row, mapping, 'name')); - if (name != null && name.isNotEmpty) { - normalizedData['name'] = name; - } + // Normalisation du prix + if (mapping.containsKey('price')) { + final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); + final price = _normalizeNumber(priceValue); + if (price != null && price > 0) { + normalizedData['price'] = price; + print('✅ Prix normalisé: $price'); } + } - // Normalisation du prix (maintenant simple car corrigé en amont) - if (mapping.containsKey('price')) { - final priceValue = _cleanValue(_getColumnValue(row, mapping, 'price')); - final price = _normalizeNumber(priceValue); - if (price != null && price > 0) { - normalizedData['price'] = price; - print('✅ Prix normalisé: $price'); - } + // Normalisation de la référence + if (mapping.containsKey('reference')) { + final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); + if (reference != null && reference.isNotEmpty) { + normalizedData['reference'] = reference; + } else { + normalizedData['reference'] = _generateUniqueReference(); } + } - // Normalisation de la référence - if (mapping.containsKey('reference')) { - final reference = _cleanValue(_getColumnValue(row, mapping, 'reference')); - if (reference != null && reference.isNotEmpty) { - normalizedData['reference'] = reference; - } else { - // Génération automatique si non fournie - normalizedData['reference'] = _generateUniqueReference(); - } - } + // Normalisation de la catégorie + if (mapping.containsKey('category')) { + final category = _cleanValue(_getColumnValue(row, mapping, 'category')); + normalizedData['category'] = category ?? 'Non catégorisé'; + } else { + normalizedData['category'] = 'Non catégorisé'; + } - // Normalisation de la catégorie - if (mapping.containsKey('category')) { - final category = _cleanValue(_getColumnValue(row, mapping, 'category')); - normalizedData['category'] = category ?? 'Non catégorisé'; - } else { - normalizedData['category'] = 'Non catégorisé'; + // Normalisation de la marque + if (mapping.containsKey('marque')) { + final marque = _cleanValue(_getColumnValue(row, mapping, 'marque')); + if (marque != null && marque.isNotEmpty) { + normalizedData['marque'] = marque; } + } - // Normalisation de la marque - if (mapping.containsKey('marque')) { - final marque = _cleanValue(_getColumnValue(row, mapping, 'marque')); - if (marque != null && marque.isNotEmpty) { - normalizedData['marque'] = marque; - } + // Normalisation de la RAM + if (mapping.containsKey('ram')) { + final ram = _cleanValue(_getColumnValue(row, mapping, 'ram')); + if (ram != null && ram.isNotEmpty) { + final ramValue = ram.replaceAll('GB', 'Go').replaceAll('go', 'Go'); + normalizedData['ram'] = ramValue; } + } - // Normalisation de la RAM - 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; - } + // Normalisation de la mémoire interne + if (mapping.containsKey('memoire_interne')) { + final memoire = _cleanValue(_getColumnValue(row, mapping, 'memoire_interne')); + if (memoire != null && memoire.isNotEmpty) { + final memoireValue = memoire.replaceAll('GB', 'Go').replaceAll('go', 'Go'); + normalizedData['memoire_interne'] = memoireValue; } + } - // Normalisation de la mémoire interne - if (mapping.containsKey('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'); - normalizedData['memoire_interne'] = memoireValue; - } + // 🔥 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) { + // 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})'); } + } +} - // Normalisation de l'IMEI - 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 { - normalizedData['imei'] = imeiValue; - } - } - } +// 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(); - } + // 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) { + normalizedData['point_de_vente'] = pv.replaceAll(RegExp(r'\s+'), ' ').trim(); } + } - // Valeurs par défaut - normalizedData['description'] = ''; // Description toujours vide + // Valeurs par défaut + 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; - } else { - normalizedData['stock'] = 1; // Valeur par défaut - } - // Validation des données obligatoires - if (normalizedData['name'] == null || normalizedData['price'] == null) { - throw Exception( - 'Ligne ${rowIndex + 1}: Données obligatoires manquantes (nom ou prix)'); - } +// Try parsing as int first +int? stock = int.tryParse(stockValue ?? ''); - return normalizedData; +// 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(); } +} -// Méthode pour mapper les en-têtes aux colonnes (CORRIGÉE) - Map _mapHeaders(List headerRow) { - Map columnMapping = {}; - - for (int i = 0; i < headerRow.length; i++) { - if (headerRow[i]?.value == null) continue; +// Final fallback: ensure at least 1 +stock ??= 1; - String header = headerRow[i]!.value.toString().trim().toUpperCase(); +// Never allow 0 or negative values +normalizedData['stock'] = stock > 0 ? stock : 1; - // Debug : afficher chaque en-tête trouvé - print('En-tête trouvé: "$header" à la colonne $i'); - - // Mapping amélioré pour gérer les variations - if ((header.contains('NOM') && - (header.contains('PRODUIT') || header.contains('DU'))) || - header == 'NOM DU PRODUITS' || - 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') { - 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') { - columnMapping['category'] = i; - print('→ Mappé vers category'); - } else if (header == 'MARQUE' || header == 'BRAND') { - columnMapping['marque'] = i; - print('→ Mappé vers marque'); - } 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')) { - columnMapping['memoire_interne'] = i; - print('→ Mappé vers memoire_interne'); - } else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { - columnMapping['imei'] = i; - print('→ Mappé vers imei'); - } else if (header == 'PRIX' || header == 'PRICE') { - columnMapping['price'] = i; - print('→ Mappé vers price'); - } 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') { - columnMapping['point_de_vente'] = i; - print('→ Mappé vers point_de_vente'); - } else { - print('→ Non reconnu'); - } + print('📦 Stock depuis Excel: $stock (pas d\'IMEI)'); + } else { + normalizedData['stock'] = 1; + print('📦 Stock par défaut: 1 (pas d\'IMEI, pas de colonne stock)'); } + } - // Debug : afficher le mapping final - print('Mapping final: $columnMapping'); + // Validation des données obligatoires + if (normalizedData['name'] == null || normalizedData['price'] == null) { + throw Exception( + 'Ligne ${rowIndex + 1}: Données obligatoires manquantes (nom ou prix)'); + } - return columnMapping; + return normalizedData; +} +Map _mapHeaders(List headerRow) { + Map columnMapping = {}; + + for (int i = 0; i < headerRow.length; i++) { + if (headerRow[i]?.value == null) continue; + + String header = headerRow[i]!.value.toString().trim().toUpperCase(); + + print('📋 En-tête colonne $i: "$header"'); + + // Nom du produit + if ((header.contains('NOM') && header.contains('PRODUIT')) || header == 'NOM') { + columnMapping['name'] = i; + print(' ✅ Mappé vers name'); + } + // Référence + else if (header.contains('REFERENCE')) { + columnMapping['reference'] = i; + print(' ✅ Mappé vers reference'); + } + // Catégorie - VERSION TOLÉRANTE ⭐ + else if (header.contains('CATEG') && header.contains('PRODUIT')) { + columnMapping['category'] = i; + print(' ✅ Mappé vers category'); + } + // Marque + else if (header == 'MARQUE' || header == 'BRAND') { + columnMapping['marque'] = i; + print(' ✅ Mappé vers marque'); + } + // RAM + else if (header == 'RAM' || header.contains('MEMOIRE RAM')) { + columnMapping['ram'] = i; + 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'); + } + // IMEI + else if (header == 'IMEI' || header.contains('NUMERO IMEI')) { + columnMapping['imei'] = i; + print(' ✅ Mappé vers imei'); + } + // Prix + else if (header == 'PRIX' || header == 'PRICE') { + columnMapping['price'] = i; + print(' ✅ Mappé vers price'); + } + // Stock + else if (header == 'STOCK' || header == 'QUANTITY' || header == 'QTE') { + columnMapping['stock'] = i; + 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('\n🎯 MAPPING FINAL: $columnMapping\n'); + return columnMapping; +} + Widget _buildAssignmentScannerPage() { return Scaffold( appBar: AppBar( @@ -2184,7 +2259,7 @@ Future _showDemandeTransfertDialog(Product product) async { 'RAM', // ram 'INTERNE', // memoire_interne 'IMEI', // imei - 'STOCK' + 'STOCK', 'PRIX', // price 'BOUTIQUE', // point_de_vente ]; @@ -3255,132 +3330,219 @@ Future _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!; - - return AlertDialog( - title: Row( + Get.dialog( + Obx(() { + final qrData = showFullUrl.value + ? 'https://stock.guycom.mg/${product.reference}' + : product.reference!; + + return AlertDialog( + title: Row( + children: [ + const Icon(Icons.qr_code_2, color: Colors.blue), + const SizedBox(width: 8), + Expanded( + child: Text( + 'QR Code - ${product.name}', + style: const TextStyle(fontSize: 18), + ), + ), + ], + ), + content: Container( + width: 350, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.qr_code_2, color: Colors.blue), - const SizedBox(width: 8), - Expanded( - child: Text( - 'QR Code - ${product.name}', - style: const TextStyle(fontSize: 18), + // Bouton bascule URL / Référence + ElevatedButton.icon( + onPressed: () { + showFullUrl.value = !showFullUrl.value; + }, + icon: Icon( + showFullUrl.value ? Icons.link : Icons.tag, + size: 16, + ), + label: Text( + showFullUrl.value ? 'URL Complète' : 'Référence Seulement', + style: const TextStyle(fontSize: 14), + ), + style: ElevatedButton.styleFrom( + backgroundColor: showFullUrl.value ? Colors.blue : Colors.green, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 40), ), ), - ], - ), - content: Container( - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Bouton pour basculer entre URL et référence - ElevatedButton.icon( - onPressed: () { - showFullUrl.value = !showFullUrl.value; - }, - icon: Icon( - showFullUrl.value ? Icons.link : Icons.tag, - size: 16, - ), - label: Text( - showFullUrl.value ? 'URL/Référence' : 'Référence', - style: const TextStyle(fontSize: 14), - ), - style: ElevatedButton.styleFrom( - backgroundColor: - showFullUrl.value ? Colors.blue : Colors.green, - foregroundColor: Colors.white, - minimumSize: const Size(double.infinity, 36), - ), + const SizedBox(height: 16), + + // QR Code affiché + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), ), - const SizedBox(height: 16), - - // Container du QR Code - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - child: QrImageView( - data: qrData, - version: QrVersions.auto, - size: 200, - backgroundColor: Colors.white, - ), + child: QrImageView( + data: qrData, + version: QrVersions.auto, + size: 200, + backgroundColor: Colors.white, + errorCorrectionLevel: QrErrorCorrectLevel.M, ), - const SizedBox(height: 16), - - // Affichage des données actuelles - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - children: [ - Text( - showFullUrl.value - ? 'URL Complète' - : 'Référence Seulement', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - Text( - qrData, - style: - const TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), - ], - ), + ), + const SizedBox(height: 16), + + // Informations du produit + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), ), - ], - ), + child: Column( + children: [ + 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: 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( + 'Format: Étiquette Niimbot B1 (50x15mm)', + style: TextStyle( + fontSize: 12, + color: Colors.orange.shade700, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ), + ], ), - actions: [ - TextButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: qrData)); - Get.back(); - Get.snackbar( - 'Copié', - '${showFullUrl.value ? "URL" : "Référence"} copiée dans le presse-papiers', - backgroundColor: Colors.green, - colorText: Colors.white, + ), + actions: [ + // Copier + TextButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: qrData)); + Get.snackbar( + 'Copié', + '${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), + ); + }, + icon: const Icon(Icons.copy, size: 18), + label: const Text('Copier'), + ), + + // 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, ); - }, - child: Text('Copier ${showFullUrl.value ? "URL" : "Référence"}'), - ), - TextButton( - onPressed: () => _generatePDF(product, qrData), - child: const Text('Imprimer en PDF'), - ), - TextButton( - onPressed: () => Get.back(), - child: const Text('Fermer'), + 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'), + ), + ], + ); + }), + ); +} Future _generatePDF(Product product, String qrUrl) async { final pdf = pw.Document(); @@ -3439,9 +3601,9 @@ Future _showDemandeTransfertDialog(Product product) async { List> 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 _showDemandeTransfertDialog(Product product) async { // Catégorie avec gestion des valeurs non présentes DropdownButtonFormField( value: selectedCategory, - items: _predefinedCategories + items: _availableCategories .map((category) => DropdownMenuItem( value: category, child: Text(category))) .toList(), diff --git a/lib/Views/commandManagement.dart b/lib/Views/commandManagement.dart index ad1776b..4822812 100644 --- a/lib/Views/commandManagement.dart +++ b/lib/Views/commandManagement.dart @@ -163,11 +163,11 @@ class _GestionCommandesPageState extends State { 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); } diff --git a/lib/Views/demande_sortie_personnelle_page.dart b/lib/Views/demande_sortie_personnelle_page.dart index 65d5688..79850c5 100644 --- a/lib/Views/demande_sortie_personnelle_page.dart +++ b/lib/Views/demande_sortie_personnelle_page.dart @@ -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 _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 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 = []; + 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( - width: double.maxFinite, - height: 400, - child: MobileScanner( - onDetect: (BarcodeCapture barcodeCap) { - print("BarcodeCapture: $barcodeCap"); - // Now accessing the barcodes attribute - final List 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(); - } - }, + 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: (capture) { + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + final scanResult = barcodes.first.rawValue; + if (scanResult != null && scanResult.isNotEmpty) { + Navigator.of(context).pop(); + setState(() => _searchController.text = scanResult); + _filterProducts(); + _showSuccessSnackbar("QR Code détecté : $scanResult"); + } + } + }, + ), + ), + 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; -} diff --git a/lib/config/DatabaseConfig.dart b/lib/config/DatabaseConfig.dart index c5b3d0b..aa099e8 100644 --- a/lib/config/DatabaseConfig.dart +++ b/lib/config/DatabaseConfig.dart @@ -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 _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; } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 6713b98..d11adb5 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,23 +6,23 @@ #include "generated_plugin_registrant.h" -#include #include #include +#include #include #include #include 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); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e1cc94e..3b425b3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -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 diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ce26282..2161829 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index 1f77e0b..c22574f 100644 --- a/pubspec.lock +++ b/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" diff --git a/pubspec.yaml b/pubspec.yaml index bf1a3f6..dbfbf6e 100644 --- a/pubspec.yaml +++ b/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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5716043..b4a642a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,17 +6,20 @@ #include "generated_plugin_registrant.h" -#include +#include #include +#include #include #include #include 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( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 239eabd..1889687 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST - charset_converter + camera_windows file_selector_windows + printing screen_retriever url_launcher_windows window_manager