diff --git a/lib/layouts/main_layout.dart b/lib/layouts/main_layout.dart index a888281..646ad26 100644 --- a/lib/layouts/main_layout.dart +++ b/lib/layouts/main_layout.dart @@ -36,6 +36,8 @@ class _MainLayoutState extends State { return 4; case '/encaissement': return 5; + case '/historique': + return 6; default: return 0; } @@ -66,6 +68,9 @@ class _MainLayoutState extends State { case 5: route = '/encaissement'; break; + case 6: + route = '/historique'; + break; default: route = '/tables'; } diff --git a/lib/main.dart b/lib/main.dart index 4aca3c4..26c60de 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/historique_commande.dart'; import 'pages/encaissement_screen.dart'; // NOUVEAU void main() { @@ -52,6 +53,11 @@ class MyApp extends StatelessWidget { currentRoute: '/encaissement', child: EncaissementScreen(), ), + '/historique': + (context) => MainLayout( + currentRoute: '/historique', + child: OrderHistoryPage(), + ), }, ); } diff --git a/lib/pages/historique_commande.dart b/lib/pages/historique_commande.dart new file mode 100644 index 0000000..7b553b7 --- /dev/null +++ b/lib/pages/historique_commande.dart @@ -0,0 +1,802 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class OrderHistoryPage extends StatefulWidget { + @override + _OrderHistoryPageState createState() => _OrderHistoryPageState(); +} + +class _OrderHistoryPageState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + List _cardAnimationControllers = []; + List commandes = []; + bool isLoading = true; + String? error; + + // Informations de pagination + int currentPage = 1; + int totalPages = 1; + int totalItems = 0; + int itemsPerPage = 10; + + final String baseUrl = 'https://restaurant.careeracademy.mg'; // Remplacez par votre URL + final Map _headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: Duration(milliseconds: 1000), + vsync: this, + ); + + _loadCommandes(); + } + + Future _loadCommandes() async { + try { + setState(() { + isLoading = true; + error = null; + }); + + final response = await http.get( + Uri.parse('$baseUrl/api/commandes?statut=payee'), + headers: _headers, + ); + + final dynamic responseBody = json.decode(response.body); + print('Réponse getCommandes: ${responseBody}'); + + if (response.statusCode == 200) { + // Adapter la structure de réponse {data: [...], pagination: {...}} + final Map responseData = json.decode(response.body); + final List data = responseData['data'] ?? []; + final Map pagination = responseData['pagination'] ?? {}; + + setState(() { + commandes = data.map((json) => CommandeData.fromJson(json)).toList(); + + // Mettre à jour les informations de pagination + currentPage = pagination['currentPage'] ?? 1; + totalPages = pagination['totalPages'] ?? 1; + totalItems = pagination['totalItems'] ?? 0; + itemsPerPage = pagination['itemsPerPage'] ?? 10; + + isLoading = false; + }); + + _initializeAnimations(); + _startAnimations(); + } else { + setState(() { + error = 'Erreur lors du chargement des commandes'; + isLoading = false; + }); + } + } catch (e) { + setState(() { + error = 'Erreur de connexion: $e'; + isLoading = false; + }); + } + } + + void _initializeAnimations() { + _cardAnimationControllers = List.generate( + commandes.length, + (index) => AnimationController( + duration: Duration(milliseconds: 600), + vsync: this, + ), + ); + } + + void _startAnimations() async { + _animationController.forward(); + + for (int i = 0; i < _cardAnimationControllers.length; i++) { + await Future.delayed(Duration(milliseconds: 150)); + if (mounted) { + _cardAnimationControllers[i].forward(); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + for (var controller in _cardAnimationControllers) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: Text('Historique des commandes'), + backgroundColor: Colors.white, + foregroundColor: Colors.black, + elevation: 0, + ), + body: RefreshIndicator( + onRefresh: _loadCommandes, + child: Column( + children: [ + _buildHeader(), + Expanded( + child: _buildContent(), + ), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, -50 * (1 - _animationController.value)), + child: Opacity( + opacity: _animationController.value, + child: Container( + width: double.infinity, + margin: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.08), + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Text( + 'Historique des commandes payées', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF2c3e50), + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 2), + Text( + 'Consultez toutes les commandes qui ont été payées', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + if (totalItems > 0) + Padding( + padding: EdgeInsets.only(top: 4), + child: Text( + '$totalItems commande${totalItems > 1 ? 's' : ''} • Page $currentPage/$totalPages', + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade500, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _buildContent() { + if (isLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text(error!, style: TextStyle(color: Colors.grey)), + SizedBox(height: 16), + ElevatedButton( + onPressed: _loadCommandes, + child: Text('Réessayer'), + ), + ], + ), + ); + } + + if (commandes.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.payment, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text( + 'Aucune commande payée', + style: TextStyle(color: Colors.grey, fontSize: 16), + ), + ], + ), + ); + } + + return ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 20), + itemCount: commandes.length, + itemBuilder: (context, index) { + return _buildOrderCard(commandes[index], index); + }, + ); + } + + Widget _buildOrderCard(CommandeData commande, int index) { + if (index >= _cardAnimationControllers.length) { + return SizedBox.shrink(); + } + + return AnimatedBuilder( + animation: _cardAnimationControllers[index], + builder: (context, child) { + return Transform.translate( + offset: Offset(0, 50 * (1 - _cardAnimationControllers[index].value)), + child: Opacity( + opacity: _cardAnimationControllers[index].value, + child: Container( + margin: EdgeInsets.only(bottom: 12), + child: Material( + elevation: 8, + borderRadius: BorderRadius.circular(15), + child: InkWell( + borderRadius: BorderRadius.circular(15), + onTap: () { + // Action au tap + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.white, + border: Border( + left: BorderSide( + color: Color(0xFF4CAF50), + width: 3, + ), + ), + ), + child: Column( + children: [ + _buildOrderHeader(commande), + _buildOrderItems(commande), + _buildOrderFooter(commande), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildOrderHeader(CommandeData commande) { + return Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade200, + width: 1, + ), + ), + ), + child: Row( + children: [ + Container( + width: 35, + height: 35, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF667eea), Color(0xFF764ba2)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + commande.getTableShortName(), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + commande.tablename, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF2c3e50), + ), + ), + SizedBox(height: 2), + Text( + commande.numeroCommande, + style: TextStyle( + fontSize: 10, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 2), + Row( + children: [ + Icon(Icons.calendar_today, size: 12, color: Colors.grey), + SizedBox(width: 3), + Text( + _formatDateTime(commande.dateCommande), + style: TextStyle(color: Colors.grey, fontSize: 10), + ), + SizedBox(width: 8), + Icon(Icons.person, size: 12, color: Colors.grey), + SizedBox(width: 3), + Text( + commande.serveur, + style: TextStyle(color: Colors.grey, fontSize: 10), + ), + ], + ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF4CAF50), Color(0xFF45a049)], + ), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.check_circle, + color: Colors.white, + size: 10, + ), + SizedBox(width: 3), + Text( + 'PAYÉE', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 8, + letterSpacing: 0.3, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildOrderItems(CommandeData commande) { + return Container( + padding: EdgeInsets.all(10), + child: Column( + children: commande.items.map((item) => _buildOrderItem(item)).toList(), + ), + ); + } + + Widget _buildOrderItem(CommandeItem item) { + return Container( + padding: EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade100, + width: 1, + ), + ), + ), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFFffeaa7), Color(0xFFfdcb6e)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + _getMenuIcon(item.menuNom), + style: TextStyle(fontSize: 14), + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.menuNom, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFF2c3e50), + ), + ), + if (item.commentaires != null && item.commentaires!.isNotEmpty) + Text( + item.commentaires!, + style: TextStyle( + fontSize: 9, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + Text( + '${item.quantite}x × ${_formatPrice(item.prixUnitaire)}', + style: TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ), + ], + ), + ), + Text( + _formatPrice(item.totalItem), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Color(0xFF4CAF50), + ), + ), + ], + ), + ); + } + + Widget _buildOrderFooter(CommandeData commande) { + return Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(15), + bottomRight: Radius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF4CAF50), Color(0xFF45a049)], + ), + borderRadius: BorderRadius.circular(6), + ), + child: Center( + child: Icon( + _getPaymentIcon(commande.modePaiement), + color: Colors.white, + size: 14, + ), + ), + ), + SizedBox(width: 6), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _getPaymentMethodText(commande.modePaiement), + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade600, + ), + ), + if (commande.totalTva > 0) + Text( + 'TVA: ${_formatPrice(commande.totalTva)}', + style: TextStyle( + fontSize: 9, + color: Colors.grey.shade500, + ), + ), + ], + ), + ], + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Color(0xFF4CAF50).withOpacity(0.1), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.attach_money, + color: Color(0xFF4CAF50), + size: 14, + ), + Text( + _formatPrice(commande.totalTtc), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF4CAF50), + ), + ), + ], + ), + ), + ], + ), + ); + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year} à ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } + + String _formatPrice(double priceInCents) { + // Les prix sont déjà en centimes dans votre API (ex: 20000.00 = 200.00 €) + return '${(priceInCents / 100).toStringAsFixed(2)} Ar'; + } + + String _getMenuIcon(String menuNom) { + String lowerName = menuNom.toLowerCase(); + if (lowerName.contains('pizza')) return '🍕'; + if (lowerName.contains('steak') || lowerName.contains('steack')) return '🥩'; + if (lowerName.contains('frite')) return '🍟'; + if (lowerName.contains('salade')) return '🥗'; + if (lowerName.contains('soupe')) return '🍲'; + if (lowerName.contains('burger')) return '🍔'; + if (lowerName.contains('poisson')) return '🐟'; + if (lowerName.contains('poulet')) return '🍗'; + if (lowerName.contains('pâtes') || lowerName.contains('pasta')) return '🍝'; + if (lowerName.contains('dessert') || lowerName.contains('gâteau')) return '🍰'; + if (lowerName.contains('boisson')) return '🥤'; + return '🍽️'; + } + + IconData _getPaymentIcon(String? method) { + if (method == null) return Icons.help_outline; + switch (method.toLowerCase()) { + case 'especes': + case 'cash': + return Icons.money; + case 'carte': + case 'card': + return Icons.credit_card; + case 'mobile': + case 'paypal': + return Icons.smartphone; + default: + return Icons.payment; + } + } + + String _getPaymentMethodText(String? method) { + if (method == null) return 'Non défini'; + switch (method.toLowerCase()) { + case 'especes': + case 'cash': + return 'Espèces'; + case 'carte': + case 'card': + return 'Carte'; + case 'mobile': + case 'paypal': + return 'Mobile'; + default: + return method; + } + } +} + +// Modèles de données adaptés à votre API +class CommandeData { + 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; + final String tablename; + + CommandeData({ + required this.id, + required this.clientId, + required this.tableId, + required 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, + required this.tablename, + }); + + factory CommandeData.fromJson(Map json) { + return CommandeData( + id: json['id'], + clientId: json['client_id'], + tableId: json['table_id'], + reservationId: json['reservation_id'], + numeroCommande: json['numero_commande'], + statut: json['statut'], + totalHt: (json['total_ht'] ?? 0).toDouble(), + totalTva: (json['total_tva'] ?? 0).toDouble(), + totalTtc: (json['total_ttc'] ?? 0).toDouble(), + modePaiement: json['mode_paiement'], + commentaires: json['commentaires'], + serveur: json['serveur'], + dateCommande: DateTime.parse(json['date_commande']), + dateService: json['date_service'] != null + ? DateTime.parse(json['date_service']) + : null, + createdAt: DateTime.parse(json['created_at']), + updatedAt: DateTime.parse(json['updated_at']), + items: (json['items'] as List) + .map((item) => CommandeItem.fromJson(item)) + .toList(), + tablename: json['tablename'] ?? 'Table inconnue', + ); + } + + String getTableShortName() { + if (tablename.toLowerCase().contains('caisse')) return 'C'; + if (tablename.toLowerCase().contains('terrasse')) return 'T'; + + // Extraire le numéro de la table + RegExp regExp = RegExp(r'\d+'); + Match? match = regExp.firstMatch(tablename); + if (match != null) { + return 'T${match.group(0)}'; + } + + return tablename.substring(0, 1).toUpperCase(); + } +} + +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; + final String tablename; + + 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, + required this.menuDescription, + required this.menuPrixActuel, + required this.tablename, + }); + + factory CommandeItem.fromJson(Map json) { + return CommandeItem( + id: json['id'], + commandeId: json['commande_id'], + menuId: json['menu_id'], + quantite: json['quantite'], + prixUnitaire: (json['prix_unitaire'] ?? 0).toDouble(), + totalItem: (json['total_item'] ?? 0).toDouble(), + commentaires: json['commentaires'], + statut: json['statut'], + createdAt: DateTime.parse(json['created_at']), + updatedAt: DateTime.parse(json['updated_at']), + menuNom: json['menu_nom'], + menuDescription: json['menu_description'], + menuPrixActuel: (json['menu_prix_actuel'] ?? 0).toDouble(), + tablename: json['tablename'] ?? '', + ); + } +} + +// Usage dans votre app principale +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Historique des Commandes', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: OrderHistoryPage(), + debugShowCheckedModeBanner: false, + ); + } +} + +void main() { + runApp(MyApp()); +} \ No newline at end of file diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart index f846e6d..42d1adf 100644 --- a/lib/widgets/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation.dart @@ -237,6 +237,49 @@ class AppBottomNavigation extends StatelessWidget { ), ), ), + + const SizedBox(width: 20), + + GestureDetector( + onTap: () => onItemTapped(6), + 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( + 'Historique', + style: TextStyle( + color: + selectedIndex == 5 + ? Colors.white + : Colors.grey.shade600, + fontWeight: + selectedIndex == 5 + ? FontWeight.w500 + : FontWeight.normal, + ), + ), + ], + ), + ), + ), const Spacer(),