|
|
|
@ -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<AddProductPage> { |
|
|
|
final TextEditingController _priceController = TextEditingController(); |
|
|
|
final TextEditingController _imageController = TextEditingController(); |
|
|
|
final TextEditingController _descriptionController = TextEditingController(); |
|
|
|
final TextEditingController _stockController = TextEditingController(); |
|
|
|
|
|
|
|
final List<String> _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux']; |
|
|
|
final List<String> _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<AddProductPage> { |
|
|
|
_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<void> _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<AddProductPage> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Future<String> _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<String> _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<void> _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<String> 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<void> _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<AddProductPage> { |
|
|
|
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<AddProductPage> { |
|
|
|
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<Color>(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<String>( |
|
|
|
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<AddProductPage> { |
|
|
|
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<AddProductPage> { |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |