diff --git a/lib/pages/cart_page.dart b/lib/pages/cart_page.dart index 1047bf9..bd68637 100644 --- a/lib/pages/cart_page.dart +++ b/lib/pages/cart_page.dart @@ -2,6 +2,10 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'package:itrimobe/pages/tables.dart'; + +import '../layouts/main_layout.dart'; + class CartPage extends StatefulWidget { final int tableId; final int personne; @@ -119,30 +123,28 @@ class _CartPageState extends State { child: Text('Annuler', style: TextStyle(color: Colors.grey[600])), ), ElevatedButton( - onPressed: - _isValidating - ? null - : () { - Navigator.of(context).pop(); - _validateOrder(); - }, + onPressed: _isValidating + ? null + : () { + Navigator.of(context).pop(); + _validateOrder(); + }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green[700], foregroundColor: Colors.white, ), - child: - _isValidating - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), + child: _isValidating + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, ), - ) - : Text('Valider'), + ), + ) + : Text('Valider'), ), ], ); @@ -163,16 +165,15 @@ class _CartPageState extends State { "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.notes.isNotEmpty ? item.notes : null, - }, - ) - .toList(), + "items": _cartItems + .map( + (item) => { + "menu_id": item.id, + "quantite": item.quantity, + "commentaires": item.notes.isNotEmpty ? item.notes : null, + }, + ) + .toList(), }; // Appel API pour créer la commande @@ -187,7 +188,7 @@ class _CartPageState extends State { if (response.statusCode == 200 || response.statusCode == 201) { // Succès - _updateTableStatus(); + _updateTableStatus(); _showSuccessDialog(); } else { // Erreur @@ -206,15 +207,15 @@ class _CartPageState extends State { String _getOrderComments() { // Concaténer toutes les notes des articles pour les commentaires généraux - List allNotes = - _cartItems - .where((item) => item.notes.isNotEmpty) - .map((item) => '${item.nom}: ${item.notes}') - .toList(); + List allNotes = _cartItems + .where((item) => item.notes.isNotEmpty) + .map((item) => '${item.nom}: ${item.notes}') + .toList(); return allNotes.join('; '); } + // FONCTION CORRIGÉE POUR LA NAVIGATION VERS LES TABLES void _showSuccessDialog() { showDialog( context: context, @@ -234,14 +235,13 @@ class _CartPageState extends State { actions: [ ElevatedButton( onPressed: () { - Navigator.of(context).pop(); // Fermer le dialog - Navigator.of(context).pop(); // Retourner au menu - Navigator.of(context).pop(); // Retourner aux tables + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => MainLayout(child: TablesScreen()), + ), + (route) => false, + ); }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - ), child: Text('OK'), ), ], @@ -251,17 +251,17 @@ class _CartPageState extends State { } Future _updateTableStatus() async { - try { - final updateResponse = 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) { - print("Erreur lors de la mise à jour du statut de la table: $e"); + try { + final updateResponse = 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) { + print("Erreur lors de la mise à jour du statut de la table: $e"); + } } -} - void _showErrorDialog(String message) { showDialog( @@ -317,297 +317,290 @@ class _CartPageState extends State { SizedBox(width: 16), ], ), - body: - _cartItems.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.shopping_cart_outlined, - size: 80, - color: Colors.grey[400], - ), - SizedBox(height: 16), - Text( - 'Votre panier est vide', - style: TextStyle(fontSize: 18, color: Colors.grey[600]), - ), - ], - ), - ) - : Column( + body: _cartItems.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - // Header avec infos table - Container( - width: double.infinity, - padding: EdgeInsets.all(20), - color: Colors.white, - child: Text( - 'Table ${widget.tableId} • ${widget.personne} personne${widget.personne > 1 ? 's' : ''}', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), + Icon( + Icons.shopping_cart_outlined, + size: 80, + color: Colors.grey[400], ), + 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: EdgeInsets.all(20), + color: Colors.white, + child: Text( + 'Table ${widget.tableId} • ${widget.personne} personne${widget.personne > 1 ? 's' : ''}', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ), - // Liste des articles - Expanded( - child: ListView.separated( - padding: EdgeInsets.all(16), - itemCount: _cartItems.length, - separatorBuilder: - (context, index) => SizedBox(height: 12), - itemBuilder: (context, index) { - final item = _cartItems[index]; - return Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 5, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - item.nom, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + // Liste des articles + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(16), + itemCount: _cartItems.length, + separatorBuilder: (context, index) => SizedBox(height: 12), + itemBuilder: (context, index) { + final item = _cartItems[index]; + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item.nom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), ), - IconButton( - onPressed: () => _removeItem(index), - icon: Icon( - Icons.delete_outline, - color: Colors.red, - ), - constraints: BoxConstraints(), - padding: EdgeInsets.zero, + ), + IconButton( + onPressed: () => _removeItem(index), + icon: Icon( + Icons.delete_outline, + color: Colors.red, ), - ], + constraints: BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + Text( + '${item.prix.toStringAsFixed(2)} MGA l\'unité', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], ), + ), + if (item.notes.isNotEmpty) ...[ + SizedBox(height: 8), Text( - '${item.prix.toStringAsFixed(2)} MGA l\'unité', + 'Notes: ${item.notes}', style: TextStyle( fontSize: 14, color: Colors.grey[600], + fontStyle: FontStyle.italic, ), ), - if (item.notes.isNotEmpty) ...[ - SizedBox(height: 8), - Text( - 'Notes: ${item.notes}', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - ], - SizedBox(height: 16), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - // Contrôles de quantité - Row( - children: [ - IconButton( - onPressed: - () => _updateQuantity( - index, - item.quantity - 1, - ), - icon: Icon(Icons.remove), - style: IconButton.styleFrom( - backgroundColor: Colors.grey[200], - minimumSize: Size(40, 40), - ), + ], + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Contrôles de quantité + Row( + children: [ + IconButton( + onPressed: () => _updateQuantity( + index, + item.quantity - 1, + ), + icon: Icon(Icons.remove), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), + ), + ), + SizedBox(width: 16), + Text( + item.quantity.toString(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - SizedBox(width: 16), - Text( - item.quantity.toString(), - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + ), + SizedBox(width: 16), + IconButton( + onPressed: () => _updateQuantity( + index, + item.quantity + 1, ), - SizedBox(width: 16), - IconButton( - onPressed: - () => _updateQuantity( - index, - item.quantity + 1, - ), - icon: Icon(Icons.add), - style: IconButton.styleFrom( - backgroundColor: Colors.grey[200], - minimumSize: Size(40, 40), - ), + icon: Icon(Icons.add), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), ), - ], - ), - // Prix total de l'article - Text( - '${(item.prix * item.quantity).toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.green[700], ), + ], + ), + // Prix total de l'article + Text( + '${(item.prix * item.quantity).toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], ), - ], - ), - ], - ), - ); - }, - ), - ), - - // Récapitulatif - Container( - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey[200]!)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Récapitulatif', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Articles:', style: TextStyle(fontSize: 16)), - Text( - _getTotalArticles().toString(), - style: TextStyle(fontSize: 16), - ), - ], - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Table:', style: TextStyle(fontSize: 16)), - Text( - widget.tableId.toString(), - style: TextStyle(fontSize: 16), + ), + ], ), ], ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Personnes:', style: TextStyle(fontSize: 16)), - Text( - widget.personne.toString(), - style: TextStyle(fontSize: 16), - ), - ], + ); + }, + ), + ), + + // Récapitulatif + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey[200]!)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Récapitulatif', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, ), - SizedBox(height: 16), - Divider(), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Total:', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Articles:', style: TextStyle(fontSize: 16)), + Text( + _getTotalArticles().toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Table:', style: TextStyle(fontSize: 16)), + Text( + widget.tableId.toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Personnes:', style: TextStyle(fontSize: 16)), + Text( + widget.personne.toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 16), + Divider(), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - Text( - '${_calculateTotal().toStringAsFixed(2)} MGA', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + ), + Text( + '${_calculateTotal().toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - ], - ), - SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: - _cartItems.isNotEmpty && !_isValidating - ? _showConfirmationDialog - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green[700], - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - disabledBackgroundColor: Colors.grey[300], + ), + ], + ), + SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _cartItems.isNotEmpty && !_isValidating + ? _showConfirmationDialog + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_isValidating) ...[ - SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isValidating) ...[ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white, ), ), - SizedBox(width: 8), - Text( - 'Validation en cours...', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + ), + SizedBox(width: 8), + Text( + 'Validation en cours...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), - ] else ...[ - Icon(Icons.check, size: 20), - SizedBox(width: 8), - Text( - 'Valider la commande', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + ), + ] else ...[ + Icon(Icons.check, size: 20), + SizedBox(width: 8), + Text( + 'Valider la commande', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, ), - ], + ), ], - ), + ], ), ), - ], - ), + ), + ], ), - ], - ), + ), + ], + ), ); } } @@ -626,4 +619,4 @@ class CartItemModel { required this.quantity, required this.notes, }); -} +} \ No newline at end of file diff --git a/lib/pages/commande_item_screen.dart b/lib/pages/commande_item_screen.dart new file mode 100644 index 0000000..1d0129d --- /dev/null +++ b/lib/pages/commande_item_screen.dart @@ -0,0 +1,695 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +// Import de la page de validation (à ajuster selon votre structure de dossiers) +import 'commande_item_validation.dart'; + +class AddItemsToOrderPage extends StatefulWidget { + final int commandeId; + final String numeroCommande; + + const AddItemsToOrderPage({ + Key? key, + required this.commandeId, + required this.numeroCommande + }) : super(key: key); + + @override + State createState() => _AddItemsToOrderPageState(); +} + +class _AddItemsToOrderPageState extends State { + int? _selectedCategory; + List _categories = []; + List _menus = []; + List _cart = []; + bool _isLoading = false; + Map? _commandeDetails; + + @override + void initState() { + super.initState(); + fetchCategories(); + fetchCommandeDetails(); + } + + Future fetchCategories() async { + try { + final url = Uri.parse( + "https://restaurant.careeracademy.mg/api/menu-categories", + ); + final response = await http.get(url); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final categoriesList = + (jsonResponse['data']?['categories'] ?? []) as List; + + setState(() { + _categories = categoriesList; + if (_categories.isNotEmpty) { + _selectedCategory = _categories[0]['id']; + fetchMenus(_selectedCategory!); + } + }); + } else { + print("Erreur API catégories: ${response.statusCode}"); + } + } catch (e) { + print("Exception fetchCategories: $e"); + } + } + + Future fetchMenus(int categoryId) async { + try { + final url = Uri.parse( + "https://restaurant.careeracademy.mg/api/menus/category/$categoryId?disponible=true", + ); + final response = await http.get(url); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final List menusList = + jsonResponse is List ? jsonResponse : (jsonResponse['data'] ?? []); + + setState(() { + _menus = menusList; + }); + } else { + print("Erreur API menus: ${response.statusCode}"); + } + } catch (e) { + print("Exception fetchMenus: $e"); + } + } + + Future fetchCommandeDetails() async { + try { + final url = Uri.parse("https://restaurant.careeracademy.mg/api/commandes/${widget.commandeId}"); + final response = await http.get(url); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final data = jsonResponse['data']; + print(data); + + setState(() { + _commandeDetails = data; + }); + } else { + print("Erreur chargement commande: ${response.statusCode}"); + } + } catch (e) { + print("Exception fetchCommandeDetails: $e"); + } + } + + void changeCategory(int id) { + setState(() { + _selectedCategory = id; + _menus = []; + }); + fetchMenus(id); + } + + void addToCart(dynamic item, int quantity, String notes) { + setState(() { + for (int i = 0; i < quantity; i++) { + Map cartItem = Map.from(item); + if (notes.isNotEmpty) { + cartItem['notes'] = notes; + } + _cart.add(cartItem); + } + }); + } + + void showAddToCartModal(dynamic item) { + showDialog( + context: context, + builder: (BuildContext context) { + return AddToCartModal(item: item, onAddToCart: addToCart); + }, + ); + } + + // NOUVELLE FONCTION: Navigation vers la page de validation + Future _navigateToValidationPage() async { + if (_cart.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Aucun article à ajouter'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + // Naviguer vers la page de validation + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ValidateAddItemsPage( + commandeId: widget.commandeId, + numeroCommande: widget.numeroCommande, + newItems: _cart, + commandeDetails: _commandeDetails, + ), + ), + ); + + // Si l'ajout a été effectué avec succès, retourner à la page précédente + if (result == true) { + Navigator.of(context).pop(true); + } + } + + /// Conversion sécurisée du prix en string avec 2 décimales + String formatPrix(dynamic prix) { + if (prix == null) return ""; + double? val; + if (prix is num) { + val = prix.toDouble(); + } else if (prix is String) { + val = double.tryParse(prix); + } + return val != null ? val.toStringAsFixed(2) : ""; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Ajouter des articles"), + backgroundColor: Colors.white, + elevation: 1, + actions: [ + IconButton( + onPressed: () { + if (_selectedCategory != null) fetchMenus(_selectedCategory!); + }, + icon: const Icon(Icons.refresh), + ), + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header avec info commande + Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + color: Colors.grey[50], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Commande: ${widget.numeroCommande}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 4), + const Text( + "Sélectionnez les articles à ajouter", + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 12), + + if (_commandeDetails != null && _commandeDetails!['items'] != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Articles déjà commandés:", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + ..._commandeDetails!['items'].map((item) { + final nom = item['menu_nom'] ?? 'Inconnu'; + final quantite = item['quantite'] ?? 1; + final description = item['menu_description'] ?? ''; + // Correction: utiliser 'menu_prix_actuel' pour afficher le prix + final prix = item['menu_prix_actuel'] ?? item['prix_unitaire'] ?? 0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + nom, + style: const TextStyle(fontSize: 14) + ), + ), + Text( + "x$quantite - ${formatPrix(prix)} MGA", + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500) + ), + ], + ), + if (description.isNotEmpty) + Text( + description, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + const Divider(height: 8), + ], + ), + ); + }).toList(), + ], + ) + else + const Text( + "Aucun article encore commandé.", + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + ), + + // Catégories + if (_categories.isNotEmpty) + Container( + color: Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: _categories.map((cat) { + return buildCategoryButton(cat['nom'], cat['id']); + }).toList(), + ), + ) + else + const Center(child: CircularProgressIndicator()), + + // Liste des menus + Expanded( + child: _menus.isNotEmpty + ? ListView.builder( + itemCount: _menus.length, + itemBuilder: (context, index) { + final item = _menus[index]; + return Card( + margin: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: ListTile( + onTap: () => showAddToCartModal(item), + leading: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.green[100], + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.restaurant_menu, + color: Colors.green[700], + size: 30, + ), + ), + title: Text( + item['nom'] ?? 'Nom non disponible', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(item['commentaire'] ?? ''), + trailing: Text( + "${formatPrix(item['prix'])} MGA", + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ); + }, + ) + : const Center(child: Text("Aucun menu disponible")), + ), + + // Bouton pour valider l'ajout - MODIFIÉ + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + color: Colors.white, + child: Column( + children: [ + if (_cart.isNotEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + "${_cart.length} article(s) sélectionné(s)", + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _cart.isEmpty ? null : _navigateToValidationPage, // CHANGÉ ICI + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + disabledBackgroundColor: Colors.grey[300], // Ajouté pour le style désactivé + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _cart.isEmpty ? Icons.shopping_cart_outlined : Icons.preview, + size: 20, + ), + const SizedBox(width: 8), + Text( + _cart.isEmpty + ? "Sélectionner des articles" + : "Valider l'ajout (${_cart.length})", // CHANGÉ LE TEXTE + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget buildCategoryButton(String label, int id) { + final selected = _selectedCategory == id; + return Expanded( + child: GestureDetector( + onTap: () => changeCategory(id), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: selected ? Colors.green[700] : Colors.grey[100], + border: Border( + bottom: BorderSide( + color: selected ? Colors.green[700]! : Colors.transparent, + width: 2, + ), + ), + ), + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + color: selected ? Colors.white : Colors.black87, + ), + ), + ), + ), + ), + ); + } +} + +// Modal pour ajouter au panier (même que MenuPage) +class AddToCartModal extends StatefulWidget { + final dynamic item; + final Function(dynamic, int, String) onAddToCart; + + const AddToCartModal({ + Key? key, + required this.item, + required this.onAddToCart, + }) : super(key: key); + + @override + State createState() => _AddToCartModalState(); +} + +class _AddToCartModalState extends State { + int _quantity = 1; + final TextEditingController _notesController = TextEditingController(); + + String formatPrix(dynamic prix) { + if (prix == null) return "0.00"; + double? val; + if (prix is num) { + val = prix.toDouble(); + } else if (prix is String) { + val = double.tryParse(prix); + } + return val != null ? val.toStringAsFixed(2) : "0.00"; + } + + double calculateTotal() { + double prix = 0.0; + if (widget.item['prix'] is num) { + prix = widget.item['prix'].toDouble(); + } else if (widget.item['prix'] is String) { + prix = double.tryParse(widget.item['prix']) ?? 0.0; + } + return prix * _quantity; + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + padding: const EdgeInsets.all(20), + constraints: const BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.item['nom'] ?? 'Menu', + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 16), + + // Image placeholder + Container( + width: double.infinity, + height: 150, + decoration: BoxDecoration( + color: Colors.green[100], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.restaurant_menu, + size: 60, + color: Colors.green[700], + ), + ), + const SizedBox(height: 16), + + // Description + if (widget.item['commentaire'] != null && + widget.item['commentaire'].toString().isNotEmpty) + Text( + widget.item['commentaire'], + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 16), + + // Prix unitaire + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Prix unitaire", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + Text( + "${formatPrix(widget.item['prix'])} MGA", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Quantité + const Text( + "Quantité", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: _quantity > 1 + ? () { + setState(() { + _quantity--; + }); + } + : null, + icon: const Icon(Icons.remove), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + ), + ), + const SizedBox(width: 20), + Text( + _quantity.toString(), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 20), + IconButton( + onPressed: () { + setState(() { + _quantity++; + }); + }, + icon: const Icon(Icons.add), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Notes + const Text( + "Notes (optionnel)", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + TextField( + controller: _notesController, + maxLines: 3, + decoration: InputDecoration( + hintText: "Commentaires spéciaux (sans oignons, bien cuit...)", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.all(12), + ), + ), + const SizedBox(height: 24), + + // Total et bouton d'ajout + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Total", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + "${calculateTotal().toStringAsFixed(2)} MGA", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + widget.onAddToCart( + widget.item, + _quantity, + _notesController.text, + ); + Navigator.of(context).pop(); + + // Afficher un snackbar de confirmation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "${widget.item['nom']} ajouté au panier", + ), + backgroundColor: Colors.green, + duration: const Duration(seconds: 2), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + "Ajouter au panier", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/commande_item_validation.dart b/lib/pages/commande_item_validation.dart new file mode 100644 index 0000000..b7fa273 --- /dev/null +++ b/lib/pages/commande_item_validation.dart @@ -0,0 +1,727 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class ValidateAddItemsPage extends StatefulWidget { + final int commandeId; + final String numeroCommande; + final List newItems; // Nouveaux articles à ajouter + final Map? commandeDetails; // Détails de la commande existante + + const ValidateAddItemsPage({ + Key? key, + required this.commandeId, + required this.numeroCommande, + required this.newItems, + required this.commandeDetails, + }) : super(key: key); + + @override + State createState() => _ValidateAddItemsPageState(); +} + +class _ValidateAddItemsPageState extends State { + List _newCartItems = []; + bool _isValidating = false; + + @override + void initState() { + super.initState(); + _processNewItems(); + } + + void _processNewItems() { + // Grouper les nouveaux articles identiques + Map groupedItems = {}; + + for (var item in widget.newItems) { + 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, + notes: item['notes'] ?? '', + ); + } + } + + setState(() { + _newCartItems = 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) { + _newCartItems[index].quantity = newQuantity; + } else { + _newCartItems.removeAt(index); + } + }); + } + + void _removeItem(int index) { + setState(() { + _newCartItems.removeAt(index); + }); + } + + // Calculer le total des nouveaux articles + double _calculateNewItemsTotal() { + return _newCartItems.fold( + 0.0, + (sum, item) => sum + (item.prix * item.quantity), + ); + } + + // Calculer le total des articles existants + double _calculateExistingItemsTotal() { + if (widget.commandeDetails == null || widget.commandeDetails!['items'] == null) { + return 0.0; + } + + double total = 0.0; + for (var item in widget.commandeDetails!['items']) { + // Correction: utiliser 'menu_prix_actuel' au lieu de 'menu_prix' + double prix = _parsePrice(item['menu_prix_actuel'] ?? item['prix_unitaire']); + int quantite = item['quantite'] ?? 1; + total += prix * quantite; + } + return total; + } + + // Calculer le total général + double _calculateGrandTotal() { + return _calculateExistingItemsTotal() + _calculateNewItemsTotal(); + } + + int _getTotalNewArticles() { + return _newCartItems.fold(0, (sum, item) => sum + item.quantity); + } + + int _getTotalExistingArticles() { + if (widget.commandeDetails == null || widget.commandeDetails!['items'] == null) { + return 0; + } + return widget.commandeDetails!['items'].fold(0, (sum, item) => sum + (item['quantite'] ?? 1)); + } + + void _showConfirmationDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Confirmer l\'ajout', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Êtes-vous sûr de vouloir ajouter ces articles à la commande ?'), + SizedBox(height: 16), + Text( + 'Récapitulatif:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text('• Commande: ${widget.numeroCommande}'), + Text('• Nouveaux articles: ${_getTotalNewArticles()}'), + Text('• Nouveau total: ${_calculateGrandTotal().toStringAsFixed(2)} 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(); + _validateAddItems(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + ), + child: _isValidating + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text('Confirmer'), + ), + ], + ); + }, + ); + } + + Future _validateAddItems() async { + if (_newCartItems.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Aucun article à ajouter'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + setState(() { + _isValidating = true; + }); + + try { + // Préparer les données des nouveaux articles +// 1. Construire la liste des items sans commande_id à l’intérieur +List> items = _newCartItems.map((cartItem) { + return { + 'menu_id': cartItem.id, + 'quantite': cartItem.quantity, + 'commentaires': cartItem.notes.isNotEmpty ? cartItem.notes : null, + }; +}).toList(); + +// 2. Construire le body correctement avec commande_id à la racine +final body = { + 'commande_id': widget.commandeId, + 'items': items, +}; + +print("📦 Données envoyées : ${json.encode(body)}"); + +// 3. Envoi vers l'API +final response = await http.post( + Uri.parse('https://restaurant.careeracademy.mg/api/commande-items/add-multiple'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode(body), +); + +// 4. Afficher la réponse backend +final responseData = jsonDecode(response.body); +print('✅ Réponse backend : $responseData'); + + if (response.statusCode == 200 || response.statusCode == 201) { + final responseData = json.decode(response.body); + print(responseData); + + if (responseData['success'] == true) { + _showSuccessDialog(); + } else { + throw Exception('API returned success: false'); + } + } else { + throw Exception('Failed to add items: ${response.statusCode}'); + } + } catch (e) { + _showErrorDialog('Erreur lors de l\'ajout: $e'); + } finally { + setState(() { + _isValidating = false; + }); + } + } + + void _showSuccessDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon(Icons.check_circle, color: Colors.green[700]), + SizedBox(width: 8), + Text('Articles ajoutés'), + ], + ), + content: Text( + 'Les nouveaux articles ont été ajoutés à la commande avec succès !', + ), + actions: [ + ElevatedButton( + onPressed: () { + // Retourner à la page précédente avec un indicateur de succès + Navigator.of(context).pop(); + Navigator.of(context).pop(true); + }, + child: Text('OK'), + ), + ], + ); + }, + ); + } + + void _showErrorDialog(String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon(Icons.error, color: Colors.red), + SizedBox(width: 8), + Text('Erreur'), + ], + ), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('OK'), + ), + ], + ); + }, + ); + } + + @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: Icon(Icons.arrow_back, color: Colors.black), + ), + title: Text( + 'Valider l\'ajout', + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Annuler', + style: TextStyle(color: Colors.black, fontSize: 16), + ), + ), + SizedBox(width: 16), + ], + ), + body: Column( + children: [ + // Header avec infos commande et totaux + Container( + width: double.infinity, + padding: EdgeInsets.all(20), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Commande: ${widget.numeroCommande}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + SizedBox(height: 16), + + // Résumé des articles existants + if (widget.commandeDetails != null && widget.commandeDetails!['items'] != null) ...[ + Text( + 'Articles déjà commandés:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey[700], + ), + ), + SizedBox(height: 8), + ...widget.commandeDetails!['items'].map((item) { + final nom = item['menu_nom'] ?? 'Inconnu'; + final quantite = item['quantite'] ?? 1; + // Correction: utiliser 'menu_prix_actuel' au lieu de 'menu_prix' + final prix = _parsePrice(item['menu_prix_actuel'] ?? item['prix_unitaire']); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '$nom x$quantite', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ), + Text( + '${(prix * quantite).toStringAsFixed(2)} MGA', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ], + ), + ); + }).toList(), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Sous-total existant:', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + Text( + '${_calculateExistingItemsTotal().toStringAsFixed(2)} MGA', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + ], + ), + Divider(height: 20), + ], + + Text( + 'Nouveaux articles à ajouter: ${_getTotalNewArticles()}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.green[700], + ), + ), + ], + ), + ), + + // Liste des nouveaux articles + Expanded( + child: _newCartItems.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.shopping_cart_outlined, + size: 80, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Aucun nouvel article sélectionné', + style: TextStyle(fontSize: 18, color: Colors.grey[600]), + ), + ], + ), + ) + : ListView.separated( + padding: EdgeInsets.all(16), + itemCount: _newCartItems.length, + separatorBuilder: (context, index) => SizedBox(height: 12), + itemBuilder: (context, index) { + final item = _newCartItems[index]; + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green[200]!, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Icon(Icons.add_circle, color: Colors.green[600], size: 20), + SizedBox(width: 8), + Expanded( + child: Text( + item.nom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + IconButton( + onPressed: () => _removeItem(index), + icon: Icon( + Icons.delete_outline, + color: Colors.red, + ), + constraints: BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + Text( + '${item.prix.toStringAsFixed(2)} MGA l\'unité', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + if (item.notes.isNotEmpty) ...[ + SizedBox(height: 8), + Text( + 'Notes: ${item.notes}', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Contrôles de quantité + Row( + children: [ + IconButton( + onPressed: () => _updateQuantity( + index, + item.quantity - 1, + ), + icon: Icon(Icons.remove), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), + ), + ), + SizedBox(width: 16), + Text( + item.quantity.toString(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 16), + IconButton( + onPressed: () => _updateQuantity( + index, + item.quantity + 1, + ), + icon: Icon(Icons.add), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), + ), + ), + ], + ), + // Prix total de l'article + Text( + '${(item.prix * item.quantity).toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + + // Récapitulatif final + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey[200]!)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Récapitulatif final', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Articles existants:', style: TextStyle(fontSize: 16)), + Text( + _getTotalExistingArticles().toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Sous-total existant:', style: TextStyle(fontSize: 16)), + Text( + '${_calculateExistingItemsTotal().toStringAsFixed(2)} MGA', + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Nouveaux articles:', style: TextStyle(fontSize: 16, color: Colors.green[700])), + Text( + _getTotalNewArticles().toString(), + style: TextStyle(fontSize: 16, color: Colors.green[700]), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Sous-total nouveaux:', style: TextStyle(fontSize: 16, color: Colors.green[700])), + Text( + '${_calculateNewItemsTotal().toStringAsFixed(2)} MGA', + style: TextStyle(fontSize: 16, color: Colors.green[700]), + ), + ], + ), + SizedBox(height: 16), + Divider(), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total général:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${_calculateGrandTotal().toStringAsFixed(2)} MGA', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _newCartItems.isNotEmpty && !_isValidating + ? _showConfirmationDialog + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isValidating) ...[ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + SizedBox(width: 8), + Text( + 'Ajout en cours...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ] else ...[ + Icon(Icons.add_shopping_cart, size: 20), + SizedBox(width: 8), + Text( + _newCartItems.isEmpty + ? 'Aucun article à ajouter' + : 'Ajouter à la commande (${_getTotalNewArticles()})', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// Réutiliser la même classe CartItemModel +class CartItemModel { + final int id; + final String nom; + final double prix; + int quantity; + final String notes; + + CartItemModel({ + required this.id, + required this.nom, + required this.prix, + required this.quantity, + required this.notes, + }); +} \ No newline at end of file diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart index 5e760b2..11d31b5 100644 --- a/lib/pages/commandes_screen.dart +++ b/lib/pages/commandes_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; +import 'commande_item_screen.dart'; + class OrdersManagementScreen extends StatefulWidget { const OrdersManagementScreen({super.key}); @@ -138,6 +140,8 @@ class _OrdersManagementScreenState extends State { return null; } + + Future updateOrderStatus( Order order, String newStatus, { @@ -285,6 +289,9 @@ class _OrdersManagementScreenState extends State { } } + + + List get activeOrders { return orders .where( @@ -340,6 +347,7 @@ class _OrdersManagementScreenState extends State { style: TextStyle(color: Colors.white), ), ), + ], ); }, @@ -348,23 +356,22 @@ class _OrdersManagementScreenState extends State { ); } - Future updateTableStatus(int tableId, String newStatus) async { - const String apiUrl = - 'https://restaurant.careeracademy.mg/api/tables'; // ← adapte l’URL si besoin + Future updateTableStatus(int tableId, String newStatus) async { + const String apiUrl = 'https://restaurant.careeracademy.mg/api/tables'; // ← adapte l’URL si besoin - final response = await http.put( - Uri.parse('$apiUrl/$tableId'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'status': newStatus}), - ); + final response = await http.put( + Uri.parse('$apiUrl/$tableId'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'status': newStatus}), + ); - if (response.statusCode != 200) { - print('Erreur lors de la mise à jour du statut de la table'); - throw Exception('Erreur: ${response.body}'); - } else { - print('✅ Table $tableId mise à jour en $newStatus'); - } + if (response.statusCode != 200) { + print('Erreur lors de la mise à jour du statut de la table'); + throw Exception('Erreur: ${response.body}'); + } else { + print('✅ Table $tableId mise à jour en $newStatus'); } +} @override Widget build(BuildContext context) { @@ -595,37 +602,44 @@ class OrderCard extends StatelessWidget { // Header row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: onViewDetails, - child: Text( - 'Table ${order.tableId}', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: _getStatusColor(order.statut), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _getStatusText(order.statut), - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: _getStatusColor(order.statut), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + _getStatusText(order.statut), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), ), - ), - ), - ], + const SizedBox(width: 4), + if (order.statut == 'en_attente') + IconButton( + icon: const Icon(Icons.add_circle_outline, size: 20, color: Colors.blue), + tooltip: 'Ajouter un article', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddItemsToOrderPage( + commandeId: order.id, + numeroCommande: order.numero ?? 'Commande #${order.id}', + ), + ), + ); + }, + ), + + ], ), const SizedBox(height: 8), @@ -846,6 +860,8 @@ class OrderCard extends StatelessWidget { } } + + // Updated Order model to include items class Order { final int id; @@ -918,6 +934,8 @@ class Order { items: orderItems, ); } + + get numero => null; } class OrderItem {