From fa8d4429d6a44671759d5cc45abb649e66098dd5 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sat, 2 Aug 2025 11:04:55 +0300 Subject: [PATCH] liste commande --- lib/main.dart | 2 +- lib/pages/commandes_screen.dart | 906 ++++++++++++++++++++++++++++++++ 2 files changed, 907 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 08fb8d3..b8a21f2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,7 +37,7 @@ class MyApp extends StatelessWidget { '/commandes': (context) => const MainLayout( currentRoute: '/commandes', - child: CategoriesPage(), + child: OrdersManagementScreen(), ), // Uncomment and update these as needed: // '/commandes': (context) => const MainLayout( diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart index 613744d..b205d8f 100644 --- a/lib/pages/commandes_screen.dart +++ b/lib/pages/commandes_screen.dart @@ -1 +1,907 @@ // TODO Implement this library. +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class OrdersManagementScreen extends StatefulWidget { + const OrdersManagementScreen({super.key}); + + @override + State createState() => _OrdersManagementScreenState(); +} + +class _OrdersManagementScreenState extends State { + List orders = []; + bool isLoading = true; + String? errorMessage; + final String baseUrl = 'https://restaurant.careeracademy.mg/api'; + + @override + void initState() { + super.initState(); + loadOrders(); + } + + Future loadOrders() async { + try { + setState(() { + isLoading = true; + errorMessage = null; + }); + + // Get all orders with filtering for active ones only + final response = await http.get( + Uri.parse('$baseUrl/commandes?statut=en_attente,en_preparation'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + if (responseData['success'] == true) { + final List commandesData = responseData['data']['commandes']; + + setState(() { + orders = + commandesData + .map((orderData) => Order.fromJson(orderData)) + .toList(); + isLoading = false; + }); + } else { + throw Exception('API returned success: false'); + } + } else { + throw Exception('Failed to load orders: ${response.statusCode}'); + } + } catch (e) { + setState(() { + isLoading = false; + errorMessage = 'Erreur de chargement: $e'; + }); + if (kDebugMode) { + print('Error loading orders: $e'); + } + } + } + + Future loadKitchenOrders() async { + try { + final response = await http.get( + Uri.parse('$baseUrl/commandes/kitchen'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + json.decode(response.body); + // Handle kitchen orders + if (kDebugMode) { + print('Kitchen orders loaded'); + } + } + } catch (e) { + if (kDebugMode) { + print('Error loading kitchen orders: $e'); + } + } + } + + Future loadOrderStats() async { + try { + final response = await http.get( + Uri.parse('$baseUrl/commandes/stats'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + json.decode(response.body); + // Handle stats + if (kDebugMode) { + print('Order stats loaded'); + } + } + } catch (e) { + print('Error loading stats: $e'); + } + } + + Future getOrderById(int orderId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/commandes/$orderId'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + if (responseData['success'] == true) { + return Order.fromJson(responseData['data']); + } + } + } catch (e) { + print('Error getting order: $e'); + } + return null; + } + + Future updateOrderStatus( + Order order, + String newStatus, { + String? modePaiement, + }) async { + try { + final Map updateData = {'statut': newStatus}; + + if (modePaiement != null) { + updateData['mode_paiement'] = modePaiement; + } + + final response = await http.put( + Uri.parse('$baseUrl/commandes/${order.id}/status'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode(updateData), + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + if (responseData['success'] == true) { + setState(() { + order.statut = newStatus; + order.modePaiement = modePaiement; + order.updatedAt = DateTime.now(); + if (newStatus == "payee") { + order.dateService = DateTime.now(); + } + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Commande mise à jour avec succès'), + backgroundColor: Colors.green, + ), + ); + + // Remove from active orders list if status changed to completed + if (newStatus == "payee" || newStatus == "annulee") { + loadOrders(); // Refresh the list + } + } else { + throw Exception('API returned success: false'); + } + } else { + throw Exception('Failed to update order: ${response.statusCode}'); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la mise à jour: $e'), + backgroundColor: Colors.red, + ), + ); + print('Error updating order: $e'); + } + } + + Future createNewOrder() async { + // Sample order creation - you can customize this + try { + final Map newOrderData = { + 'client_id': 1, + 'table_id': 1, + 'reservation_id': 1, + 'serveur': 'Marie', + 'commentaires': 'Nouvelle commande', + 'items': [ + {'menu_id': 1, 'quantite': 2, 'commentaires': 'Bien cuit'}, + {'menu_id': 2, 'quantite': 1, 'commentaires': ''}, + ], + }; + + final response = await http.post( + Uri.parse('$baseUrl/commandes'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode(newOrderData), + ); + + if (response.statusCode == 201) { + final responseData = json.decode(response.body); + + if (responseData['success'] == true) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Nouvelle commande créée avec succès'), + backgroundColor: Colors.green, + ), + ); + loadOrders(); // Refresh the list + } + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la création: $e'), + backgroundColor: Colors.red, + ), + ); + print('Error creating order: $e'); + } + } + + Future deleteOrder(Order order) async { + try { + final response = await http.delete( + Uri.parse('$baseUrl/commandes/${order.id}'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + if (responseData['success'] == true) { + setState(() { + orders.remove(order); + }); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Commande supprimée avec succès'), + backgroundColor: Colors.green, + ), + ); + } + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression: $e'), + backgroundColor: Colors.red, + ), + ); + print('Error deleting order: $e'); + } + } + + List get activeOrders { + return orders + .where( + (order) => + order.statut == "en_attente" || order.statut == "en_preparation", + ) + .toList(); + } + + void processPayment(Order order) { + showDialog( + context: context, + builder: (BuildContext context) { + String selectedPaymentMethod = 'carte'; + + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: const Text('Mettre en caisse'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Table ${order.tableId} - ${order.totalTtc.toStringAsFixed(2)} €', + ), + const SizedBox(height: 16), + const Text('Mode de paiement:'), + const SizedBox(height: 8), + DropdownButton( + value: selectedPaymentMethod, + isExpanded: true, + items: const [ + DropdownMenuItem( + value: 'carte', + child: Text('Carte bancaire'), + ), + DropdownMenuItem( + value: 'especes', + child: Text('Espèces'), + ), + DropdownMenuItem(value: 'cheque', child: Text('Chèque')), + ], + onChanged: (value) { + setState(() { + selectedPaymentMethod = value!; + }); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + updateOrderStatus( + order, + "payee", + modePaiement: selectedPaymentMethod, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: const Text( + 'Confirmer le paiement', + style: TextStyle(color: Colors.white), + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8F9FA), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 1, + title: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gestion des commandes', + style: TextStyle( + color: Colors.black87, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Suivez et gérez toutes les commandes', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + toolbarHeight: 80, + actions: [ + IconButton( + icon: const Icon(Icons.kitchen, color: Colors.grey), + onPressed: loadKitchenOrders, + tooltip: 'Commandes cuisine', + ), + IconButton( + icon: const Icon(Icons.bar_chart, color: Colors.grey), + onPressed: loadOrderStats, + tooltip: 'Statistiques', + ), + IconButton( + icon: const Icon(Icons.add, color: Colors.green), + onPressed: createNewOrder, + tooltip: 'Nouvelle commande', + ), + IconButton( + icon: const Icon(Icons.refresh, color: Colors.grey), + onPressed: loadOrders, + tooltip: 'Actualiser', + ), + // IconButton( + // icon: const Icon(Icons.logout, color: Colors.grey), + // onPressed: () { + // Navigator.of(context).pop(); + // }, + // ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Active orders header + Row( + children: [ + const Icon(Icons.access_time, size: 20, color: Colors.black87), + const SizedBox(width: 8), + Text( + 'Commandes actives (${activeOrders.length})', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Content + Expanded(child: _buildContent()), + ], + ), + ), + ); + } + + Widget _buildContent() { + if (isLoading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.green), + SizedBox(height: 16), + Text( + 'Chargement des commandes...', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ); + } + + if (errorMessage != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 64, color: Colors.red), + const SizedBox(height: 16), + Text( + errorMessage!, + style: const TextStyle(fontSize: 16, color: Colors.red), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: loadOrders, + child: const Text('Réessayer'), + ), + ], + ), + ); + } + + if (activeOrders.isEmpty) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.restaurant_menu, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Aucune commande active', + style: TextStyle(fontSize: 18, color: Colors.grey), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: loadOrders, + child: ListView.builder( + itemCount: activeOrders.length, + itemBuilder: (context, index) { + final order = activeOrders[index]; + return OrderCard( + order: order, + onStatusUpdate: updateOrderStatus, + onProcessPayment: processPayment, + onDelete: deleteOrder, + onViewDetails: () => getOrderById(order.id), + ); + }, + ), + ); + } +} + +class OrderCard extends StatelessWidget { + final Order order; + final Function(Order, String, {String? modePaiement}) onStatusUpdate; + final Function(Order) onProcessPayment; + final Function(Order) onDelete; + final VoidCallback onViewDetails; + + const OrderCard({ + Key? key, + required this.order, + required this.onStatusUpdate, + required this.onProcessPayment, + required this.onDelete, + required this.onViewDetails, + }) : super(key: key); + + String _formatTime(DateTime dateTime) { + return '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')} •${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year} •1 personne'; + } + + Color _getStatusColor(String status) { + switch (status) { + case 'en_attente': + return Colors.green; + case 'en_preparation': + return Colors.orange; + case 'servie': + return Colors.blue; + case 'payee': + return Colors.grey; + default: + return Colors.grey; + } + } + + String _getStatusText(String status) { + switch (status) { + case 'en_attente': + return 'En cours'; + case 'en_preparation': + return 'En préparation'; + case 'servie': + return 'Servie'; + case 'payee': + return 'Payée'; + default: + return status; + } + } + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 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, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + + // Time and details + Text( + _formatTime(order.dateCommande), + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + const SizedBox(height: 8), + + // Order number and server + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + order.numeroCommande, + style: const TextStyle(fontSize: 12, color: Colors.black54), + ), + Text( + 'Serveur: ${order.serveur}', + style: const TextStyle(fontSize: 12, color: Colors.black54), + ), + ], + ), + const SizedBox(height: 16), + + // Order items placeholder + if (order.items.isNotEmpty) + ...order.items.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${item.quantite}x ${item.menuId}', // You might want to resolve menu name + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + Text( + '${(item.quantite * 8.00).toStringAsFixed(2)} €', // Placeholder price + style: const TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + ) + else + const Text( + 'Détails de la commande...', + style: TextStyle(fontSize: 14, color: Colors.black87), + ), + + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 8), + + // Total row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Total', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '${order.totalTtc.toStringAsFixed(2)} €', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Action buttons + if (order.statut == 'en_attente' || + order.statut == 'en_preparation') + Row( + children: [ + Expanded( + flex: 3, + child: ElevatedButton.icon( + onPressed: () => onProcessPayment(order), + icon: const Icon( + Icons.point_of_sale, + color: Colors.white, + size: 18, + ), + label: const Text( + 'Mettre en caisse', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + elevation: 0, + ), + ), + ), + const SizedBox(width: 8), + if (order.statut == 'en_attente') + Expanded( + child: ElevatedButton( + onPressed: + () => onStatusUpdate(order, 'en_preparation'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + 'Préparer', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.red.shade200), + borderRadius: BorderRadius.circular(6), + ), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Supprimer la commande'), + content: const Text( + 'Êtes-vous sûr de vouloir supprimer cette commande ?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + onDelete(order); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text( + 'Supprimer', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ); + }, + icon: Icon( + Icons.delete, + color: Colors.red.shade400, + size: 18, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +// Updated Order model to include items +class Order { + final int id; + final int clientId; + final int tableId; + final int? reservationId; + final String numeroCommande; + String statut; + final double totalHt; + final double totalTva; + final double totalTtc; + String? modePaiement; + final String? commentaires; + final String serveur; + final DateTime dateCommande; + DateTime? dateService; + final DateTime createdAt; + DateTime updatedAt; + final List items; + + Order({ + required this.id, + required this.clientId, + required this.tableId, + this.reservationId, + required this.numeroCommande, + required this.statut, + required this.totalHt, + required this.totalTva, + required this.totalTtc, + this.modePaiement, + this.commentaires, + required this.serveur, + required this.dateCommande, + this.dateService, + required this.createdAt, + required this.updatedAt, + this.items = const [], + }); + + factory Order.fromJson(Map json) { + List orderItems = []; + if (json['items'] != null) { + orderItems = + (json['items'] as List) + .map((item) => OrderItem.fromJson(item)) + .toList(); + } + + return Order( + id: json['id'], + clientId: json['client_id'], + tableId: json['table_id'], + reservationId: json['reservation_id'], + numeroCommande: json['numero_commande'], + statut: json['statut'], + totalHt: double.parse(json['total_ht'].toString()), + totalTva: double.parse(json['total_tva'].toString()), + totalTtc: double.parse(json['total_ttc'].toString()), + modePaiement: json['mode_paiement'], + commentaires: json['commentaires'], + serveur: json['serveur'], + dateCommande: DateTime.parse(json['date_commande']), + dateService: + json['date_service'] != null + ? DateTime.parse(json['date_service']) + : null, + createdAt: DateTime.parse(json['created_at']), + updatedAt: DateTime.parse(json['updated_at']), + items: orderItems, + ); + } +} + +class OrderItem { + final int menuId; + final int quantite; + final String? commentaires; + + OrderItem({required this.menuId, required this.quantite, this.commentaires}); + + factory OrderItem.fromJson(Map json) { + return OrderItem( + menuId: json['menu_id'], + quantite: json['quantite'], + commentaires: json['commentaires'], + ); + } +}