diff --git a/lib/pages/caisse_screen.dart b/lib/pages/caisse_screen.dart index ac589db..5ae672a 100644 --- a/lib/pages/caisse_screen.dart +++ b/lib/pages/caisse_screen.dart @@ -4,6 +4,7 @@ import 'package:itrimobe/pages/facture_screen.dart'; import '../models/command_detail.dart'; import '../models/payment_method.dart'; import '../services/restaurant_api_service.dart'; +import 'package:intl/intl.dart'; class CaisseScreen extends StatefulWidget { final String commandeId; @@ -82,40 +83,50 @@ class _CaisseScreenState extends State { } // Dans caisse_screen.dart, modifiez la méthode _processPayment - Future _processPayment() async { - if (selectedPaymentMethod == null || commande == null) return; + Future _processPayment() async { + if (selectedPaymentMethod == null || commande == null) return; - setState(() => isProcessingPayment = true); + setState(() => isProcessingPayment = true); - try { - final success = await RestaurantApiService.processPayment( + try { + final success = await RestaurantApiService.processPayment( + commandeId: widget.commandeId, + paymentMethodId: selectedPaymentMethod!.id, + amount: commande!.totalTtc, + ); + + if (success) { + final updateSuccess = await RestaurantApiService.updateCommandeStatus( commandeId: widget.commandeId, - paymentMethodId: selectedPaymentMethod!.id, - amount: commande!.totalTtc, + newStatus: 'payee', ); - if (success) { - // Navigation vers la facture au lieu du dialog de succès - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: - (context) => FactureScreen( - commande: commande!, - paymentMethod: selectedPaymentMethod!.id, - ), - ), - ); - } else { - _showErrorDialog('Le paiement a échoué. Veuillez réessayer.'); - } - } catch (e) { - _showErrorDialog('Erreur lors du traitement du paiement: $e'); - } finally { - if (mounted) { - setState(() => isProcessingPayment = false); + if (!updateSuccess) { + _showErrorDialog("Paiement effectué, mais échec lors de la mise à jour du statut."); + return; } + + // 🔄 Redirige vers la facture + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => FactureScreen( + commande: commande!, + paymentMethod: selectedPaymentMethod!.id, + ), + ), + ); + } else { + _showErrorDialog('Le paiement a échoué. Veuillez réessayer.'); + } + } catch (e) { + _showErrorDialog('Erreur lors du traitement du paiement: $e'); + } finally { + if (mounted) { + setState(() => isProcessingPayment = false); } } +} + void _showErrorDialog(String message) { showDialog( @@ -148,7 +159,7 @@ class _CaisseScreenState extends State { ], ), content: Text( - 'Le paiement de ${commande!.totalTtc.toStringAsFixed(2)} MGA a été traité avec succès via ${selectedPaymentMethod!.name}.', + 'Le paiement de ${NumberFormat("#,##0.00", "fr_FR").format(commande!.totalTtc)} MGA a été traité avec succès via ${selectedPaymentMethod!.name}.', ), actions: [ TextButton( @@ -233,7 +244,7 @@ class _CaisseScreenState extends State { ), ), Text( - '${commande!.totalTtc.toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(commande!.totalTtc)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -302,7 +313,7 @@ class _CaisseScreenState extends State { ), const SizedBox(width: 12), Text( - '${item.totalItem.toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(item.totalItem)} MGA', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -405,7 +416,7 @@ class _CaisseScreenState extends State { const SizedBox(width: 16), Text( - '${amount.toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(amount)} MGA', style: const TextStyle( color: Colors.white, fontSize: 18, @@ -467,7 +478,7 @@ class _CaisseScreenState extends State { const SizedBox(width: 8), Text( selectedPaymentMethod != null - ? 'Payer ${commande?.totalTtc.toStringAsFixed(2)} MGA' + ? 'Payer ${NumberFormat("#,##0.00", "fr_FR").format(commande?.totalTtc)} MGA' : 'Sélectionnez une méthode de paiement', style: const TextStyle( fontSize: 16, diff --git a/lib/pages/cart_page.dart b/lib/pages/cart_page.dart index 6002721..45a6ec3 100644 --- a/lib/pages/cart_page.dart +++ b/lib/pages/cart_page.dart @@ -11,6 +11,7 @@ import 'package:itrimobe/pages/tables.dart'; import 'package:itrimobe/services/pdf_service.dart'; import '../layouts/main_layout.dart'; +import 'package:intl/intl.dart'; class CartPage extends StatefulWidget { final int tableId; @@ -122,7 +123,7 @@ class _CartPageState extends State { Text('• Table: ${widget.tableId}'), Text('• Personnes: ${widget.personne}'), Text('• Articles: ${_getTotalArticles()}'), - Text('• Total: ${_calculateTotal().toStringAsFixed(2)} MGA'), + Text('• Total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA'), ], ), actions: [ @@ -340,7 +341,7 @@ class _CartPageState extends State { Text('Articles: ${_cartItems.length}'), const SizedBox(height: 8), Text( - 'Total: ${total.toStringAsFixed(0)} MGA', + 'Total: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -612,7 +613,7 @@ class _CartPageState extends State { Text('Table ${widget.tablename} libérée'), const SizedBox(height: 8), Text( - 'Montant: ${total.toStringAsFixed(0)} MGA', + 'Montant: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA', style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), @@ -887,7 +888,7 @@ class _CartPageState extends State { ], ), Text( - '${item.prix.toStringAsFixed(2)} MGA l\'unité', + '${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité', style: TextStyle( fontSize: 14, color: Colors.grey[600], @@ -949,7 +950,7 @@ class _CartPageState extends State { ), // Prix total de l'article Text( - '${(item.prix * item.quantity).toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -1038,7 +1039,7 @@ class _CartPageState extends State { ), ), Text( - '${_calculateTotal().toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -1047,6 +1048,8 @@ class _CartPageState extends State { ], ), const SizedBox(height: 20), + + // Bouton Valider la commande (toujours visible) SizedBox( width: double.infinity, child: ElevatedButton( @@ -1101,62 +1104,64 @@ class _CartPageState extends State { ), ), - const SizedBox(height: 12), + // Espacement conditionnel + if (MediaQuery.of(context).size.width >= 768) const SizedBox(height: 12), - // Bouton Payer directement - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: - _cartItems.isNotEmpty && !_isValidating - ? _payerDirectement - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange[600], - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + // Bouton Payer directement (uniquement sur desktop - largeur >= 768px) + if (MediaQuery.of(context).size.width >= 768) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + _cartItems.isNotEmpty && !_isValidating + ? _payerDirectement + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange[600], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey[300], ), - disabledBackgroundColor: Colors.grey[300], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_isValidating) ...[ - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isValidating) ...[ + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), ), ), - ), - const SizedBox(width: 8), - const Text( - 'Traitement...', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + const SizedBox(width: 8), + const Text( + 'Traitement...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - ), - ] else ...[ - const Icon(Icons.payment, size: 20), - const SizedBox(width: 8), - const Text( - 'Payer directement', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + ] else ...[ + const Icon(Icons.payment, size: 20), + const SizedBox(width: 8), + const Text( + 'Payer directement', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - ), + ], ], - ], + ), ), ), - ), ], ), ), @@ -1180,4 +1185,4 @@ class CartItemModel { required this.quantity, required this.commentaire, }); -} +} \ No newline at end of file diff --git a/lib/pages/commande_item_screen.dart b/lib/pages/commande_item_screen.dart index 1d0129d..db73e48 100644 --- a/lib/pages/commande_item_screen.dart +++ b/lib/pages/commande_item_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'package:intl/intl.dart'; // Import de la page de validation (à ajuster selon votre structure de dossiers) import 'commande_item_validation.dart'; @@ -635,7 +636,7 @@ class _AddToCartModalState extends State { ), ), Text( - "${calculateTotal().toStringAsFixed(2)} MGA", + "${NumberFormat("#,##0.00", "fr_FR").format(calculateTotal())} MGA", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, diff --git a/lib/pages/commande_item_validation.dart b/lib/pages/commande_item_validation.dart index 8194210..bc0b9cb 100644 --- a/lib/pages/commande_item_validation.dart +++ b/lib/pages/commande_item_validation.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'package:intl/intl.dart'; class ValidateAddItemsPage extends StatefulWidget { final int commandeId; @@ -151,7 +152,7 @@ class _ValidateAddItemsPageState extends State { Text('• Commande: ${widget.numeroCommande}'), Text('• Nouveaux articles: ${_getTotalNewArticles()}'), Text( - '• Nouveau total: ${_calculateGrandTotal().toStringAsFixed(2)} MGA', + '• Nouveau total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateGrandTotal())} MGA', ), ], ), @@ -473,7 +474,7 @@ class _ValidateAddItemsPageState extends State { ], ), Text( - '${item.prix.toStringAsFixed(2)} MGA l\'unité', + '${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité', style: TextStyle( fontSize: 14, color: Colors.grey[600], @@ -535,7 +536,7 @@ class _ValidateAddItemsPageState extends State { ), // Prix total de l'article Text( - '${(item.prix * item.quantity).toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -584,7 +585,7 @@ class _ValidateAddItemsPageState extends State { style: TextStyle(fontSize: 16), ), Text( - '${_calculateExistingItemsTotal().toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(_calculateExistingItemsTotal())} MGA', style: TextStyle(fontSize: 16), ), ], @@ -611,7 +612,7 @@ class _ValidateAddItemsPageState extends State { style: TextStyle(fontSize: 16, color: Colors.green[700]), ), Text( - '${_calculateNewItemsTotal().toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(_calculateNewItemsTotal())} MGA', style: TextStyle(fontSize: 16, color: Colors.green[700]), ), ], @@ -630,7 +631,7 @@ class _ValidateAddItemsPageState extends State { ), ), Text( - '${_calculateGrandTotal().toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(_calculateGrandTotal())} MGA', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart index 2c3b9d0..6e978ae 100644 --- a/lib/pages/commandes_screen.dart +++ b/lib/pages/commandes_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; - +import 'package:intl/intl.dart'; import 'commande_item_screen.dart'; class OrdersManagementScreen extends StatefulWidget { @@ -291,7 +291,7 @@ class _OrdersManagementScreenState extends State { return orders .where( (order) => - order.statut == "en_attente" || order.statut == "en_preparation", + order.statut == "en_attente" || order.statut == "en_preparation" || order.statut == "prete", ) .toList(); } @@ -311,7 +311,7 @@ class _OrdersManagementScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${order.tablename} - ${order.totalTtc.toStringAsFixed(2)} MGA', + '${order.tablename} - ${NumberFormat("#,##0.00", "fr_FR").format(order.totalTtc)} MGA', ), ], ), @@ -545,6 +545,8 @@ class OrderCard extends StatelessWidget { return Colors.blue; case 'payee': return Colors.grey; + case 'prete': + return Colors.grey; default: return Colors.grey; } @@ -560,6 +562,8 @@ class OrderCard extends StatelessWidget { return 'Servie'; case 'payee': return 'Payée'; + case 'prete': + return 'prête'; default: return status; } @@ -655,7 +659,7 @@ class OrderCard extends StatelessWidget { ), ), Text( - '${(item.pu ?? 0) * item.quantite} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format((item.pu ?? 0) * item.quantite)} MGA', style: const TextStyle( fontSize: 14, color: Colors.black87, @@ -688,7 +692,7 @@ class OrderCard extends StatelessWidget { ), ), Text( - '${order.totalHt.toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(order.totalHt)} MGA', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -701,11 +705,11 @@ class OrderCard extends StatelessWidget { // Action buttons if (order.statut == 'en_attente' || - order.statut == 'en_preparation') + order.statut == 'en_preparation' || order.statut == 'prete') Row( children: [ if (order.statut == 'en_attente' || - order.statut == 'en_preparation') + order.statut == 'en_preparation' || order.statut == 'prete') Expanded( child: ElevatedButton( onPressed: @@ -787,6 +791,25 @@ class OrderCard extends StatelessWidget { ), ), const SizedBox(width: 8), + if (order.statut == 'en_preparation') + Expanded( + child: ElevatedButton( + onPressed: + () => onStatusUpdate(order, 'prete'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + 'Prête', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + const SizedBox(width: 8), Container( decoration: BoxDecoration( border: Border.all(color: Colors.red.shade200), diff --git a/lib/pages/facture_screen.dart b/lib/pages/facture_screen.dart index a561ace..99c25da 100644 --- a/lib/pages/facture_screen.dart +++ b/lib/pages/facture_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../models/command_detail.dart'; import '../services/pdf_service.dart'; +import 'package:intl/intl.dart'; class FactureScreen extends StatefulWidget { final CommandeDetail commande; @@ -218,7 +219,7 @@ class _FactureScreenState extends State { ), ), Text( - '${(item.prixUnitaire * item.quantite).toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)} AR', style: const TextStyle( fontSize: 12, color: Colors.black87, @@ -246,7 +247,7 @@ class _FactureScreenState extends State { style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( - '${widget.commande.totalTtc.toStringAsFixed(2)} MGA', + '${NumberFormat("#,##0.00", "fr_FR").format(widget.commande.totalTtc)} AR', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], @@ -267,7 +268,7 @@ class _FactureScreenState extends State { } void _printReceipt() async { - bool isPrinting; + bool isPrinting = false; setState(() => isPrinting = true); try { @@ -345,7 +346,7 @@ class _FactureScreenState extends State { _showSuccessMessage( action == 'print' ? 'Facture envoyée à l\'imprimante ${_getPlatformName()}' - : 'PDF sauvegardé et partagé', + : 'PDF sauvegardé avec succès', ); } else { _showErrorMessage( @@ -446,4 +447,4 @@ class _FactureScreenState extends State { if (Platform.isWindows) return 'Windows'; return 'cette plateforme'; } -} +} \ No newline at end of file diff --git a/lib/pages/historique_commande.dart b/lib/pages/historique_commande.dart index 7b553b7..93ada34 100644 --- a/lib/pages/historique_commande.dart +++ b/lib/pages/historique_commande.dart @@ -15,13 +15,13 @@ class _OrderHistoryPageState extends State bool isLoading = true; String? error; - // Informations de pagination + // Informations d'affichage et pagination + int totalItems = 0; int currentPage = 1; int totalPages = 1; - int totalItems = 0; - int itemsPerPage = 10; + final int itemsPerPage = 10; // Nombre d'éléments par page - final String baseUrl = 'https://restaurant.careeracademy.mg'; // Remplacez par votre URL + final String baseUrl = 'https://restaurant.careeracademy.mg'; final Map _headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -38,48 +38,187 @@ class _OrderHistoryPageState extends State _loadCommandes(); } - Future _loadCommandes() async { + Future _loadCommandes({int page = 1}) async { try { setState(() { isLoading = true; error = null; }); - final response = await http.get( - Uri.parse('$baseUrl/api/commandes?statut=payee'), - headers: _headers, - ); - - final dynamic responseBody = json.decode(response.body); - print('Réponse getCommandes: ${responseBody}'); + // Ajouter les paramètres de pagination à l'URL + final uri = Uri.parse('$baseUrl/api/commandes').replace(queryParameters: { + 'statut': 'payee', + 'page': page.toString(), + 'limit': itemsPerPage.toString(), + }); + final response = await http.get(uri, headers: _headers); + + print('=== DÉBUT DEBUG RESPONSE ==='); + print('Status Code: ${response.statusCode}'); + print('Response Body: ${response.body}'); + if (response.statusCode == 200) { - // Adapter la structure de réponse {data: [...], pagination: {...}} - final Map responseData = json.decode(response.body); - final List data = responseData['data'] ?? []; - final Map pagination = responseData['pagination'] ?? {}; + final dynamic responseBody = json.decode(response.body); + print('=== PARSED RESPONSE ==='); + print('Type: ${responseBody.runtimeType}'); + print('Content: $responseBody'); - setState(() { - commandes = data.map((json) => CommandeData.fromJson(json)).toList(); - - // Mettre à jour les informations de pagination - currentPage = pagination['currentPage'] ?? 1; - totalPages = pagination['totalPages'] ?? 1; - totalItems = pagination['totalItems'] ?? 0; - itemsPerPage = pagination['itemsPerPage'] ?? 10; + List data = []; + + // Gestion améliorée de la réponse + if (responseBody is Map) { + print('=== RESPONSE EST UN MAP ==='); + print('Keys disponibles: ${responseBody.keys.toList()}'); + // Structure: {"success": true, "data": {"commandes": [...], "pagination": {...}}} + if (responseBody.containsKey('data') && responseBody['data'] is Map) { + final dataMap = responseBody['data'] as Map; + print('=== DATA MAP TROUVÉ ==='); + print('Data keys: ${dataMap.keys.toList()}'); + + if (dataMap.containsKey('commandes')) { + final commandesValue = dataMap['commandes']; + print('=== COMMANDES TROUVÉES ==='); + print('Type commandes: ${commandesValue.runtimeType}'); + print('Nombre de commandes: ${commandesValue is List ? commandesValue.length : 'pas une liste'}'); + + if (commandesValue is List) { + data = commandesValue; + } else if (commandesValue != null) { + data = [commandesValue]; + } + + // Pagination + if (dataMap.containsKey('pagination')) { + final pagination = dataMap['pagination'] as Map?; + if (pagination != null) { + currentPage = pagination['currentPage'] ?? page; + totalPages = pagination['totalPages'] ?? 1; + totalItems = pagination['totalItems'] ?? data.length; + print('=== PAGINATION ==='); + print('Page: $currentPage/$totalPages, Total: $totalItems'); + } + } else { + // Si pas de pagination dans la réponse, calculer approximativement + totalItems = data.length; + currentPage = page; + totalPages = (totalItems / itemsPerPage).ceil(); + } + } else { + print('=== PAS DE COMMANDES DANS DATA ==='); + totalItems = 0; + currentPage = 1; + totalPages = 1; + } + } else if (responseBody.containsKey('commandes')) { + // Fallback: commandes directement dans responseBody + final commandesValue = responseBody['commandes']; + print('=== COMMANDES DIRECTES ==='); + + if (commandesValue is List) { + data = commandesValue; + } else if (commandesValue != null) { + data = [commandesValue]; + } + totalItems = data.length; + currentPage = page; + totalPages = (totalItems / itemsPerPage).ceil(); + } else { + print('=== STRUCTURE INCONNUE ==='); + print('Clés disponibles: ${responseBody.keys.toList()}'); + totalItems = 0; + currentPage = 1; + totalPages = 1; + } + } else if (responseBody is List) { + print('=== RESPONSE EST UNE LISTE ==='); + data = responseBody; + totalItems = data.length; + currentPage = page; + totalPages = (totalItems / itemsPerPage).ceil(); + } else { + throw Exception('Format de réponse inattendu: ${responseBody.runtimeType}'); + } + + print('=== DONNÉES EXTRAITES ==='); + print('Nombre d\'éléments: ${data.length}'); + print('Data: $data'); + + // Conversion sécurisée avec prints détaillés + List parsedCommandes = []; + for (int i = 0; i < data.length; i++) { + try { + final item = data[i]; + print('=== ITEM $i ==='); + print('Type: ${item.runtimeType}'); + print('Contenu complet: $item'); + + if (item is Map) { + print('--- ANALYSE DES CHAMPS ---'); + item.forEach((key, value) { + print('$key: $value (${value.runtimeType})'); + }); + + final commandeData = CommandeData.fromJson(item); + print('--- COMMANDE PARSÉE ---'); + print('ID: ${commandeData.id}'); + print('Numéro: ${commandeData.numeroCommande}'); + print('Table name: ${commandeData.tablename}'); + print('Serveur: ${commandeData.serveur}'); + print('Date commande: ${commandeData.dateCommande}'); + print('Date paiement: ${commandeData.datePaiement}'); + print('Total TTC: ${commandeData.totalTtc}'); + print('Mode paiement: ${commandeData.modePaiement}'); + print('Nombre d\'items: ${commandeData.items?.length ?? 0}'); + + if (commandeData.items != null) { + print('--- ITEMS DE LA COMMANDE ---'); + for (int j = 0; j < commandeData.items!.length; j++) { + final commandeItem = commandeData.items![j]; + print('Item $j:'); + print(' - Menu nom: ${commandeItem.menuNom}'); + print(' - Quantité: ${commandeItem.quantite}'); + print(' - Prix unitaire: ${commandeItem.prixUnitaire}'); + print(' - Total: ${commandeItem.totalItem}'); + print(' - Commentaires: ${commandeItem.commentaires}'); + } + } + + parsedCommandes.add(commandeData); + } else { + print('ERROR: Item $i n\'est pas un Map: ${item.runtimeType}'); + } + } catch (e, stackTrace) { + print('ERROR: Erreur lors du parsing de l\'item $i: $e'); + print('Stack trace: $stackTrace'); + // Continue avec les autres items + } + } + + print('=== RÉSULTAT FINAL ==='); + print('Nombre de commandes parsées: ${parsedCommandes.length}'); + + setState(() { + commandes = parsedCommandes; isLoading = false; }); + // Initialiser les animations après avoir mis à jour l'état _initializeAnimations(); _startAnimations(); + } else { + print('ERROR: HTTP ${response.statusCode}: ${response.reasonPhrase}'); setState(() { - error = 'Erreur lors du chargement des commandes'; + error = 'Erreur HTTP ${response.statusCode}: ${response.reasonPhrase}'; isLoading = false; }); } - } catch (e) { + } catch (e, stackTrace) { + print('=== ERREUR GÉNÉRALE ==='); + print('Erreur: $e'); + print('Stack trace: $stackTrace'); setState(() { error = 'Erreur de connexion: $e'; isLoading = false; @@ -87,7 +226,33 @@ class _OrderHistoryPageState extends State } } + // Fonction pour aller à la page suivante + void _goToNextPage() { + if (currentPage < totalPages) { + _loadCommandes(page: currentPage + 1); + } + } + + // Fonction pour aller à la page précédente + void _goToPreviousPage() { + if (currentPage > 1) { + _loadCommandes(page: currentPage - 1); + } + } + + // Fonction pour aller à une page spécifique + void _goToPage(int page) { + if (page >= 1 && page <= totalPages && page != currentPage) { + _loadCommandes(page: page); + } + } + void _initializeAnimations() { + // Disposer les anciens contrôleurs + for (var controller in _cardAnimationControllers) { + controller.dispose(); + } + _cardAnimationControllers = List.generate( commandes.length, (index) => AnimationController( @@ -98,11 +263,13 @@ class _OrderHistoryPageState extends State } void _startAnimations() async { + if (!mounted) return; + _animationController.forward(); for (int i = 0; i < _cardAnimationControllers.length; i++) { await Future.delayed(Duration(milliseconds: 150)); - if (mounted) { + if (mounted && i < _cardAnimationControllers.length) { _cardAnimationControllers[i].forward(); } } @@ -128,13 +295,14 @@ class _OrderHistoryPageState extends State elevation: 0, ), body: RefreshIndicator( - onRefresh: _loadCommandes, + onRefresh: () => _loadCommandes(page: currentPage), child: Column( children: [ _buildHeader(), Expanded( child: _buildContent(), ), + if (totalPages > 1) _buildPagination(), ], ), ), @@ -188,7 +356,9 @@ class _OrderHistoryPageState extends State Padding( padding: EdgeInsets.only(top: 4), child: Text( - '$totalItems commande${totalItems > 1 ? 's' : ''} • Page $currentPage/$totalPages', + totalPages > 1 + ? '$totalItems commande${totalItems > 1 ? 's' : ''} • Page $currentPage/$totalPages' + : '$totalItems commande${totalItems > 1 ? 's' : ''} trouvée${totalItems > 1 ? 's' : ''}', style: TextStyle( fontSize: 10, color: Colors.grey.shade500, @@ -206,10 +376,158 @@ class _OrderHistoryPageState extends State ); } + Widget _buildPagination() { + return Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: Offset(0, -2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Bouton Précédent + ElevatedButton.icon( + onPressed: currentPage > 1 ? _goToPreviousPage : null, + icon: Icon(Icons.chevron_left, size: 18), + label: Text('Précédent'), + style: ElevatedButton.styleFrom( + backgroundColor: currentPage > 1 ? Color(0xFF4CAF50) : Colors.grey.shade300, + foregroundColor: currentPage > 1 ? Colors.white : Colors.grey.shade600, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: currentPage > 1 ? 2 : 0, + ), + ), + + // Indicateur de page actuelle avec navigation rapide + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (totalPages <= 7) + // Afficher toutes les pages si <= 7 pages + ...List.generate(totalPages, (index) { + final pageNum = index + 1; + return _buildPageButton(pageNum); + }) + else + // Afficher une navigation condensée si > 7 pages + ..._buildCondensedPagination(), + ], + ), + ), + + // Bouton Suivant + ElevatedButton.icon( + onPressed: currentPage < totalPages ? _goToNextPage : null, + icon: Icon(Icons.chevron_right, size: 18), + label: Text('Suivant'), + style: ElevatedButton.styleFrom( + backgroundColor: currentPage < totalPages ? Color(0xFF4CAF50) : Colors.grey.shade300, + foregroundColor: currentPage < totalPages ? Colors.white : Colors.grey.shade600, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: currentPage < totalPages ? 2 : 0, + ), + ), + ], + ), + ); + } + + Widget _buildPageButton(int pageNum) { + final isCurrentPage = pageNum == currentPage; + + return GestureDetector( + onTap: () => _goToPage(pageNum), + child: Container( + margin: EdgeInsets.symmetric(horizontal: 2), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: isCurrentPage ? Color(0xFF4CAF50) : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: isCurrentPage ? Color(0xFF4CAF50) : Colors.grey.shade300, + width: 1, + ), + ), + child: Text( + pageNum.toString(), + style: TextStyle( + color: isCurrentPage ? Colors.white : Colors.grey.shade700, + fontWeight: isCurrentPage ? FontWeight.bold : FontWeight.normal, + fontSize: 12, + ), + ), + ), + ); + } + + List _buildCondensedPagination() { + List pages = []; + + // Toujours afficher la première page + pages.add(_buildPageButton(1)); + + if (currentPage > 4) { + pages.add(Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...', style: TextStyle(color: Colors.grey)), + )); + } + + // Afficher les pages autour de la page actuelle + int start = (currentPage - 2).clamp(2, totalPages - 1); + int end = (currentPage + 2).clamp(2, totalPages - 1); + + for (int i = start; i <= end; i++) { + if (i != 1 && i != totalPages) { + pages.add(_buildPageButton(i)); + } + } + + if (currentPage < totalPages - 3) { + pages.add(Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...', style: TextStyle(color: Colors.grey)), + )); + } + + // Toujours afficher la dernière page si > 1 + if (totalPages > 1) { + pages.add(_buildPageButton(totalPages)); + } + + return pages; + } + Widget _buildContent() { if (isLoading) { return Center( - child: CircularProgressIndicator(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFF4CAF50)), + ), + SizedBox(height: 16), + Text( + 'Chargement des commandes...', + style: TextStyle(color: Colors.grey.shade600), + ), + ], + ), ); } @@ -220,11 +538,22 @@ class _OrderHistoryPageState extends State children: [ Icon(Icons.error_outline, size: 64, color: Colors.grey), SizedBox(height: 16), - Text(error!, style: TextStyle(color: Colors.grey)), + Padding( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text( + error!, + style: TextStyle(color: Colors.grey), + textAlign: TextAlign.center, + ), + ), SizedBox(height: 16), ElevatedButton( - onPressed: _loadCommandes, + onPressed: () => _loadCommandes(page: currentPage), child: Text('Réessayer'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF4CAF50), + foregroundColor: Colors.white, + ), ), ], ), @@ -236,12 +565,25 @@ class _OrderHistoryPageState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.payment, size: 64, color: Colors.grey), + Icon(Icons.restaurant_menu, size: 64, color: Colors.grey), SizedBox(height: 16), Text( - 'Aucune commande payée', + currentPage > 1 + ? 'Aucune commande sur cette page' + : 'Aucune commande payée', style: TextStyle(color: Colors.grey, fontSize: 16), ), + if (currentPage > 1) ...[ + SizedBox(height: 16), + ElevatedButton( + onPressed: () => _goToPage(1), + child: Text('Retour à la première page'), + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF4CAF50), + foregroundColor: Colors.white, + ), + ), + ], ], ), ); @@ -345,7 +687,7 @@ class _OrderHistoryPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - commande.tablename, + commande.tablename ?? 'Table inconnue', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -354,7 +696,7 @@ class _OrderHistoryPageState extends State ), SizedBox(height: 2), Text( - commande.numeroCommande, + commande.numeroCommande ?? 'N/A', style: TextStyle( fontSize: 10, color: Colors.grey, @@ -367,18 +709,31 @@ class _OrderHistoryPageState extends State Icon(Icons.calendar_today, size: 12, color: Colors.grey), SizedBox(width: 3), Text( - _formatDateTime(commande.dateCommande), + commande.dateCommande != null + ? _formatDateTime(commande.dateCommande!) + : 'Date inconnue', style: TextStyle(color: Colors.grey, fontSize: 10), ), SizedBox(width: 8), Icon(Icons.person, size: 12, color: Colors.grey), SizedBox(width: 3), Text( - commande.serveur, + commande.serveur ?? 'Serveur inconnu', style: TextStyle(color: Colors.grey, fontSize: 10), ), ], ), + if (commande.datePaiement != null) + Row( + children: [ + Icon(Icons.payment, size: 12, color: Colors.green), + SizedBox(width: 3), + Text( + 'Payée: ${_formatDateTime(commande.datePaiement!)}', + style: TextStyle(color: Colors.green, fontSize: 10), + ), + ], + ), ], ), ), @@ -386,7 +741,7 @@ class _OrderHistoryPageState extends State padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( gradient: LinearGradient( - colors: [Color(0xFF4CAF50), Color(0xFF45a049)], + colors: [Color(0xFF4CAF50), Color(0xFF388E3C)], ), borderRadius: BorderRadius.circular(10), ), @@ -420,7 +775,7 @@ class _OrderHistoryPageState extends State return Container( padding: EdgeInsets.all(10), child: Column( - children: commande.items.map((item) => _buildOrderItem(item)).toList(), + children: (commande.items ?? []).map((item) => _buildOrderItem(item)).toList(), ), ); } @@ -477,7 +832,7 @@ class _OrderHistoryPageState extends State ), ), Text( - '${item.quantite}x × ${_formatPrice(item.prixUnitaire)}', + '${item.quantite} × ${_formatPrice(item.prixUnitaire)}', style: TextStyle( fontSize: 10, color: Colors.grey, @@ -519,7 +874,7 @@ class _OrderHistoryPageState extends State height: 28, decoration: BoxDecoration( gradient: LinearGradient( - colors: [Color(0xFF4CAF50), Color(0xFF45a049)], + colors: [Color(0xFF4CAF50), Color(0xFF388E3C)], ), borderRadius: BorderRadius.circular(6), ), @@ -542,9 +897,9 @@ class _OrderHistoryPageState extends State color: Colors.grey.shade600, ), ), - if (commande.totalTva > 0) + if ((commande.totalTva ?? 0) > 0) Text( - 'TVA: ${_formatPrice(commande.totalTva)}', + 'TVA: ${_formatPrice(commande.totalTva ?? 0)}', style: TextStyle( fontSize: 9, color: Colors.grey.shade500, @@ -569,7 +924,7 @@ class _OrderHistoryPageState extends State size: 14, ), Text( - _formatPrice(commande.totalTtc), + _formatPrice(commande.totalTtc ?? 0), style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -589,7 +944,6 @@ class _OrderHistoryPageState extends State } String _formatPrice(double priceInCents) { - // Les prix sont déjà en centimes dans votre API (ex: 20000.00 = 200.00 €) return '${(priceInCents / 100).toStringAsFixed(2)} Ar'; } @@ -644,87 +998,178 @@ class _OrderHistoryPageState extends State } } -// Modèles de données adaptés à votre API +// Modèles de données avec gestion des valeurs nulles et debug amélioré class CommandeData { - final int id; - final int clientId; - final int tableId; - final int reservationId; - final String numeroCommande; - final String statut; - final double totalHt; - final double totalTva; - final double totalTtc; + final int? id; + final int? clientId; + final int? tableId; + final int? reservationId; + final String? numeroCommande; + final String? statut; + final double? totalHt; + final double? totalTva; + final double? totalTtc; final String? modePaiement; final String? commentaires; - final String serveur; - final DateTime dateCommande; - final DateTime? dateService; - final DateTime createdAt; - final DateTime updatedAt; - final List items; - final String tablename; + final String? serveur; + final DateTime? dateCommande; + final DateTime? datePaiement; + final DateTime? createdAt; + final DateTime? updatedAt; + final List? items; + final String? tablename; CommandeData({ - required this.id, - required this.clientId, - required this.tableId, - required this.reservationId, - required this.numeroCommande, - required this.statut, - required this.totalHt, - required this.totalTva, - required this.totalTtc, + this.id, + this.clientId, + this.tableId, + this.reservationId, + this.numeroCommande, + this.statut, + this.totalHt, + this.totalTva, + this.totalTtc, this.modePaiement, this.commentaires, - required this.serveur, - required this.dateCommande, - this.dateService, - required this.createdAt, - required this.updatedAt, - required this.items, - required this.tablename, + this.serveur, + this.dateCommande, + this.datePaiement, + this.createdAt, + this.updatedAt, + this.items, + this.tablename, }); factory CommandeData.fromJson(Map json) { - return CommandeData( - id: json['id'], - clientId: json['client_id'], - tableId: json['table_id'], - reservationId: json['reservation_id'], - numeroCommande: json['numero_commande'], - statut: json['statut'], - totalHt: (json['total_ht'] ?? 0).toDouble(), - totalTva: (json['total_tva'] ?? 0).toDouble(), - totalTtc: (json['total_ttc'] ?? 0).toDouble(), - modePaiement: json['mode_paiement'], - commentaires: json['commentaires'], - serveur: json['serveur'], - dateCommande: DateTime.parse(json['date_commande']), - dateService: json['date_service'] != null - ? DateTime.parse(json['date_service']) - : null, - createdAt: DateTime.parse(json['created_at']), - updatedAt: DateTime.parse(json['updated_at']), - items: (json['items'] as List) - .map((item) => CommandeItem.fromJson(item)) - .toList(), - tablename: json['tablename'] ?? 'Table inconnue', - ); + try { + + // Parsing avec debug détaillé + final id = json['id']; + + final numeroCommande = json['numero_commande']?.toString(); + + final tablename = json['tablename']?.toString() ?? json['table_name']?.toString() ?? 'Table inconnue'; + final serveur = json['serveur']?.toString() ?? json['server']?.toString() ?? 'Serveur inconnu'; + + final dateCommande = _parseDateTime(json['date_commande']) ?? _parseDateTime(json['created_at']); + + final datePaiement = _parseDateTime(json['date_paiement']) ?? _parseDateTime(json['date_service']); + + final totalTtc = _parseDouble(json['total_ttc']) ?? _parseDouble(json['total']); + + final modePaiement = json['mode_paiement']?.toString() ?? json['payment_method']?.toString(); + + final items = _parseItems(json['items']); + + final result = CommandeData( + id: id, + clientId: json['client_id'], + tableId: json['table_id'], + reservationId: json['reservation_id'], + numeroCommande: numeroCommande, + statut: json['statut']?.toString(), + totalHt: _parseDouble(json['total_ht']), + totalTva: _parseDouble(json['total_tva']), + totalTtc: totalTtc, + modePaiement: modePaiement, + commentaires: json['commentaires']?.toString(), + serveur: serveur, + dateCommande: dateCommande, + datePaiement: datePaiement, + createdAt: _parseDateTime(json['created_at']), + updatedAt: _parseDateTime(json['updated_at']), + items: items, + tablename: tablename, + ); + + print('=== COMMANDE PARSÉE AVEC SUCCÈS ==='); + return result; + } catch (e, stackTrace) { + print('=== ERREUR PARSING COMMANDE ==='); + print('Erreur: $e'); + print('JSON: $json'); + print('Stack trace: $stackTrace'); + rethrow; + } + } + + static double? _parseDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) { + final result = double.tryParse(value); + return result; + } + return null; + } + + static DateTime? _parseDateTime(dynamic value) { + if (value == null) return null; + if (value is String) { + try { + final result = DateTime.parse(value); + print('String to datetime: "$value" -> $result'); + return result; + } catch (e) { + print('Erreur parsing date: $value - $e'); + return null; + } + } + print('Impossible de parser en datetime: $value'); + return null; + } + + static List? _parseItems(dynamic value) { + print('=== PARSING ITEMS ==='); + print('Items bruts: $value (${value.runtimeType})'); + + if (value == null) { + print('Items null'); + return null; + } + + if (value is! List) { + print('Items n\'est pas une liste: ${value.runtimeType}'); + return null; + } + + try { + List result = []; + for (int i = 0; i < value.length; i++) { + print('--- ITEM $i ---'); + final item = value[i]; + print('Item brut: $item (${item.runtimeType})'); + + if (item is Map) { + final commandeItem = CommandeItem.fromJson(item); + result.add(commandeItem); + print('Item parsé: ${commandeItem.menuNom}'); + } else { + print('Item $i n\'est pas un Map'); + } + } + + print('Total items parsés: ${result.length}'); + return result; + } catch (e) { + print('Erreur parsing items: $e'); + return null; + } } String getTableShortName() { - if (tablename.toLowerCase().contains('caisse')) return 'C'; - if (tablename.toLowerCase().contains('terrasse')) return 'T'; + final name = tablename ?? 'Table'; + if (name.toLowerCase().contains('caisse')) return 'C'; + if (name.toLowerCase().contains('terrasse')) return 'T'; - // Extraire le numéro de la table RegExp regExp = RegExp(r'\d+'); - Match? match = regExp.firstMatch(tablename); + Match? match = regExp.firstMatch(name); if (match != null) { return 'T${match.group(0)}'; } - return tablename.substring(0, 1).toUpperCase(); + return name.isNotEmpty ? name.substring(0, 1).toUpperCase() : 'T'; } } @@ -737,8 +1182,8 @@ class CommandeItem { final double totalItem; final String? commentaires; final String statut; - final DateTime createdAt; - final DateTime updatedAt; + final DateTime? createdAt; + final DateTime? updatedAt; final String menuNom; final String menuDescription; final double menuPrixActuel; @@ -753,8 +1198,8 @@ class CommandeItem { required this.totalItem, this.commentaires, required this.statut, - required this.createdAt, - required this.updatedAt, + this.createdAt, + this.updatedAt, required this.menuNom, required this.menuDescription, required this.menuPrixActuel, @@ -762,31 +1207,82 @@ class CommandeItem { }); factory CommandeItem.fromJson(Map json) { - return CommandeItem( - id: json['id'], - commandeId: json['commande_id'], - menuId: json['menu_id'], - quantite: json['quantite'], - prixUnitaire: (json['prix_unitaire'] ?? 0).toDouble(), - totalItem: (json['total_item'] ?? 0).toDouble(), - commentaires: json['commentaires'], - statut: json['statut'], - createdAt: DateTime.parse(json['created_at']), - updatedAt: DateTime.parse(json['updated_at']), - menuNom: json['menu_nom'], - menuDescription: json['menu_description'], - menuPrixActuel: (json['menu_prix_actuel'] ?? 0).toDouble(), - tablename: json['tablename'] ?? '', - ); + try { + print('=== PARSING COMMANDE ITEM ==='); + print('JSON item: $json'); + + // Debug chaque champ + final id = json['id'] ?? 0; + print('ID: ${json['id']} -> $id'); + + final commandeId = json['commande_id'] ?? 0; + print('Commande ID: ${json['commande_id']} -> $commandeId'); + + final menuId = json['menu_id'] ?? 0; + print('Menu ID: ${json['menu_id']} -> $menuId'); + + final quantite = json['quantite'] ?? json['quantity'] ?? 0; + print('Quantité: ${json['quantite']} / ${json['quantity']} -> $quantite'); + + final prixUnitaire = CommandeData._parseDouble(json['prix_unitaire']) ?? + CommandeData._parseDouble(json['unit_price']) ?? 0.0; + print('Prix unitaire: ${json['prix_unitaire']} / ${json['unit_price']} -> $prixUnitaire'); + + final totalItem = CommandeData._parseDouble(json['total_item']) ?? + CommandeData._parseDouble(json['total']) ?? 0.0; + print('Total item: ${json['total_item']} / ${json['total']} -> $totalItem'); + + final commentaires = json['commentaires']?.toString() ?? json['comments']?.toString(); + print('Commentaires: ${json['commentaires']} / ${json['comments']} -> $commentaires'); + + final statut = json['statut']?.toString() ?? json['status']?.toString() ?? ''; + final menuNom = json['menu_nom']?.toString() ?? + json['menu_name']?.toString() ?? + json['name']?.toString() ?? 'Menu inconnu'; + + final menuDescription = json['menu_description']?.toString() ?? + json['description']?.toString() ?? ''; + print('Menu description: ${json['menu_description']} / ${json['description']} -> $menuDescription'); + + final menuPrixActuel = CommandeData._parseDouble(json['menu_prix_actuel']) ?? + CommandeData._parseDouble(json['current_price']) ?? 0.0; + print('Menu prix actuel: ${json['menu_prix_actuel']} / ${json['current_price']} -> $menuPrixActuel'); + + final tablename = json['tablename']?.toString() ?? + json['table_name']?.toString() ?? ''; + print('Table name: ${json['tablename']} / ${json['table_name']} -> $tablename'); + + final result = CommandeItem( + id: id, + commandeId: commandeId, + menuId: menuId, + quantite: quantite, + prixUnitaire: prixUnitaire, + totalItem: totalItem, + commentaires: commentaires, + statut: statut, + createdAt: CommandeData._parseDateTime(json['created_at']), + updatedAt: CommandeData._parseDateTime(json['updated_at']), + menuNom: menuNom, + menuDescription: menuDescription, + menuPrixActuel: menuPrixActuel, + tablename: tablename, + ); + return result; + } catch (e, stackTrace) { + print('=== ERREUR PARSING ITEM ==='); + print('Erreur: $e'); + print('JSON: $json'); + print('Stack trace: $stackTrace'); + rethrow; + } } } -// Usage dans votre app principale class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Historique des Commandes', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, diff --git a/lib/services/pdf_service.dart b/lib/services/pdf_service.dart index c18a76f..8686ee9 100644 --- a/lib/services/pdf_service.dart +++ b/lib/services/pdf_service.dart @@ -11,14 +11,15 @@ import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:permission_handler/permission_handler.dart'; import '../models/command_detail.dart'; +import 'package:intl/intl.dart'; class PlatformPrintService { - // Format spécifique 58mm pour petites imprimantes + // Format spécifique 58mm pour petites imprimantes - CENTRÉ POUR L'IMPRESSION static const PdfPageFormat ticket58mmFormat = PdfPageFormat( - 58 * PdfPageFormat.mm, // Largeur exacte 58mm + 48 * PdfPageFormat.mm, // Largeur exacte 58mm double.infinity, // Hauteur automatique - marginLeft: 1 * PdfPageFormat.mm, - marginRight: 1 * PdfPageFormat.mm, + marginLeft: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer + marginRight: 4 * PdfPageFormat.mm, // ✅ Marges équilibrées pour centrer marginTop: 2 * PdfPageFormat.mm, marginBottom: 2 * PdfPageFormat.mm, ); @@ -40,305 +41,314 @@ class PlatformPrintService { } } - // Générer PDF optimisé pour 58mm - static Future _generate58mmTicketPdf({ - required CommandeDetail commande, - required String paymentMethod, - }) async { - final pdf = pw.Document(); - - // Configuration pour 58mm (très petit) - const double titleSize = 9; - const double headerSize = 8; - const double bodySize = 7; - const double smallSize = 6; - const double lineHeight = 1.2; - - final restaurantInfo = { - 'nom': 'RESTAURANT ITRIMOBE', - 'adresse': 'Moramanga, Antananarivo', - 'ville': 'Madagascar', - 'contact': '261348415301', - 'email': 'contact@careeragency.mg', - 'nif': '4002141594', - 'stat': '10715 33 2025 0 00414', - }; - - final factureNumber = - 'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}'; - final dateTime = DateTime.now(); - - pdf.addPage( - pw.Page( - pageFormat: ticket58mmFormat, - margin: const pw.EdgeInsets.all(2), // 🔧 Marges minimales - build: (pw.Context context) { - return pw.Container( - width: double.infinity, // 🔧 Forcer la largeur complète - child: pw.Column( - crossAxisAlignment: - pw.CrossAxisAlignment.start, // 🔧 Alignement à gauche - children: [ - // En-tête Restaurant (centré et compact) - pw.Container( - width: double.infinity, - child: pw.Text( - restaurantInfo['nom']!, - style: pw.TextStyle( - fontSize: titleSize, - fontWeight: pw.FontWeight.bold, - ), - textAlign: pw.TextAlign.center, - ), - ), - - pw.SizedBox(height: 1), + // Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN +static Future _generate58mmTicketPdf({ + required CommandeDetail commande, + required String paymentMethod, +}) async { + final pdf = pw.Document(); + + const double titleSize = 8; + const double headerSize = 8; + const double bodySize = 7; + const double smallSize = 6; + const double lineHeight = 1.2; + + final restaurantInfo = { + 'nom': 'RESTAURANT ITRIMOBE', + 'adresse': 'Moramanga, Madagascar', + 'contact': '+261 34 12 34 56', + 'nif': '4002141594', + 'stat': '10715 33 2025 0 00414', + }; + + final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; + final dateTime = DateTime.now(); + + String paymentMethodText; + switch (paymentMethod) { + case 'mvola': + paymentMethodText = 'MVola'; + break; + case 'carte': + paymentMethodText = 'CB'; + break; + case 'especes': + paymentMethodText = 'Espèces'; + break; + default: + paymentMethodText = 'CB'; + } - pw.Container( - width: double.infinity, - child: pw.Text( - restaurantInfo['adresse']!, - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, + pdf.addPage( + pw.Page( + pageFormat: ticket58mmFormat, + margin: const pw.EdgeInsets.all(2), + + build: (pw.Context context) { + return pw.Container( + width: double.infinity, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // TITRE CENTRÉ + pw.Container( + width: double.infinity, + child: pw.Text( + restaurantInfo['nom']!, + style: pw.TextStyle( + fontSize: titleSize, + fontWeight: pw.FontWeight.bold, ), + textAlign: pw.TextAlign.center, ), - - pw.Container( - width: double.infinity, - child: pw.Text( - restaurantInfo['ville']!, - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, - ), + ), + + pw.SizedBox(height: 2), + + // ADRESSE GAUCHE DÉCALÉE VERS LA GAUCHE (marginRight) + pw.Container( + width: double.infinity, + margin: const pw.EdgeInsets.only(right: 6), + child: pw.Text( + 'Adresse: ${restaurantInfo['adresse']!}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.left, ), - - pw.Container( - width: double.infinity, - child: pw.Text( - 'Tel: ${restaurantInfo['contact']!}', - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, - ), + ), + + // CONTACT GAUCHE DÉCALÉE + pw.Container( + width: double.infinity, + margin: const pw.EdgeInsets.only(right: 8), + child: pw.Text( + 'Contact: ${restaurantInfo['contact']!}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.left, ), - - pw.SizedBox(height: 3), - - // Ligne de séparation - pw.Container( - width: double.infinity, - height: 0.5, - color: PdfColors.black, + ), + + // NIF GAUCHE DÉCALÉE + pw.Container( + width: double.infinity, + margin: const pw.EdgeInsets.only(right: 8), + child: pw.Text( + 'NIF: ${restaurantInfo['nif']!}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.left, ), - - pw.SizedBox(height: 2), - - // Informations ticket - pw.Container( - width: double.infinity, - child: pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text( - 'Ticket: $factureNumber', - style: pw.TextStyle( - fontSize: bodySize, - fontWeight: pw.FontWeight.bold, - ), - ), - ], + ), + + // STAT GAUCHE DÉCALÉE + pw.Container( + width: double.infinity, + margin: const pw.EdgeInsets.only(right: 8), + child: pw.Text( + 'STAT: ${restaurantInfo['stat']!}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.left, + ), + ), + + pw.SizedBox(height: 3), + + // Ligne de séparation + pw.Container( + width: double.infinity, + height: 0.5, + color: PdfColors.black, + ), + + pw.SizedBox(height: 2), + + // FACTURE CENTRÉE + pw.Container( + width: double.infinity, + child: pw.Text( + 'Facture n° $factureNumber', + style: pw.TextStyle( + fontSize: bodySize, + fontWeight: pw.FontWeight.bold, ), + textAlign: pw.TextAlign.center, ), + ), - pw.SizedBox(height: 1), + pw.SizedBox(height: 1), - pw.Container( - width: double.infinity, - child: pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text( - _formatDate(dateTime), - style: pw.TextStyle(fontSize: smallSize), + // DATE CENTRÉE + pw.Container( + width: double.infinity, + child: pw.Text( + 'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.center, + ), + ), + + // TABLE CENTRÉE + pw.Container( + width: double.infinity, + child: pw.Text( + 'Via: ${commande.tablename ?? "N/A"}', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.center, + ), + ), + + // PAIEMENT CENTRÉ + pw.Container( + width: double.infinity, + child: pw.Text( + 'Paiement: $paymentMethodText', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.center, + ), + ), + + pw.SizedBox(height: 3), + + // Ligne de séparation + pw.Container( + width: double.infinity, + height: 0.5, + color: PdfColors.black, + ), + + pw.SizedBox(height: 2), + + // EN-TÊTE DES ARTICLES + pw.Container( + width: double.infinity, + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'Qte Designation', + style: pw.TextStyle( + fontSize: bodySize, + fontWeight: pw.FontWeight.bold, ), - pw.Text( - _formatTime(dateTime), - style: pw.TextStyle(fontSize: smallSize), + ), + pw.Text( + 'Prix', + style: pw.TextStyle( + fontSize: bodySize, + fontWeight: pw.FontWeight.bold, ), - ], - ), - ), - - pw.SizedBox(height: 2), - - // Ligne de séparation - pw.Container( - width: double.infinity, - height: 0.5, - color: PdfColors.black, + ), + ], ), - - pw.SizedBox(height: 2), - - // Articles (format très compact) - ...commande.items - .map( - (item) => pw.Container( - width: double.infinity, // 🔧 Largeur complète - margin: const pw.EdgeInsets.only(bottom: 1), - child: pw.Column( - crossAxisAlignment: pw.CrossAxisAlignment.start, - children: [ - // Nom du plat - pw.Container( - width: double.infinity, - child: pw.Text( - '${item.menuNom}', - style: pw.TextStyle(fontSize: bodySize), - maxLines: 2, - ), + ), + + pw.Container( + width: double.infinity, + height: 0.5, + color: PdfColors.black, + ), + + pw.SizedBox(height: 2), + + // ARTICLES + ...commande.items + .map( + (item) => pw.Container( + width: double.infinity, + margin: const pw.EdgeInsets.only(bottom: 1), + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Expanded( + child: pw.Text( + '${item.quantite} ${item.menuNom}', + style: pw.TextStyle(fontSize: smallSize), + maxLines: 2, ), - - // Quantité, prix unitaire et total sur une ligne - pw.Container( - width: double.infinity, - child: pw.Row( - mainAxisAlignment: - pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text( - '${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}MGA', - style: pw.TextStyle(fontSize: smallSize), - ), - pw.Text( - '${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}MGA', - style: pw.TextStyle( - fontSize: bodySize, - fontWeight: pw.FontWeight.bold, - ), - ), - ], - ), - ), - ], - ), + ), + pw.Text( + '${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)}AR', + style: pw.TextStyle(fontSize: smallSize), + ), + ], ), - ) - .toList(), - - pw.SizedBox(height: 2), - - // Ligne de séparation - pw.Container( - width: double.infinity, - height: 0.5, - color: PdfColors.black, - ), - - pw.SizedBox(height: 2), - - // Total - pw.Container( - width: double.infinity, - child: pw.Row( - mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, - children: [ - pw.Text( - 'TOTAL', - style: pw.TextStyle( - fontSize: titleSize, - fontWeight: pw.FontWeight.bold, - ), + ), + ) + .toList(), + + pw.SizedBox(height: 2), + + // Ligne de séparation + pw.Container( + width: double.infinity, + height: 0.5, + color: PdfColors.black, + ), + + pw.SizedBox(height: 2), + + // TOTAL + pw.Container( + width: double.infinity, + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'Total:', + style: pw.TextStyle( + fontSize: titleSize, + fontWeight: pw.FontWeight.bold, ), - pw.Text( - '${commande.totalTtc.toStringAsFixed(2)}MGA', - style: pw.TextStyle( - fontSize: titleSize, - fontWeight: pw.FontWeight.bold, - ), + ), + pw.Text( + '${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR', + style: pw.TextStyle( + fontSize: titleSize, + fontWeight: pw.FontWeight.bold, ), - ], - ), - ), - - pw.SizedBox(height: 3), - - // Mode de paiement - pw.Container( - width: double.infinity, - child: pw.Text( - 'Paiement: ${paymentMethod.toLowerCase()}', - style: pw.TextStyle(fontSize: bodySize), - textAlign: pw.TextAlign.center, - ), - ), - - pw.SizedBox(height: 3), - - // Ligne de séparation - pw.Container( - width: double.infinity, - height: 0.5, - color: PdfColors.black, - ), - - pw.SizedBox(height: 2), - - // Message de remerciement - pw.Container( - width: double.infinity, - child: pw.Text( - 'Merci de votre visite !', - style: pw.TextStyle( - fontSize: bodySize, - fontStyle: pw.FontStyle.italic, ), - textAlign: pw.TextAlign.center, - ), - ), - - pw.Container( - width: double.infinity, - child: pw.Text( - 'A bientôt !', - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, - ), + ], ), - - pw.SizedBox(height: 3), - - // Code de suivi (optionnel) - pw.Container( - width: double.infinity, - child: pw.Text( - 'Code: ${factureNumber}', - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, + ), + + pw.SizedBox(height: 4), + + // MESSAGE FINAL CENTRÉ + pw.Container( + width: double.infinity, + child: pw.Text( + 'Merci et a bientot !', + style: pw.TextStyle( + fontSize: bodySize, + fontStyle: pw.FontStyle.italic, ), + textAlign: pw.TextAlign.center, ), + ), - pw.SizedBox(height: 4), + pw.SizedBox(height: 4), - // Ligne de découpe - pw.Container( - width: double.infinity, - child: pw.Text( - '- - - - - - - - - - - - - - - -', - style: pw.TextStyle(fontSize: smallSize), - textAlign: pw.TextAlign.center, - ), + // Ligne de découpe + pw.Container( + width: double.infinity, + child: pw.Text( + '- - - - - - - - - - - - - - - -', + style: pw.TextStyle(fontSize: smallSize), + textAlign: pw.TextAlign.center, ), + ), + + pw.SizedBox(height: 2), + ], + ), + ); + }, + ), + ); - pw.SizedBox(height: 2), - ], - ), - ); - }, - ), - ); + return pdf.save(); +} - return pdf.save(); - } // Imprimer ticket 58mm static Future printTicket({ @@ -399,20 +409,24 @@ class PlatformPrintService { } final fileName = - 'Ticket_58mm_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; + 'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; final file = File('${directory.path}/$fileName'); await file.writeAsBytes(pdfData); - await Share.shareXFiles( - [XFile(file.path)], - subject: 'Ticket ${commande.numeroCommande}', - text: 'Ticket de caisse 58mm', - ); + // ✅ VRAIE SAUVEGARDE au lieu de partage automatique + if (Platform.isAndroid) { + // Sur Android, on peut proposer les deux options + await Share.shareXFiles( + [XFile(file.path)], + subject: 'Facture ${commande.numeroCommande}', + text: 'Facture de restaurant', + ); + } return true; } catch (e) { - print('Erreur sauvegarde 58mm: $e'); + print('Erreur sauvegarde: $e'); return false; } } @@ -435,7 +449,7 @@ class PlatformPrintService { return await printTicket(commande: commande, paymentMethod: paymentMethod); } - // Utilitaires de formatageπ + // Utilitaires de formatage static String _formatDate(DateTime dateTime) { return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}'; } @@ -456,4 +470,4 @@ class PlatformPrintService { return 'Non spécifié'; } } -} +} \ No newline at end of file diff --git a/lib/services/restaurant_api_service.dart b/lib/services/restaurant_api_service.dart index ae15384..d84077d 100644 --- a/lib/services/restaurant_api_service.dart +++ b/lib/services/restaurant_api_service.dart @@ -17,7 +17,33 @@ class RestaurantApiService { 'Accept': 'application/json', }; + static Future updateCommandeStatus({ + required String commandeId, + required String newStatus, +}) async { + try { + final response = await http.put( + Uri.parse('$baseUrl/api/commandes/$commandeId/status'), + headers: _headers, + body: json.encode({'statut': newStatus}), + ); + + if (response.statusCode == 200 || response.statusCode == 204) { + return true; + } else { + print('Erreur updateCommandeStatus: ${response.statusCode} ${response.body}'); + return false; + } + } catch (e) { + print('Exception updateCommandeStatus: $e'); + return false; + } +} + + + // Récupérer les commandes + static Future> getCommandes() async { try { final response = await http diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart index f57acf9..2972f20 100644 --- a/lib/widgets/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation.dart @@ -238,48 +238,48 @@ class AppBottomNavigation extends StatelessWidget { ), ), - // const SizedBox(width: 20), + const SizedBox(width: 20), - // GestureDetector( - // onTap: () => onItemTapped(6), - // child: Container( - // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - // decoration: BoxDecoration( - // color: - // selectedIndex == 5 - // ? Colors.green.shade700 - // : Colors.transparent, - // borderRadius: BorderRadius.circular(20), - // ), - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Icon( - // Icons.payment, - // color: - // selectedIndex == 5 - // ? Colors.white - // : Colors.grey.shade600, - // size: 16, - // ), - // const SizedBox(width: 6), - // Text( - // 'Historique', - // style: TextStyle( - // color: - // selectedIndex == 5 - // ? Colors.white - // : Colors.grey.shade600, - // fontWeight: - // selectedIndex == 5 - // ? FontWeight.w500 - // : FontWeight.normal, - // ), - // ), - // ], - // ), - // ), - // ), + GestureDetector( + onTap: () => onItemTapped(6), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: + selectedIndex == 6 + ? Colors.green.shade700 + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.payment, + color: + selectedIndex == 6 + ? Colors.white + : Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Historique', + style: TextStyle( + color: + selectedIndex == 6 + ? Colors.white + : Colors.grey.shade600, + fontWeight: + selectedIndex == 6 + ? FontWeight.w500 + : FontWeight.normal, + ), + ), + ], + ), + ), + ), const Spacer(), diff --git a/pubspec.yaml b/pubspec.yaml index 0124d0c..8e6c710 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: path_provider: ^2.1.1 share_plus: ^7.2.1 permission_handler: ^11.1.0 + intl: ^0.18.1 # Dépendances de développement/test dev_dependencies: diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index c04e20c..2a78c1d 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ