10 changed files with 3330 additions and 1371 deletions
@ -0,0 +1,191 @@ |
|||
// Models/client.dart |
|||
class Client { |
|||
final int? id; |
|||
final String nom; |
|||
final String prenom; |
|||
final String email; |
|||
final String telephone; |
|||
final String? adresse; |
|||
final DateTime dateCreation; |
|||
final bool actif; |
|||
|
|||
Client({ |
|||
this.id, |
|||
required this.nom, |
|||
required this.prenom, |
|||
required this.email, |
|||
required this.telephone, |
|||
this.adresse, |
|||
required this.dateCreation, |
|||
this.actif = true, |
|||
}); |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'nom': nom, |
|||
'prenom': prenom, |
|||
'email': email, |
|||
'telephone': telephone, |
|||
'adresse': adresse, |
|||
'dateCreation': dateCreation.toIso8601String(), |
|||
'actif': actif ? 1 : 0, |
|||
}; |
|||
} |
|||
|
|||
factory Client.fromMap(Map<String, dynamic> map) { |
|||
return Client( |
|||
id: map['id'], |
|||
nom: map['nom'], |
|||
prenom: map['prenom'], |
|||
email: map['email'], |
|||
telephone: map['telephone'], |
|||
adresse: map['adresse'], |
|||
dateCreation: DateTime.parse(map['dateCreation']), |
|||
actif: map['actif'] == 1, |
|||
); |
|||
} |
|||
|
|||
String get nomComplet => '$prenom $nom'; |
|||
} |
|||
|
|||
// Models/commande.dart |
|||
enum StatutCommande { |
|||
enAttente, |
|||
confirmee, |
|||
enPreparation, |
|||
expediee, |
|||
livree, |
|||
annulee |
|||
} |
|||
|
|||
class Commande { |
|||
final int? id; |
|||
final int clientId; |
|||
final DateTime dateCommande; |
|||
final StatutCommande statut; |
|||
final double montantTotal; |
|||
final String? notes; |
|||
final DateTime? dateLivraison; |
|||
|
|||
// Données du client (pour les jointures) |
|||
final String? clientNom; |
|||
final String? clientPrenom; |
|||
final String? clientEmail; |
|||
|
|||
Commande({ |
|||
this.id, |
|||
required this.clientId, |
|||
required this.dateCommande, |
|||
this.statut = StatutCommande.enAttente, |
|||
required this.montantTotal, |
|||
this.notes, |
|||
this.dateLivraison, |
|||
this.clientNom, |
|||
this.clientPrenom, |
|||
this.clientEmail, |
|||
}); |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'clientId': clientId, |
|||
'dateCommande': dateCommande.toIso8601String(), |
|||
'statut': statut.index, |
|||
'montantTotal': montantTotal, |
|||
'notes': notes, |
|||
'dateLivraison': dateLivraison?.toIso8601String(), |
|||
}; |
|||
} |
|||
|
|||
factory Commande.fromMap(Map<String, dynamic> map) { |
|||
return Commande( |
|||
id: map['id'], |
|||
clientId: map['clientId'], |
|||
dateCommande: DateTime.parse(map['dateCommande']), |
|||
statut: StatutCommande.values[map['statut']], |
|||
montantTotal: map['montantTotal'].toDouble(), |
|||
notes: map['notes'], |
|||
dateLivraison: map['dateLivraison'] != null |
|||
? DateTime.parse(map['dateLivraison']) |
|||
: null, |
|||
clientNom: map['clientNom'], |
|||
clientPrenom: map['clientPrenom'], |
|||
clientEmail: map['clientEmail'], |
|||
); |
|||
} |
|||
|
|||
String get statutLibelle { |
|||
switch (statut) { |
|||
case StatutCommande.enAttente: |
|||
return 'En attente'; |
|||
case StatutCommande.confirmee: |
|||
return 'Confirmée'; |
|||
case StatutCommande.enPreparation: |
|||
return 'En préparation'; |
|||
case StatutCommande.expediee: |
|||
return 'Expédiée'; |
|||
case StatutCommande.livree: |
|||
return 'Livrée'; |
|||
case StatutCommande.annulee: |
|||
return 'Annulée'; |
|||
} |
|||
} |
|||
|
|||
String get clientNomComplet => |
|||
clientPrenom != null && clientNom != null |
|||
? '$clientPrenom $clientNom' |
|||
: 'Client inconnu'; |
|||
} |
|||
|
|||
// Models/detail_commande.dart |
|||
class DetailCommande { |
|||
final int? id; |
|||
final int commandeId; |
|||
final int produitId; |
|||
final int quantite; |
|||
final double prixUnitaire; |
|||
final double sousTotal; |
|||
|
|||
// Données du produit (pour les jointures) |
|||
final String? produitNom; |
|||
final String? produitImage; |
|||
final String? produitReference; |
|||
|
|||
DetailCommande({ |
|||
this.id, |
|||
required this.commandeId, |
|||
required this.produitId, |
|||
required this.quantite, |
|||
required this.prixUnitaire, |
|||
required this.sousTotal, |
|||
this.produitNom, |
|||
this.produitImage, |
|||
this.produitReference, |
|||
}); |
|||
|
|||
Map<String, dynamic> toMap() { |
|||
return { |
|||
'id': id, |
|||
'commandeId': commandeId, |
|||
'produitId': produitId, |
|||
'quantite': quantite, |
|||
'prixUnitaire': prixUnitaire, |
|||
'sousTotal': sousTotal, |
|||
}; |
|||
} |
|||
|
|||
factory DetailCommande.fromMap(Map<String, dynamic> map) { |
|||
return DetailCommande( |
|||
id: map['id'], |
|||
commandeId: map['commandeId'], |
|||
produitId: map['produitId'], |
|||
quantite: map['quantite'], |
|||
prixUnitaire: map['prixUnitaire'].toDouble(), |
|||
sousTotal: map['sousTotal'].toDouble(), |
|||
produitNom: map['produitNom'], |
|||
produitImage: map['produitImage'], |
|||
produitReference: map['produitReference'], |
|||
); |
|||
} |
|||
} |
|||
@ -1,812 +0,0 @@ |
|||
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'; |
|||
import '../Models/produit.dart'; |
|||
import '../Services/productDatabase.dart'; |
|||
|
|||
class AddProductPage extends StatefulWidget { |
|||
const AddProductPage({super.key}); |
|||
|
|||
@override |
|||
_AddProductPageState createState() => _AddProductPageState(); |
|||
} |
|||
|
|||
class _AddProductPageState extends State<AddProductPage> { |
|||
final TextEditingController _nameController = TextEditingController(); |
|||
final TextEditingController _priceController = TextEditingController(); |
|||
final TextEditingController _imageController = TextEditingController(); |
|||
final TextEditingController _descriptionController = TextEditingController(); |
|||
final TextEditingController _stockController = TextEditingController(); |
|||
|
|||
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(); |
|||
_productDatabase = ProductDatabase.instance; |
|||
_productDatabase.initDatabase(); |
|||
_nameController.addListener(_updateQrData); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_nameController.removeListener(_updateQrData); |
|||
_nameController.dispose(); |
|||
_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) { |
|||
// 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) { |
|||
setState(() { |
|||
_pickedImage = File(result.files.single.path!); |
|||
_imageController.text = _pickedImage!.path; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// 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, |
|||
); |
|||
|
|||
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'; // 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 ?? 'Non catégorisé'; |
|||
final description = _descriptionController.text.trim(); |
|||
final stock = int.tryParse(_stockController.text.trim()) ?? 0; |
|||
|
|||
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 { |
|||
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(() { |
|||
_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) { |
|||
_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'); |
|||
} |
|||
} |
|||
|
|||
Widget _displayImage() { |
|||
if (_pickedImage != null) { |
|||
return ClipRRect( |
|||
borderRadius: BorderRadius.circular(8.0), |
|||
child: Image.file( |
|||
_pickedImage!, |
|||
width: 100, |
|||
height: 100, |
|||
fit: BoxFit.cover, |
|||
), |
|||
); |
|||
} else { |
|||
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), |
|||
|
|||
)); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: const CustomAppBar(title: 'Ajouter un produit'), |
|||
drawer: CustomDrawer(), |
|||
body: SingleChildScrollView( |
|||
padding: const EdgeInsets.all(16), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
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, |
|||
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(), |
|||
), |
|||
), |
|||
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, |
|||
enabled: !_isImporting, |
|||
decoration: const InputDecoration( |
|||
labelText: 'Chemin de l\'image (optionnel)', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
readOnly: true, |
|||
), |
|||
), |
|||
const SizedBox(width: 8), |
|||
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: _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 (optionnel)', |
|||
border: OutlineInputBorder(), |
|||
), |
|||
), |
|||
const SizedBox(height: 16), |
|||
|
|||
if (_qrData != null) ...[ |
|||
const Text('Aperçu du QR Code :'), |
|||
const SizedBox(height: 8), |
|||
Center( |
|||
child: QrImageView( |
|||
data: _qrData!, |
|||
version: QrVersions.auto, |
|||
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: _isImporting ? null : _addProduct, |
|||
child: const Text('Ajouter le produit'), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,623 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:youmazgestion/Components/app_bar.dart'; |
|||
import 'package:youmazgestion/Components/appDrawer.dart'; |
|||
import 'package:youmazgestion/Models/client.dart'; |
|||
import 'package:youmazgestion/Models/produit.dart'; |
|||
import 'package:youmazgestion/Services/productDatabase.dart'; |
|||
|
|||
class NouvelleCommandePage extends StatefulWidget { |
|||
const NouvelleCommandePage({super.key}); |
|||
|
|||
@override |
|||
_NouvelleCommandePageState createState() => _NouvelleCommandePageState(); |
|||
} |
|||
|
|||
class _NouvelleCommandePageState extends State<NouvelleCommandePage> { |
|||
final ProductDatabase _database = ProductDatabase.instance; |
|||
final _formKey = GlobalKey<FormState>(); |
|||
|
|||
// Informations client |
|||
final TextEditingController _nomController = TextEditingController(); |
|||
final TextEditingController _prenomController = TextEditingController(); |
|||
final TextEditingController _emailController = TextEditingController(); |
|||
final TextEditingController _telephoneController = TextEditingController(); |
|||
final TextEditingController _adresseController = TextEditingController(); |
|||
|
|||
// Panier |
|||
final List<Product> _products = []; |
|||
final Map<int, int> _quantites = {}; // productId -> quantity |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadProducts(); |
|||
} |
|||
|
|||
Future<void> _loadProducts() async { |
|||
final products = await _database.getProducts(); |
|||
setState(() { |
|||
_products.addAll(products); |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
appBar: const CustomAppBar(title: 'Nouvelle Commande'), |
|||
drawer: CustomDrawer(), |
|||
body: Column( |
|||
children: [ |
|||
// Header avec logo et titre |
|||
Container( |
|||
padding: const EdgeInsets.all(16.0), |
|||
decoration: BoxDecoration( |
|||
gradient: LinearGradient( |
|||
colors: [Colors.blue.shade50, Colors.white], |
|||
begin: Alignment.topCenter, |
|||
end: Alignment.bottomCenter, |
|||
), |
|||
), |
|||
child: Column( |
|||
children: [ |
|||
// Logo et titre |
|||
Row( |
|||
children: [ |
|||
// Logo de l'entreprise |
|||
Container( |
|||
width: 50, |
|||
height: 50, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(8), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.1), |
|||
blurRadius: 4, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(8), |
|||
child: Image.asset( |
|||
'assets/logo.png', |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade800, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon( |
|||
Icons.shopping_cart, |
|||
color: Colors.white, |
|||
size: 30, |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
), |
|||
const SizedBox(width: 12), |
|||
const Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Nouvelle Commande', |
|||
style: TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
Text( |
|||
'Créez une nouvelle commande pour un client', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.grey, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
// Contenu principal |
|||
Expanded( |
|||
child: SingleChildScrollView( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
children: [ |
|||
_buildClientForm(), |
|||
const SizedBox(height: 20), |
|||
_buildProductList(), |
|||
const SizedBox(height: 20), |
|||
_buildCartSection(), |
|||
const SizedBox(height: 20), |
|||
_buildTotalSection(), |
|||
const SizedBox(height: 20), |
|||
_buildSubmitButton(), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildClientForm() { |
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Form( |
|||
key: _formKey, |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Informations Client', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
const SizedBox(height: 16), |
|||
TextFormField( |
|||
controller: _nomController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Nom', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer un nom'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 12), |
|||
TextFormField( |
|||
controller: _prenomController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Prénom', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer un prénom'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 12), |
|||
TextFormField( |
|||
controller: _emailController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Email', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
keyboardType: TextInputType.emailAddress, |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer un email'; |
|||
} |
|||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { |
|||
return 'Veuillez entrer un email valide'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 12), |
|||
TextFormField( |
|||
controller: _telephoneController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Téléphone', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
keyboardType: TextInputType.phone, |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer un numéro de téléphone'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
const SizedBox(height: 12), |
|||
TextFormField( |
|||
controller: _adresseController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Adresse de livraison', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
filled: true, |
|||
fillColor: Colors.white, |
|||
), |
|||
maxLines: 2, |
|||
validator: (value) { |
|||
if (value == null || value.isEmpty) { |
|||
return 'Veuillez entrer une adresse'; |
|||
} |
|||
return null; |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildProductList() { |
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Produits Disponibles', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
const SizedBox(height: 16), |
|||
_products.isEmpty |
|||
? const Center(child: CircularProgressIndicator()) |
|||
: ListView.builder( |
|||
shrinkWrap: true, |
|||
physics: const NeverScrollableScrollPhysics(), |
|||
itemCount: _products.length, |
|||
itemBuilder: (context, index) { |
|||
final product = _products[index]; |
|||
final quantity = _quantites[product.id] ?? 0; |
|||
|
|||
return Card( |
|||
margin: const EdgeInsets.symmetric(vertical: 8), |
|||
elevation: 2, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: ListTile( |
|||
contentPadding: const EdgeInsets.symmetric( |
|||
horizontal: 16, |
|||
vertical: 8, |
|||
), |
|||
leading: Container( |
|||
width: 50, |
|||
height: 50, |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon(Icons.shopping_bag, |
|||
color: Colors.blue), |
|||
), |
|||
title: Text( |
|||
product.name, |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
subtitle: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
'${product.price.toStringAsFixed(2)} DA', |
|||
style: TextStyle( |
|||
color: Colors.green.shade700, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
if (product.stock != null) |
|||
Text( |
|||
'Stock: ${product.stock}', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey.shade600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
trailing: Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
IconButton( |
|||
icon: const Icon(Icons.remove, size: 18), |
|||
onPressed: () { |
|||
if (quantity > 0) { |
|||
setState(() { |
|||
_quantites[product.id!] = quantity - 1; |
|||
}); |
|||
} |
|||
}, |
|||
), |
|||
Text( |
|||
quantity.toString(), |
|||
style: const TextStyle(fontWeight: FontWeight.bold), |
|||
), |
|||
IconButton( |
|||
icon: const Icon(Icons.add, size: 18), |
|||
onPressed: () { |
|||
if (product.stock == null || quantity < product.stock!) { |
|||
setState(() { |
|||
_quantites[product.id!] = quantity + 1; |
|||
}); |
|||
} else { |
|||
Get.snackbar( |
|||
'Stock insuffisant', |
|||
'Quantité demandée non disponible', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
); |
|||
} |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildCartSection() { |
|||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); |
|||
|
|||
if (itemsInCart.isEmpty) { |
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: const Padding( |
|||
padding: EdgeInsets.all(16.0), |
|||
child: Center( |
|||
child: Text( |
|||
'Votre panier est vide', |
|||
style: TextStyle(color: Colors.grey), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Votre Panier', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
const SizedBox(height: 16), |
|||
...itemsInCart.map((entry) { |
|||
final product = _products.firstWhere((p) => p.id == entry.key); |
|||
return Card( |
|||
margin: const EdgeInsets.only(bottom: 8), |
|||
elevation: 1, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: ListTile( |
|||
contentPadding: const EdgeInsets.symmetric( |
|||
horizontal: 16, |
|||
vertical: 8, |
|||
), |
|||
leading: Container( |
|||
width: 40, |
|||
height: 40, |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: const Icon(Icons.shopping_bag, size: 20), |
|||
), |
|||
title: Text(product.name), |
|||
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'), |
|||
trailing: Text( |
|||
'${(entry.value * product.price).toStringAsFixed(2)} DA', |
|||
style: TextStyle( |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.blue.shade800, |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildTotalSection() { |
|||
double total = 0; |
|||
_quantites.forEach((productId, quantity) { |
|||
final product = _products.firstWhere((p) => p.id == productId); |
|||
total += quantity * product.price; |
|||
}); |
|||
|
|||
return Card( |
|||
elevation: 4, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Padding( |
|||
padding: const EdgeInsets.all(16.0), |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue.shade50, |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
padding: const EdgeInsets.all(12), |
|||
child: Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total:', |
|||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), |
|||
), |
|||
Text( |
|||
'${total.toStringAsFixed(2)} DA', |
|||
style: const TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.green, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildSubmitButton() { |
|||
return ElevatedButton( |
|||
style: ElevatedButton.styleFrom( |
|||
padding: const EdgeInsets.symmetric(vertical: 16), |
|||
backgroundColor: Colors.blue.shade600, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
elevation: 4, |
|||
), |
|||
onPressed: _submitOrder, |
|||
child: const Text( |
|||
'Valider la Commande', |
|||
style: TextStyle(fontSize: 16, color: Colors.white), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Future<void> _submitOrder() async { |
|||
if (!_formKey.currentState!.validate()) { |
|||
return; |
|||
} |
|||
|
|||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); |
|||
if (itemsInCart.isEmpty) { |
|||
Get.snackbar( |
|||
'Panier vide', |
|||
'Veuillez ajouter des produits à votre commande', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
); |
|||
return; |
|||
} |
|||
|
|||
// Créer le client |
|||
final client = Client( |
|||
nom: _nomController.text, |
|||
prenom: _prenomController.text, |
|||
email: _emailController.text, |
|||
telephone: _telephoneController.text, |
|||
adresse: _adresseController.text, |
|||
dateCreation: DateTime.now(), |
|||
); |
|||
|
|||
// Calculer le total et préparer les détails |
|||
double total = 0; |
|||
final details = <DetailCommande>[]; |
|||
|
|||
for (final entry in itemsInCart) { |
|||
final product = _products.firstWhere((p) => p.id == entry.key); |
|||
total += entry.value * product.price; |
|||
|
|||
details.add(DetailCommande( |
|||
commandeId: 0, // Valeur temporaire, sera remplacée dans la transaction |
|||
produitId: product.id!, |
|||
quantite: entry.value, |
|||
prixUnitaire: product.price, |
|||
sousTotal: entry.value * product.price, |
|||
)); |
|||
} |
|||
|
|||
// Créer la commande |
|||
final commande = Commande( |
|||
clientId: 0, // sera mis à jour après création du client |
|||
dateCommande: DateTime.now(), |
|||
statut: StatutCommande.enAttente, |
|||
montantTotal: total, |
|||
notes: 'Commande passée via l\'application', |
|||
); |
|||
|
|||
try { |
|||
// Enregistrer la commande dans la base de données |
|||
await _database.createCommandeComplete(client, commande, details); |
|||
|
|||
Get.snackbar( |
|||
'Succès', |
|||
'Votre commande a été enregistrée', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.green, |
|||
colorText: Colors.white, |
|||
); |
|||
|
|||
// Réinitialiser le formulaire |
|||
_formKey.currentState!.reset(); |
|||
setState(() { |
|||
_quantites.clear(); |
|||
}); |
|||
|
|||
} catch (e) { |
|||
Get.snackbar( |
|||
'Erreur', |
|||
'Une erreur est survenue lors de l\'enregistrement de la commande: ${e.toString()}', |
|||
snackPosition: SnackPosition.BOTTOM, |
|||
backgroundColor: Colors.red, |
|||
colorText: Colors.white, |
|||
); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_nomController.dispose(); |
|||
_prenomController.dispose(); |
|||
_emailController.dispose(); |
|||
_telephoneController.dispose(); |
|||
_adresseController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue