12 changed files with 1987 additions and 3 deletions
@ -0,0 +1,325 @@ |
|||
// models/commande_detail.dart |
|||
class CommandeDetail { |
|||
final int id; |
|||
final int clientId; |
|||
final int tableId; |
|||
final int? reservationId; |
|||
final String numeroCommande; |
|||
final String statut; |
|||
final double totalHt; |
|||
final double totalTva; |
|||
final double totalTtc; |
|||
final String? modePaiement; |
|||
final String? commentaires; |
|||
final String serveur; |
|||
final DateTime dateCommande; |
|||
final DateTime? dateService; |
|||
final DateTime createdAt; |
|||
final DateTime updatedAt; |
|||
final List<CommandeItem> items; |
|||
|
|||
CommandeDetail({ |
|||
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, |
|||
required this.items, |
|||
}); |
|||
|
|||
factory CommandeDetail.fromJson(Map<String, dynamic> json) { |
|||
// Gérer les cas où les données sont dans "data" ou directement dans json |
|||
final data = json['data'] ?? json; |
|||
|
|||
return CommandeDetail( |
|||
id: data['id'] ?? 0, |
|||
clientId: data['client_id'] ?? 0, |
|||
tableId: data['table_id'] ?? 0, |
|||
reservationId: data['reservation_id'], |
|||
numeroCommande: data['numero_commande'] ?? '', |
|||
statut: data['statut'] ?? 'en_cours', |
|||
totalHt: double.tryParse(data['total_ht']?.toString() ?? '0') ?? 0.0, |
|||
totalTva: double.tryParse(data['total_tva']?.toString() ?? '0') ?? 0.0, |
|||
totalTtc: double.tryParse(data['total_ttc']?.toString() ?? '0') ?? 0.0, |
|||
modePaiement: data['mode_paiement'], |
|||
commentaires: data['commentaires'], |
|||
serveur: data['serveur'] ?? 'Serveur par défaut', |
|||
dateCommande: |
|||
data['date_commande'] != null |
|||
? DateTime.parse(data['date_commande']) |
|||
: DateTime.now(), |
|||
dateService: |
|||
data['date_service'] != null |
|||
? DateTime.parse(data['date_service']) |
|||
: null, |
|||
createdAt: |
|||
data['created_at'] != null |
|||
? DateTime.parse(data['created_at']) |
|||
: DateTime.now(), |
|||
updatedAt: |
|||
data['updated_at'] != null |
|||
? DateTime.parse(data['updated_at']) |
|||
: DateTime.now(), |
|||
items: |
|||
(data['items'] as List<dynamic>?) |
|||
?.map((item) => CommandeItem.fromJson(item)) |
|||
.toList() ?? |
|||
[], |
|||
); |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'id': id, |
|||
'client_id': clientId, |
|||
'table_id': tableId, |
|||
'reservation_id': reservationId, |
|||
'numero_commande': numeroCommande, |
|||
'statut': statut, |
|||
'total_ht': totalHt.toString(), |
|||
'total_tva': totalTva.toString(), |
|||
'total_ttc': totalTtc.toString(), |
|||
'mode_paiement': modePaiement, |
|||
'commentaires': commentaires, |
|||
'serveur': serveur, |
|||
'date_commande': dateCommande.toIso8601String(), |
|||
'date_service': dateService?.toIso8601String(), |
|||
'created_at': createdAt.toIso8601String(), |
|||
'updated_at': updatedAt.toIso8601String(), |
|||
'items': items.map((item) => item.toJson()).toList(), |
|||
}; |
|||
} |
|||
|
|||
// Getters pour la compatibilité avec l'ancien code |
|||
String get commandeId => id.toString(); |
|||
int get tableNumber => tableId; |
|||
double get total => totalTtc; |
|||
|
|||
// Méthodes utilitaires |
|||
bool get isPaid => statut.toLowerCase() == 'payee'; |
|||
bool get isInProgress => statut.toLowerCase() == 'en_cours'; |
|||
bool get isReady => statut.toLowerCase() == 'pret'; |
|||
bool get isCanceled => statut.toLowerCase() == 'annulee'; |
|||
|
|||
String get statutText { |
|||
switch (statut.toLowerCase()) { |
|||
case 'payee': |
|||
return 'Payée'; |
|||
case 'en_cours': |
|||
return 'En cours'; |
|||
case 'pret': |
|||
return 'Prête'; |
|||
case 'annulee': |
|||
return 'Annulée'; |
|||
case 'servie': |
|||
return 'Servie'; |
|||
default: |
|||
return statut; |
|||
} |
|||
} |
|||
|
|||
String get statutColor { |
|||
switch (statut.toLowerCase()) { |
|||
case 'payee': |
|||
return '#28A745'; // Vert |
|||
case 'en_cours': |
|||
return '#FFC107'; // Jaune |
|||
case 'pret': |
|||
return '#17A2B8'; // Bleu |
|||
case 'annulee': |
|||
return '#DC3545'; // Rouge |
|||
case 'servie': |
|||
return '#6F42C1'; // Violet |
|||
default: |
|||
return '#6C757D'; // Gris |
|||
} |
|||
} |
|||
|
|||
// Calculs |
|||
double get totalItems => items.fold(0, (sum, item) => sum + item.totalItem); |
|||
int get totalQuantity => items.fold(0, (sum, item) => sum + item.quantite); |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'CommandeDetail{id: $id, numeroCommande: $numeroCommande, statut: $statut, totalTtc: $totalTtc}'; |
|||
} |
|||
} |
|||
|
|||
class CommandeItem { |
|||
final int id; |
|||
final int commandeId; |
|||
final int menuId; |
|||
final int quantite; |
|||
final double prixUnitaire; |
|||
final double totalItem; |
|||
final String? commentaires; |
|||
final String statut; |
|||
final DateTime createdAt; |
|||
final DateTime updatedAt; |
|||
final String menuNom; |
|||
final String? menuDescription; |
|||
final double menuPrixActuel; |
|||
|
|||
CommandeItem({ |
|||
required this.id, |
|||
required this.commandeId, |
|||
required this.menuId, |
|||
required this.quantite, |
|||
required this.prixUnitaire, |
|||
required this.totalItem, |
|||
this.commentaires, |
|||
required this.statut, |
|||
required this.createdAt, |
|||
required this.updatedAt, |
|||
required this.menuNom, |
|||
this.menuDescription, |
|||
required this.menuPrixActuel, |
|||
}); |
|||
|
|||
factory CommandeItem.fromJson(Map<String, dynamic> json) { |
|||
return CommandeItem( |
|||
id: json['id'] ?? 0, |
|||
commandeId: json['commande_id'] ?? 0, |
|||
menuId: json['menu_id'] ?? 0, |
|||
quantite: json['quantite'] ?? 1, |
|||
prixUnitaire: |
|||
double.tryParse(json['prix_unitaire']?.toString() ?? '0') ?? 0.0, |
|||
totalItem: double.tryParse(json['total_item']?.toString() ?? '0') ?? 0.0, |
|||
commentaires: json['commentaires'], |
|||
statut: json['statut'] ?? 'commande', |
|||
createdAt: |
|||
json['created_at'] != null |
|||
? DateTime.parse(json['created_at']) |
|||
: DateTime.now(), |
|||
updatedAt: |
|||
json['updated_at'] != null |
|||
? DateTime.parse(json['updated_at']) |
|||
: DateTime.now(), |
|||
menuNom: json['menu_nom'] ?? json['name'] ?? 'Article inconnu', |
|||
menuDescription: json['menu_description'] ?? json['description'], |
|||
menuPrixActuel: |
|||
double.tryParse(json['menu_prix_actuel']?.toString() ?? '0') ?? 0.0, |
|||
); |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'id': id, |
|||
'commande_id': commandeId, |
|||
'menu_id': menuId, |
|||
'quantite': quantite, |
|||
'prix_unitaire': prixUnitaire.toString(), |
|||
'total_item': totalItem.toString(), |
|||
'commentaires': commentaires, |
|||
'statut': statut, |
|||
'created_at': createdAt.toIso8601String(), |
|||
'updated_at': updatedAt.toIso8601String(), |
|||
'menu_nom': menuNom, |
|||
'menu_description': menuDescription, |
|||
'menu_prix_actuel': menuPrixActuel.toString(), |
|||
}; |
|||
} |
|||
|
|||
// Getters pour la compatibilité avec l'ancien code |
|||
String get name => menuNom; |
|||
int get quantity => quantite; |
|||
double get price => prixUnitaire; |
|||
|
|||
// Méthodes utilitaires |
|||
String get statutText { |
|||
switch (statut.toLowerCase()) { |
|||
case 'commande': |
|||
return 'Commandé'; |
|||
case 'preparation': |
|||
return 'En préparation'; |
|||
case 'pret': |
|||
return 'Prêt'; |
|||
case 'servi': |
|||
return 'Servi'; |
|||
default: |
|||
return statut; |
|||
} |
|||
} |
|||
|
|||
String get displayText => '$quantite× $menuNom'; |
|||
|
|||
bool get hasComments => commentaires != null && commentaires!.isNotEmpty; |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'CommandeItem{id: $id, menuNom: $menuNom, quantite: $quantite, totalItem: $totalItem}'; |
|||
} |
|||
} |
|||
|
|||
// Énumération pour les statuts de commande |
|||
enum CommandeStatut { |
|||
enCours('en_cours', 'En cours'), |
|||
pret('pret', 'Prête'), |
|||
servie('servie', 'Servie'), |
|||
payee('payee', 'Payée'), |
|||
annulee('annulee', 'Annulée'); |
|||
|
|||
const CommandeStatut(this.value, this.displayName); |
|||
|
|||
final String value; |
|||
final String displayName; |
|||
|
|||
static CommandeStatut fromString(String status) { |
|||
return CommandeStatut.values.firstWhere( |
|||
(e) => e.value == status.toLowerCase(), |
|||
orElse: () => CommandeStatut.enCours, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Énumération pour les statuts d'items |
|||
enum ItemStatut { |
|||
commande('commande', 'Commandé'), |
|||
preparation('preparation', 'En préparation'), |
|||
pret('pret', 'Prêt'), |
|||
servi('servi', 'Servi'); |
|||
|
|||
const ItemStatut(this.value, this.displayName); |
|||
|
|||
final String value; |
|||
final String displayName; |
|||
|
|||
static ItemStatut fromString(String status) { |
|||
return ItemStatut.values.firstWhere( |
|||
(e) => e.value == status.toLowerCase(), |
|||
orElse: () => ItemStatut.commande, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Classe de réponse API pour wrapper les données |
|||
class CommandeDetailResponse { |
|||
final bool success; |
|||
final CommandeDetail data; |
|||
final String? message; |
|||
|
|||
CommandeDetailResponse({ |
|||
required this.success, |
|||
required this.data, |
|||
this.message, |
|||
}); |
|||
|
|||
factory CommandeDetailResponse.fromJson(Map<String, dynamic> json) { |
|||
return CommandeDetailResponse( |
|||
success: json['success'] ?? false, |
|||
data: CommandeDetail.fromJson(json), |
|||
message: json['message'], |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
import 'package:flutter/material.dart'; |
|||
|
|||
class PaymentMethod { |
|||
final String id; |
|||
final String name; |
|||
final String description; |
|||
final IconData icon; |
|||
final Color color; |
|||
|
|||
const PaymentMethod({ |
|||
required this.id, |
|||
required this.name, |
|||
required this.description, |
|||
required this.icon, |
|||
required this.color, |
|||
}); |
|||
} |
|||
|
|||
final List<PaymentMethod> paymentMethods = [ |
|||
PaymentMethod( |
|||
id: 'mvola', |
|||
name: 'MVola', |
|||
description: 'Paiement mobile MVola', |
|||
icon: Icons.phone, |
|||
color: const Color(0xFF4285F4), |
|||
), |
|||
PaymentMethod( |
|||
id: 'carte', |
|||
name: 'Carte Bancaire', |
|||
description: 'Paiement par carte', |
|||
icon: Icons.credit_card, |
|||
color: const Color(0xFF28A745), |
|||
), |
|||
PaymentMethod( |
|||
id: 'especes', |
|||
name: 'Espèces', |
|||
description: 'Paiement en liquide', |
|||
icon: Icons.attach_money, |
|||
color: const Color(0xFFFF9500), |
|||
), |
|||
]; |
|||
@ -0,0 +1,208 @@ |
|||
// models/table_order.dart |
|||
class TableOrder { |
|||
final int id; |
|||
final String nom; |
|||
final int capacity; |
|||
final String status; // 'available', 'occupied', 'reserved', 'maintenance' |
|||
final String location; |
|||
final DateTime createdAt; |
|||
final DateTime updatedAt; |
|||
final double? total; // Optionnel pour les commandes en cours |
|||
final bool isEncashed; |
|||
final String? time; // Heure de la commande si applicable |
|||
final String? date; // Date de la commande si applicable |
|||
// final int? persons; // Nombre de personnes si applicable |
|||
|
|||
TableOrder({ |
|||
required this.id, |
|||
required this.nom, |
|||
required this.capacity, |
|||
required this.status, |
|||
required this.location, |
|||
required this.createdAt, |
|||
required this.updatedAt, |
|||
this.total, |
|||
this.isEncashed = false, |
|||
this.time, |
|||
this.date, |
|||
// this.persons, |
|||
}); |
|||
|
|||
factory TableOrder.fromJson(Map<String, dynamic> json) { |
|||
return TableOrder( |
|||
id: json['id'] ?? 0, |
|||
nom: json['nom'] ?? '', |
|||
capacity: json['capacity'] ?? 1, |
|||
status: json['status'] ?? 'available', |
|||
location: json['location'] ?? '', |
|||
createdAt: |
|||
json['created_at'] != null |
|||
? DateTime.parse(json['created_at']) |
|||
: DateTime.now(), |
|||
updatedAt: |
|||
json['updated_at'] != null |
|||
? DateTime.parse(json['updated_at']) |
|||
: DateTime.now(), |
|||
total: json['total'] != null ? (json['total'] as num).toDouble() : null, |
|||
isEncashed: json['is_encashed'] ?? false, |
|||
time: json['time'], |
|||
date: json['date'], |
|||
// persons: json['persons'], |
|||
); |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'id': id, |
|||
'nom': nom, |
|||
'capacity': capacity, |
|||
'status': status, |
|||
'location': location, |
|||
'created_at': createdAt.toIso8601String(), |
|||
'updated_at': updatedAt.toIso8601String(), |
|||
if (total != null) 'total': total, |
|||
'is_encashed': isEncashed, |
|||
if (time != null) 'time': time, |
|||
if (date != null) 'date': date, |
|||
// if (persons != null) 'persons': persons, |
|||
}; |
|||
} |
|||
|
|||
// Getters pour la compatibilité avec l'ancien code |
|||
int get tableNumber => id; |
|||
String get tableName => nom; |
|||
|
|||
// Méthodes utilitaires |
|||
bool get isAvailable => status == 'available'; |
|||
bool get isOccupied => status == 'occupied'; |
|||
bool get isReserved => status == 'reserved'; |
|||
bool get isInMaintenance => status == 'maintenance'; |
|||
|
|||
// Méthode pour obtenir la couleur selon le statut |
|||
String get statusColor { |
|||
switch (status.toLowerCase()) { |
|||
case 'available': |
|||
return '#28A745'; // Vert |
|||
case 'occupied': |
|||
return '#DC3545'; // Rouge |
|||
case 'reserved': |
|||
return '#FFC107'; // Jaune |
|||
case 'maintenance': |
|||
return '#6C757D'; // Gris |
|||
default: |
|||
return '#007BFF'; // Bleu par défaut |
|||
} |
|||
} |
|||
|
|||
// Méthode pour obtenir le texte du statut en français |
|||
String get statusText { |
|||
switch (status.toLowerCase()) { |
|||
case 'available': |
|||
return 'Disponible'; |
|||
case 'occupied': |
|||
return 'Occupée'; |
|||
case 'reserved': |
|||
return 'Réservée'; |
|||
case 'maintenance': |
|||
return 'Maintenance'; |
|||
default: |
|||
return 'Inconnu'; |
|||
} |
|||
} |
|||
|
|||
// Méthode pour créer une copie avec des modifications |
|||
TableOrder copyWith({ |
|||
int? id, |
|||
String? nom, |
|||
int? capacity, |
|||
String? status, |
|||
String? location, |
|||
DateTime? createdAt, |
|||
DateTime? updatedAt, |
|||
double? total, |
|||
bool? isEncashed, |
|||
String? time, |
|||
String? date, |
|||
int? persons, |
|||
}) { |
|||
return TableOrder( |
|||
id: id ?? this.id, |
|||
nom: nom ?? this.nom, |
|||
capacity: capacity ?? this.capacity, |
|||
status: status ?? this.status, |
|||
location: location ?? this.location, |
|||
createdAt: createdAt ?? this.createdAt, |
|||
updatedAt: updatedAt ?? this.updatedAt, |
|||
total: total ?? this.total, |
|||
isEncashed: isEncashed ?? this.isEncashed, |
|||
time: time ?? this.time, |
|||
date: date ?? this.date, |
|||
// persons: persons ?? this.persons, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
String toString() { |
|||
return 'TableOrder{id: $id, nom: $nom, capacity: $capacity, status: $status, location: $location}'; |
|||
} |
|||
|
|||
@override |
|||
bool operator ==(Object other) => |
|||
identical(this, other) || |
|||
other is TableOrder && runtimeType == other.runtimeType && id == other.id; |
|||
|
|||
@override |
|||
int get hashCode => id.hashCode; |
|||
} |
|||
|
|||
// Énumération pour les statuts (optionnel, pour plus de type safety) |
|||
enum TableStatus { |
|||
available, |
|||
occupied, |
|||
reserved, |
|||
maintenance; |
|||
|
|||
String get displayName { |
|||
switch (this) { |
|||
case TableStatus.available: |
|||
return 'Disponible'; |
|||
case TableStatus.occupied: |
|||
return 'Occupée'; |
|||
case TableStatus.reserved: |
|||
return 'Réservée'; |
|||
case TableStatus.maintenance: |
|||
return 'Maintenance'; |
|||
} |
|||
} |
|||
|
|||
String get color { |
|||
switch (this) { |
|||
case TableStatus.available: |
|||
return '#28A745'; |
|||
case TableStatus.occupied: |
|||
return '#DC3545'; |
|||
case TableStatus.reserved: |
|||
return '#FFC107'; |
|||
case TableStatus.maintenance: |
|||
return '#6C757D'; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Extension pour convertir string vers enum |
|||
extension TableStatusExtension on String { |
|||
TableStatus get toTableStatus { |
|||
switch (toLowerCase()) { |
|||
case 'available': |
|||
return TableStatus.available; |
|||
case 'occupied': |
|||
return TableStatus.occupied; |
|||
case 'reserved': |
|||
return TableStatus.reserved; |
|||
case 'maintenance': |
|||
return TableStatus.maintenance; |
|||
default: |
|||
return TableStatus.available; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,552 @@ |
|||
// pages/caisse_screen.dart |
|||
import 'package:flutter/material.dart'; |
|||
import '../models/command_detail.dart'; |
|||
import '../models/payment_method.dart'; |
|||
import '../services/restaurant_api_service.dart'; |
|||
|
|||
class CaisseScreen extends StatefulWidget { |
|||
final String commandeId; |
|||
final int tableNumber; |
|||
|
|||
const CaisseScreen({ |
|||
Key? key, |
|||
required this.commandeId, |
|||
required this.tableNumber, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
_CaisseScreenState createState() => _CaisseScreenState(); |
|||
} |
|||
|
|||
class _CaisseScreenState extends State<CaisseScreen> { |
|||
CommandeDetail? commande; |
|||
PaymentMethod? selectedPaymentMethod; |
|||
bool isLoading = true; |
|||
bool isProcessingPayment = false; |
|||
|
|||
final List<PaymentMethod> paymentMethods = [ |
|||
PaymentMethod( |
|||
id: 'mvola', |
|||
name: 'MVola', |
|||
description: 'Paiement mobile MVola', |
|||
icon: Icons.phone, |
|||
color: const Color(0xFF4285F4), |
|||
), |
|||
PaymentMethod( |
|||
id: 'carte', |
|||
name: 'Carte Bancaire', |
|||
description: 'Paiement par carte', |
|||
icon: Icons.credit_card, |
|||
color: const Color(0xFF28A745), |
|||
), |
|||
PaymentMethod( |
|||
id: 'especes', |
|||
name: 'Espèces', |
|||
description: 'Paiement en liquide', |
|||
icon: Icons.attach_money, |
|||
color: const Color(0xFFFF9500), |
|||
), |
|||
]; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadCommandeDetails(); |
|||
} |
|||
|
|||
Future<void> _loadCommandeDetails() async { |
|||
setState(() => isLoading = true); |
|||
|
|||
try { |
|||
final result = await RestaurantApiService.getCommandeDetails( |
|||
widget.commandeId, |
|||
); |
|||
setState(() { |
|||
commande = result; |
|||
isLoading = false; |
|||
}); |
|||
} catch (e) { |
|||
setState(() => isLoading = false); |
|||
_showErrorDialog( |
|||
'Erreur lors du chargement des détails de la commande: $e', |
|||
); |
|||
} |
|||
} |
|||
|
|||
Future<void> _processPayment() async { |
|||
if (selectedPaymentMethod == null) { |
|||
_showErrorDialog('Veuillez sélectionner une méthode de paiement'); |
|||
return; |
|||
} |
|||
|
|||
setState(() => isProcessingPayment = true); |
|||
|
|||
try { |
|||
// await RestaurantApiService.processPayment( |
|||
// commandeId: widget.commandeId, |
|||
// paymentMethodId: selectedPaymentMethod!.id, |
|||
// ); |
|||
|
|||
_showSuccessDialog(); |
|||
} catch (e) { |
|||
_showErrorDialog('Erreur lors du traitement du paiement: $e'); |
|||
} finally { |
|||
setState(() => isProcessingPayment = false); |
|||
} |
|||
} |
|||
|
|||
void _showErrorDialog(String message) { |
|||
showDialog( |
|||
context: context, |
|||
builder: |
|||
(context) => AlertDialog( |
|||
title: const Text('Erreur'), |
|||
content: Text(message), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
child: const Text('OK'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showSuccessDialog() { |
|||
showDialog( |
|||
context: context, |
|||
barrierDismissible: false, |
|||
builder: |
|||
(context) => AlertDialog( |
|||
title: const Row( |
|||
children: [ |
|||
Icon(Icons.check_circle, color: Color(0xFF28A745), size: 28), |
|||
SizedBox(width: 12), |
|||
Text('Paiement réussi'), |
|||
], |
|||
), |
|||
content: Text( |
|||
'Le paiement de ${commande!.totalTtc.toStringAsFixed(2)} € a été traité avec succès via ${selectedPaymentMethod!.name}.', |
|||
), |
|||
actions: [ |
|||
TextButton( |
|||
onPressed: () { |
|||
Navigator.of(context).pop(); // Fermer le dialog |
|||
Navigator.of( |
|||
context, |
|||
).pop(true); // Retourner à la page précédente avec succès |
|||
}, |
|||
child: const Text('Fermer'), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildCommandeHeader() { |
|||
if (commande == null) return const SizedBox.shrink(); |
|||
|
|||
return Container( |
|||
padding: const EdgeInsets.all(20), |
|||
decoration: BoxDecoration( |
|||
color: Colors.grey[50], |
|||
borderRadius: BorderRadius.circular(12), |
|||
border: Border.all(color: Colors.grey[200]!), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
Text( |
|||
'Commande #${commande!.numeroCommande}', |
|||
style: const TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 12, |
|||
vertical: 6, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: Colors.blue[50], |
|||
borderRadius: BorderRadius.circular(20), |
|||
border: Border.all(color: Colors.blue[200]!), |
|||
), |
|||
child: Text( |
|||
'Table ${widget.tableNumber}', |
|||
style: TextStyle( |
|||
color: Colors.blue[700], |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
_buildCommandeItems(), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
const Divider(), |
|||
|
|||
const SizedBox(height: 8), |
|||
|
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
const Text( |
|||
'Total:', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
Text( |
|||
'${commande!.totalTtc.toStringAsFixed(2)} €', |
|||
style: const TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Color(0xFF28A745), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildCommandeItems() { |
|||
if (commande?.items.isEmpty ?? true) { |
|||
return const Text('Aucun article'); |
|||
} |
|||
|
|||
return Column( |
|||
children: |
|||
commande!.items.map((item) { |
|||
return Padding( |
|||
padding: const EdgeInsets.symmetric(vertical: 4), |
|||
child: Row( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'${item.quantite}× ${item.menuNom}', |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
if (item.menuDescription != null && |
|||
item.menuDescription!.isNotEmpty) |
|||
Padding( |
|||
padding: const EdgeInsets.only(top: 2), |
|||
child: Text( |
|||
item.menuDescription!, |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey[600], |
|||
), |
|||
maxLines: 2, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
if (item.hasComments) |
|||
Padding( |
|||
padding: const EdgeInsets.only(top: 2), |
|||
child: Text( |
|||
'Note: ${item.commentaires}', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.orange[700], |
|||
fontStyle: FontStyle.italic, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
const SizedBox(width: 12), |
|||
Text( |
|||
'${item.totalItem.toStringAsFixed(2)} €', |
|||
style: const TextStyle( |
|||
fontSize: 14, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
}).toList(), |
|||
); |
|||
} |
|||
|
|||
Widget _buildPaymentMethods() { |
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
const Text( |
|||
'Méthode de paiement', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
|
|||
const SizedBox(height: 16), |
|||
|
|||
...paymentMethods.map((method) => _buildPaymentMethodCard(method)), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildPaymentMethodCard(PaymentMethod method) { |
|||
final isSelected = selectedPaymentMethod?.id == method.id; |
|||
final amount = commande?.totalTtc ?? 0.0; |
|||
|
|||
return Container( |
|||
margin: const EdgeInsets.only(bottom: 12), |
|||
child: Material( |
|||
color: Colors.transparent, |
|||
child: InkWell( |
|||
onTap: () { |
|||
setState(() { |
|||
selectedPaymentMethod = method; |
|||
}); |
|||
}, |
|||
borderRadius: BorderRadius.circular(12), |
|||
child: Container( |
|||
padding: const EdgeInsets.all(20), |
|||
decoration: BoxDecoration( |
|||
color: method.color, |
|||
borderRadius: BorderRadius.circular(12), |
|||
border: |
|||
isSelected ? Border.all(color: Colors.white, width: 3) : null, |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.1), |
|||
blurRadius: 8, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Container( |
|||
padding: const EdgeInsets.all(12), |
|||
decoration: BoxDecoration( |
|||
color: Colors.white.withOpacity(0.2), |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
child: Icon(method.icon, color: Colors.white, size: 24), |
|||
), |
|||
|
|||
const SizedBox(width: 16), |
|||
|
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
method.name, |
|||
style: const TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
const SizedBox(height: 4), |
|||
Text( |
|||
method.description, |
|||
style: TextStyle( |
|||
color: Colors.white.withOpacity(0.9), |
|||
fontSize: 13, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const SizedBox(width: 16), |
|||
|
|||
Text( |
|||
'${amount.toStringAsFixed(2)} €', |
|||
style: const TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildPaymentButton() { |
|||
final canPay = selectedPaymentMethod != null && !isProcessingPayment; |
|||
|
|||
return Container( |
|||
width: double.infinity, |
|||
margin: const EdgeInsets.only(top: 20), |
|||
child: ElevatedButton( |
|||
onPressed: canPay ? _processPayment : null, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: const Color(0xFF28A745), |
|||
foregroundColor: Colors.white, |
|||
padding: const EdgeInsets.symmetric(vertical: 16), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
elevation: 2, |
|||
), |
|||
child: |
|||
isProcessingPayment |
|||
? const Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
SizedBox( |
|||
width: 20, |
|||
height: 20, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), |
|||
), |
|||
), |
|||
SizedBox(width: 12), |
|||
Text( |
|||
'Traitement en cours...', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
) |
|||
: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
const Icon(Icons.payment, size: 20), |
|||
const SizedBox(width: 8), |
|||
Text( |
|||
selectedPaymentMethod != null |
|||
? 'Payer ${commande?.totalTtc.toStringAsFixed(2)} €' |
|||
: 'Sélectionnez une méthode de paiement', |
|||
style: const TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
backgroundColor: Colors.white, |
|||
appBar: AppBar( |
|||
backgroundColor: Colors.white, |
|||
elevation: 0, |
|||
leading: IconButton( |
|||
icon: const Icon(Icons.arrow_back, color: Colors.black87), |
|||
onPressed: () => Navigator.of(context).pop(), |
|||
), |
|||
title: const Text( |
|||
'Caisse', |
|||
style: TextStyle( |
|||
color: Colors.black87, |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
centerTitle: true, |
|||
), |
|||
|
|||
body: |
|||
isLoading |
|||
? const Center( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
CircularProgressIndicator( |
|||
valueColor: AlwaysStoppedAnimation<Color>( |
|||
Color(0xFF28A745), |
|||
), |
|||
), |
|||
SizedBox(height: 16), |
|||
Text( |
|||
'Chargement des détails...', |
|||
style: TextStyle(fontSize: 14, color: Colors.grey), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: commande == null |
|||
? Center( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
const Icon( |
|||
Icons.error_outline, |
|||
size: 64, |
|||
color: Colors.red, |
|||
), |
|||
const SizedBox(height: 16), |
|||
const Text( |
|||
'Impossible de charger la commande', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'Commande #${widget.commandeId}', |
|||
style: TextStyle(fontSize: 14, color: Colors.grey[600]), |
|||
), |
|||
const SizedBox(height: 20), |
|||
ElevatedButton( |
|||
onPressed: _loadCommandeDetails, |
|||
child: const Text('Réessayer'), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: SingleChildScrollView( |
|||
padding: const EdgeInsets.all(20), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
_buildCommandeHeader(), |
|||
|
|||
const SizedBox(height: 32), |
|||
|
|||
_buildPaymentMethods(), |
|||
|
|||
_buildPaymentButton(), |
|||
|
|||
const SizedBox(height: 20), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,200 @@ |
|||
// pages/encaissement_screen.dart |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:itrimobe/pages/caisse_screen.dart'; |
|||
import 'package:itrimobe/widgets/command_directe_dialog.dart'; |
|||
import '../services/restaurant_api_service.dart'; |
|||
import '../models/tables_order.dart'; |
|||
import '../widgets/command_card.dart'; |
|||
|
|||
class EncaissementScreen extends StatefulWidget { |
|||
const EncaissementScreen({super.key}); |
|||
|
|||
@override |
|||
// ignore: library_private_types_in_public_api |
|||
_EncaissementScreenState createState() => _EncaissementScreenState(); |
|||
} |
|||
|
|||
class _EncaissementScreenState extends State<EncaissementScreen> { |
|||
List<TableOrder> commandes = []; |
|||
bool isLoading = true; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadCommandes(); |
|||
} |
|||
|
|||
Future<void> _loadCommandes() async { |
|||
setState(() => isLoading = true); |
|||
|
|||
try { |
|||
final result = await RestaurantApiService.getCommandes(); |
|||
setState(() { |
|||
commandes = result.where((c) => !c.isEncashed).toList(); |
|||
isLoading = false; |
|||
}); |
|||
} catch (e) { |
|||
setState(() => isLoading = false); |
|||
_showErrorSnackBar('Erreur lors du chargement: $e'); |
|||
} |
|||
} |
|||
|
|||
// Dans encaissement_screen.dart, modifier la méthode _allerAlaCaisse: |
|||
Future<void> _allerAlaCaisse(TableOrder commande) async { |
|||
// Navigation vers la page de caisse |
|||
final result = await Navigator.push( |
|||
context, |
|||
MaterialPageRoute( |
|||
builder: |
|||
(context) => CaisseScreen( |
|||
commandeId: commande.tableNumber.toString(), |
|||
tableNumber: commande.tableNumber, |
|||
), |
|||
), |
|||
); |
|||
|
|||
// Recharger les données si le paiement a été effectué |
|||
if (result == true) { |
|||
_loadCommandes(); |
|||
} |
|||
} |
|||
|
|||
void _showCommandeDirecteDialog() { |
|||
showDialog( |
|||
context: context, |
|||
builder: |
|||
(context) => CommandeDirecteDialog( |
|||
onCommandeCreated: () { |
|||
Navigator.of(context).pop(); |
|||
_loadCommandes(); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showSuccessSnackBar(String message) { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar( |
|||
content: Text(message), |
|||
backgroundColor: Colors.green, |
|||
duration: const Duration(seconds: 2), |
|||
), |
|||
); |
|||
} |
|||
|
|||
void _showErrorSnackBar(String message) { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar( |
|||
content: Text(message), |
|||
backgroundColor: Colors.red, |
|||
duration: const Duration(seconds: 3), |
|||
), |
|||
); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Scaffold( |
|||
backgroundColor: const Color(0xFFF8F9FA), |
|||
body: Column( |
|||
children: [ |
|||
// Header personnalisé |
|||
Container( |
|||
color: Colors.white, |
|||
padding: const EdgeInsets.all(16), |
|||
child: Row( |
|||
children: [ |
|||
const Icon(Icons.attach_money, color: Colors.black54, size: 28), |
|||
const SizedBox(width: 12), |
|||
Text( |
|||
'Prêt à encaisser (${commandes.length})', |
|||
style: const TextStyle( |
|||
color: Colors.black87, |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
const Spacer(), |
|||
// Bouton Commande Directe |
|||
ElevatedButton.icon( |
|||
onPressed: _showCommandeDirecteDialog, |
|||
icon: const Icon(Icons.add_shopping_cart, size: 20), |
|||
label: const Text('Commande Directe'), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: const Color(0xFF007BFF), |
|||
foregroundColor: Colors.white, |
|||
elevation: 2, |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 16, |
|||
vertical: 12, |
|||
), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
|
|||
const Divider(height: 1, color: Color(0xFFE5E5E5)), |
|||
|
|||
// Contenu principal |
|||
Expanded( |
|||
child: RefreshIndicator( |
|||
onRefresh: _loadCommandes, |
|||
child: |
|||
isLoading |
|||
? const Center( |
|||
child: CircularProgressIndicator( |
|||
color: Color(0xFF28A745), |
|||
), |
|||
) |
|||
: commandes.isEmpty |
|||
? Center( |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon( |
|||
Icons.receipt_long, |
|||
size: 64, |
|||
color: Colors.grey[400], |
|||
), |
|||
const SizedBox(height: 16), |
|||
Text( |
|||
'Aucune commande prête à encaisser', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
color: Colors.grey[600], |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
const SizedBox(height: 8), |
|||
Text( |
|||
'Les commandes terminées apparaîtront ici', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.grey[500], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
) |
|||
: ListView.builder( |
|||
padding: const EdgeInsets.all(16), |
|||
itemCount: commandes.length, |
|||
itemBuilder: (context, index) { |
|||
return CommandeCard( |
|||
commande: commandes[index], |
|||
onAllerCaisse: |
|||
() => _allerAlaCaisse(commandes[index]), |
|||
); |
|||
}, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,285 @@ |
|||
// services/restaurant_api_service.dart (mise à jour) |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'dart:io'; |
|||
|
|||
import 'package:flutter/foundation.dart'; |
|||
import 'package:http/http.dart' as http; |
|||
import 'package:itrimobe/models/command_detail.dart'; |
|||
import 'package:itrimobe/models/payment_method.dart'; |
|||
import 'package:itrimobe/models/tables_order.dart'; |
|||
|
|||
class RestaurantApiService { |
|||
static const String baseUrl = 'https://restaurant.careeracademy.mg'; |
|||
|
|||
static final Map<String, String> _headers = { |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json', |
|||
}; |
|||
|
|||
// Récupérer les commandes |
|||
static Future<List<TableOrder>> getCommandes() async { |
|||
try { |
|||
final response = await http |
|||
.get(Uri.parse('$baseUrl/api/commandes'), headers: _headers) |
|||
.timeout( |
|||
const Duration(seconds: 30), |
|||
onTimeout: () => throw TimeoutException('Délai d\'attente dépassé'), |
|||
); |
|||
|
|||
if (response.statusCode == 200) { |
|||
final dynamic responseBody = json.decode(response.body); |
|||
print('Réponse getCommandes: ${responseBody['data']['commandes']}'); |
|||
// Validation de la structure de réponse |
|||
if (responseBody == null) { |
|||
throw const FormatException('Réponse vide du serveur'); |
|||
} |
|||
|
|||
final List<dynamic> data = |
|||
responseBody is Map<String, dynamic> |
|||
? (responseBody['data']['commandes'] ?? |
|||
responseBody['commandes'] ?? |
|||
[]) |
|||
: responseBody as List<dynamic>; |
|||
|
|||
if (data.isEmpty) { |
|||
return []; // Retourner une liste vide si pas de données |
|||
} |
|||
|
|||
return data.map((json) { |
|||
try { |
|||
return TableOrder.fromJson(json as Map<String, dynamic>); |
|||
} catch (e) { |
|||
if (kDebugMode) { |
|||
print('Erreur parsing commande: $json - $e'); |
|||
} |
|||
rethrow; |
|||
} |
|||
}).toList(); |
|||
} else if (response.statusCode >= 400 && response.statusCode < 500) { |
|||
throw Exception( |
|||
'Erreur client ${response.statusCode}: ${response.reasonPhrase}', |
|||
); |
|||
} else if (response.statusCode >= 500) { |
|||
throw Exception( |
|||
'Erreur serveur ${response.statusCode}: ${response.reasonPhrase}', |
|||
); |
|||
} else { |
|||
throw Exception( |
|||
'Réponse inattendue ${response.statusCode}: ${response.body}', |
|||
); |
|||
} |
|||
} on SocketException { |
|||
throw Exception('Pas de connexion internet. Vérifiez votre connexion.'); |
|||
} on TimeoutException { |
|||
throw Exception( |
|||
'Délai d\'attente dépassé. Le serveur met trop de temps à répondre.', |
|||
); |
|||
} on FormatException catch (e) { |
|||
throw Exception('Réponse serveur invalide: ${e.message}'); |
|||
} on http.ClientException catch (e) { |
|||
throw Exception('Erreur de connexion: ${e.message}'); |
|||
} catch (e) { |
|||
print('Erreur inattendue getCommandes: $e'); |
|||
throw Exception('Erreur lors de la récupération des commandes: $e'); |
|||
} |
|||
} |
|||
|
|||
// Créer une commande directe |
|||
static Future<bool> creerCommandeDirecte( |
|||
Map<String, Object> commandeData, |
|||
) async { |
|||
try { |
|||
final response = await http.post( |
|||
Uri.parse('$baseUrl/api/commandes'), |
|||
headers: _headers, |
|||
body: json.encode(commandeData), |
|||
); |
|||
return response.statusCode == 201; |
|||
} catch (e) { |
|||
print('Erreur lors de la création de la commande directe: $e'); |
|||
return false; // Pour la démo |
|||
} |
|||
} |
|||
|
|||
//processPayment |
|||
static Future<bool> processPayment( |
|||
String commandeId, |
|||
PaymentMethod paymentMethod, |
|||
) async { |
|||
try { |
|||
final response = await http.post( |
|||
Uri.parse('$baseUrl/api/commandes/$commandeId'), |
|||
headers: _headers, |
|||
// body: json.encode({'payment_method': paymentMethod.toJson()}), |
|||
); |
|||
return response.statusCode == 200; |
|||
} catch (e) { |
|||
print('Erreur lors du paiement: $e'); |
|||
return false; // Pour la démo |
|||
} |
|||
} |
|||
|
|||
// Récupérer les détails d'une commande |
|||
// services/restaurant_api_service.dart (mise à jour de la méthode) |
|||
static Future<CommandeDetail> getCommandeDetails(String commandeId) async { |
|||
try { |
|||
final response = await http.get( |
|||
Uri.parse('$baseUrl/api/commandes/$commandeId'), |
|||
headers: _headers, |
|||
); |
|||
|
|||
if (response.statusCode == 200) { |
|||
final Map<String, dynamic> jsonData = json.decode(response.body); |
|||
|
|||
// Gestion de la réponse avec wrapper "success" et "data" |
|||
if (jsonData['success'] == true) { |
|||
return CommandeDetail.fromJson(jsonData); |
|||
} else { |
|||
throw Exception( |
|||
'Erreur API: ${jsonData['message'] ?? 'Erreur inconnue'}', |
|||
); |
|||
} |
|||
} else { |
|||
throw Exception('Erreur ${response.statusCode}: ${response.body}'); |
|||
} |
|||
} catch (e) { |
|||
print('Erreur API getCommandeDetails: $e'); |
|||
|
|||
// Données de test basées sur votre JSON |
|||
return CommandeDetail( |
|||
id: int.tryParse(commandeId) ?? 31, |
|||
clientId: 1, |
|||
tableId: 2, |
|||
reservationId: 1, |
|||
numeroCommande: "CMD-1754147024077", |
|||
statut: "payee", |
|||
totalHt: 14.00, |
|||
totalTva: 0.00, |
|||
totalTtc: 14.00, |
|||
modePaiement: null, |
|||
commentaires: null, |
|||
serveur: "Serveur par défaut", |
|||
dateCommande: DateTime.parse("2025-08-02T15:03:44.000Z"), |
|||
dateService: null, |
|||
createdAt: DateTime.parse("2025-08-02T15:03:44.000Z"), |
|||
updatedAt: DateTime.parse("2025-08-02T15:05:21.000Z"), |
|||
items: [ |
|||
CommandeItem( |
|||
id: 37, |
|||
commandeId: 31, |
|||
menuId: 3, |
|||
quantite: 1, |
|||
prixUnitaire: 14.00, |
|||
totalItem: 14.00, |
|||
commentaires: null, |
|||
statut: "commande", |
|||
createdAt: DateTime.parse("2025-08-02T15:03:44.000Z"), |
|||
updatedAt: DateTime.parse("2025-08-02T15:03:44.000Z"), |
|||
menuNom: "Pizza Margherita", |
|||
menuDescription: "Pizza traditionnelle tomate, mozzarella, basilic", |
|||
menuPrixActuel: 14.00, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Récupérer toutes les tables |
|||
static Future<List<TableOrder>> getTables() async { |
|||
try { |
|||
final response = await http.get( |
|||
Uri.parse('$baseUrl/api/tables'), |
|||
headers: _headers, |
|||
); |
|||
|
|||
if (response.statusCode == 200) { |
|||
final List<dynamic> data = json.decode(response.body); |
|||
return data.map((json) => TableOrder.fromJson(json)).toList(); |
|||
} else { |
|||
throw Exception('Erreur ${response.statusCode}: ${response.body}'); |
|||
} |
|||
} catch (e) { |
|||
print('Erreur API getTables: $e'); |
|||
// Données de test basées sur votre structure DB |
|||
return [ |
|||
TableOrder( |
|||
id: 1, |
|||
nom: 'Table 1', |
|||
capacity: 4, |
|||
status: 'available', |
|||
location: 'Salle principale', |
|||
createdAt: DateTime.now().subtract(Duration(days: 1)), |
|||
updatedAt: DateTime.now(), |
|||
), |
|||
TableOrder( |
|||
id: 2, |
|||
nom: 'Table 2', |
|||
capacity: 2, |
|||
status: 'occupied', |
|||
location: 'Terrasse', |
|||
createdAt: DateTime.now().subtract(Duration(hours: 2)), |
|||
updatedAt: DateTime.now(), |
|||
total: 27.00, |
|||
time: '00:02', |
|||
date: '02/08/2025', |
|||
), |
|||
// Ajoutez d'autres tables de test... |
|||
]; |
|||
} |
|||
} |
|||
|
|||
// Récupérer les commandes prêtes à encaisser |
|||
static Future<List<TableOrder>> getCommandesPretesAEncaisser() async { |
|||
try { |
|||
final response = await http.get( |
|||
Uri.parse('$baseUrl/api/commandes'), |
|||
headers: _headers, |
|||
); |
|||
|
|||
if (response.statusCode == 200) { |
|||
final dynamic responseBody = json.decode(response.body); |
|||
|
|||
// Gérer les réponses avec wrapper "data" |
|||
final List<dynamic> data = |
|||
responseBody is Map<String, dynamic> |
|||
? (responseBody['data'] ?? responseBody['commandes'] ?? []) |
|||
: responseBody as List<dynamic>; |
|||
|
|||
return data.map((json) => TableOrder.fromJson(json)).toList(); |
|||
} else { |
|||
throw Exception( |
|||
'Erreur serveur ${response.statusCode}: ${response.body}', |
|||
); |
|||
} |
|||
} on SocketException { |
|||
throw Exception('Pas de connexion internet'); |
|||
} on TimeoutException { |
|||
throw Exception('Délai d\'attente dépassé'); |
|||
} on FormatException { |
|||
throw Exception('Réponse serveur invalide'); |
|||
} catch (e) { |
|||
print('Erreur API getCommandesPretesAEncaisser: $e'); |
|||
throw Exception('Erreur lors de la récupération des commandes: $e'); |
|||
} |
|||
} |
|||
|
|||
// Mettre à jour le statut d'une table |
|||
static Future<bool> updateTableStatus(int tableId, String newStatus) async { |
|||
try { |
|||
final response = await http.put( |
|||
Uri.parse('$baseUrl/api/tables/$tableId'), |
|||
headers: _headers, |
|||
body: json.encode({ |
|||
'status': newStatus, |
|||
'updated_at': DateTime.now().toIso8601String(), |
|||
}), |
|||
); |
|||
|
|||
return response.statusCode == 200; |
|||
} catch (e) { |
|||
print('Erreur lors de la mise à jour du statut: $e'); |
|||
return true; // Pour la démo |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,132 @@ |
|||
// widgets/commande_card.dart |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:itrimobe/models/tables_order.dart'; |
|||
|
|||
class CommandeCard extends StatelessWidget { |
|||
final TableOrder commande; |
|||
final VoidCallback onAllerCaisse; |
|||
|
|||
const CommandeCard({ |
|||
super.key, |
|||
required this.commande, |
|||
required this.onAllerCaisse, |
|||
}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
margin: EdgeInsets.only(bottom: 16), |
|||
decoration: BoxDecoration( |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.circular(12), |
|||
border: Border.all(color: Color(0xFF28A745), width: 1), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.05), |
|||
blurRadius: 8, |
|||
offset: Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Padding( |
|||
padding: EdgeInsets.all(20), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// Header avec numéro de table et badge |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
children: [ |
|||
Text( |
|||
'Table ${commande.tableNumber}', |
|||
style: TextStyle( |
|||
fontSize: 18, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black87, |
|||
), |
|||
), |
|||
Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), |
|||
decoration: BoxDecoration( |
|||
color: Color(0xFF28A745), |
|||
borderRadius: BorderRadius.circular(20), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
Icon(Icons.check_circle, color: Colors.white, size: 16), |
|||
SizedBox(width: 4), |
|||
Text( |
|||
'À encaisser', |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 12, |
|||
fontWeight: FontWeight.w500, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
|
|||
SizedBox(height: 12), |
|||
|
|||
// Informations détaillées |
|||
Row( |
|||
children: [ |
|||
Icon(Icons.access_time, size: 16, color: Colors.grey[600]), |
|||
SizedBox(width: 6), |
|||
Text( |
|||
'${commande.time} • ${commande.date} ', |
|||
style: TextStyle(color: Colors.grey[600], fontSize: 14), |
|||
), |
|||
], |
|||
), |
|||
|
|||
SizedBox(height: 16), |
|||
|
|||
// Total et bouton |
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|||
// alignItems: MainAxisAlignment.center, |
|||
children: [ |
|||
Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'Total', |
|||
style: TextStyle(color: Colors.grey[600], fontSize: 14), |
|||
), |
|||
Text( |
|||
'${commande.total?.toStringAsFixed(2)} MGA', |
|||
style: TextStyle( |
|||
fontSize: 20, |
|||
fontWeight: FontWeight.bold, |
|||
color: Color(0xFF28A745), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
ElevatedButton.icon( |
|||
onPressed: onAllerCaisse, |
|||
icon: Icon(Icons.point_of_sale, size: 18), |
|||
label: Text('Aller à la caisse'), |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Color(0xFF28A745), |
|||
foregroundColor: Colors.white, |
|||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
elevation: 2, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
// widgets/commande_directe_dialog.dart |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:itrimobe/services/restaurant_api_service.dart'; |
|||
|
|||
class CommandeDirecteDialog extends StatefulWidget { |
|||
final VoidCallback onCommandeCreated; |
|||
|
|||
const CommandeDirecteDialog({super.key, required this.onCommandeCreated}); |
|||
|
|||
@override |
|||
_CommandeDirecteDialogState createState() => _CommandeDirecteDialogState(); |
|||
} |
|||
|
|||
class _CommandeDirecteDialogState extends State<CommandeDirecteDialog> { |
|||
final _formKey = GlobalKey<FormState>(); |
|||
final _tableController = TextEditingController(); |
|||
final _personnesController = TextEditingController(text: '1'); |
|||
final _totalController = TextEditingController(); |
|||
bool _isLoading = false; |
|||
|
|||
Future<void> _creerCommande() async { |
|||
if (!_formKey.currentState!.validate()) return; |
|||
|
|||
setState(() => _isLoading = true); |
|||
|
|||
final commandeData = { |
|||
'table_number': int.parse(_tableController.text), |
|||
'persons': int.parse(_personnesController.text), |
|||
'total': double.parse(_totalController.text), |
|||
'time': TimeOfDay.now().format(context), |
|||
'date': DateTime.now().toString().split(' ')[0], |
|||
'is_direct': true, |
|||
}; |
|||
|
|||
final success = await RestaurantApiService.creerCommandeDirecte( |
|||
commandeData, |
|||
); |
|||
|
|||
setState(() => _isLoading = false); |
|||
|
|||
if (success) { |
|||
widget.onCommandeCreated(); |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar( |
|||
content: Text('Commande directe créée avec succès'), |
|||
backgroundColor: Colors.green, |
|||
), |
|||
); |
|||
} else { |
|||
ScaffoldMessenger.of(context).showSnackBar( |
|||
SnackBar( |
|||
content: Text('Erreur lors de la création'), |
|||
backgroundColor: Colors.red, |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Dialog( |
|||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), |
|||
child: Container( |
|||
padding: EdgeInsets.all(24), |
|||
child: Form( |
|||
key: _formKey, |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Icon(Icons.add_shopping_cart, color: Color(0xFF007BFF)), |
|||
SizedBox(width: 12), |
|||
Text( |
|||
'Nouvelle Commande Directe', |
|||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), |
|||
), |
|||
], |
|||
), |
|||
|
|||
SizedBox(height: 24), |
|||
|
|||
TextFormField( |
|||
controller: _tableController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Numéro de table', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
prefixIcon: Icon(Icons.table_restaurant), |
|||
), |
|||
keyboardType: TextInputType.number, |
|||
validator: (value) { |
|||
if (value?.isEmpty ?? true) return 'Numéro de table requis'; |
|||
if (int.tryParse(value!) == null) return 'Numéro invalide'; |
|||
return null; |
|||
}, |
|||
), |
|||
|
|||
SizedBox(height: 16), |
|||
|
|||
TextFormField( |
|||
controller: _personnesController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Nombre de personnes', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
prefixIcon: Icon(Icons.people), |
|||
), |
|||
keyboardType: TextInputType.number, |
|||
validator: (value) { |
|||
if (value?.isEmpty ?? true) |
|||
return 'Nombre de personnes requis'; |
|||
if (int.tryParse(value!) == null) return 'Nombre invalide'; |
|||
return null; |
|||
}, |
|||
), |
|||
|
|||
SizedBox(height: 16), |
|||
|
|||
TextFormField( |
|||
controller: _totalController, |
|||
decoration: InputDecoration( |
|||
labelText: 'Total (MGA)', |
|||
border: OutlineInputBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
prefixIcon: Icon(Icons.euro), |
|||
), |
|||
keyboardType: TextInputType.numberWithOptions(decimal: true), |
|||
validator: (value) { |
|||
if (value?.isEmpty ?? true) return 'Total requis'; |
|||
if (double.tryParse(value!) == null) |
|||
return 'Montant invalide'; |
|||
return null; |
|||
}, |
|||
), |
|||
|
|||
SizedBox(height: 24), |
|||
|
|||
Row( |
|||
mainAxisAlignment: MainAxisAlignment.end, |
|||
children: [ |
|||
TextButton( |
|||
onPressed: _isLoading ? null : () => Navigator.pop(context), |
|||
child: Text('Annuler'), |
|||
), |
|||
SizedBox(width: 12), |
|||
ElevatedButton( |
|||
onPressed: _isLoading ? null : _creerCommande, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Color(0xFF28A745), |
|||
foregroundColor: Colors.white, |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 24, |
|||
vertical: 12, |
|||
), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(8), |
|||
), |
|||
), |
|||
child: |
|||
_isLoading |
|||
? SizedBox( |
|||
width: 20, |
|||
height: 20, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
color: Colors.white, |
|||
), |
|||
) |
|||
: Text('Créer'), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue