page caisse
This commit is contained in:
parent
6dd215e417
commit
be3d5264cf
@ -61,6 +61,9 @@ class _MainLayoutState extends State<MainLayout> {
|
||||
case 4:
|
||||
route = '/plats';
|
||||
break;
|
||||
case 5:
|
||||
route = '/encaissement';
|
||||
break;
|
||||
default:
|
||||
route = '/tables';
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import 'pages/categorie.dart';
|
||||
import 'pages/commandes_screen.dart';
|
||||
import 'pages/login_screen.dart';
|
||||
import 'pages/menus_screen.dart';
|
||||
import 'pages/encaissement_screen.dart'; // NOUVEAU
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@ -40,12 +41,16 @@ class MyApp extends StatelessWidget {
|
||||
currentRoute: '/commandes',
|
||||
child: OrdersManagementScreen(),
|
||||
),
|
||||
// MODIFICATION : Route simple pour le menu
|
||||
'/plats':
|
||||
(context) => const MainLayout(
|
||||
currentRoute: '/plats',
|
||||
child:
|
||||
PlatsManagementScreen(), // Pas de paramètres requis maintenant
|
||||
child: PlatsManagementScreen(),
|
||||
),
|
||||
// NOUVELLE ROUTE pour l'encaissement
|
||||
'/encaissement':
|
||||
(context) => const MainLayout(
|
||||
currentRoute: '/encaissement',
|
||||
child: EncaissementScreen(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
325
lib/models/command_detail.dart
Normal file
325
lib/models/command_detail.dart
Normal file
@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/models/payment_method.dart
Normal file
41
lib/models/payment_method.dart
Normal file
@ -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),
|
||||
),
|
||||
];
|
||||
208
lib/models/tables_order.dart
Normal file
208
lib/models/tables_order.dart
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
552
lib/pages/caisse_screen.dart
Normal file
552
lib/pages/caisse_screen.dart
Normal file
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
200
lib/pages/encaissement_screen.dart
Normal file
200
lib/pages/encaissement_screen.dart
Normal file
@ -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]),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
285
lib/services/restaurant_api_service.dart
Normal file
285
lib/services/restaurant_api_service.dart
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,6 +195,49 @@ class AppBottomNavigation extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 20),
|
||||
|
||||
GestureDetector(
|
||||
onTap: () => onItemTapped(5),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
selectedIndex == 5
|
||||
? Colors.green.shade700
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.payment,
|
||||
color:
|
||||
selectedIndex == 5
|
||||
? Colors.white
|
||||
: Colors.grey.shade600,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Encaissement',
|
||||
style: TextStyle(
|
||||
color:
|
||||
selectedIndex == 5
|
||||
? Colors.white
|
||||
: Colors.grey.shade600,
|
||||
fontWeight:
|
||||
selectedIndex == 5
|
||||
? FontWeight.w500
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// User Profile Section
|
||||
|
||||
132
lib/widgets/command_card.dart
Normal file
132
lib/widgets/command_card.dart
Normal file
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
184
lib/widgets/command_directe_dialog.dart
Normal file
184
lib/widgets/command_directe_dialog.dart
Normal file
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -35,3 +35,9 @@ flutter:
|
||||
# Décommente ceci si tu veux ajouter des images par exemple :
|
||||
assets:
|
||||
- assets/logo_transparent.png
|
||||
|
||||
flutter_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/logo_transparent.png"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user