You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1340 lines
41 KiB

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<OrderHistoryPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
List<AnimationController> _cardAnimationControllers = [];
List<CommandeData> commandes = [];
List<CommandeData> allCommandes = []; // contiendra toutes les commandes
bool isLoading = true;
String? error;
DateTime? _startDate;
DateTime? _endDate;
// Informations d'affichage et pagination
int totalItems = 0;
int currentPage = 1;
int totalPages = 1;
final int itemsPerPage = 10; // Nombre d'éléments par page
final String baseUrl = 'https://restaurant.careeracademy.mg';
final Map<String, String> _headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
);
_loadCommandes();
}
Future<void> _loadCommandes({int page = 1}) async {
try {
setState(() {
isLoading = true;
error = null;
});
// Construction des paramètres de requête
Map<String, String> queryParams = {
'statut': 'payee',
'page': page.toString(),
'limit': itemsPerPage.toString(),
};
// Gestion des filtres de date
if (_startDate != null) {
final startUtc = DateTime.utc(
_startDate!.year,
_startDate!.month,
_startDate!.day,
0, 0, 0,
);
queryParams['date_start'] = startUtc.toIso8601String();
print('📅 Date début (UTC): ${startUtc.toIso8601String()}');
}
if (_endDate != null) {
final endUtc = DateTime.utc(
_endDate!.year,
_endDate!.month,
_endDate!.day,
23, 59, 59, 999,
);
queryParams['date_end'] = endUtc.toIso8601String();
print('📅 Date fin (UTC): ${endUtc.toIso8601String()}');
}
// URL complète
final uri = Uri.parse('$baseUrl/api/commandes').replace(queryParameters: queryParams);
print('🌐 URL appelée: $uri');
final response = await http.get(uri, headers: _headers);
print('📡 Status code: ${response.statusCode}');
if (response.statusCode == 200) {
final dynamic responseBody = json.decode(response.body);
List<dynamic> data = [];
// Gestion du format de réponse
if (responseBody is Map<String, dynamic>) {
final dataMap = responseBody['data'] as Map<String, dynamic>?;
if (dataMap != null) {
final commandesValue = dataMap['commandes'];
if (commandesValue is List) {
data = commandesValue;
} else if (commandesValue != null) {
data = [commandesValue];
}
final pagination = dataMap['pagination'] as Map<String, dynamic>?;
currentPage = pagination?['currentPage'] ?? page;
totalPages = pagination?['totalPages'] ?? (data.length / itemsPerPage).ceil();
totalItems = pagination?['totalItems'] ?? data.length;
}
} else if (responseBody is List) {
data = responseBody;
totalItems = data.length;
currentPage = page;
totalPages = (totalItems / itemsPerPage).ceil();
} else {
throw Exception('Format de réponse inattendu: ${responseBody.runtimeType}');
}
// Parsing sécurisé des commandes
List<CommandeData> parsedCommandes = [];
for (int i = 0; i < data.length; i++) {
try {
final item = data[i];
if (item is Map<String, dynamic>) {
parsedCommandes.add(CommandeData.fromJson(item));
} else {
print('Item $i invalide: ${item.runtimeType}');
}
} catch (e, stackTrace) {
print('Erreur parsing item $i: $e');
print(stackTrace);
}
}
print('${parsedCommandes.length} commandes chargées');
// ⚡ Mise à jour du state avec toutes les commandes
setState(() {
allCommandes = parsedCommandes; // toutes les commandes
commandes = parsedCommandes; // commandes affichées (filtrage frontend possible)
isLoading = false;
});
_initializeAnimations();
_startAnimations();
} else {
setState(() {
error = 'Erreur HTTP ${response.statusCode}: ${response.reasonPhrase}';
isLoading = false;
});
print('Erreur HTTP ${response.statusCode}: ${response.reasonPhrase}');
}
} catch (e, stackTrace) {
print('=== ERREUR GÉNÉRALE ===');
print('Erreur: $e');
print('Stack trace: $stackTrace');
setState(() {
error = 'Erreur de connexion: $e';
isLoading = false;
});
}
}
void _filterByDate() {
if (_startDate == null && _endDate == null) {
setState(() {
commandes = allCommandes;
currentPage = 1;
totalItems = commandes.length;
totalPages = (totalItems / itemsPerPage).ceil();
});
return;
}
setState(() {
commandes = allCommandes.where((c) {
final date = c.datePaiement ?? c.dateCommande;
if (date == null) return false;
final start = _startDate ?? DateTime(2000);
// Fin de journée pour inclure la date complète
final end = _endDate?.add(Duration(days: 1)).subtract(Duration(milliseconds: 1)) ?? DateTime(2100);
return !date.isBefore(start) && !date.isAfter(end);
}).toList();
currentPage = 1;
totalItems = commandes.length;
totalPages = (totalItems / itemsPerPage).ceil();
});
}
Future<void> _selectDate(BuildContext context, bool isStart) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: isStart ? (_startDate ?? DateTime.now()) : (_endDate ?? DateTime.now()),
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
if (isStart) {
_startDate = picked;
} else {
_endDate = picked;
}
});
}
}
// Fonction pour aller à la page suivante
void _goToNextPage() {
if (currentPage < totalPages) {
_loadCommandes(page: currentPage + 1);
}
}
// Fonction pour aller à la page précédente
void _goToPreviousPage() {
if (currentPage > 1) {
_loadCommandes(page: currentPage - 1);
}
}
// Fonction pour aller à une page spécifique
void _goToPage(int page) {
if (page >= 1 && page <= totalPages && page != currentPage) {
_loadCommandes(page: page);
}
}
void _initializeAnimations() {
// Disposer les anciens contrôleurs
for (var controller in _cardAnimationControllers) {
controller.dispose();
}
_cardAnimationControllers = List.generate(
commandes.length,
(index) => AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
),
);
}
void _startAnimations() async {
if (!mounted) return;
_animationController.forward();
for (int i = 0; i < _cardAnimationControllers.length; i++) {
await Future.delayed(Duration(milliseconds: 150));
if (mounted && i < _cardAnimationControllers.length) {
_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(page: currentPage),
child: Column(
children: [
_buildHeader(),
Expanded(
child: _buildContent(),
),
if (totalPages > 1) _buildPagination(),
],
),
),
);
}
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,
),
SizedBox(height: 6),
// Barre des filtres
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => _selectDate(context, true),
child: Text(_startDate == null
? 'Date début'
: 'Début: ${_startDate!.day}/${_startDate!.month}/${_startDate!.year}'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF4CAF50),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
textStyle: TextStyle(fontSize: 10),
),
),
SizedBox(width: 5),
ElevatedButton(
onPressed: () => _selectDate(context, false),
child: Text(_endDate == null
? 'Date fin'
: 'Fin: ${_endDate!.day}/${_endDate!.month}/${_endDate!.year}'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF4CAF50),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
textStyle: TextStyle(fontSize: 10),
),
),
SizedBox(width: 5),
ElevatedButton(
onPressed: () => _filterByDate(),
child: Text('Filtrer'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
textStyle: TextStyle(fontSize: 10),
),
),
SizedBox(width: 5),
ElevatedButton(
onPressed: () {
setState(() {
_startDate = null;
_endDate = null;
});
_loadCommandes(page: 1);
},
child: Text('Réinitialiser'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
textStyle: TextStyle(fontSize: 10),
),
),
SizedBox(width: 5),
ElevatedButton(
onPressed: () {
final today = DateTime.now();
setState(() {
_startDate = DateTime(today.year, today.month, today.day);
_endDate = DateTime(today.year, today.month, today.day, 23, 59, 59);
_filterByDate(); // filtre local sur toutes les commandes déjà chargées
});
},
child: Text('Aujourd’hui'),
),
],
),
SizedBox(height: 4),
if (totalItems > 0)
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
totalPages > 1
? '$totalItems commande${totalItems > 1 ? 's' : ''} • Page $currentPage/$totalPages'
: '$totalItems commande${totalItems > 1 ? 's' : ''} trouvée${totalItems > 1 ? 's' : ''}',
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
),
),
),
);
},
);
}
Widget _buildPagination() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: Offset(0, -2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Bouton Précédent
ElevatedButton.icon(
onPressed: currentPage > 1 ? _goToPreviousPage : null,
icon: Icon(Icons.chevron_left, size: 18),
label: Text('Précédent'),
style: ElevatedButton.styleFrom(
backgroundColor: currentPage > 1 ? Color(0xFF4CAF50) : Colors.grey.shade300,
foregroundColor: currentPage > 1 ? Colors.white : Colors.grey.shade600,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: currentPage > 1 ? 2 : 0,
),
),
// Indicateur de page actuelle avec navigation rapide
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (totalPages <= 7)
// Afficher toutes les pages si <= 7 pages
...List.generate(totalPages, (index) {
final pageNum = index + 1;
return _buildPageButton(pageNum);
})
else
// Afficher une navigation condensée si > 7 pages
..._buildCondensedPagination(),
],
),
),
// Bouton Suivant
ElevatedButton.icon(
onPressed: currentPage < totalPages ? _goToNextPage : null,
icon: Icon(Icons.chevron_right, size: 18),
label: Text('Suivant'),
style: ElevatedButton.styleFrom(
backgroundColor: currentPage < totalPages ? Color(0xFF4CAF50) : Colors.grey.shade300,
foregroundColor: currentPage < totalPages ? Colors.white : Colors.grey.shade600,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: currentPage < totalPages ? 2 : 0,
),
),
],
),
);
}
Widget _buildPageButton(int pageNum) {
final isCurrentPage = pageNum == currentPage;
return GestureDetector(
onTap: () => _goToPage(pageNum),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 2),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: isCurrentPage ? Color(0xFF4CAF50) : Colors.transparent,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: isCurrentPage ? Color(0xFF4CAF50) : Colors.grey.shade300,
width: 1,
),
),
child: Text(
pageNum.toString(),
style: TextStyle(
color: isCurrentPage ? Colors.white : Colors.grey.shade700,
fontWeight: isCurrentPage ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
),
),
);
}
List<Widget> _buildCondensedPagination() {
List<Widget> pages = [];
// Toujours afficher la première page
pages.add(_buildPageButton(1));
if (currentPage > 4) {
pages.add(Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...', style: TextStyle(color: Colors.grey)),
));
}
// Afficher les pages autour de la page actuelle
int start = (currentPage - 2).clamp(2, totalPages - 1);
int end = (currentPage + 2).clamp(2, totalPages - 1);
for (int i = start; i <= end; i++) {
if (i != 1 && i != totalPages) {
pages.add(_buildPageButton(i));
}
}
if (currentPage < totalPages - 3) {
pages.add(Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...', style: TextStyle(color: Colors.grey)),
));
}
// Toujours afficher la dernière page si > 1
if (totalPages > 1) {
pages.add(_buildPageButton(totalPages));
}
return pages;
}
Widget _buildContent() {
if (isLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFF4CAF50)),
);
}
if (error != null) {
return Center(
child: Text(error!, style: TextStyle(color: Colors.red)),
);
}
if (commandes.isEmpty) {
return const Center(
child: Text("Aucune commande trouvée"),
);
}
// ✅ Afficher les données sous forme de tableau
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width * 0.9), // 1.5x la largeur écran
child: SingleChildScrollView(
child: _buildTableView(),
),
),
);
}
Widget _buildTableView() {
return DataTable(
columnSpacing: 20,
headingRowColor: MaterialStateProperty.all(const Color(0xFF4CAF50)),
headingTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
dataRowHeight: 48,
columns: const [
DataColumn(label: Text("")),
DataColumn(label: Text("Table")),
DataColumn(label: Text("Total")),
DataColumn(label: Text("Détails")), // Nouvelle colonne pour le bouton
],
rows: commandes.map((commande) {
return DataRow(
cells: [
DataCell(Text(commande.numeroCommande ?? '-')),
DataCell(Text(commande.tablename ?? '-')),
DataCell(Text(_formatPrice(commande.totalTtc ?? 0))),
DataCell(
IconButton(
icon: Icon(Icons.info, color: Color(0xFF4CAF50)),
tooltip: 'Voir les détails',
onPressed: () {
_showCommandeDetails(commande);
},
),
),
],
);
}).toList(),
);
}
// Exemple de fonction pour afficher les détails dans un dialog
void _showCommandeDetails(CommandeData commande) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Détails commande ${commande.numeroCommande ?? ""}'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Table: ${commande.tablename ?? "-"}'),
Text(
'Date de paiement: ${commande.datePaiement != null ? _formatDateTime(commande.datePaiement!) : "-"}',
),
Text('Total TTC: ${_formatPrice(commande.totalTtc ?? 0)}'),
SizedBox(height: 10),
const Text('Articles:', style: TextStyle(fontWeight: FontWeight.bold)),
...?commande.items?.map((item) => Text(
'${item.quantite} × ${item.menuNom} - ${_formatPrice(item.totalItem)}')),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
);
},
);
}
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 ?? 'Table inconnue',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF2c3e50),
),
),
SizedBox(height: 2),
Text(
commande.numeroCommande ?? 'N/A',
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(
commande.dateCommande != null
? _formatDateTime(commande.dateCommande!)
: 'Date inconnue',
style: TextStyle(color: Colors.grey, fontSize: 10),
),
SizedBox(width: 8),
Icon(Icons.person, size: 12, color: Colors.grey),
SizedBox(width: 3),
Text(
commande.serveur ?? 'Serveur inconnu',
style: TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
if (commande.datePaiement != null)
Row(
children: [
Icon(Icons.payment, size: 12, color: Colors.green),
SizedBox(width: 3),
Text(
'Payée: ${_formatDateTime(commande.datePaiement!)}',
style: TextStyle(color: Colors.green, fontSize: 10),
),
],
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF388E3C)],
),
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} × ${_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(0xFF388E3C)],
),
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) > 0)
Text(
'TVA: ${_formatPrice(commande.totalTva ?? 0)}',
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 ?? 0),
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) {
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 avec gestion des valeurs nulles et debug amélioré
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? datePaiement;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<CommandeItem>? items;
final String? tablename;
CommandeData({
this.id,
this.clientId,
this.tableId,
this.reservationId,
this.numeroCommande,
this.statut,
this.totalHt,
this.totalTva,
this.totalTtc,
this.modePaiement,
this.commentaires,
this.serveur,
this.dateCommande,
this.datePaiement,
this.createdAt,
this.updatedAt,
this.items,
this.tablename,
});
factory CommandeData.fromJson(Map<String, dynamic> json) {
try {
// Parsing avec debug détaillé
final id = json['id'];
final numeroCommande = json['numero_commande']?.toString();
final tablename = json['tablename']?.toString() ?? json['table_name']?.toString() ?? 'Table inconnue';
final serveur = json['serveur']?.toString() ?? json['server']?.toString() ?? 'Serveur inconnu';
final dateCommande = _parseDateTime(json['date_commande']) ?? _parseDateTime(json['created_at']);
final datePaiement = _parseDateTime(json['date_paiement']) ?? _parseDateTime(json['date_service']);
final totalTtc = _parseDouble(json['total_ttc']) ?? _parseDouble(json['total']);
final modePaiement = json['mode_paiement']?.toString() ?? json['payment_method']?.toString();
final items = _parseItems(json['items']);
final result = CommandeData(
id: id,
clientId: json['client_id'],
tableId: json['table_id'],
reservationId: json['reservation_id'],
numeroCommande: numeroCommande,
statut: json['statut']?.toString(),
totalHt: _parseDouble(json['total_ht']),
totalTva: _parseDouble(json['total_tva']),
totalTtc: totalTtc,
modePaiement: modePaiement,
commentaires: json['commentaires']?.toString(),
serveur: serveur,
dateCommande: dateCommande,
datePaiement: datePaiement,
createdAt: _parseDateTime(json['created_at']),
updatedAt: _parseDateTime(json['updated_at']),
items: items,
tablename: tablename,
);
return result;
} catch (e, stackTrace) {
print('=== ERREUR PARSING COMMANDE ===');
print('Erreur: $e');
print('JSON: $json');
print('Stack trace: $stackTrace');
rethrow;
}
}
static double? _parseDouble(dynamic value) {
if (value == null) return null;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) {
final result = double.tryParse(value);
return result;
}
return null;
}
static DateTime? _parseDateTime(dynamic value) {
if (value == null) return null;
if (value is String) {
try {
return DateTime.parse(value).toLocal(); // converti en heure locale
} catch (e) {
print('Erreur parsing date: $value - $e');
return null;
}
}
return null;
}
static List<CommandeItem>? _parseItems(dynamic value) {
if (value == null) {
print('Items null');
return null;
}
if (value is! List) {
print('Items n\'est pas une liste: ${value.runtimeType}');
return null;
}
try {
List<CommandeItem> result = [];
for (int i = 0; i < value.length; i++) {
final item = value[i];
if (item is Map<String, dynamic>) {
final commandeItem = CommandeItem.fromJson(item);
result.add(commandeItem);
} else {
}
}
return result;
} catch (e) {
print('Erreur parsing items: $e');
return null;
}
}
String getTableShortName() {
final name = tablename ?? 'Table';
if (name.toLowerCase().contains('caisse')) return 'C';
if (name.toLowerCase().contains('terrasse')) return 'T';
RegExp regExp = RegExp(r'\d+');
Match? match = regExp.firstMatch(name);
if (match != null) {
return 'T${match.group(0)}';
}
return name.isNotEmpty ? name.substring(0, 1).toUpperCase() : 'T';
}
}
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,
this.createdAt,
this.updatedAt,
required this.menuNom,
required this.menuDescription,
required this.menuPrixActuel,
required this.tablename,
});
factory CommandeItem.fromJson(Map<String, dynamic> json) {
try {
// Debug chaque champ
final id = json['id'] ?? 0;
final commandeId = json['commande_id'] ?? 0;
final menuId = json['menu_id'] ?? 0;
final quantite = json['quantite'] ?? json['quantity'] ?? 0;
final prixUnitaire = CommandeData._parseDouble(json['prix_unitaire']) ??
CommandeData._parseDouble(json['unit_price']) ?? 0.0;
final totalItem = CommandeData._parseDouble(json['total_item']) ??
CommandeData._parseDouble(json['total']) ?? 0.0;
final commentaires = json['commentaires']?.toString() ?? json['comments']?.toString();
final statut = json['statut']?.toString() ?? json['status']?.toString() ?? '';
final menuNom = json['menu_nom']?.toString() ??
json['menu_name']?.toString() ??
json['name']?.toString() ?? 'Menu inconnu';
final menuDescription = json['menu_description']?.toString() ??
json['description']?.toString() ?? '';
final menuPrixActuel = CommandeData._parseDouble(json['menu_prix_actuel']) ??
CommandeData._parseDouble(json['current_price']) ?? 0.0;
final tablename = json['tablename']?.toString() ??
json['table_name']?.toString() ?? '';
final result = CommandeItem(
id: id,
commandeId: commandeId,
menuId: menuId,
quantite: quantite,
prixUnitaire: prixUnitaire,
totalItem: totalItem,
commentaires: commentaires,
statut: statut,
createdAt: CommandeData._parseDateTime(json['created_at']),
updatedAt: CommandeData._parseDateTime(json['updated_at']),
menuNom: menuNom,
menuDescription: menuDescription,
menuPrixActuel: menuPrixActuel,
tablename: tablename,
);
return result;
} catch (e, stackTrace) {
print('=== ERREUR PARSING ITEM ===');
print('Erreur: $e');
print('JSON: $json');
print('Stack trace: $stackTrace');
rethrow;
}
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: OrderHistoryPage(),
debugShowCheckedModeBanner: false,
);
}
}
void main() {
runApp(MyApp());
}