From 31c3d72a71a74b1bf0b0c2f2ac68fc8cd5b9f28c Mon Sep 17 00:00:00 2001 From: andrymodeste Date: Tue, 5 Aug 2025 03:41:27 +0200 Subject: [PATCH] commit --- lib/pages/caisse_screen.dart | 73 ++- lib/pages/cart_page.dart | 113 ++-- lib/pages/commande_item_screen.dart | 3 +- lib/pages/commande_item_validation.dart | 13 +- lib/pages/commandes_screen.dart | 37 +- lib/pages/facture_screen.dart | 11 +- lib/pages/historique_commande.dart | 756 +++++++++++++++++++---- lib/services/pdf_service.dart | 586 +++++++++--------- lib/services/restaurant_api_service.dart | 26 + lib/widgets/bottom_navigation.dart | 82 +-- pubspec.yaml | 1 + windows/runner/resources/app_icon.ico | Bin 33772 -> 4154 bytes 12 files changed, 1140 insertions(+), 561 deletions(-) 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 c04e20caf6370ebb9253ad831cc31de4a9c965f6..2a78c1d412ced5f48756f455b7ff73f96ab85b26 100644 GIT binary patch literal 4154 zcmbVP2~d^S75=FU#n=*KG%j?CxJ*YD_lXOjt)kXA6BCU%YOTqnj;Il#iIrwXaG|u0 zGKeff6+?=+A}EF+$f5|MAWJAJm9RV%R5saU)vxEf_`f1rGRb2O|Nrj0=X__s=W3dX z{8|25<9}`3o0>LU)3o0RSqeD{ah)G8b4?p-*G+2S2mU>K_Dlmn8X#oz{Q2{(jg5^_ zrKP1sIXO8E*REab%*e>-%+JqnFc=I)&CShGvd2~S+RBqWT1Q9651_pMXY#eTw^N2= zOG`^wc6PSmz<~o@D_5?>tXZ=#Wy%yq)22o;%S3|m`UI5|1N%gYPVF)_G#^Ck)l3l;G_M%H%i+J!}n z7Gct)N!YSw3&<<@!6IuceL>lMefYch)xjh)#+`F524K z6!BeO=bpsGM66o1O7T*LtmTW$g%Tr^|EB}j%gf76Iy*a8K9JAGjT>QSXNQQ02sAV_ zfEcZNvLdIASn^v&Gw{zyKSaevAnKb4e04eyXJW$<{q1q2 zW?Vu^`8|AJ_8;WmzJ&`3r;whNf(#)K9~brhsJaT5(=Ou3SBEiS!UP;YejI`oDY$03 z4x;1xk{c}!4i50~@j+K-7fzlJLuiCAlG5T35fy^i#MAgk;FpL?ItTx-zaipWD7<{# zka+bX{EqCw*^6J}K!_)fp76)lXM*wR7azd?s5b(R`QYz?e?@*_Hoo|aJLb%pgR-)+ z7QtNh0)LR`6La|RVdW#q4|H~PDjR)6cj4RQXt6l}H*&ARC%^+S@h6aQB^DXksc`f9 zBaWR45WoBjMa6}66op0kC^ZxdUOMjF zD?pRvL+gvYgwtm$%DC46MM6=vQ(|I zk1%JOni`dz^xMDh=HeUCg}9AP4XWNLmzZsB-913;c&R3zG(5qr?=qDf{>FXGQPxpj zUY^2APfv$ndkdw6goI$hf(59ptyTN#>+6wnB?+-{=W#Xd3aFp-RMqp!%gWHvE`3Mk zz0_o@$fs<6@1ciYv%PhrZ1&NhpdjL35qs~uxw*l`#YO4I{ZfZ`{J0tqA3lWi1BkB_ z)znm@^g%V^vMNweQjNQYT6|YpgWTe3+%B&{T|=|dqc>*H@6?O^Xxp}J^mV1wUhC@B ztFdR#9@T5~z5lE0?)oNQWmOe6`o_b``4B!0xPgxYvf%7{10Q;);CCMJ_&g|CY_hPhz{rs!F>2H(MWaWL z28rvWF=NIkeaXWaK*!d7@8RL0W+dskH4+O)Sy%m^^4XhZ^mrw9%o+MY<|$miejPG* zA|)jS@$vD9j*eDmT4-n}_U+pTo+IoV z^HV)o=Vi?F{&V5N1vOXAm@z~7!ph1@`C!P9AuuyDQ}&02g}o9(%A)UiwzAKPjXlpl z&L8Uh4-XGlvB}KLH0r>7WnQp8Id2|0as=!b*4EY-I&>(84I8HBVAfA^ax#0$OLptn zSFc`G^IU9fENzeE%pt$2*@sWj&sySjd+XM%m^W`8ii(PiIy5ykseEOhC@3gEO-+re zG1d*&D4X_?h}V<2Mjp=W+qZ9L&$%Nu8Rs9#Tcv~O#5lYseWq&d+O?`ia&vQ)OvZq6 zcJAB><~3tWzfcdECBPi*iK)MHALl>j)w*@-m=_OaZK=pon8emU!lTdLA4#uyuwlan zl`D~vkxIVQDfoW!euLdAw>o7hz;_PuugeT@!IVp|?(1GXV0Fk{3P|;B{ApyFYKq>|9a}cclttzdKmA&l5=+@HyfmvcJdC)yRfVk zNuB8TU+yK3#yfOhA9$JIxu$t`Y1-T;nr2?6X{Mx9P3z|+2MtO42_Ky|RpgYtkV*N( IAs%u63$bzYpa1{> literal 33772 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