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