From 1aceb5669a2171e219fbd7fd7ed02f0ceb9edba7 Mon Sep 17 00:00:00 2001 From: "b.razafimandimbihery" Date: Thu, 29 May 2025 12:38:28 +0300 Subject: [PATCH] last commit excel gestion mety --- lib/Components/appDrawer.dart | 27 +- lib/Components/app_bar.dart | 55 +- lib/Models/produit.dart | 32 +- lib/Services/productDatabase.dart | 22 +- lib/Services/workDatabase.dart | 63 +- lib/Views/HandleProduct.dart | 1532 +++++++++++++++++++++++++++++ lib/Views/addProduct.dart | 711 +++++++++++-- lib/Views/gestionProduct.dart | 116 ++- lib/Views/produitsCard.dart | 2 +- lib/accueil.dart | 294 +++--- pubspec.lock | 8 + pubspec.yaml | 3 +- 12 files changed, 2500 insertions(+), 365 deletions(-) create mode 100644 lib/Views/HandleProduct.dart diff --git a/lib/Components/appDrawer.dart b/lib/Components/appDrawer.dart index c676b06..cdea699 100644 --- a/lib/Components/appDrawer.dart +++ b/lib/Components/appDrawer.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:youmazgestion/Views/HandleProduct.dart'; import 'package:youmazgestion/Views/RoleListPage.dart'; import 'package:youmazgestion/Views/historique.dart'; import 'package:youmazgestion/Views/addProduct.dart'; @@ -113,11 +114,11 @@ class CustomDrawer extends StatelessWidget { ListTile( leading: const Icon(Icons.add), iconColor: Colors.indigoAccent, - title: const Text("Ajouter un produit"), + title: const Text("Gestion des produit"), onTap: () async { bool hasPermission = await userController.hasPermission('create', '/ajouter-produit'); if (hasPermission) { - Get.to(const AddProductPage()); + Get.to(() => const ProductManagementPage()); } else { Get.snackbar( "Accès refusé", @@ -131,27 +132,7 @@ class CustomDrawer extends StatelessWidget { } }, ), - ListTile( - leading: const Icon(Icons.edit), - iconColor: Colors.redAccent, - title: const Text("Modifier/Supprimer un produit"), - onTap: () async { - bool hasPermission = await userController.hasPermission('update', '/modifier-produit'); - if (hasPermission) { - Get.to(GestionProduit()); - } else { - Get.snackbar( - "Accès refusé", - "Vous n'avez pas les droits pour modifier/supprimer un produit", - backgroundColor: Colors.red, - colorText: Colors.white, - icon: const Icon(Icons.error), - duration: const Duration(seconds: 3), - snackPosition: SnackPosition.TOP, - ); - } - }, - ), + ListTile( leading: const Icon(Icons.bar_chart), title: const Text("Bilan"), diff --git a/lib/Components/app_bar.dart b/lib/Components/app_bar.dart index 83b8e85..9878ef3 100644 --- a/lib/Components/app_bar.dart +++ b/lib/Components/app_bar.dart @@ -2,53 +2,30 @@ import 'package:flutter/material.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; + final Widget? subtitle; - const CustomAppBar({Key? key, required this.title}) : super(key: key); + const CustomAppBar({ + Key? key, + required this.title, + this.subtitle, + }) : super(key: key); @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); + Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0); @override Widget build(BuildContext context) { return AppBar( - backgroundColor: Colors.transparent, - elevation: 5, - flexibleSpace: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Colors.white, const Color.fromARGB(255, 4, 54, 95)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Row( - children: [ - const Spacer(), - Align( - alignment: Alignment.center, - child: Text( - title, - style: const TextStyle( - fontSize: 25, - color: Colors.white, - ), - ), + title: subtitle == null + ? Text(title) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontSize: 20)), + subtitle!, + ], ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Align( - alignment: Alignment.centerRight, - child: Image.asset( - 'assets/youmaz2.png', - width: 100, - height: 50, - ), - ), - ), - ], - ), - ), + // autres propriétés si besoin ); } } diff --git a/lib/Models/produit.dart b/lib/Models/produit.dart index 8239e94..abf5eae 100644 --- a/lib/Models/produit.dart +++ b/lib/Models/produit.dart @@ -1,26 +1,25 @@ class Product { - final int? id; + int? id; final String name; final double price; - String image; + final String? image; final String category; - int? stock; // Paramètre optionnel pour le stock - String? description; // Nouveau champ - String? qrCode; // Nouveau champ - String? reference; + final int? stock; + final String? description; + String? qrCode; + final String? reference; Product({ this.id, required this.name, required this.price, - required this.image, + this.image, required this.category, this.stock = 0, - this.description, + this.description = '', this.qrCode, this.reference, }); - // Vérifie si le stock est défini bool isStockDefined() { if (stock != null) { @@ -30,18 +29,17 @@ class Product { return false; } } - Map toMap() { return { 'id': id, 'name': name, 'price': price, - 'image': image, + 'image': image ?? '', 'category': category, - 'stock': stock, - 'description': description, - 'qrCode': qrCode, - 'reference':reference + 'stock': stock ?? 0, + 'description': description ?? '', + 'qrCode': qrCode ?? '', + 'reference': reference ?? '', }; } @@ -55,7 +53,7 @@ class Product { stock: map['stock'], description: map['description'], qrCode: map['qrCode'], - reference:map['reference'] + reference: map['reference'], ); } -} +} \ No newline at end of file diff --git a/lib/Services/productDatabase.dart b/lib/Services/productDatabase.dart index cc47bb1..ce66a7e 100644 --- a/lib/Services/productDatabase.dart +++ b/lib/Services/productDatabase.dart @@ -42,7 +42,7 @@ class ProductDatabase { return await databaseFactoryFfi.openDatabase(path); } - Future _createDB(Database db, int version) async { + Future _createDB(Database db, int version) async { // Récupère la liste des colonnes de la table "products" final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'"); final tableNames = tables.map((row) => row['name'] as String).toList(); @@ -58,13 +58,13 @@ class ProductDatabase { category TEXT, stock INTEGER, description TEXT, - qrCode TEXT + qrCode TEXT, reference TEXT ) '''); print("Table 'products' créée avec toutes les colonnes."); } else { - // Vérifie si les colonnes "description" et "qrCode" existent déjà + // Vérifie si les colonnes "description", "qrCode" et "reference" existent déjà final columns = await db.rawQuery('PRAGMA table_info(products)'); final columnNames = columns.map((e) => e['name'] as String).toList(); @@ -87,6 +87,7 @@ class ProductDatabase { print("Erreur ajout colonne qrCode : $e"); } } + // Ajoute la colonne "reference" si elle n'existe pas if (!columnNames.contains('reference')) { try { @@ -153,4 +154,19 @@ class ProductDatabase { return await db .rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]); } + // Ajouter cette méthode dans la classe ProductDatabase + +Future getProductByReference(String reference) async { + final db = await database; + final maps = await db.query( + 'products', + where: 'reference = ?', + whereArgs: [reference], + ); + + if (maps.isNotEmpty) { + return Product.fromMap(maps.first); + } + return null; +} } diff --git a/lib/Services/workDatabase.dart b/lib/Services/workDatabase.dart index 3502248..e9560af 100644 --- a/lib/Services/workDatabase.dart +++ b/lib/Services/workDatabase.dart @@ -8,40 +8,45 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class WorkDatabase { static final WorkDatabase instance = WorkDatabase._init(); - late Database _database; + + static Database? _database; + bool _isInitialized = false; WorkDatabase._init() { sqflite_ffi.sqfliteFfiInit(); } Future initDatabase() async { - _database = await _initDB('work.db'); - await _createDB(_database, 1); + if (!_isInitialized) { + _database = await _initDB('work.db'); + await _createDB(_database!, 1); + _isInitialized = true; + } } Future get database async { - if (_database.isOpen) return _database; - - _database = await _initDB('work.db'); - return _database; + if (!_isInitialized) { + await initDatabase(); + } + return _database!; } Future _initDB(String filePath) async { - // Obtenez le répertoire de stockage local de l'application final documentsDirectory = await getApplicationDocumentsDirectory(); final path = join(documentsDirectory.path, filePath); - // Vérifiez si le fichier de base de données existe déjà dans le répertoire de stockage local bool dbExists = await File(path).exists(); if (!dbExists) { - // Si le fichier n'existe pas, copiez-le depuis le dossier assets/database - ByteData data = await rootBundle.load('assets/database/$filePath'); - List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(path).writeAsBytes(bytes); + try { + ByteData data = await rootBundle.load('assets/database/$filePath'); + List bytes = + data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + await File(path).writeAsBytes(bytes); + } catch (e) { + print("No pre-existing database found in assets, creating new one"); + } } - // Ouvrez la base de données return await databaseFactoryFfi.openDatabase(path); } @@ -49,42 +54,30 @@ class WorkDatabase { await db.execute(''' CREATE TABLE IF NOT EXISTS work ( id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT + date TEXT UNIQUE ) '''); } Future insertDate(String date) async { final db = await database; - final existingDates = - await db.query('work', where: 'date = ?', whereArgs: [date]); - - if (existingDates.isNotEmpty) { - // Date already exists, return 0 to indicate no new insertion + try { + return await db.insert('work', {'date': date}); + } catch (e) { + // En cas de doublon (date déjà existante) return 0; } - - return await db.insert('work', {'date': date}); } - /*Future> getDates() async { - final db = await database; - final result = await db.query('work'); - - return result.map((json) => Work.fromJson(json)).toList(); - }*/ Future> getDates() async { final db = await database; final result = await db.query('work'); - return List.generate( - result.length, (index) => result[index]['date'] as String); + return result.map((row) => row['date'] as String).toList(); } - // recuperer les dates par ordre du plus recent au plus ancien Future> getDatesDesc() async { final db = await database; final result = await db.query('work', orderBy: 'date DESC'); - return List.generate( - result.length, (index) => result[index]['date'] as String); + return result.map((row) => row['date'] as String).toList(); } -} +} \ No newline at end of file diff --git a/lib/Views/HandleProduct.dart b/lib/Views/HandleProduct.dart new file mode 100644 index 0000000..f2af6af --- /dev/null +++ b/lib/Views/HandleProduct.dart @@ -0,0 +1,1532 @@ +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:excel/excel.dart' hide Border; +import '../Components/appDrawer.dart'; +import '../Components/app_bar.dart'; +import '../Models/produit.dart'; +import '../Services/productDatabase.dart'; + + +class ProductManagementPage extends StatefulWidget { + const ProductManagementPage({super.key}); + + @override + _ProductManagementPageState createState() => _ProductManagementPageState(); +} + +class _ProductManagementPageState extends State { + final ProductDatabase _productDatabase = ProductDatabase.instance; + List _products = []; + List _filteredProducts = []; + final TextEditingController _searchController = TextEditingController(); + String _selectedCategory = 'Tous'; + List _categories = ['Tous']; + bool _isLoading = true; + + // Catégories prédéfinies pour l'ajout de produits + final List _predefinedCategories = [ + 'Sucré', 'Salé', 'Jus', 'Gateaux', 'Snacks', 'Boissons', 'Non catégorisé' + ]; + + @override + void initState() { + super.initState(); + _loadProducts(); + _searchController.addListener(_filterProducts); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + + + + + + +//====================================================================================================== +// Ajoutez ces variables à la classe _ProductManagementPageState +bool _isImporting = false; +double _importProgress = 0.0; +String _importStatusText = ''; + +// Ajoutez ces méthodes à la classe _ProductManagementPageState +void _resetImportState() { + setState(() { + _isImporting = false; + _importProgress = 0.0; + _importStatusText = ''; + }); +} + +void _showExcelCompatibilityError() { + Get.dialog( + AlertDialog( + title: const Text('Fichier Excel incompatible'), + content: const Text( + 'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n' + 'Solutions recommandées :\n' + '• Téléchargez notre modèle Excel et copiez-y vos données\n' + '• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n' + '• Ou créez un nouveau fichier Excel simple sans formatage complexe' + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () { + Get.back(); + _downloadExcelTemplate(); + }, + child: const Text('Télécharger modèle'), + style: TextButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ], + ), + ); +} + +Future _downloadExcelTemplate() async { + try { + final excel = Excel.createExcel(); + excel.delete('Sheet1'); + excel.copy('Sheet1', 'Produits'); + excel.delete('Sheet1'); + + final sheet = excel['Produits']; + + final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock']; + for (int i = 0; i < headers.length; i++) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); + cell.value = headers[i]; + cell.cellStyle = CellStyle( + bold: true, + backgroundColorHex: '#E8F4FD', + ); + } + + final examples = [ + ['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'], + ['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'], + ['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'], + ['Gâteau chocolat', '18.00', 'Gateaux', 'Gâteau au chocolat portion 8 personnes', '5'], + ]; + + for (int row = 0; row < examples.length; row++) { + for (int col = 0; col < examples[row].length; col++) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1)); + cell.value = examples[row][col]; + } + } + + sheet.setColWidth(0, 20); + sheet.setColWidth(1, 10); + sheet.setColWidth(2, 15); + sheet.setColWidth(3, 30); + sheet.setColWidth(4, 10); + + final bytes = excel.save(); + + if (bytes == null) { + Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); + return; + } + + final String? outputFile = await FilePicker.platform.saveFile( + fileName: 'modele_import_produits.xlsx', + allowedExtensions: ['xlsx'], + type: FileType.custom, + ); + + if (outputFile != null) { + try { + await File(outputFile).writeAsBytes(bytes); + Get.snackbar( + 'Succès', + 'Modèle téléchargé avec succès\n$outputFile', + duration: const Duration(seconds: 4), + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } catch (e) { + Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); + } + } + } catch (e) { + Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); + debugPrint('Erreur création modèle Excel: $e'); + } +} + +Future _importFromExcel() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls','csv'], + allowMultiple: false, + ); + + if (result == null || result.files.isEmpty) { + Get.snackbar('Annulé', 'Aucun fichier sélectionné'); + return; + } + + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Lecture du fichier...'; + }); + + final file = File(result.files.single.path!); + + if (!await file.exists()) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); + return; + } + + setState(() { + _importProgress = 0.1; + _importStatusText = 'Vérification du fichier...'; + }); + + final bytes = await file.readAsBytes(); + + if (bytes.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel est vide'); + return; + } + + setState(() { + _importProgress = 0.2; + _importStatusText = 'Décodage du fichier Excel...'; + }); + + Excel excel; + try { + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Initialisation...'; + }); + + await Future.delayed(Duration(milliseconds: 50)); + excel = Excel.decodeBytes(bytes); + } catch (e) { + _resetImportState(); + debugPrint('Erreur décodage Excel: $e'); + + if (e.toString().contains('styles') || e.toString().contains('Damaged')) { + _showExcelCompatibilityError(); + return; + } else { + Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.'); + return; + } + } + + if (excel.tables.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); + return; + } + + setState(() { + _importProgress = 0.3; + _importStatusText = 'Analyse des données...'; + }); + + int successCount = 0; + int errorCount = 0; + List errorMessages = []; + + final sheetName = excel.tables.keys.first; + final sheet = excel.tables[sheetName]!; + + if (sheet.rows.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'La feuille Excel est vide'); + return; + } + + final totalRows = sheet.rows.length - 1; + + setState(() { + _importStatusText = 'Importation en cours... (0/$totalRows)'; + }); + + for (var i = 1; i < sheet.rows.length; i++) { + try { + final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); + setState(() { + _importProgress = currentProgress; + _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; + }); + + await Future.delayed(const Duration(milliseconds: 10)); + + final row = sheet.rows[i]; + + if (row.isEmpty || row.length < 2) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); + continue; + } + + String? nameValue; + String? priceValue; + + if (row[0]?.value != null) { + nameValue = row[0]!.value.toString().trim(); + } + + if (row[1]?.value != null) { + priceValue = row[1]!.value.toString().trim(); + } + + if (nameValue == null || nameValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Nom du produit manquant'); + continue; + } + + if (priceValue == null || priceValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix manquant'); + continue; + } + + final name = nameValue; + final price = double.tryParse(priceValue.replaceAll(',', '.')); + + if (price == null || price <= 0) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); + continue; + } + + String category = 'Non catégorisé'; + if (row.length > 2 && row[2]?.value != null) { + final categoryValue = row[2]!.value.toString().trim(); + if (categoryValue.isNotEmpty) { + category = categoryValue; + } + } + + String description = ''; + if (row.length > 3 && row[3]?.value != null) { + description = row[3]!.value.toString().trim(); + } + + int stock = 0; + if (row.length > 4 && row[4]?.value != null) { + final stockStr = row[4]!.value.toString().trim(); + stock = int.tryParse(stockStr) ?? 0; + } + + String reference = _generateUniqueReference(); + var existingProduct = await _productDatabase.getProductByReference(reference); + while (existingProduct != null) { + reference = _generateUniqueReference(); + existingProduct = await _productDatabase.getProductByReference(reference); + } + + final product = Product( + name: name, + price: price, + image: '', + category: category, + description: description, + stock: stock, + qrCode: '', + reference: reference, + ); + + setState(() { + _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; + }); + + final qrPath = await _generateAndSaveQRCode(reference); + product.qrCode = qrPath; + + await _productDatabase.createProduct(product); + successCount++; + + } catch (e) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); + debugPrint('Erreur ligne ${i + 1}: $e'); + } + } + + setState(() { + _importProgress = 1.0; + _importStatusText = 'Finalisation...'; + }); + + await Future.delayed(const Duration(milliseconds: 500)); + + _resetImportState(); + + String message = '$successCount produits importés avec succès'; + if (errorCount > 0) { + message += ', $errorCount erreurs'; + + if (errorMessages.length <= 5) { + message += ':\n${errorMessages.join('\n')}'; + } + } + + Get.snackbar( + 'Importation terminée', + message, + duration: const Duration(seconds: 6), + colorText: Colors.white, + backgroundColor: successCount > 0 ? Colors.green : Colors.orange, + ); + + // Recharger la liste des produits après importation + _loadProducts(); + + } catch (e) { + _resetImportState(); + Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); + debugPrint('Erreur générale import Excel: $e'); + } +} + +// Ajoutez ce widget dans votre méthode build, par exemple dans la partie supérieure +Widget _buildImportProgressIndicator() { + if (!_isImporting) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Importation en cours...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _importProgress, + backgroundColor: Colors.blue.shade100, + valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), + ), + const SizedBox(height: 8), + Text( + _importStatusText, + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade700, + ), + ), + const SizedBox(height: 8), + Text( + '${(_importProgress * 100).round()}%', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade600, + ), + ), + ], + ), + ); +} +//============================================================================================================================= + Future _loadProducts() async { + setState(() => _isLoading = true); + + try { + await _productDatabase.initDatabase(); + final products = await _productDatabase.getProducts(); + final categories = await _productDatabase.getCategories(); + + setState(() { + _products = products; + _filteredProducts = products; + _categories = ['Tous', ...categories]; + _isLoading = false; + }); + } catch (e) { + setState(() => _isLoading = false); + Get.snackbar('Erreur', 'Impossible de charger les produits: $e'); + } + } + + void _filterProducts() { + final query = _searchController.text.toLowerCase(); + + setState(() { + _filteredProducts = _products.where((product) { + final matchesSearch = product.name.toLowerCase().contains(query) || + product.description!.toLowerCase().contains(query) || + product.reference!.toLowerCase().contains(query); + + final matchesCategory = _selectedCategory == 'Tous' || + product.category == _selectedCategory; + + return matchesSearch && matchesCategory; + }).toList(); + }); + } + + // Méthode pour générer une référence unique + String _generateUniqueReference() { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); + return 'PROD_${timestamp}${randomSuffix}'; + } + + // Méthode pour générer et sauvegarder le QR Code + Future _generateAndSaveQRCode(String reference) async { + final qrUrl = 'https://stock.guycom.mg/$reference'; + + final validation = QrValidator.validate( + data: qrUrl, + version: QrVersions.auto, + errorCorrectionLevel: QrErrorCorrectLevel.L, + ); + + if (validation.status != QrValidationStatus.valid) { + throw Exception('Données QR invalides: ${validation.error}'); + } + + final qrCode = validation.qrCode!; + final painter = QrPainter.withQr( + qr: qrCode, + color: Colors.black, + emptyColor: Colors.white, + gapless: true, + ); + + final directory = await getApplicationDocumentsDirectory(); + final path = '${directory.path}/$reference.png'; + + try { + final picData = await painter.toImageData(2048, format: ImageByteFormat.png); + if (picData != null) { + await File(path).writeAsBytes(picData.buffer.asUint8List()); + } else { + throw Exception('Impossible de générer l\'image QR'); + } + } catch (e) { + throw Exception('Erreur lors de la génération du QR code: $e'); + } + + return path; + } + + void _showAddProductDialog() { + final nameController = TextEditingController(); + final priceController = TextEditingController(); + final stockController = TextEditingController(); + final descriptionController = TextEditingController(); + final imageController = TextEditingController(); + + String selectedCategory = _predefinedCategories.last; // 'Non catégorisé' par défaut + File? pickedImage; + String? qrPreviewData; + String? currentReference; + + // Fonction pour mettre à jour le QR preview + void updateQrPreview() { + if (nameController.text.isNotEmpty) { + if (currentReference == null) { + currentReference = _generateUniqueReference(); + } + qrPreviewData = 'https://stock.guycom.mg/$currentReference'; + } else { + currentReference = null; + qrPreviewData = null; + } + } + + Get.dialog( + AlertDialog( + title: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.add_shopping_cart, color: Colors.green.shade700), + ), + const SizedBox(width: 12), + const Text('Ajouter un produit'), + ], + ), + content: Container( + width: 600, + constraints: const BoxConstraints(maxHeight: 600), + child: SingleChildScrollView( + child: StatefulBuilder( + builder: (context, setDialogState) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Champs obligatoires + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade200), + ), + child: Row( + children: [ + Icon(Icons.info, color: Colors.red.shade600, size: 16), + const SizedBox(width: 8), + const Text( + 'Les champs marqués d\'un * sont obligatoires', + style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Nom du produit + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: 'Nom du produit *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.shopping_bag), + filled: true, + fillColor: Colors.grey.shade50, + ), + onChanged: (value) { + setDialogState(() { + updateQrPreview(); + }); + }, + ), + const SizedBox(height: 16), + + // Prix et Stock sur la même ligne + Row( + children: [ + Expanded( + child: TextField( + controller: priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Prix (FCFA) *', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.attach_money), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: stockController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Stock initial', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.inventory), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Catégorie + DropdownButtonFormField( + value: selectedCategory, + items: _predefinedCategories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setDialogState(() => selectedCategory = value!); + }, + decoration: InputDecoration( + labelText: 'Catégorie', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.category), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 16), + + // Description + TextField( + controller: descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.description), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 16), + + // Section Image + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.image, color: Colors.blue.shade700), + const SizedBox(width: 8), + Text( + 'Image du produit (optionnel)', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.blue.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: imageController, + decoration: const InputDecoration( + labelText: 'Chemin de l\'image', + border: OutlineInputBorder(), + isDense: true, + ), + readOnly: true, + ), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + setDialogState(() { + pickedImage = File(result.files.single.path!); + imageController.text = pickedImage!.path; + }); + } + }, + icon: const Icon(Icons.folder_open, size: 16), + label: const Text('Choisir'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(12), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Aperçu de l'image + if (pickedImage != null) + Center( + child: Container( + height: 100, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file(pickedImage!, fit: BoxFit.cover), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + + // Aperçu QR Code + if (qrPreviewData != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green.shade200), + ), + child: Column( + children: [ + Row( + children: [ + Icon(Icons.qr_code_2, color: Colors.green.shade700), + const SizedBox(width: 8), + Text( + 'Aperçu du QR Code', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: QrImageView( + data: qrPreviewData!, + version: QrVersions.auto, + size: 80, + backgroundColor: Colors.white, + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Réf: $currentReference', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton.icon( + onPressed: () async { + final name = nameController.text.trim(); + final price = double.tryParse(priceController.text.trim()) ?? 0.0; + final stock = int.tryParse(stockController.text.trim()) ?? 0; + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + try { + // Générer une référence unique et vérifier son unicité + String finalReference = currentReference ?? _generateUniqueReference(); + var existingProduct = await _productDatabase.getProductByReference(finalReference); + + while (existingProduct != null) { + finalReference = _generateUniqueReference(); + existingProduct = await _productDatabase.getProductByReference(finalReference); + } + + // Générer le QR code + final qrPath = await _generateAndSaveQRCode(finalReference); + + final product = Product( + name: name, + price: price, + image: imageController.text, + category: selectedCategory, + description: descriptionController.text.trim(), + stock: stock, + qrCode: qrPath, + reference: finalReference, + ); + + await _productDatabase.createProduct(product); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit ajouté avec succès!\nRéférence: $finalReference', + backgroundColor: Colors.green, + colorText: Colors.white, + duration: const Duration(seconds: 4), + icon: const Icon(Icons.check_circle, color: Colors.white), + ); + _loadProducts(); + } catch (e) { + Get.snackbar('Erreur', 'Ajout du produit échoué: $e'); + } + }, + icon: const Icon(Icons.save), + label: const Text('Ajouter le produit'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + ), + ], + ), + ); + } + + void _showQRCode(Product product) { + final qrUrl = 'https://stock.guycom.mg/${product.reference}'; + + Get.dialog( + 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: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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: qrUrl, + version: QrVersions.auto, + size: 200, + backgroundColor: Colors.white, + ), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text( + 'Référence: ${product.reference}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + qrUrl, + style: const TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: qrUrl)); + Get.back(); + Get.snackbar( + 'Copié', + 'URL copiée dans le presse-papiers', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }, + child: const Text('Copier URL'), + ), + TextButton( + onPressed: () => Get.back(), + child: const Text('Fermer'), + ), + ], + ), + ); + } + + void _editProduct(Product product) { + final nameController = TextEditingController(text: product.name); + final priceController = TextEditingController(text: product.price.toString()); + final stockController = TextEditingController(text: product.stock.toString()); + final descriptionController = TextEditingController(text: product.description ?? ''); + final imageController = TextEditingController(text: product.image); + + String selectedCategory = product.category; + File? pickedImage; + + Get.dialog( + AlertDialog( + title: const Text('Modifier le produit'), + content: Container( + width: 500, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: const InputDecoration( + labelText: 'Nom du produit*', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + TextField( + controller: priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: 'Prix*', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + TextField( + controller: stockController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Stock', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + StatefulBuilder( + builder: (context, setDialogState) { + return Column( + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: imageController, + decoration: const InputDecoration( + labelText: 'Image', + border: OutlineInputBorder(), + ), + readOnly: true, + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + final result = await FilePicker.platform.pickFiles(type: FileType.image); + if (result != null && result.files.single.path != null) { + if (context.mounted) { + setDialogState(() { + pickedImage = File(result.files.single.path!); + imageController.text = pickedImage!.path; + }); + } + } + }, + child: const Text('Choisir'), + ), + ], + ), + const SizedBox(height: 16), + + if (pickedImage != null || product.image!.isNotEmpty) + Container( + height: 100, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: pickedImage != null + ? Image.file(pickedImage!, fit: BoxFit.cover) + : (product.image!.isNotEmpty + ? Image.file(File(product.image!), fit: BoxFit.cover) + : const Icon(Icons.image, size: 50)), + ), + ), + const SizedBox(height: 16), + + DropdownButtonFormField( + value: selectedCategory, + items: _categories.skip(1).map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + if (context.mounted) { + setDialogState(() => selectedCategory = value!); + } + }, + decoration: const InputDecoration( + labelText: 'Catégorie', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + TextField( + controller: descriptionController, + maxLines: 3, + decoration: const InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () async { + final name = nameController.text.trim(); + final price = double.tryParse(priceController.text.trim()) ?? 0.0; + final stock = int.tryParse(stockController.text.trim()) ?? 0; + + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + final updatedProduct = Product( + id: product.id, + name: name, + price: price, + image: imageController.text, + category: selectedCategory, + description: descriptionController.text.trim(), + stock: stock, + qrCode: product.qrCode, + reference: product.reference, + ); + + try { + await _productDatabase.updateProduct(updatedProduct); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit modifié avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + _loadProducts(); + } catch (e) { + Get.snackbar('Erreur', 'Modification échouée: $e'); + } + }, + child: const Text('Sauvegarder'), + ), + ], + ), + ); + } + + void _deleteProduct(Product product) { + Get.dialog( + AlertDialog( + title: const Text('Confirmer la suppression'), + content: Text('Êtes-vous sûr de vouloir supprimer "${product.name}" ?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () async { + try { + await _productDatabase.deleteProduct(product.id); + Get.back(); + Get.snackbar( + 'Succès', + 'Produit supprimé avec succès', + backgroundColor: Colors.green, + colorText: Colors.white, + ); + _loadProducts(); + } catch (e) { + Get.back(); + Get.snackbar('Erreur', 'Suppression échouée: $e'); + } + }, + child: const Text('Supprimer', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + } + + Widget _buildProductCard(Product product) { + return Card( + margin: const EdgeInsets.all(8), + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Image du produit + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: product.image!.isNotEmpty + ? Image.file( + File(product.image!), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + const Icon(Icons.image, size: 40), + ) + : const Icon(Icons.image, size: 40), + ), + ), + const SizedBox(width: 16), + + // Informations du produit + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${NumberFormat('#,##0').format(product.price)} FCFA', + style: const TextStyle( + fontSize: 16, + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + product.category, + style: TextStyle( + fontSize: 12, + color: Colors.blue.shade800, + ), + ), + ), + const SizedBox(width: 8), + Text( + 'Stock: ${product.stock}', + style: TextStyle( + fontSize: 12, + color: product.stock! > 0 ? Colors.green : Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + if (product.description!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + product.description!, + style: const TextStyle(fontSize: 12, color: Colors.grey), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 4), + Text( + 'Réf: ${product.reference}', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + + // Actions + Column( + children: [ + IconButton( + onPressed: () => _showQRCode(product), + icon: const Icon(Icons.qr_code_2, color: Colors.blue), + tooltip: 'Voir QR Code', + ), + IconButton( + onPressed: () => _editProduct(product), + icon: const Icon(Icons.edit, color: Colors.orange), + tooltip: 'Modifier', + ), + IconButton( + onPressed: () => _deleteProduct(product), + icon: const Icon(Icons.delete, color: Colors.red), + tooltip: 'Supprimer', + ), + ], + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const CustomAppBar(title: 'Gestion des produits'), + drawer: CustomDrawer(), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + heroTag: 'importBtn', + onPressed: _isImporting ? null : _importFromExcel, + mini: true, + child: const Icon(Icons.upload), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + const SizedBox(height: 8), + FloatingActionButton.extended( + heroTag: 'addBtn', + onPressed: _showAddProductDialog, + icon: const Icon(Icons.add), + label: const Text('Ajouter'), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ], + ), + body: Column( + children: [ + // Barre de recherche et filtres + Container( + padding: const EdgeInsets.all(16), + color: Colors.grey.shade100, + child: Column( + children: [ + // Ajoutez cette Row pour les boutons d'import + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isImporting ? null : _importFromExcel, + icon: const Icon(Icons.upload), + label: const Text('Importer depuis Excel'), + ), + ), + const SizedBox(width: 10), + TextButton( + onPressed: _isImporting ? null : _downloadExcelTemplate, + child: const Text('Modèle'), + ), + ], + ), + const SizedBox(height: 16), + + // Barre de recherche existante + Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + labelText: 'Rechercher...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.white, + ), + ), + ), + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: DropdownButton( + value: _selectedCategory, + items: _categories.map((category) => + DropdownMenuItem(value: category, child: Text(category))).toList(), + onChanged: (value) { + setState(() { + _selectedCategory = value!; + _filterProducts(); + }); + }, + underline: const SizedBox(), + hint: const Text('Catégorie'), + ), + ), + ], + ), + const SizedBox(height: 12), + + // Indicateur de progression d'importation + _buildImportProgressIndicator(), + + // Compteur de produits + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${_filteredProducts.length} produit(s) trouvé(s)', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + ), + if (_searchController.text.isNotEmpty || _selectedCategory != 'Tous') + TextButton.icon( + onPressed: () { + setState(() { + _searchController.clear(); + _selectedCategory = 'Tous'; + _filterProducts(); + }); + }, + icon: const Icon(Icons.clear, size: 16), + label: const Text('Réinitialiser'), + style: TextButton.styleFrom( + foregroundColor: Colors.orange, + ), + ), + ], + ), + ], + ), + ), + + // Liste des produits + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _filteredProducts.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.inventory_2_outlined, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + _products.isEmpty + ? 'Aucun produit enregistré' + : 'Aucun produit trouvé pour cette recherche', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade600, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + _products.isEmpty + ? 'Commencez par ajouter votre premier produit' + : 'Essayez de modifier vos critères de recherche', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + ), + if (_products.isEmpty) ...[ + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: _showAddProductDialog, + icon: const Icon(Icons.add), + label: const Text('Ajouter un produit'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ], + ), + ) + : RefreshIndicator( + onRefresh: _loadProducts, + child: ListView.builder( + itemCount: _filteredProducts.length, + itemBuilder: (context, index) { + final product = _filteredProducts[index]; + return _buildProductCard(product); + }, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/Views/addProduct.dart b/lib/Views/addProduct.dart index 85a7ed9..dcdaf3a 100644 --- a/lib/Views/addProduct.dart +++ b/lib/Views/addProduct.dart @@ -1,10 +1,13 @@ import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:excel/excel.dart' hide Border; +import 'package:flutter/services.dart'; import '../Components/appDrawer.dart'; import '../Components/app_bar.dart'; @@ -23,13 +26,20 @@ class _AddProductPageState extends State { final TextEditingController _priceController = TextEditingController(); final TextEditingController _imageController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _stockController = TextEditingController(); - final List _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux']; + final List _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux', 'Non catégorisé']; String? _selectedCategory; File? _pickedImage; String? _qrData; + String? _currentReference; // Ajout pour stocker la référence actuelle late ProductDatabase _productDatabase; + // Variables pour la barre de progression + bool _isImporting = false; + double _importProgress = 0.0; + String _importStatusText = ''; + @override void initState() { super.initState(); @@ -45,18 +55,35 @@ class _AddProductPageState extends State { _priceController.dispose(); _imageController.dispose(); _descriptionController.dispose(); + _stockController.dispose(); super.dispose(); } + // Méthode pour générer une référence unique + String _generateUniqueReference() { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0'); + return 'PROD_${timestamp}${randomSuffix}'; + } + void _updateQrData() { - if (_nameController.text.isNotEmpty) { - final reference = 'PROD_PREVIEW_${_nameController.text}_${DateTime.now().millisecondsSinceEpoch}'; - setState(() { - _qrData = 'https://tonsite.com/$reference'; - }); + if (_nameController.text.isNotEmpty) { + // Générer une nouvelle référence si elle n'existe pas encore + if (_currentReference == null) { + _currentReference = _generateUniqueReference(); } + + setState(() { + // Utiliser la référence courante dans l'URL du QR code + _qrData = 'https://stock.guycom.mg/$_currentReference'; + }); + } else { + setState(() { + _currentReference = null; + _qrData = null; + }); } - +} Future _selectImage() async { final result = await FilePicker.platform.pickFiles(type: FileType.image); if (result != null && result.files.single.path != null) { @@ -67,69 +94,488 @@ class _AddProductPageState extends State { } } - Future _generateAndSaveQRCode(String reference) async { - final validation = QrValidator.validate( - data: 'https://tonsite.com/$reference', - version: QrVersions.auto, - errorCorrectionLevel: QrErrorCorrectLevel.L, - ); + // Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée : +Future _generateAndSaveQRCode(String reference) async { + final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre reference + + final validation = QrValidator.validate( + data: qrUrl, + version: QrVersions.auto, + errorCorrectionLevel: QrErrorCorrectLevel.L, + ); - final qrCode = validation.qrCode!; - final painter = QrPainter.withQr( - qr: qrCode, - color: Colors.black, - emptyColor: Colors.white, - gapless: true, - ); + if (validation.status != QrValidationStatus.valid) { + throw Exception('Données QR invalides: ${validation.error}'); + } - final directory = await getApplicationDocumentsDirectory(); - final path = '${directory.path}/$reference.png'; - final picData = await painter.toImageData(2048, format: ImageByteFormat.png); - await File(path).writeAsBytes(picData!.buffer.asUint8List()); + final qrCode = validation.qrCode!; + final painter = QrPainter.withQr( + qr: qrCode, + color: Colors.black, + emptyColor: Colors.white, + gapless: true, + ); - return path; + final directory = await getApplicationDocumentsDirectory(); + final path = '${directory.path}/$reference.png'; // Utilise le paramètre reference + + try { + final picData = await painter.toImageData(2048, format: ImageByteFormat.png); + if (picData != null) { + await File(path).writeAsBytes(picData.buffer.asUint8List()); + } else { + throw Exception('Impossible de générer l\'image QR'); + } + } catch (e) { + throw Exception('Erreur lors de la génération du QR code: $e'); } + return path; +} + void _addProduct() async { - final name = _nameController.text.trim(); - final price = double.tryParse(_priceController.text.trim()) ?? 0.0; - final image = _imageController.text.trim(); - final category = _selectedCategory; - final description = _descriptionController.text.trim(); - - if (name.isEmpty || price <= 0 || image.isEmpty || category == null) { - Get.snackbar('Erreur', 'Veuillez remplir tous les champs requis'); - return; - } + final name = _nameController.text.trim(); + final price = double.tryParse(_priceController.text.trim()) ?? 0.0; + final image = _imageController.text.trim(); + final category = _selectedCategory ?? 'Non catégorisé'; + final description = _descriptionController.text.trim(); + final stock = int.tryParse(_stockController.text.trim()) ?? 0; - final reference = 'PROD_${DateTime.now().millisecondsSinceEpoch}'; - final qrPath = await _generateAndSaveQRCode(reference); - - final product = Product( - name: name, - price: price, - image: image, - category: category, - description: description, - qrCode: qrPath, - reference: reference, - ); + if (name.isEmpty || price <= 0) { + Get.snackbar('Erreur', 'Nom et prix sont obligatoires'); + return; + } + + // Utiliser la référence générée ou en créer une nouvelle + String finalReference = _currentReference ?? _generateUniqueReference(); + + // Vérifier l'unicité de la référence en base + var existingProduct = await _productDatabase.getProductByReference(finalReference); + + // Si la référence existe déjà, en générer une nouvelle + while (existingProduct != null) { + finalReference = _generateUniqueReference(); + existingProduct = await _productDatabase.getProductByReference(finalReference); + } + + // Mettre à jour la référence courante avec la référence finale + _currentReference = finalReference; + + // Générer le QR code avec la référence finale + final qrPath = await _generateAndSaveQRCode(finalReference); + final product = Product( + name: name, + price: price, + image: image, + category: category, + description: description, + qrCode: qrPath, + reference: finalReference, // Utiliser la référence finale + stock: stock, + ); + + try { + await _productDatabase.createProduct(product); + Get.snackbar('Succès', 'Produit ajouté avec succès\nRéférence: $finalReference'); + + setState(() { + _nameController.clear(); + _priceController.clear(); + _imageController.clear(); + _descriptionController.clear(); + _stockController.clear(); + _selectedCategory = null; + _pickedImage = null; + _qrData = null; + _currentReference = null; // Reset de la référence + }); + } catch (e) { + Get.snackbar('Erreur', 'Ajout du produit échoué : $e'); + print(e); + } +} + // Méthode pour réinitialiser l'état d'importation + void _resetImportState() { + setState(() { + _isImporting = false; + _importProgress = 0.0; + _importStatusText = ''; + }); + } + + Future _importFromExcel() async { + try { - await _productDatabase.createProduct(product); - Get.snackbar('Succès', 'Produit ajouté avec succès'); + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xlsx', 'xls','csv'], + allowMultiple: false, + ); + + if (result == null || result.files.isEmpty) { + Get.snackbar('Annulé', 'Aucun fichier sélectionné'); + return; + } + + // Démarrer la progression + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Lecture du fichier...'; + }); + + final file = File(result.files.single.path!); + + // Vérifier que le fichier existe + if (!await file.exists()) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas'); + return; + } + + setState(() { + _importProgress = 0.1; + _importStatusText = 'Vérification du fichier...'; + }); + + final bytes = await file.readAsBytes(); + + // Vérifier que le fichier n'est pas vide + if (bytes.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel est vide'); + return; + } + + setState(() { + _importProgress = 0.2; + _importStatusText = 'Décodage du fichier Excel...'; + }); + + Excel excel; + try { + // Initialisation + setState(() { + _isImporting = true; + _importProgress = 0.0; + _importStatusText = 'Initialisation...'; + }); + + // Petit délai pour permettre au build de s'exécuter + await Future.delayed(Duration(milliseconds: 50)); + excel = Excel.decodeBytes(bytes); + } catch (e) { + _resetImportState(); + debugPrint('Erreur décodage Excel: $e'); + + if (e.toString().contains('styles') || e.toString().contains('Damaged')) { + _showExcelCompatibilityError(); + return; + } else { + Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.'); + return; + } + } + + if (excel.tables.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille'); + return; + } setState(() { - _nameController.clear(); - _priceController.clear(); - _imageController.clear(); - _descriptionController.clear(); - _selectedCategory = null; - _pickedImage = null; - _qrData = null; + _importProgress = 0.3; + _importStatusText = 'Analyse des données...'; }); + + int successCount = 0; + int errorCount = 0; + List errorMessages = []; + + // Prendre la première feuille disponible + final sheetName = excel.tables.keys.first; + final sheet = excel.tables[sheetName]!; + + if (sheet.rows.isEmpty) { + _resetImportState(); + Get.snackbar('Erreur', 'La feuille Excel est vide'); + return; + } + + final totalRows = sheet.rows.length - 1; // -1 pour exclure l'en-tête + + setState(() { + _importStatusText = 'Importation en cours... (0/$totalRows)'; + }); + + // Ignorer la première ligne (en-têtes) et traiter les données + for (var i = 1; i < sheet.rows.length; i++) { + try { + // Mettre à jour la progression + final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows); + setState(() { + _importProgress = currentProgress; + _importStatusText = 'Importation en cours... (${i - 1}/$totalRows)'; + }); + + // Petite pause pour permettre à l'UI de se mettre à jour + await Future.delayed(const Duration(milliseconds: 10)); + + final row = sheet.rows[i]; + + // Vérifier que la ligne a au moins les colonnes obligatoires (nom et prix) + if (row.isEmpty || row.length < 2) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Données insuffisantes'); + continue; + } + + // Extraire les valeurs avec vérifications sécurisées + final nameCell = row[0]; + final priceCell = row[1]; + + // Extraction sécurisée des valeurs + String? nameValue; + String? priceValue; + + if (nameCell?.value != null) { + nameValue = nameCell!.value.toString().trim(); + } + + if (priceCell?.value != null) { + priceValue = priceCell!.value.toString().trim(); + } + + if (nameValue == null || nameValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Nom du produit manquant'); + continue; + } + + if (priceValue == null || priceValue.isEmpty) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix manquant'); + continue; + } + + final name = nameValue; + // Remplacer les virgules par des points pour les décimaux + final price = double.tryParse(priceValue.replaceAll(',', '.')); + + if (price == null || price <= 0) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)'); + continue; + } + + // Extraire les autres colonnes optionnelles de manière sécurisée + String category = 'Non catégorisé'; + if (row.length > 2 && row[2]?.value != null) { + final categoryValue = row[2]!.value.toString().trim(); + if (categoryValue.isNotEmpty) { + category = categoryValue; + } + } + + String description = ''; + if (row.length > 3 && row[3]?.value != null) { + description = row[3]!.value.toString().trim(); + } + + int stock = 0; + if (row.length > 4 && row[4]?.value != null) { + final stockStr = row[4]!.value.toString().trim(); + stock = int.tryParse(stockStr) ?? 0; + } + + // Générer une référence unique et vérifier son unicité + String reference = _generateUniqueReference(); + + // Vérifier l'unicité en base de données + var existingProduct = await _productDatabase.getProductByReference(reference); + while (existingProduct != null) { + reference = _generateUniqueReference(); + existingProduct = await _productDatabase.getProductByReference(reference); + } + + // Créer le produit + final product = Product( + name: name, + price: price, + image: '', // Pas d'image lors de l'import + category: category, + description: description, + stock: stock, + qrCode: '', // Sera généré après + reference: reference, + ); + + // Générer et sauvegarder le QR code avec la nouvelle URL + setState(() { + _importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)'; + }); + + final qrPath = await _generateAndSaveQRCode(reference); + product.qrCode = qrPath; + + // Sauvegarder en base de données + await _productDatabase.createProduct(product); + successCount++; + + } catch (e) { + errorCount++; + errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e'); + debugPrint('Erreur ligne ${i + 1}: $e'); + } + } + + // Finalisation + setState(() { + _importProgress = 1.0; + _importStatusText = 'Finalisation...'; + }); + + await Future.delayed(const Duration(milliseconds: 500)); + + // Réinitialiser l'état d'importation + _resetImportState(); + + // Afficher le résultat + String message = '$successCount produits importés avec succès'; + if (errorCount > 0) { + message += ', $errorCount erreurs'; + + // Afficher les détails des erreurs si pas trop nombreuses + if (errorMessages.length <= 5) { + message += ':\n${errorMessages.join('\n')}'; + } + } + + Get.snackbar( + 'Importation terminée', + message, + duration: const Duration(seconds: 6), + colorText: Colors.white, + backgroundColor: successCount > 0 ? Colors.green : Colors.orange, + ); + } catch (e) { - Get.snackbar('Erreur', 'Ajout du produit échoué : $e'); + _resetImportState(); + Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e'); + debugPrint('Erreur générale import Excel: $e'); + } + } + + void _showExcelCompatibilityError() { + Get.dialog( + AlertDialog( + title: const Text('Fichier Excel incompatible'), + content: const Text( + 'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n' + 'Solutions recommandées :\n' + '• Téléchargez notre modèle Excel et copiez-y vos données\n' + '• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n' + '• Ou créez un nouveau fichier Excel simple sans formatage complexe' + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () { + Get.back(); + _downloadExcelTemplate(); + }, + child: const Text('Télécharger modèle'), + style: TextButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ], + ), + ); + } + + Future _downloadExcelTemplate() async { + try { + // Créer un fichier Excel temporaire comme modèle + final excel = Excel.createExcel(); + + // Supprimer la feuille par défaut et créer une nouvelle + excel.delete('Sheet1'); + excel.copy('Sheet1', 'Produits'); + excel.delete('Sheet1'); + + final sheet = excel['Produits']; + + // Ajouter les en-têtes avec du style + final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock']; + for (int i = 0; i < headers.length; i++) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)); + cell.value = headers[i]; + cell.cellStyle = CellStyle( + bold: true, + backgroundColorHex: '#E8F4FD', + ); + } + + // Ajouter des exemples + final examples = [ + ['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'], + ['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'], + ['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'], + ['Gâteau chocolat', '18.00', 'Gateaux', 'Gâteau au chocolat portion 8 personnes', '5'], + ]; + + for (int row = 0; row < examples.length; row++) { + for (int col = 0; col < examples[row].length; col++) { + final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1)); + cell.value = examples[row][col]; + } + } + + // Ajuster la largeur des colonnes + sheet.setColWidth(0, 20); // Nom + sheet.setColWidth(1, 10); // Prix + sheet.setColWidth(2, 15); // Catégorie + sheet.setColWidth(3, 30); // Description + sheet.setColWidth(4, 10); // Stock + + // Sauvegarder en mémoire + final bytes = excel.save(); + + if (bytes == null) { + Get.snackbar('Erreur', 'Impossible de créer le fichier modèle'); + return; + } + + // Demander où sauvegarder + final String? outputFile = await FilePicker.platform.saveFile( + fileName: 'modele_import_produits.xlsx', + allowedExtensions: ['xlsx'], + type: FileType.custom, + ); + + if (outputFile != null) { + try { + await File(outputFile).writeAsBytes(bytes); + Get.snackbar( + 'Succès', + 'Modèle téléchargé avec succès\n$outputFile', + duration: const Duration(seconds: 4), + backgroundColor: Colors.green, + colorText: Colors.white, + ); + } catch (e) { + Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e'); + } + } + } catch (e) { + Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e'); + debugPrint('Erreur création modèle Excel: $e'); } } @@ -148,12 +594,18 @@ class _AddProductPageState extends State { return Container( width: 100, height: 100, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.image, size: 32, color: Colors.grey), + Text('Aucune image', style: TextStyle(color: Colors.grey)), + ], + ), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(8.0), - ), - child: const Icon(Icons.image, size: 48, color: Colors.grey), - ); + + )); } } @@ -169,49 +621,161 @@ class _AddProductPageState extends State { children: [ const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)), const SizedBox(height: 16), + + // Boutons d'importation + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _isImporting ? null : _importFromExcel, + icon: const Icon(Icons.upload), + label: const Text('Importer depuis Excel'), + ), + ), + const SizedBox(width: 10), + TextButton( + onPressed: _isImporting ? null : _downloadExcelTemplate, + child: const Text('Modèle'), + ), + ], + ), + const SizedBox(height: 16), + + // Barre de progression + if (_isImporting) ...[ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Importation en cours...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: _importProgress, + backgroundColor: Colors.blue.shade100, + valueColor: AlwaysStoppedAnimation(Colors.blue.shade600), + ), + const SizedBox(height: 8), + Text( + _importStatusText, + style: TextStyle( + fontSize: 14, + color: Colors.blue.shade700, + ), + ), + const SizedBox(height: 8), + Text( + '${(_importProgress * 100).round()}%', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.blue.shade600, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + + const Divider(), + const SizedBox(height: 16), + + // Formulaire d'ajout manuel TextField( controller: _nameController, - decoration: const InputDecoration(labelText: 'Nom du produit', border: OutlineInputBorder()), + enabled: !_isImporting, + decoration: const InputDecoration( + labelText: 'Nom du produit*', + border: OutlineInputBorder(), + ), ), const SizedBox(height: 16), + TextField( controller: _priceController, + enabled: !_isImporting, keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: const InputDecoration(labelText: 'Prix', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Prix*', + border: OutlineInputBorder(), + ), ), const SizedBox(height: 16), + + TextField( + controller: _stockController, + enabled: !_isImporting, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Stock', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + + // Section image (optionnelle) Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: _imageController, - decoration: const InputDecoration(labelText: 'Chemin de l\'image', border: OutlineInputBorder()), + enabled: !_isImporting, + decoration: const InputDecoration( + labelText: 'Chemin de l\'image (optionnel)', + border: OutlineInputBorder(), + ), readOnly: true, ), ), const SizedBox(width: 8), - ElevatedButton(onPressed: _selectImage, child: const Text('Sélectionner')), + ElevatedButton( + onPressed: _isImporting ? null : _selectImage, + child: const Text('Sélectionner'), + ), ], ), const SizedBox(height: 16), _displayImage(), const SizedBox(height: 16), + DropdownButtonFormField( value: _selectedCategory, items: _categories .map((c) => DropdownMenuItem(value: c, child: Text(c))) .toList(), - onChanged: (value) => setState(() => _selectedCategory = value), - decoration: const InputDecoration(labelText: 'Catégorie', border: OutlineInputBorder()), + onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value), + decoration: const InputDecoration( + labelText: 'Catégorie', + border: OutlineInputBorder(), + ), ), const SizedBox(height: 16), + TextField( controller: _descriptionController, + enabled: !_isImporting, maxLines: 3, - decoration: const InputDecoration(labelText: 'Description', border: OutlineInputBorder()), + decoration: const InputDecoration( + labelText: 'Description (optionnel)', + border: OutlineInputBorder(), + ), ), const SizedBox(height: 16), + if (_qrData != null) ...[ const Text('Aperçu du QR Code :'), const SizedBox(height: 8), @@ -222,12 +786,21 @@ class _AddProductPageState extends State { size: 120, ), ), + const SizedBox(height: 8), + Center( + child: Text( + _qrData!, + style: const TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ), ], const SizedBox(height: 24), + SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: _addProduct, + onPressed: _isImporting ? null : _addProduct, child: const Text('Ajouter le produit'), ), ), @@ -236,4 +809,4 @@ class _AddProductPageState extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/Views/gestionProduct.dart b/lib/Views/gestionProduct.dart index 30dc8bc..a6f726f 100644 --- a/lib/Views/gestionProduct.dart +++ b/lib/Views/gestionProduct.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:youmazgestion/Components/app_bar.dart'; - import '../Components/appDrawer.dart'; import '../Models/produit.dart'; import '../Services/productDatabase.dart'; @@ -11,7 +10,7 @@ import 'dart:io'; class GestionProduit extends StatelessWidget { final ProductDatabase _productDatabase = ProductDatabase.instance; - GestionProduit({super.key}); + GestionProduit({super.key}); @override Widget build(BuildContext context) { @@ -19,28 +18,22 @@ class GestionProduit extends StatelessWidget { return Scaffold( appBar: const CustomAppBar(title: 'Gestion des produits'), - drawer: CustomDrawer(), + drawer: CustomDrawer(), body: FutureBuilder>( future: _productDatabase.getProducts(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { - return const Center( - child: Text('Une erreur s\'est produite'), - ); + return const Center(child: Text('Une erreur s\'est produite')); } final products = snapshot.data; if (products == null || products.isEmpty) { - return const Center( - child: Text('Aucun produit disponible'), - ); + return const Center(child: Text('Aucun produit disponible')); } return ListView.builder( @@ -52,68 +45,30 @@ class GestionProduit extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Colors.grey, - width: 1.0, - ), + border: Border.all(color: Colors.grey, width: 1.0), ), child: ListTile( - leading: CircleAvatar( - backgroundImage: product.image != null - ? FileImage(File(product.image)) as ImageProvider< - Object> // Charger l'image à partir du chemin d'accès - : const AssetImage( - 'assets/placeholder_image.png'), // Image de substitution si le chemin d'accès est vide - ), + leading: _buildProductImage(product.image), title: Text(product.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Price: \$${product.price.toStringAsFixed(2)}'), - Text('Category: ${product.category}'), + Text('Prix: \$${product.price.toStringAsFixed(2)}'), + Text('Catégorie: ${product.category}'), + if (product.stock != null) + Text('Stock: ${product.stock}'), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( - onPressed: () { - _productDatabase - .deleteProduct(product.id) - .then((value) { - Get.snackbar( - 'Produit supprimé', - 'Le produit a été supprimé avec succès', - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - }); - }, + onPressed: () => _deleteProduct(product.id), icon: const Icon(Icons.delete), color: Colors.red, ), IconButton( - onPressed: () { - Get.to(EditProductPage(product: product)) - ?.then((result) { - if (result != null && result is Product) { - _productDatabase - .updateProduct(result) - .then((value) { - Get.snackbar( - 'Produit mis à jour', - 'Le produit a été mis à jour avec succès', - snackPosition: SnackPosition.TOP, - duration: const Duration(seconds: 3), - backgroundColor: Colors.green, - colorText: Colors.white, - ); - }); - } - }); - }, + onPressed: () => _editProduct(product), icon: const Icon(Icons.edit), color: Colors.blue, ), @@ -127,4 +82,47 @@ class GestionProduit extends StatelessWidget { ), ); } -} + + Widget _buildProductImage(String? imagePath) { + if (imagePath != null && imagePath.isNotEmpty && File(imagePath).existsSync()) { + return CircleAvatar( + backgroundImage: FileImage(File(imagePath)), + ); + } else { + return const CircleAvatar( + backgroundColor: Colors.grey, + child: Icon(Icons.shopping_bag, color: Colors.white), + ); + } + } + + void _deleteProduct(int? id) { + _productDatabase.deleteProduct(id).then((value) { + Get.snackbar( + 'Produit supprimé', + 'Le produit a été supprimé avec succès', + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }); + } + + void _editProduct(Product product) { + Get.to(() => EditProductPage(product: product))?.then((result) { + if (result != null && result is Product) { + _productDatabase.updateProduct(result).then((value) { + Get.snackbar( + 'Produit mis à jour', + 'Le produit a été mis à jour avec succès', + snackPosition: SnackPosition.TOP, + duration: const Duration(seconds: 3), + backgroundColor: Colors.green, + colorText: Colors.white, + ); + }); + } + }); + } +} \ No newline at end of file diff --git a/lib/Views/produitsCard.dart b/lib/Views/produitsCard.dart index 9c262b2..efc9ca1 100644 --- a/lib/Views/produitsCard.dart +++ b/lib/Views/produitsCard.dart @@ -114,7 +114,7 @@ class _ProductCardState extends State with TickerProviderStateMixin ), child: widget.product.image != null ? Image.file( - File(widget.product.image), + File(widget.product.image!), fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return _buildPlaceholderImage(); diff --git a/lib/accueil.dart b/lib/accueil.dart index 189058d..e0dfcd3 100644 --- a/lib/accueil.dart +++ b/lib/accueil.dart @@ -47,6 +47,30 @@ class _AccueilPageState extends State { initwork(); loadUserData(); productsFuture = _initDatabaseAndFetchProducts(); + _initializeRegister(); + super.initState(); + _initializeDatabases(); + loadUserData(); + productsFuture = _initDatabaseAndFetchProducts(); + } + + +Future _initializeDatabases() async { + await orderDatabase.initDatabase(); + await workDatabase.initDatabase(); // Attendre l'initialisation complète + await _initializeRegister(); +} + + Future _initializeRegister() async { + if (!MyApp.isRegisterOpen) { + setState(() { + MyApp.isRegisterOpen = true; + String formattedDate = DateFormat('yyyy-MM-dd').format(DateTime.now()); + startDate = DateFormat('yyyy-MM-dd').parse(formattedDate); + MyApp.startDate = startDate; + workDatabase.insertDate(formattedDate); + }); + } } Future loadUserData() async { @@ -169,11 +193,16 @@ class _AccueilPageState extends State { ); } } + @override + @override Widget build(BuildContext context) { return Scaffold( - appBar: const CustomAppBar(title: "Accueil"), + appBar: CustomAppBar( + title: "Accueil", + subtitle: Text('Bienvenue $username ! (Rôle: $role)'), + ), drawer: CustomDrawer(), body: ParticleBackground( child: Container( @@ -181,8 +210,7 @@ class _AccueilPageState extends State { gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, - colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)], - ), + colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)]), ), child: FutureBuilder>>( future: productsFuture, @@ -195,146 +223,176 @@ class _AccueilPageState extends State { final productsByCategory = snapshot.data!; final categories = productsByCategory.keys.toList(); - if (!MyApp.isRegisterOpen) { - return Column( - children: [ - Text('Bienvenue $username ! (Rôle: $role)'), - Center( - child: ElevatedButton( - onPressed: () { - setState(() { - MyApp.isRegisterOpen = true; - String formattedDate = - DateFormat('yyyy-MM-dd').format(DateTime.now()); - startDate = - DateFormat('yyyy-MM-dd').parse(formattedDate); - MyApp.startDate = startDate; - workDatabase.insertDate(formattedDate); - }); - }, - child: const Text('Démarrer la caisse'), - ), - ), - ], - ); - } else { - return Row( - children: [ - Expanded( - flex: 3, - child: ListView.builder( - itemCount: categories.length, - itemBuilder: (context, index) { - final category = categories[index]; - final products = productsByCategory[category]!; + return Row( + children: [ + Expanded( + flex: 3, + child: ListView.builder( + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + final products = productsByCategory[category]!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - category, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + category, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, ), ), ), - GridView.builder( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 4, - childAspectRatio: 1, - ), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: products.length, - itemBuilder: (context, index) { - final product = products[index]; - return ProductCard( - product: product, - onAddToCart: (product, quantity) { - addToCartWithDetails(product, quantity); - }, - ); - }, + ), + GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + childAspectRatio: 1, ), - ], - ); - }, - ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return ProductCard( + product: product, + onAddToCart: (product, quantity) { + addToCartWithDetails(product, quantity); + }, + ); + }, + ), + ], + ); + }, ), - Expanded( - flex: 1, - child: Container( + ), + Expanded( + flex: 1, + child: Container( + decoration: BoxDecoration( color: Colors.grey[200], - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text( - 'Panier', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Panier', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 16), - Expanded( - child: selectedProducts.isEmpty - ? const Center( - child: Text("Votre panier est vide"), - ) - : ListView.builder( - itemCount: selectedProducts.length, - itemBuilder: (context, index) { - final cartItem = selectedProducts[index]; - return ListTile( - title: Text(cartItem.product.name), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Expanded( + child: selectedProducts.isEmpty + ? const Center( + child: Text( + "Votre panier est vide", + style: TextStyle(fontSize: 16), + ), + ) + : ListView.builder( + itemCount: selectedProducts.length, + itemBuilder: (context, index) { + final cartItem = selectedProducts[index]; + return Card( + margin: const EdgeInsets.symmetric( + vertical: 4), + child: ListTile( + title: Text( + cartItem.product.name, + style: const TextStyle( + fontWeight: FontWeight.bold), + ), subtitle: Text( - '${cartItem.product.price} FCFA x ${cartItem.quantity}'), + '${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}', + style: const TextStyle( + fontSize: 14), + ), trailing: IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(Icons.delete, + color: Colors.red), onPressed: () { setState(() { - selectedProducts.removeAt(index); + selectedProducts + .removeAt(index); }); }, ), - ); - }, - ), - ), - Text( - 'Total: ${calculateTotalPrice().toStringAsFixed(2)} FCFA', + ), + ); + }, + ), + ), + const Divider(thickness: 1), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + 'Total: ${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), + textAlign: TextAlign.center, ), - TextField( - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Montant payé', + ), + TextField( + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Montant payé', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), ), - onChanged: (value) { - amountPaid = double.tryParse(value) ?? 0; - }, + filled: true, + fillColor: Colors.white, ), - ElevatedButton( - onPressed: saveOrderToDatabase, - child: const Text('Valider la commande'), + onChanged: (value) { + amountPaid = double.tryParse(value) ?? 0; + }, + ), + const SizedBox(height: 16), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), - ], - ), + onPressed: saveOrderToDatabase, + child: const Text( + 'Valider la commande', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white), + ), + ), + ], ), ), - ], - ); - } + ), + ], + ); } else { return const Center(child: Text("Aucun produit disponible")); } diff --git a/pubspec.lock b/pubspec.lock index 3a5ccee..cf80165 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + excel: + dependency: "direct main" + description: + name: excel + sha256: f6a76fff6ac14f48fd44a6528e72705965e02cbc593e00427ab1d9a9f5d3bffa + url: "https://pub.dev" + source: hosted + version: "2.1.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b7ffea9..c9f6aeb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,8 @@ dependencies: particles_fly: ^0.0.8 qr_flutter: ^4.0.0 path_provider: ^2.0.15 - shared_preferences: ^2.2.2 + shared_preferences: ^2.2.2 + excel: ^2.0.1