From be3d5264cf3ef3f3208f6988fcc2e2642e5afde9 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sun, 3 Aug 2025 15:20:49 +0300 Subject: [PATCH] page caisse --- lib/layouts/main_layout.dart | 3 + lib/main.dart | 11 +- lib/models/command_detail.dart | 325 +++++++++++++ lib/models/payment_method.dart | 41 ++ lib/models/tables_order.dart | 208 +++++++++ lib/pages/caisse_screen.dart | 552 +++++++++++++++++++++++ lib/pages/encaissement_screen.dart | 200 ++++++++ lib/services/restaurant_api_service.dart | 285 ++++++++++++ lib/widgets/bottom_navigation.dart | 43 ++ lib/widgets/command_card.dart | 132 ++++++ lib/widgets/command_directe_dialog.dart | 184 ++++++++ pubspec.yaml | 6 + 12 files changed, 1987 insertions(+), 3 deletions(-) create mode 100644 lib/models/command_detail.dart create mode 100644 lib/models/payment_method.dart create mode 100644 lib/models/tables_order.dart create mode 100644 lib/pages/caisse_screen.dart create mode 100644 lib/pages/encaissement_screen.dart create mode 100644 lib/services/restaurant_api_service.dart create mode 100644 lib/widgets/command_card.dart create mode 100644 lib/widgets/command_directe_dialog.dart diff --git a/lib/layouts/main_layout.dart b/lib/layouts/main_layout.dart index d13b67b..3deeabe 100644 --- a/lib/layouts/main_layout.dart +++ b/lib/layouts/main_layout.dart @@ -61,6 +61,9 @@ class _MainLayoutState extends State { case 4: route = '/plats'; break; + case 5: + route = '/encaissement'; + break; default: route = '/tables'; } diff --git a/lib/main.dart b/lib/main.dart index ab944ab..4aca3c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ), }, ); diff --git a/lib/models/command_detail.dart b/lib/models/command_detail.dart new file mode 100644 index 0000000..ee3825d --- /dev/null +++ b/lib/models/command_detail.dart @@ -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 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 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?) + ?.map((item) => CommandeItem.fromJson(item)) + .toList() ?? + [], + ); + } + + Map 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 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 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 json) { + return CommandeDetailResponse( + success: json['success'] ?? false, + data: CommandeDetail.fromJson(json), + message: json['message'], + ); + } +} diff --git a/lib/models/payment_method.dart b/lib/models/payment_method.dart new file mode 100644 index 0000000..d65c236 --- /dev/null +++ b/lib/models/payment_method.dart @@ -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 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), + ), +]; diff --git a/lib/models/tables_order.dart b/lib/models/tables_order.dart new file mode 100644 index 0000000..ffea5b3 --- /dev/null +++ b/lib/models/tables_order.dart @@ -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 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 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; + } + } +} diff --git a/lib/pages/caisse_screen.dart b/lib/pages/caisse_screen.dart new file mode 100644 index 0000000..9dd589e --- /dev/null +++ b/lib/pages/caisse_screen.dart @@ -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 { + CommandeDetail? commande; + PaymentMethod? selectedPaymentMethod; + bool isLoading = true; + bool isProcessingPayment = false; + + final List 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 _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 _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(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(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), + ], + ), + ), + ); + } +} diff --git a/lib/pages/encaissement_screen.dart b/lib/pages/encaissement_screen.dart new file mode 100644 index 0000000..f420817 --- /dev/null +++ b/lib/pages/encaissement_screen.dart @@ -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 { + List commandes = []; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadCommandes(); + } + + Future _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 _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]), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/restaurant_api_service.dart b/lib/services/restaurant_api_service.dart new file mode 100644 index 0000000..f8f8463 --- /dev/null +++ b/lib/services/restaurant_api_service.dart @@ -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 _headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + // Récupérer les commandes + static Future> 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 data = + responseBody is Map + ? (responseBody['data']['commandes'] ?? + responseBody['commandes'] ?? + []) + : responseBody as List; + + if (data.isEmpty) { + return []; // Retourner une liste vide si pas de données + } + + return data.map((json) { + try { + return TableOrder.fromJson(json as Map); + } 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 creerCommandeDirecte( + Map 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 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 getCommandeDetails(String commandeId) async { + try { + final response = await http.get( + Uri.parse('$baseUrl/api/commandes/$commandeId'), + headers: _headers, + ); + + if (response.statusCode == 200) { + final Map 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> getTables() async { + try { + final response = await http.get( + Uri.parse('$baseUrl/api/tables'), + headers: _headers, + ); + + if (response.statusCode == 200) { + final List 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> 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 data = + responseBody is Map + ? (responseBody['data'] ?? responseBody['commandes'] ?? []) + : responseBody as List; + + 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 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 + } + } +} diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart index 6e1d5a4..f846e6d 100644 --- a/lib/widgets/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation.dart @@ -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 diff --git a/lib/widgets/command_card.dart b/lib/widgets/command_card.dart new file mode 100644 index 0000000..51a9113 --- /dev/null +++ b/lib/widgets/command_card.dart @@ -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, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/command_directe_dialog.dart b/lib/widgets/command_directe_dialog.dart new file mode 100644 index 0000000..fa91b6d --- /dev/null +++ b/lib/widgets/command_directe_dialog.dart @@ -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 { + final _formKey = GlobalKey(); + final _tableController = TextEditingController(); + final _personnesController = TextEditingController(text: '1'); + final _totalController = TextEditingController(); + bool _isLoading = false; + + Future _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'), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index c37eae0..4225294 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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" +