import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:itrimobe/models/command_detail.dart'; import 'package:itrimobe/models/payment_method.dart'; import 'package:itrimobe/pages/caisse_screen.dart'; import 'package:itrimobe/pages/facture_screen.dart'; import 'dart:convert'; import 'package:itrimobe/pages/tables.dart'; import 'package:itrimobe/services/pdf_service.dart'; import '../layouts/main_layout.dart'; import 'package:intl/intl.dart'; import 'information.dart'; class CartPage extends StatefulWidget { final int tableId; final int personne; final String? tablename; final List cartItems; const CartPage({ super.key, required this.tableId, required this.personne, required this.cartItems, this.tablename, }); @override State createState() => _CartPageState(); } class _CartPageState extends State { List _cartItems = []; bool _isValidating = false; @override void initState() { super.initState(); _processCartItems(); } void _processCartItems() { // Grouper les articles identiques Map groupedItems = {}; for (var item in widget.cartItems) { String key = "${item['id']}_${item['notes'] ?? ''}"; if (groupedItems.containsKey(key)) { groupedItems[key]!.quantity++; } else { groupedItems[key] = CartItemModel( id: item['id'], nom: item['nom'] ?? 'Article', prix: _parsePrice(item['prix']), quantity: 1, commentaire: item['commentaire'] ?? '', ); } } setState(() { _cartItems = groupedItems.values.toList(); }); } double _parsePrice(dynamic prix) { if (prix == null) return 0.0; if (prix is num) return prix.toDouble(); if (prix is String) return double.tryParse(prix) ?? 0.0; return 0.0; } void _updateQuantity(int index, int newQuantity) { setState(() { if (newQuantity > 0) { _cartItems[index].quantity = newQuantity; } else { _cartItems.removeAt(index); } }); } void _removeItem(int index) { setState(() { _cartItems.removeAt(index); }); } double _calculateTotal() { return _cartItems.fold( 0.0, (sum, item) => sum + (item.prix * item.quantity), ); } int _getTotalArticles() { return _cartItems.fold(0, (sum, item) => sum + item.quantity); } void _showConfirmationDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text( 'Confirmer la commande', style: TextStyle(fontWeight: FontWeight.bold), ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Êtes-vous sûr de vouloir valider cette commande ?'), const SizedBox(height: 16), const Text( 'Récapitulatif:', style: TextStyle(fontWeight: FontWeight.bold), ), Text('• Table: ${widget.tableId}'), Text('• Personnes: ${widget.personne}'), Text('• Articles: ${_getTotalArticles()}'), Text('• Total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA'), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: Text('Annuler', style: TextStyle(color: Colors.grey[600])), ), ElevatedButton( onPressed: _isValidating ? null : () { Navigator.of(context).pop(); _validateOrder(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green[700], foregroundColor: Colors.white, ), child: _isValidating ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : const Text('Valider'), ), ], ); }, ); } Future _validateOrder() async { setState(() { _isValidating = true; }); try { // Préparer les données de la commande final orderData = { "client_id": 1, // Valeur par défaut comme demandé "table_id": widget.tableId, "reservation_id": 1, // Peut être null si pas de réservation "serveur": "Serveur par défaut", // Valeur par défaut comme demandé "commentaires": _getOrderComments(), "items": _cartItems .map( (item) => { "menu_id": item.id, "quantite": item.quantity, "commentaires": item.commentaire.isNotEmpty ? item.commentaire : null, }, ) .toList(), }; // Appel API pour créer la commande final response = await http.post( Uri.parse( 'https://restaurant.careeracademy.mg/api/commandes', ), // Remplacez par votre URL d'API headers: {'Content-Type': 'application/json'}, body: json.encode(orderData), ); // print('response body: ${response.body}'); if (response.statusCode == 200 || response.statusCode == 201) { // Succès _updateTableStatus(); _showSuccessDialog(); } else { // Erreur _showErrorDialog( 'Erreur lors de l\'enregistrement de la commande (${response.statusCode})', ); } } catch (e) { _showErrorDialog('Erreur de connexion: $e'); } finally { setState(() { _isValidating = false; }); } } String _getOrderComments() { // Concaténer toutes les notes des articles pour les commentaires généraux List allNotes = _cartItems .where((item) => item.commentaire.isNotEmpty) .map((item) => '${item.nom}: ${item.commentaire}') .toList(); return allNotes.join('; '); } // FONCTION CORRIGÉE POUR LA NAVIGATION VERS LES TABLES void _showSuccessDialog() { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Row( children: [ Icon(Icons.check_circle, color: Colors.green[700]), const SizedBox(width: 8), const Text('Commande validée'), ], ), content: const Text( 'Votre commande a été envoyée en cuisine avec succès !', ), actions: [ ElevatedButton( onPressed: () { Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (context) => const MainLayout(child: TablesScreen()), ), (route) => false, ); }, child: const Text('OK'), ), ], ); }, ); } Future _updateTableStatus() async { try { final _ = await http.put( Uri.parse( 'https://restaurant.careeracademy.mg/api/tables/${widget.tableId}', ), headers: {'Content-Type': 'application/json'}, body: json.encode({"status": "occupied"}), ); } catch (e) { if (kDebugMode) { print("Erreur lors de la mise à jour du statut de la table: $e"); } } } void _showErrorDialog(String message) { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Row( children: [ Icon(Icons.error, color: Colors.red), SizedBox(width: 8), Text('Erreur'), ], ), content: Text(message), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ); }, ); } Future _payerDirectement() async { if (_cartItems.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Aucun article dans le panier'), backgroundColor: Colors.red, ), ); return; } // Calculer le total double total = _cartItems.fold(0.0, (sum, item) { return sum + (item.prix * item.quantity); }); // Sélectionner le moyen de paiement PaymentMethod? selectedPaymentMethod = await _showPaymentMethodDialog(); if (selectedPaymentMethod == null) return; // Confirmer le paiement avec le moyen sélectionné final bool? confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirmer le paiement'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${widget.tablename}'), const SizedBox(height: 8), Text('Articles: ${_cartItems.length}'), const SizedBox(height: 8), Text( 'Total: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: selectedPaymentMethod.color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: selectedPaymentMethod.color.withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( selectedPaymentMethod.icon, color: selectedPaymentMethod.color, size: 24, ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Paiement: ${selectedPaymentMethod.name}', style: TextStyle( fontWeight: FontWeight.w600, color: selectedPaymentMethod.color, ), ), Text( selectedPaymentMethod.description, style: TextStyle( fontSize: 12, color: selectedPaymentMethod.color.withOpacity( 0.7, ), ), ), ], ), ], ), ), const SizedBox(height: 16), const Text('Valider et payer cette commande ?'), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: selectedPaymentMethod.color, ), child: const Text( 'Confirmer le paiement', style: TextStyle(color: Colors.white), ), ), ], ), ); if (confirm != true) return; setState(() { _isValidating = true; }); try { // Créer la commande avec le moyen de paiement sélectionné final response = await http.post( Uri.parse('https://restaurant.careeracademy.mg/api/commandes'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'table_id': widget.tableId, 'statut': 'payee', 'items': _cartItems .map( (item) => { 'menu_id': item.id, 'quantite': item.quantity, 'prix_unitaire': item.prix, 'commentaire': item.commentaire, }, ) .toList(), 'methode_paiement': selectedPaymentMethod.id, // mvola, carte, ou especes 'montant_paye': total, 'date_paiement': DateTime.now().toIso8601String(), 'client_id': 1, 'reservation_id': 1, 'serveur': 'Serveur par défaut', }), ); if (response.statusCode == 201) { final commandeData = jsonDecode(response.body); // Libérer la table await http.patch( Uri.parse( 'https://restaurant.careeracademy.mg/api/tables/${widget.tableId}', ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'status': 'available'}), ); // Convertir le Map en objet CommandeDetail var commandeDetail = CommandeDetail.fromJson(commandeData['data']); var template = PrintTemplate.fromJson(commandeData['data']); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => FactureScreen( commande: commandeDetail, template: template, paymentMethod: selectedPaymentMethod!.id, tablename: widget.tablename ?? 'Table inconnue', // Passer comme paramètre séparé ), ), ); // _showPaymentSuccessDialog(commandeData, total, selectedPaymentMethod); } else { throw Exception('Erreur lors du paiement'); } } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } finally { setState(() { _isValidating = false; }); } } // Dialog de sélection du moyen de paiement Future _showPaymentMethodDialog() async { return await showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.payment, color: Colors.blue), SizedBox(width: 8), Text('Choisir le moyen de paiement'), ], ), content: SizedBox( width: double.maxFinite, child: ListView.separated( shrinkWrap: true, itemCount: paymentMethods.length, separatorBuilder: (context, index) => const SizedBox(height: 8), itemBuilder: (context, index) { final method = paymentMethods[index]; return InkWell( onTap: () => Navigator.of(context).pop(method), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all( color: method.color.withOpacity(0.3), ), borderRadius: BorderRadius.circular(8), color: method.color.withOpacity(0.05), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: method.color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( method.icon, color: method.color, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( method.name, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: method.color, ), ), const SizedBox(height: 2), Text( method.description, style: TextStyle( fontSize: 12, color: method.color.withOpacity(0.7), ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: method.color, size: 16, ), ], ), ), ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ], ), ); } // Dialog de succès mis à jour void _showPaymentSuccessDialog( Map commandeData, double total, PaymentMethod paymentMethod, ) { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.check_circle, color: Colors.green, size: 28), SizedBox(width: 8), Text('Paiement confirmé'), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Commande #${commandeData['id']}'), const SizedBox(height: 8), Text('Table ${widget.tablename} libérée'), const SizedBox(height: 8), Text( 'Montant: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA', style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: paymentMethod.color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: paymentMethod.color.withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( paymentMethod.icon, color: paymentMethod.color, size: 16, ), const SizedBox(width: 6), Text( paymentMethod.name, style: TextStyle( color: paymentMethod.color, fontWeight: FontWeight.w600, fontSize: 14, ), ), ], ), ), const SizedBox(height: 16), const Text('Voulez-vous imprimer un reçu ?'), ], ), actions: [ TextButton( onPressed: () => _navigateToTables(), child: const Text('Non merci'), ), ElevatedButton( onPressed: () => _imprimerEtNaviguer(commandeData, total, paymentMethod), style: ElevatedButton.styleFrom(backgroundColor: Colors.green), child: const Text('Imprimer'), ), ], ), ); } void _navigateToTables() { Navigator.of(context).pop(); // Fermer le dialog Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (context) => const MainLayout(child: TablesScreen()), ), (route) => false, ); } Future _imprimerEtNaviguer( Map commandeData, double total, PaymentMethod paymentMethod, ) async { Navigator.of(context).pop(); // Fermer le dialog // Afficher un indicateur de chargement pendant l'impression showDialog( context: context, barrierDismissible: false, builder: (context) => const AlertDialog( content: Row( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(width: 16), Text('Impression en cours...'), ], ), ), ); try { await _imprimerTicketPaiement(commandeData, total); // Fermer le dialog d'impression Navigator.of(context).pop(); // Afficher un message de succès ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ticket imprimé avec succès'), backgroundColor: Colors.green, duration: Duration(seconds: 2), ), ); } catch (e) { // Fermer le dialog d'impression Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur d\'impression: ${e.toString()}'), backgroundColor: Colors.orange, duration: const Duration(seconds: 3), ), ); } // Attendre un peu puis naviguer Future.delayed(const Duration(milliseconds: 500), () { _navigateToTables(); }); } Future _imprimerTicketPaiement( Map commandeData, double total, ) async { try { // Préparer les données pour l'impression _cartItems .map( (item) => { 'nom': item.nom, 'quantite': item.quantity, 'prix_unitaire': item.prix, 'sous_total': item.prix * item.quantity, }, ) .toList(); // Appeler le service d'impression // await PlatformPrintService.printFacture( // commande: widget.commande, // paymentMethod: widget.paymentMethod, // ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ticket imprimé avec succès'), backgroundColor: Colors.green, ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur d\'impression: ${e.toString()}'), backgroundColor: Colors.orange, ), ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back, color: Colors.black), ), title: const Text( 'Panier', style: TextStyle( color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text( 'Retour au menu', style: TextStyle(color: Colors.black, fontSize: 16), ), ), const SizedBox(width: 16), ], ), body: _cartItems.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.shopping_cart_outlined, size: 80, color: Colors.grey[400], ), const SizedBox(height: 16), Text( 'Votre panier est vide', style: TextStyle(fontSize: 18, color: Colors.grey[600]), ), ], ), ) : Column( children: [ // Header avec infos table Container( width: double.infinity, padding: const EdgeInsets.all(20), color: Colors.white, child: Text( '${widget.tablename} • ${widget.personne} personne${widget.personne > 1 ? 's' : ''}', style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), ), // Liste des articles Expanded( child: ListView.separated( padding: const EdgeInsets.all(16), itemCount: _cartItems.length, separatorBuilder: (context, index) => const SizedBox(height: 12), itemBuilder: (context, index) { final item = _cartItems[index]; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( item.nom, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), IconButton( onPressed: () => _removeItem(index), icon: const Icon( Icons.delete_outline, color: Colors.red, ), constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), ], ), Text( '${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), if (item.commentaire.isNotEmpty) ...[ const SizedBox(height: 8), Text( 'Notes: ${item.commentaire}', style: TextStyle( fontSize: 14, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Contrôles de quantité Row( children: [ IconButton( onPressed: () => _updateQuantity( index, item.quantity - 1, ), icon: const Icon(Icons.remove), style: IconButton.styleFrom( backgroundColor: Colors.grey[200], minimumSize: const Size(40, 40), ), ), const SizedBox(width: 16), Text( item.quantity.toString(), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 16), IconButton( onPressed: () => _updateQuantity( index, item.quantity + 1, ), icon: const Icon(Icons.add), style: IconButton.styleFrom( backgroundColor: Colors.grey[200], minimumSize: const Size(40, 40), ), ), ], ), // Prix total de l'article Text( '${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green[700], ), ), ], ), ], ), ); }, ), ), // Récapitulatif Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.grey[200]!)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Récapitulatif', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Articles:', style: TextStyle(fontSize: 16), ), Text( _getTotalArticles().toString(), style: const TextStyle(fontSize: 16), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Table:', style: TextStyle(fontSize: 16), ), Text( widget.tableId.toString(), style: const TextStyle(fontSize: 16), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Personnes:', style: TextStyle(fontSize: 16), ), Text( widget.personne.toString(), style: const TextStyle(fontSize: 16), ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Total:', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( '${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 20), // Bouton Valider la commande (toujours visible) SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _cartItems.isNotEmpty && !_isValidating ? _showConfirmationDialog : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.green[700], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), 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, ), ), ), const SizedBox(width: 8), const Text( 'Validation en cours...', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ] else ...[ const Icon(Icons.check, size: 20), const SizedBox(width: 8), const Text( 'Valider la commande', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ], ), ), ), // Espacement conditionnel if (MediaQuery.of(context).size.width >= 768) const SizedBox(height: 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], ), 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, ), ), ] else ...[ const Icon(Icons.payment, size: 20), const SizedBox(width: 8), const Text( 'Payer directement', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ], ), ), ), ], ), ), ], ), ); } } class CartItemModel { final int id; final String nom; final double prix; int quantity; final String commentaire; CartItemModel({ required this.id, required this.nom, required this.prix, required this.quantity, required this.commentaire, }); }