diff --git a/lib/pages/menu.dart b/lib/pages/menu.dart new file mode 100644 index 0000000..fcb25df --- /dev/null +++ b/lib/pages/menu.dart @@ -0,0 +1,506 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class MenuPage extends StatefulWidget { + final int tableId; + final int personne; + + const MenuPage({Key? key, required this.tableId, required this.personne}) : super(key: key); + + @override + State createState() => _MenuPageState(); +} + +class _MenuPageState extends State { + int? _selectedCategory; + List _categories = []; + List _menus = []; + List _cart = []; + + @override + void initState() { + super.initState(); + fetchCategories(); + } + + 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"); + } + } + + 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, + ); + }, + ); + } + + /// 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: Text("Menu"), + actions: [ + IconButton( + onPressed: () { + if (_selectedCategory != null) fetchMenus(_selectedCategory!); + }, + icon: Icon(Icons.refresh), + ), + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Table ${widget.tableId} • ${widget.personne} personne${widget.personne > 1 ? 's' : ''}", + style: TextStyle(fontSize: 16), + ), + ), + if (_categories.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: _categories.map((cat) { + return buildCategoryButton(cat['nom'], cat['id']); + }).toList(), + ) + else + Center(child: CircularProgressIndicator()), + Expanded( + child: _menus.isNotEmpty + ? ListView.builder( + itemCount: _menus.length, + itemBuilder: (context, index) { + final item = _menus[index]; + return Card( + margin: EdgeInsets.all(8), + child: ListTile( + onTap: () => showAddToCartModal(item), // Clic sur tout l'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: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text(item['commentaire'] ?? ''), + trailing: Text( + "${formatPrix(item['prix'])} €", + style: TextStyle( + color: Colors.green[700], + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ); + }, + ) + : Center(child: Text("Aucun menu disponible")), + ), + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(vertical: 12), + color: Colors.green[700], + child: Center( + child: TextButton( + onPressed: () { + // TODO: Naviguer vers la page panier + }, + child: Text( + "Voir le panier (${_cart.length})", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildCategoryButton(String label, int id) { + final selected = _selectedCategory == id; + return Expanded( + child: GestureDetector( + onTap: () => changeCategory(id), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10), + color: selected ? Colors.grey[300] : Colors.grey[100], + child: Center( + child: Text( + label, + style: TextStyle( + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + ), + ), + ), + ), + ), + ); + } +} + +// Modal pour ajouter au panier +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: EdgeInsets.all(20), + constraints: BoxConstraints(maxWidth: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.item['nom'] ?? 'Menu', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon(Icons.close), + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + ), + ], + ), + 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], + ), + ), + 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], + ), + ), + SizedBox(height: 16), + + // Prix unitaire + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Prix unitaire", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + Text( + "${formatPrix(widget.item['prix'])} €", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + SizedBox(height: 20), + + // Quantité + Text( + "Quantité", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: _quantity > 1 ? () { + setState(() { + _quantity--; + }); + } : null, + icon: Icon(Icons.remove), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + ), + ), + SizedBox(width: 20), + Text( + _quantity.toString(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 20), + IconButton( + onPressed: () { + setState(() { + _quantity++; + }); + }, + icon: Icon(Icons.add), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + ), + ), + ], + ), + SizedBox(height: 20), + + // Notes + Text( + "Notes (optionnel)", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + 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: EdgeInsets.all(12), + ), + ), + SizedBox(height: 24), + + // Total et bouton d'ajout + Container( + width: double.infinity, + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Total", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + "${calculateTotal().toStringAsFixed(2)} €", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + 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: Duration(seconds: 2), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + "Ajouter au panier", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index 6aa14a7..3af4768 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; +import 'dart:io' show Platform; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'dart:convert'; + +import 'menu.dart'; // Assure-toi que ce fichier contient la page MenuPage class TableData { final int id; @@ -26,8 +30,9 @@ class TableData { } class TablesScreen extends StatefulWidget { + const TablesScreen({super.key}); @override - _TablesScreenState createState() => _TablesScreenState(); + State createState() => _TablesScreenState(); } class _TablesScreenState extends State { @@ -41,18 +46,23 @@ class _TablesScreenState extends State { } Future fetchTables() async { - final url = Uri.parse("https://restaurant.careeracademy.mg/api/tables"); - final response = await http.get(url); + try { + final url = Uri.parse("https://restaurant.careeracademy.mg/api/tables"); + final response = await http.get(url); - if (response.statusCode == 200) { - final List data = json.decode(response.body)['data']; - setState(() { - tables = data.map((json) => TableData.fromJson(json)).toList(); - isLoading = false; - }); - } else { + if (response.statusCode == 200) { + final List data = json.decode(response.body)['data']; + setState(() { + tables = data.map((json) => TableData.fromJson(json)).toList(); + isLoading = false; + }); + } else { + setState(() => isLoading = false); + print('Erreur API: ${response.statusCode}'); + } + } catch (e) { setState(() => isLoading = false); - print('Erreur API: ${response.statusCode}'); + print('Erreur réseau: $e'); } } @@ -82,19 +92,35 @@ class _TablesScreenState extends State { } } + bool isDesktop() { + if (kIsWeb) return true; + try { + return Platform.isWindows || Platform.isMacOS || Platform.isLinux; + } catch (_) { + return false; + } + } + @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; - - int crossAxisCount = 2; - if (screenWidth > 1200) { - crossAxisCount = 4; - } else if (screenWidth > 800) { - crossAxisCount = 3; - } + final crossAxisCount = screenWidth > 1200 ? 4 : screenWidth > 800 ? 3 : 2; return Scaffold( - appBar: AppBar(title: const Text('Sélectionner une table')), + appBar: AppBar( + title: const Text('Sélectionner une table'), + actions: isDesktop() + ? [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + setState(() => isLoading = true); + fetchTables(); + }, + ), + ] + : null, + ), body: isLoading ? const Center(child: CircularProgressIndicator()) : Padding( @@ -105,14 +131,13 @@ class _TablesScreenState extends State { crossAxisCount: crossAxisCount, crossAxisSpacing: 12, mainAxisSpacing: 12, - childAspectRatio: 2.3, // ⬅️ plus plat ici + childAspectRatio: 2.3, ), itemBuilder: (context, index) { final table = tables[index]; final isAvailable = table.status == 'available'; return Card( - elevation: 1.5, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), @@ -121,7 +146,6 @@ class _TablesScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Titre + badge Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -134,9 +158,7 @@ class _TablesScreenState extends State { ), Container( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), + horizontal: 8, vertical: 2), decoration: BoxDecoration( color: getStatusColor(table.status), borderRadius: BorderRadius.circular(50), @@ -171,7 +193,19 @@ class _TablesScreenState extends State { width: double.infinity, height: 30, child: ElevatedButton( - onPressed: isAvailable ? () {} : null, + onPressed: isAvailable + ? () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MenuPage( + tableId: table.id, + personne: table.capacity, + ), + ), + ); + } + : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, padding: EdgeInsets.zero,