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 ( " N° " ) ) ,
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 ( ) ) ;
}