You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

955 lines
29 KiB

// TODO Implement this library.
import 'package:flutter/foundation.dart';
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});
@override
State<OrdersManagementScreen> createState() => _OrdersManagementScreenState();
}
class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
List<Order> orders = [];
bool isLoading = true;
String? errorMessage;
final String baseUrl = 'https://restaurant.careeracademy.mg/api';
@override
void initState() {
super.initState();
loadOrders();
}
Future<void> 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'),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
if (response.statusCode == 200) {
final responseData = json.decode(response.body);
if (responseData['success'] == true) {
final List<dynamic> 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<void> 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<void> 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<Order?> 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<void> updateOrderStatus(
Order order,
String newStatus, {
String? modePaiement,
}) async {
try {
final Map<String, dynamic> 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<void> createNewOrder() async {
// Sample order creation - you can customize this
try {
final Map<String, dynamic> 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<void> 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<Order> 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)} MGA',
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
Navigator.of(context).pop();
// 1. Mettre à jour le statut de la commande
await updateOrderStatus(
order,
"payee",
modePaiement: selectedPaymentMethod,
);
// 2. Rendre la table disponible
await updateTableStatus(order.tableId, "available");
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: const Text(
'Mettre en caisse',
style: TextStyle(color: Colors.white),
),
),
],
);
},
);
},
);
}
Future<void> 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}),
);
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) {
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: [
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),
// 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)} MGA', // 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)} MGA',
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: [
if (order.statut == 'en_attente')
Expanded(
child: ElevatedButton(
onPressed:
() => {
Navigator.push(
context,
MaterialPageRoute(
builder:
(context) => AddItemsToOrderPage(
commandId: order.id,
numeroCommande: order.numeroCommande,
),
),
),
},
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),
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<OrderItem> 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<String, dynamic> json) {
List<OrderItem> 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,
);
}
get numero => null;
}
class OrderItem {
final int menuId;
final int quantite;
final String? commentaires;
OrderItem({required this.menuId, required this.quantite, this.commentaires});
factory OrderItem.fromJson(Map<String, dynamic> json) {
return OrderItem(
menuId: json['menu_id'],
quantite: json['quantite'],
commentaires: json['commentaires'],
);
}
}