From d4a5c60821ae365e203b99861b5ab635e0de7b35 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sun, 3 Aug 2025 10:26:52 +0300 Subject: [PATCH] CRUD menus --- lib/layouts/main_layout.dart | 27 +- lib/main.dart | 23 +- lib/pages/PlatEdit_screen.dart | 387 +++++++++++++ lib/pages/commandes_screen.dart | 33 +- lib/pages/menus_screen.dart | 893 +++++++++++++++-------------- lib/pages/tables.dart | 5 +- lib/widgets/bottom_navigation.dart | 16 +- 7 files changed, 914 insertions(+), 470 deletions(-) create mode 100644 lib/pages/PlatEdit_screen.dart diff --git a/lib/layouts/main_layout.dart b/lib/layouts/main_layout.dart index 8bfaa6c..d13b67b 100644 --- a/lib/layouts/main_layout.dart +++ b/lib/layouts/main_layout.dart @@ -6,11 +6,7 @@ class MainLayout extends StatefulWidget { final Widget child; final String? currentRoute; - const MainLayout({ - super.key, - required this.child, - this.currentRoute, - }); + const MainLayout({super.key, required this.child, this.currentRoute}); @override _MainLayoutState createState() => _MainLayoutState(); @@ -36,7 +32,7 @@ class _MainLayoutState extends State { return 2; case '/menu': return 3; - case '/cart': + case '/plats': return 4; default: return 0; @@ -63,7 +59,7 @@ class _MainLayoutState extends State { route = '/menu'; break; case 4: - route = '/cart'; + route = '/plats'; break; default: route = '/tables'; @@ -91,13 +87,14 @@ class _MainLayoutState extends State { ], ), // Show mobile navigation on smaller screens - bottomNavigationBar: isDesktop - ? null - : MobileBottomNavigation( - currentRoute: widget.currentRoute ?? '/tables', - selectedIndex: _selectedIndex, - onItemTapped: _onItemTapped, - ), + bottomNavigationBar: + isDesktop + ? null + : MobileBottomNavigation( + currentRoute: widget.currentRoute ?? '/tables', + selectedIndex: _selectedIndex, + onItemTapped: _onItemTapped, + ), ); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index e7dda84..ab944ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:itrimobe/pages/menu.dart'; import 'layouts/main_layout.dart'; import 'pages/tables.dart'; import 'pages/categorie.dart'; import 'pages/commandes_screen.dart'; import 'pages/login_screen.dart'; +import 'pages/menus_screen.dart'; void main() { runApp(const MyApp()); @@ -25,24 +25,29 @@ class MyApp extends StatelessWidget { initialRoute: '/login', routes: { '/login': (context) => const LoginScreen(), - '/tables': (context) => const MainLayout( + '/tables': + (context) => const MainLayout( currentRoute: '/tables', child: TablesScreen(), ), - '/categories': (context) => const MainLayout( + '/categories': + (context) => const MainLayout( currentRoute: '/categories', child: CategoriesPage(), ), - '/commandes': (context) => const MainLayout( + '/commandes': + (context) => const MainLayout( currentRoute: '/commandes', child: OrdersManagementScreen(), ), // MODIFICATION : Route simple pour le menu - // '/menu': (context) => const MainLayout( - // currentRoute: '/menu', - // child: MenuPage(), // Pas de paramètres requis maintenant - // ), + '/plats': + (context) => const MainLayout( + currentRoute: '/plats', + child: + PlatsManagementScreen(), // Pas de paramètres requis maintenant + ), }, ); } -} \ No newline at end of file +} diff --git a/lib/pages/PlatEdit_screen.dart b/lib/pages/PlatEdit_screen.dart new file mode 100644 index 0000000..78261bc --- /dev/null +++ b/lib/pages/PlatEdit_screen.dart @@ -0,0 +1,387 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +// Use your actual models and http logic +class MenuPlat { + final int id; + final String nom; + final String? commentaire; + final double prix; + final String? imageUrl; + final bool disponible; + final List categories; // Default to true + MenuPlat({ + required this.id, + required this.nom, + this.commentaire, + required this.prix, + this.imageUrl, + required this.disponible, + required this.categories, + }); +} + +class MenuCategory { + final int id; + final String nom; + final String? description; + + MenuCategory({required this.id, required this.nom, this.description}); + + factory MenuCategory.fromJson(Map json) { + return MenuCategory( + id: json['id'], + nom: json['nom'], + description: json['description'], + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MenuCategory && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} + +class PlatEditPage extends StatefulWidget { + final List categories; + final MenuPlat? plat; // if present = edit, else create + final Function()? onSaved; // callback when saved + + const PlatEditPage({ + super.key, + required this.categories, + this.plat, + this.onSaved, + }); + + @override + State createState() => _PlatEditPageState(); +} + +class _PlatEditPageState extends State { + final _formKey = GlobalKey(); + late TextEditingController nomCtrl, descCtrl, prixCtrl, imgCtrl; + late bool disponible; + late List selectedCategories; + + @override + void initState() { + super.initState(); + nomCtrl = TextEditingController(text: widget.plat?.nom ?? ""); + descCtrl = TextEditingController(text: widget.plat?.commentaire ?? ""); + prixCtrl = TextEditingController( + text: widget.plat != null ? widget.plat!.prix.toStringAsFixed(2) : "", + ); + imgCtrl = TextEditingController(text: widget.plat?.imageUrl ?? ""); + disponible = widget.plat?.disponible ?? true; + selectedCategories = widget.plat?.categories.toList() ?? []; + } + + @override + void dispose() { + nomCtrl.dispose(); + descCtrl.dispose(); + prixCtrl.dispose(); + imgCtrl.dispose(); + super.dispose(); + } + + void submit() async { + if (!_formKey.currentState!.validate()) return; + if (selectedCategories.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Sélectionnez au moins une catégorie.')), + ); + return; + } + + // Build request data + final body = { + "nom": nomCtrl.text, + "commentaire": descCtrl.text, + "prix": double.tryParse(prixCtrl.text) ?? 0, + "categories": selectedCategories.map((c) => c.id).toList(), + "disponible": disponible, + "categorie_id": + selectedCategories.isNotEmpty + ? selectedCategories.first.id + : null, // Use first category if available + }; + + try { + final res = await http.post( + Uri.parse( + 'https://restaurant.careeracademy.mg/api/menus', + ), // your API URL here + headers: {"Content-Type": "application/json"}, + body: json.encode(body), + ); + + if (res.statusCode == 200 || res.statusCode == 201) { + widget.onSaved?.call(); + Navigator.pop(context); + } else { + // show error + print('Error creating plat: ${res.body}\n here is body: $body'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors de la création du plat')), + ); + } + } catch (e) { + // show error + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Erreur réseau: $e'))); + } + } + + @override + Widget build(BuildContext context) { + final isEdit = widget.plat != null; + return Scaffold( + appBar: AppBar( + title: Text(isEdit ? 'Éditer le plat' : 'Nouveau plat'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + centerTitle: true, + elevation: 0, + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 550), + child: Card( + elevation: 2, + margin: const EdgeInsets.all(32), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(28), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Informations du plat', + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 14), + TextFormField( + controller: nomCtrl, + decoration: const InputDecoration( + labelText: "Nom du plat *", + hintText: "Ex: Steak frites", + ), + validator: + (v) => + (v == null || v.isEmpty) ? "Obligatoire" : null, + ), + const SizedBox(height: 13), + TextFormField( + controller: descCtrl, + decoration: const InputDecoration( + labelText: "Description", + hintText: "Description détaillée du plat...", + ), + maxLines: 3, + ), + const SizedBox(height: 13), + Row( + children: [ + Expanded( + child: TextFormField( + controller: prixCtrl, + decoration: const InputDecoration( + labelText: "Prix (MGA) *", + ), + validator: + (v) => + (v == null || + v.isEmpty || + double.tryParse(v) == null) + ? "Obligatoire" + : null, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + ), + ), + const SizedBox(width: 16), + // Multi-category picker + Expanded( + child: InkWell( + onTap: () async { + final result = await showDialog< + List + >( + context: context, + builder: (context) { + // temp inside a StatefulBuilder so state changes refresh UI + Set temp = + selectedCategories + .map((e) => e.id) + .toSet(); + + return StatefulBuilder( + builder: (context, setStateDialog) { + return AlertDialog( + title: const Text("Catégories"), + content: SizedBox( + width: 330, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: + widget.categories.map((cat) { + return CheckboxListTile( + value: temp.contains( + cat.id, + ), + title: Text(cat.nom), + onChanged: (checked) { + setStateDialog(() { + if (checked == true) { + temp.add(cat.id); + } else { + temp.remove(cat.id); + } + }); + }, + ); + }).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + final newList = + widget.categories + .where( + (cat) => temp.contains( + cat.id, + ), + ) + .toList(); + Navigator.pop(context, newList); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + }, + ); + + if (result != null) + setState(() => selectedCategories = result); + }, + child: InputDecorator( + decoration: const InputDecoration( + labelText: "Catégories *", + border: OutlineInputBorder(gapPadding: 2), + ), + child: Wrap( + spacing: 6, + runSpacing: -8, + children: + selectedCategories.isEmpty + ? [ + const Text( + "Aucune sélection", + style: TextStyle( + color: Colors.grey, + ), + ), + ] + : selectedCategories + .map( + (c) => Chip( + label: Text(c.nom), + visualDensity: + VisualDensity.compact, + ), + ) + .toList(), + ), + ), + ), + ), + ], + ), + // const SizedBox(height: 13), + // TextFormField( + // controller: imgCtrl, + // decoration: const InputDecoration( + // labelText: "URL de l'image", + // hintText: "/api/placeholder/300/200", + // ), + // ), + const SizedBox(height: 13), + Row( + children: [ + Switch( + value: disponible, + activeColor: Colors.green, + onChanged: (v) => setState(() => disponible = v), + ), + const SizedBox(width: 7), + const Text('Plat disponible'), + ], + ), + const SizedBox(height: 22), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: const Text( + "Annuler", + style: TextStyle(color: Colors.black), + ), + ), + const SizedBox(width: 18), + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + onPressed: submit, + icon: const Icon( + Icons.save, + size: 18, + color: Colors.white, + ), + label: Text( + isEdit ? "Enregistrer" : "Ajouter", + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart index 83a7264..0b8d2bb 100644 --- a/lib/pages/commandes_screen.dart +++ b/lib/pages/commandes_screen.dart @@ -138,8 +138,6 @@ class _OrdersManagementScreenState extends State { return null; } - - Future updateOrderStatus( Order order, String newStatus, { @@ -287,9 +285,6 @@ class _OrdersManagementScreenState extends State { } } - - - List get activeOrders { return orders .where( @@ -345,7 +340,6 @@ class _OrdersManagementScreenState extends State { style: TextStyle(color: Colors.white), ), ), - ], ); }, @@ -354,22 +348,23 @@ 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) { diff --git a/lib/pages/menus_screen.dart b/lib/pages/menus_screen.dart index bb78a2c..6e1be89 100644 --- a/lib/pages/menus_screen.dart +++ b/lib/pages/menus_screen.dart @@ -1,500 +1,563 @@ -// TODO Implement this library. +import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'dart:convert'; +import './PlatEdit_screen.dart'; -class MenuPage extends StatefulWidget { - final int? tableId; - final int? personne; - - const MenuPage({Key? key, required this.tableId, required this.personne}) - : super(key: key); +class PlatsManagementScreen extends StatefulWidget { + const PlatsManagementScreen({super.key}); @override - State createState() => _MenuPageState(); + State createState() => _PlatsManagementScreenState(); } -class _MenuPageState extends State { - int? _selectedCategory; - List _categories = []; - List _menus = []; - List _cart = []; +class _PlatsManagementScreenState extends State { + final _baseUrl = 'https://restaurant.careeracademy.mg/api'; + List plats = []; + List categories = []; + String search = ''; + int? selectedCategoryId; + String disponibilite = ''; + bool isLoading = true; + + final TextEditingController _searchController = TextEditingController(); @override void initState() { super.initState(); - fetchCategories(); + _fetchCategories(); + _fetchPlats(); } - Future fetchCategories() async { + // Fetch categories + 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"); - } + final res = await http.get(Uri.parse('$_baseUrl/menu-categories')); + final data = json.decode(res.body); + setState(() { + categories = + (data['data']['categories'] as List) + .map((item) => MenuCategory.fromJson(item)) + .toList(); + }); + } catch (_) {} } - Future fetchMenus(int categoryId) async { + // Fetch plats + Future _fetchPlats() async { + setState(() => isLoading = true); try { - final url = Uri.parse( - "https://restaurant.careeracademy.mg/api/menus/category/$categoryId?disponible=true", + final uri = Uri.parse('$_baseUrl/menus').replace( + queryParameters: { + if (search.isNotEmpty) 'search': search, + if (selectedCategoryId != null) + 'category_id': selectedCategoryId.toString(), + }, ); - 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}"); + final res = await http.get(uri); + final data = json.decode(res.body); + setState(() { + plats = + (data['data']['menus'] as List) + .map((item) => MenuPlat.fromJson(item)) + .toList(); + isLoading = false; + }); + if (kDebugMode) { + // print('fetched plat here: $plats items'); } } catch (e) { - print("Exception fetchMenus: $e"); + setState(() => isLoading = false); + if (kDebugMode) print("Error: $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); - } - }); + Future _deletePlat(int id) async { + final res = await http.delete(Uri.parse('$_baseUrl/menus/$id')); + if (res.statusCode == 200) + _fetchPlats(); + else { + if (kDebugMode) print("Error deleting plat: ${res.body}"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Des commandes sont liées à ce plat.')), + ); + } } - void showAddToCartModal(dynamic item) { + void _showEditPlatDialog(MenuPlat plat) { showDialog( context: context, - builder: (BuildContext context) { - return AddToCartModal(item: item, onAddToCart: addToCart); - }, + builder: + (_) => EditPlatDialog( + plat: plat, + onPlatUpdated: _fetchPlats, + categories: categories, + ), ); } - /// 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( + backgroundColor: const Color(0xfffcfbf9), appBar: AppBar( - title: Text("Menu"), + elevation: 0, + title: const Text( + 'Gestion des plats', + style: TextStyle(fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.transparent, + foregroundColor: Colors.black87, actions: [ - IconButton( - onPressed: () { - if (_selectedCategory != null) fetchMenus(_selectedCategory!); - }, - icon: Icon(Icons.refresh), + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: ElevatedButton.icon( + onPressed: + () => navigateToCreate( + context, + categories, + () => {_fetchPlats()}, + ), + icon: const Icon(Icons.add, size: 18), + label: const Text('Nouveau plat'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 18, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + ), ), ], ), 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), + Card( + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 18), + child: Row( + children: [ + const Icon(Icons.filter_alt_outlined, size: 22), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + onChanged: (value) { + setState(() => search = value); + _fetchPlats(); + }, + decoration: const InputDecoration( + hintText: "Rechercher un plat...", + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: Color(0xFFF7F7F7), + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: 10, + horizontal: 14, + ), + ), + ), + ), + const SizedBox(width: 10), + // Catégories + DropdownButton( + value: selectedCategoryId, + hint: const Text("Toutes les catégories"), + items: [ + const DropdownMenuItem( + value: null, + child: Text("Toutes les catégories"), + ), + ...categories.map( + (cat) => DropdownMenuItem( + value: cat.id, + child: Text(cat.nom), + ), + ), + ], + onChanged: (v) { + setState(() => selectedCategoryId = v); + _fetchPlats(); + }, + ), + ], + ), ), ), - if (_categories.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: - _categories.map((cat) { - return buildCategoryButton(cat['nom'], cat['id']); - }).toList(), - ) + if (isLoading) + const Expanded(child: Center(child: CircularProgressIndicator())) 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, + Expanded( + child: ListView.builder( + itemCount: plats.length, + padding: const EdgeInsets.symmetric(horizontal: 24), + itemBuilder: (ctx, i) { + final p = plats[i]; + return Card( + margin: const EdgeInsets.only(bottom: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 18, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: Text( + p.nom, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, ), ), - title: Text( - item['nom'] ?? 'Nom non disponible', - style: TextStyle(fontWeight: FontWeight.bold), + ), + Expanded( + flex: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + p.commentaire ?? "", + style: const TextStyle(fontSize: 15), + ), + if (p.ingredients != null && + p.ingredients!.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 3.0, + ), + child: Text( + p.ingredients!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13, + ), + ), + ), + Row( + children: [ + if (p.categories.isNotEmpty) + Wrap( + spacing: 4, + runSpacing: 4, + children: + p.categories + .map( + (c) => CategoryChip( + label: c.nom, + color: Colors.black, + ), + ) + .toList(), + ), + if (p.categories.isEmpty) + const CategoryChip( + label: "Catégorie", + color: Colors.black, + ), + ], + ), + ], ), - subtitle: Text(item['commentaire'] ?? ''), - trailing: Text( - "${formatPrix(item['prix'])} MGA", - style: TextStyle( - color: Colors.green[700], - fontWeight: FontWeight.bold, - fontSize: 16, - ), + ), + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + "${p.prix.toStringAsFixed(2)} MGA", + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green, + fontSize: 20, + ), + ), + const SizedBox(width: 10), + SizedBox( + height: 38, + width: 38, + child: IconButton( + icon: const Icon( + Icons.edit, + color: Colors.black54, + ), + onPressed: () => _showEditPlatDialog(p), + ), + ), + SizedBox( + height: 38, + width: 38, + child: IconButton( + icon: const Icon( + Icons.delete, + color: Colors.redAccent, + ), + onPressed: () async { + final confirm = await showDialog( + context: context, + builder: + (_) => AlertDialog( + title: const Text( + 'Supprimer ce plat ?', + ), + content: Text( + 'Supprimer ${p.nom} ', + ), + actions: [ + TextButton( + onPressed: + () => Navigator.pop( + context, + false, + ), + child: const Text("Annuler"), + ), + ElevatedButton( + onPressed: + () => Navigator.pop( + context, + true, + ), + style: + ElevatedButton.styleFrom( + backgroundColor: + Colors.red, + ), + child: const Text( + "Supprimer", + style: TextStyle( + color: Colors.white, + ), + ), + ), + ], + ), + ); + if (confirm == true) _deletePlat(p.id); + }, + ), + ), + ], ), ), - ); - }, - ) - : 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, - ), - ), - ), - ), - ), +class MenuPlat { + final int id; + final String nom; + final String? commentaire; + final double prix; + final String? ingredients; + final int disponible = 1; // Default to true + final List categories; // NOW A LIST! + + MenuPlat({ + required this.id, + required this.nom, + this.commentaire, + required this.prix, + this.ingredients, + required this.categories, + }); + + factory MenuPlat.fromJson(Map json) { + double parsePrix(dynamic p) { + if (p is int) return p.toDouble(); + if (p is double) return p; + if (p is String) return double.tryParse(p) ?? 0; + return 0; + } + + return MenuPlat( + id: json['id'], + nom: json['nom'], + commentaire: json['commentaire'], + prix: parsePrix(json['prix']), + ingredients: json['ingredients'], + categories: + (json['categories'] as List? ?? []) + .map((c) => MenuCategory.fromJson(c)) + .toList(), ); } } -// Modal pour ajouter au panier -class AddToCartModal extends StatefulWidget { - final dynamic item; - final Function(dynamic, int, String) onAddToCart; +class CategoryChip extends StatelessWidget { + final String label; + final Color color; + const CategoryChip({super.key, required this.label, required this.color}); + @override + Widget build(BuildContext context) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + // ignore: deprecated_member_use + color: color.withOpacity(0.16), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + label, + style: TextStyle(color: color, fontWeight: FontWeight.w600, fontSize: 13), + ), + ); +} + +Future navigateToCreate( + BuildContext context, + List categories, + Function()? onSaved, +) async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => PlatEditPage(categories: categories, onSaved: onSaved), + ), + ); +} - const AddToCartModal({ - Key? key, - required this.item, - required this.onAddToCart, - }) : super(key: key); +class EditPlatDialog extends StatefulWidget { + final MenuPlat plat; + final List categories; + final VoidCallback onPlatUpdated; + const EditPlatDialog({ + super.key, + required this.plat, + required this.onPlatUpdated, + required this.categories, + }); @override - State createState() => _AddToCartModalState(); + State createState() => _EditPlatDialogState(); } -class _AddToCartModalState extends State { - int _quantity = 1; - final TextEditingController _notesController = TextEditingController(); +class _EditPlatDialogState extends State { + late String nom; + late String commentaire; + late String ingredients; + late double prix; + // late bool disponible; + MenuCategory? cat; + final _formKey = GlobalKey(); - 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"; + @override + void initState() { + super.initState(); + nom = widget.plat.nom; + commentaire = widget.plat.commentaire ?? ''; + ingredients = widget.plat.ingredients ?? ''; + prix = widget.plat.prix; + var disponible = widget.plat.disponible; + // cat = (widget.plat.categories) as MenuCategory?; + cat = + widget.plat.categories.isNotEmpty ? widget.plat.categories.first : null; } - 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; + Future submit() async { + if (!_formKey.currentState!.validate()) return; + final res = await http.put( + Uri.parse( + 'https://restaurant.careeracademy.mg/api/menus/${widget.plat.id}', + ), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + "nom": nom, + "commentaire": commentaire, + "ingredients": ingredients, + "prix": prix, + "categorie_id": cat?.id, + }), + ); + if (res.statusCode == 200) { + widget.onPlatUpdated(); + // ignore: use_build_context_synchronously + Navigator.pop(context); } - 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), + Widget build(BuildContext context) => AlertDialog( + title: const Text("Éditer le plat"), + content: Form( + key: _formKey, + child: SingleChildScrollView( 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(), - ), - ], + TextFormField( + initialValue: nom, + onChanged: (v) => nom = v, + decoration: const InputDecoration(labelText: "Nom"), + validator: (v) => (v?.isEmpty ?? true) ? "Obligatoire" : null, ), - 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], - ), + TextFormField( + initialValue: commentaire, + onChanged: (v) => commentaire = v, + decoration: const InputDecoration(labelText: "Commentaire"), ), - 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'])} MGA", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.green[700], - ), - ), - ], - ), - SizedBox(height: 20), - - // Quantité - Text( - "Quantité", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + TextFormField( + initialValue: ingredients, + onChanged: (v) => ingredients = v, + decoration: const InputDecoration(labelText: "Ingrédients"), ), - 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), + TextFormField( + initialValue: prix.toString(), + onChanged: (v) => prix = double.tryParse(v) ?? 0, + decoration: const InputDecoration(labelText: "Prix"), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, ), ), - 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)} MGA", - 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, - ), - ), - ), - ), - ], - ), + DropdownButton( + hint: const Text("Catégorie"), + value: cat, + isExpanded: true, + items: + widget.categories + .map( + (c) => DropdownMenuItem(value: c, child: Text(c.nom)), + ) + .toList(), + onChanged: (v) => setState(() => cat = v), ), ], ), ), - ); - } + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Annuler"), + ), + ElevatedButton(onPressed: submit, child: const Text("Enregistrer")), + ], + ); } diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index 2b58088..389e631 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -306,10 +306,7 @@ class _TablesScreenState extends State { crossAxisCount: crossAxisCount, crossAxisSpacing: 12, mainAxisSpacing: 12, - childAspectRatio: - isDesktop - ? 1.7 - : 2.1, + childAspectRatio: isDesktop ? 1.7 : 2.1, ), itemCount: tables.length, itemBuilder: (context, index) { diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart index 256ae4d..6e1d5a4 100644 --- a/lib/widgets/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation.dart @@ -155,12 +155,12 @@ class AppBottomNavigation extends StatelessWidget { const SizedBox(width: 20), GestureDetector( - onTap: () => onItemTapped(3), + onTap: () => onItemTapped(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: - selectedIndex == 3 + selectedIndex == 4 ? Colors.green.shade700 : Colors.transparent, borderRadius: BorderRadius.circular(20), @@ -169,23 +169,23 @@ class AppBottomNavigation extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Icon( - Icons.restaurant_menu, + Icons.restaurant_menu, color: - selectedIndex == 3 + selectedIndex == 4 ? Colors.white : Colors.grey.shade600, size: 16, ), const SizedBox(width: 6), Text( - 'Menu', + 'Plats', style: TextStyle( color: - selectedIndex == 3 + selectedIndex == 4 ? Colors.white : Colors.grey.shade600, fontWeight: - selectedIndex == 3 + selectedIndex == 4 ? FontWeight.w500 : FontWeight.normal, ), @@ -220,4 +220,4 @@ class AppBottomNavigation extends StatelessWidget { ], ); } -} \ No newline at end of file +}