Browse Source

commit

master
andrymodeste 4 months ago
parent
commit
31c3d72a71
  1. 73
      lib/pages/caisse_screen.dart
  2. 113
      lib/pages/cart_page.dart
  3. 3
      lib/pages/commande_item_screen.dart
  4. 13
      lib/pages/commande_item_validation.dart
  5. 37
      lib/pages/commandes_screen.dart
  6. 11
      lib/pages/facture_screen.dart
  7. 756
      lib/pages/historique_commande.dart
  8. 586
      lib/services/pdf_service.dart
  9. 26
      lib/services/restaurant_api_service.dart
  10. 82
      lib/widgets/bottom_navigation.dart
  11. 1
      pubspec.yaml
  12. BIN
      windows/runner/resources/app_icon.ico

73
lib/pages/caisse_screen.dart

@ -4,6 +4,7 @@ import 'package:itrimobe/pages/facture_screen.dart';
import '../models/command_detail.dart';
import '../models/payment_method.dart';
import '../services/restaurant_api_service.dart';
import 'package:intl/intl.dart';
class CaisseScreen extends StatefulWidget {
final String commandeId;
@ -82,40 +83,50 @@ class _CaisseScreenState extends State<CaisseScreen> {
}
// Dans caisse_screen.dart, modifiez la méthode _processPayment
Future<void> _processPayment() async {
if (selectedPaymentMethod == null || commande == null) return;
Future<void> _processPayment() async {
if (selectedPaymentMethod == null || commande == null) return;
setState(() => isProcessingPayment = true);
setState(() => isProcessingPayment = true);
try {
final success = await RestaurantApiService.processPayment(
try {
final success = await RestaurantApiService.processPayment(
commandeId: widget.commandeId,
paymentMethodId: selectedPaymentMethod!.id,
amount: commande!.totalTtc,
);
if (success) {
final updateSuccess = await RestaurantApiService.updateCommandeStatus(
commandeId: widget.commandeId,
paymentMethodId: selectedPaymentMethod!.id,
amount: commande!.totalTtc,
newStatus: 'payee',
);
if (success) {
// Navigation vers la facture au lieu du dialog de succès
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder:
(context) => FactureScreen(
commande: commande!,
paymentMethod: selectedPaymentMethod!.id,
),
),
);
} else {
_showErrorDialog('Le paiement a échoué. Veuillez réessayer.');
}
} catch (e) {
_showErrorDialog('Erreur lors du traitement du paiement: $e');
} finally {
if (mounted) {
setState(() => isProcessingPayment = false);
if (!updateSuccess) {
_showErrorDialog("Paiement effectué, mais échec lors de la mise à jour du statut.");
return;
}
// 🔄 Redirige vers la facture
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => FactureScreen(
commande: commande!,
paymentMethod: selectedPaymentMethod!.id,
),
),
);
} else {
_showErrorDialog('Le paiement a échoué. Veuillez réessayer.');
}
} catch (e) {
_showErrorDialog('Erreur lors du traitement du paiement: $e');
} finally {
if (mounted) {
setState(() => isProcessingPayment = false);
}
}
}
void _showErrorDialog(String message) {
showDialog(
@ -148,7 +159,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
],
),
content: Text(
'Le paiement de ${commande!.totalTtc.toStringAsFixed(2)} MGA a été traité avec succès via ${selectedPaymentMethod!.name}.',
'Le paiement de ${NumberFormat("#,##0.00", "fr_FR").format(commande!.totalTtc)} MGA a été traité avec succès via ${selectedPaymentMethod!.name}.',
),
actions: [
TextButton(
@ -233,7 +244,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
),
),
Text(
'${commande!.totalTtc.toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(commande!.totalTtc)} MGA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -302,7 +313,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
),
const SizedBox(width: 12),
Text(
'${item.totalItem.toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(item.totalItem)} MGA',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@ -405,7 +416,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
const SizedBox(width: 16),
Text(
'${amount.toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(amount)} MGA',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
@ -467,7 +478,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
const SizedBox(width: 8),
Text(
selectedPaymentMethod != null
? 'Payer ${commande?.totalTtc.toStringAsFixed(2)} MGA'
? 'Payer ${NumberFormat("#,##0.00", "fr_FR").format(commande?.totalTtc)} MGA'
: 'Sélectionnez une méthode de paiement',
style: const TextStyle(
fontSize: 16,

113
lib/pages/cart_page.dart

@ -11,6 +11,7 @@ import 'package:itrimobe/pages/tables.dart';
import 'package:itrimobe/services/pdf_service.dart';
import '../layouts/main_layout.dart';
import 'package:intl/intl.dart';
class CartPage extends StatefulWidget {
final int tableId;
@ -122,7 +123,7 @@ class _CartPageState extends State<CartPage> {
Text('• Table: ${widget.tableId}'),
Text('• Personnes: ${widget.personne}'),
Text('• Articles: ${_getTotalArticles()}'),
Text('• Total: ${_calculateTotal().toStringAsFixed(2)} MGA'),
Text('• Total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA'),
],
),
actions: [
@ -340,7 +341,7 @@ class _CartPageState extends State<CartPage> {
Text('Articles: ${_cartItems.length}'),
const SizedBox(height: 8),
Text(
'Total: ${total.toStringAsFixed(0)} MGA',
'Total: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -612,7 +613,7 @@ class _CartPageState extends State<CartPage> {
Text('Table ${widget.tablename} libérée'),
const SizedBox(height: 8),
Text(
'Montant: ${total.toStringAsFixed(0)} MGA',
'Montant: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
@ -887,7 +888,7 @@ class _CartPageState extends State<CartPage> {
],
),
Text(
'${item.prix.toStringAsFixed(2)} MGA l\'unité',
'${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
@ -949,7 +950,7 @@ class _CartPageState extends State<CartPage> {
),
// Prix total de l'article
Text(
'${(item.prix * item.quantity).toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -1038,7 +1039,7 @@ class _CartPageState extends State<CartPage> {
),
),
Text(
'${_calculateTotal().toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -1047,6 +1048,8 @@ class _CartPageState extends State<CartPage> {
],
),
const SizedBox(height: 20),
// Bouton Valider la commande (toujours visible)
SizedBox(
width: double.infinity,
child: ElevatedButton(
@ -1101,62 +1104,64 @@ class _CartPageState extends State<CartPage> {
),
),
const SizedBox(height: 12),
// Espacement conditionnel
if (MediaQuery.of(context).size.width >= 768) const SizedBox(height: 12),
// Bouton Payer directement
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_cartItems.isNotEmpty && !_isValidating
? _payerDirectement
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
// Bouton Payer directement (uniquement sur desktop - largeur >= 768px)
if (MediaQuery.of(context).size.width >= 768)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_cartItems.isNotEmpty && !_isValidating
? _payerDirectement
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: Colors.grey[300],
),
disabledBackgroundColor: Colors.grey[300],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isValidating) ...[
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isValidating) ...[
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
),
const SizedBox(width: 8),
const Text(
'Traitement...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
const SizedBox(width: 8),
const Text(
'Traitement...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
] else ...[
const Icon(Icons.payment, size: 20),
const SizedBox(width: 8),
const Text(
'Payer directement',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
] else ...[
const Icon(Icons.payment, size: 20),
const SizedBox(width: 8),
const Text(
'Payer directement',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
],
],
),
),
),
),
],
),
),
@ -1180,4 +1185,4 @@ class CartItemModel {
required this.quantity,
required this.commentaire,
});
}
}

3
lib/pages/commande_item_screen.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:intl/intl.dart';
// Import de la page de validation (à ajuster selon votre structure de dossiers)
import 'commande_item_validation.dart';
@ -635,7 +636,7 @@ class _AddToCartModalState extends State<AddToCartModal> {
),
),
Text(
"${calculateTotal().toStringAsFixed(2)} MGA",
"${NumberFormat("#,##0.00", "fr_FR").format(calculateTotal())} MGA",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,

13
lib/pages/commande_item_validation.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:intl/intl.dart';
class ValidateAddItemsPage extends StatefulWidget {
final int commandeId;
@ -151,7 +152,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
Text('• Commande: ${widget.numeroCommande}'),
Text('• Nouveaux articles: ${_getTotalNewArticles()}'),
Text(
'• Nouveau total: ${_calculateGrandTotal().toStringAsFixed(2)} MGA',
'• Nouveau total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateGrandTotal())} MGA',
),
],
),
@ -473,7 +474,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
],
),
Text(
'${item.prix.toStringAsFixed(2)} MGA l\'unité',
'${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
@ -535,7 +536,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
),
// Prix total de l'article
Text(
'${(item.prix * item.quantity).toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -584,7 +585,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
style: TextStyle(fontSize: 16),
),
Text(
'${_calculateExistingItemsTotal().toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(_calculateExistingItemsTotal())} MGA',
style: TextStyle(fontSize: 16),
),
],
@ -611,7 +612,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
style: TextStyle(fontSize: 16, color: Colors.green[700]),
),
Text(
'${_calculateNewItemsTotal().toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(_calculateNewItemsTotal())} MGA',
style: TextStyle(fontSize: 16, color: Colors.green[700]),
),
],
@ -630,7 +631,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
),
),
Text(
'${_calculateGrandTotal().toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(_calculateGrandTotal())} MGA',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,

37
lib/pages/commandes_screen.dart

@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:intl/intl.dart';
import 'commande_item_screen.dart';
class OrdersManagementScreen extends StatefulWidget {
@ -291,7 +291,7 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
return orders
.where(
(order) =>
order.statut == "en_attente" || order.statut == "en_preparation",
order.statut == "en_attente" || order.statut == "en_preparation" || order.statut == "prete",
)
.toList();
}
@ -311,7 +311,7 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${order.tablename} - ${order.totalTtc.toStringAsFixed(2)} MGA',
'${order.tablename} - ${NumberFormat("#,##0.00", "fr_FR").format(order.totalTtc)} MGA',
),
],
),
@ -545,6 +545,8 @@ class OrderCard extends StatelessWidget {
return Colors.blue;
case 'payee':
return Colors.grey;
case 'prete':
return Colors.grey;
default:
return Colors.grey;
}
@ -560,6 +562,8 @@ class OrderCard extends StatelessWidget {
return 'Servie';
case 'payee':
return 'Payée';
case 'prete':
return 'prête';
default:
return status;
}
@ -655,7 +659,7 @@ class OrderCard extends StatelessWidget {
),
),
Text(
'${(item.pu ?? 0) * item.quantite} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format((item.pu ?? 0) * item.quantite)} MGA',
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
@ -688,7 +692,7 @@ class OrderCard extends StatelessWidget {
),
),
Text(
'${order.totalHt.toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(order.totalHt)} MGA',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -701,11 +705,11 @@ class OrderCard extends StatelessWidget {
// Action buttons
if (order.statut == 'en_attente' ||
order.statut == 'en_preparation')
order.statut == 'en_preparation' || order.statut == 'prete')
Row(
children: [
if (order.statut == 'en_attente' ||
order.statut == 'en_preparation')
order.statut == 'en_preparation' || order.statut == 'prete')
Expanded(
child: ElevatedButton(
onPressed:
@ -787,6 +791,25 @@ class OrderCard extends StatelessWidget {
),
),
const SizedBox(width: 8),
if (order.statut == 'en_preparation')
Expanded(
child: ElevatedButton(
onPressed:
() => onStatusUpdate(order, 'prete'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
child: const Text(
'Prête',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
const SizedBox(width: 8),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.red.shade200),

11
lib/pages/facture_screen.dart

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/command_detail.dart';
import '../services/pdf_service.dart';
import 'package:intl/intl.dart';
class FactureScreen extends StatefulWidget {
final CommandeDetail commande;
@ -218,7 +219,7 @@ class _FactureScreenState extends State<FactureScreen> {
),
),
Text(
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)} AR',
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
@ -246,7 +247,7 @@ class _FactureScreenState extends State<FactureScreen> {
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'${widget.commande.totalTtc.toStringAsFixed(2)} MGA',
'${NumberFormat("#,##0.00", "fr_FR").format(widget.commande.totalTtc)} AR',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
@ -267,7 +268,7 @@ class _FactureScreenState extends State<FactureScreen> {
}
void _printReceipt() async {
bool isPrinting;
bool isPrinting = false;
setState(() => isPrinting = true);
try {
@ -345,7 +346,7 @@ class _FactureScreenState extends State<FactureScreen> {
_showSuccessMessage(
action == 'print'
? 'Facture envoyée à l\'imprimante ${_getPlatformName()}'
: 'PDF sauvegardé et partagé',
: 'PDF sauvegardé avec succès',
);
} else {
_showErrorMessage(
@ -446,4 +447,4 @@ class _FactureScreenState extends State<FactureScreen> {
if (Platform.isWindows) return 'Windows';
return 'cette plateforme';
}
}
}

756
lib/pages/historique_commande.dart

@ -15,13 +15,13 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
bool isLoading = true;
String? error;
// Informations de pagination
// Informations d'affichage et pagination
int totalItems = 0;
int currentPage = 1;
int totalPages = 1;
int totalItems = 0;
int itemsPerPage = 10;
final int itemsPerPage = 10; // Nombre d'éléments par page
final String baseUrl = 'https://restaurant.careeracademy.mg'; // Remplacez par votre URL
final String baseUrl = 'https://restaurant.careeracademy.mg';
final Map<String, String> _headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
@ -38,48 +38,187 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
_loadCommandes();
}
Future<void> _loadCommandes() async {
Future<void> _loadCommandes({int page = 1}) 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}');
// Ajouter les paramètres de pagination à l'URL
final uri = Uri.parse('$baseUrl/api/commandes').replace(queryParameters: {
'statut': 'payee',
'page': page.toString(),
'limit': itemsPerPage.toString(),
});
final response = await http.get(uri, headers: _headers);
print('=== DÉBUT DEBUG RESPONSE ===');
print('Status Code: ${response.statusCode}');
print('Response Body: ${response.body}');
if (response.statusCode == 200) {
// Adapter la structure de réponse {data: [...], pagination: {...}}
final Map<String, dynamic> responseData = json.decode(response.body);
final List<dynamic> data = responseData['data'] ?? [];
final Map<String, dynamic> pagination = responseData['pagination'] ?? {};
final dynamic responseBody = json.decode(response.body);
print('=== PARSED RESPONSE ===');
print('Type: ${responseBody.runtimeType}');
print('Content: $responseBody');
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;
List<dynamic> data = [];
// Gestion améliorée de la réponse
if (responseBody is Map<String, dynamic>) {
print('=== RESPONSE EST UN MAP ===');
print('Keys disponibles: ${responseBody.keys.toList()}');
// Structure: {"success": true, "data": {"commandes": [...], "pagination": {...}}}
if (responseBody.containsKey('data') && responseBody['data'] is Map<String, dynamic>) {
final dataMap = responseBody['data'] as Map<String, dynamic>;
print('=== DATA MAP TROUVÉ ===');
print('Data keys: ${dataMap.keys.toList()}');
if (dataMap.containsKey('commandes')) {
final commandesValue = dataMap['commandes'];
print('=== COMMANDES TROUVÉES ===');
print('Type commandes: ${commandesValue.runtimeType}');
print('Nombre de commandes: ${commandesValue is List ? commandesValue.length : 'pas une liste'}');
if (commandesValue is List<dynamic>) {
data = commandesValue;
} else if (commandesValue != null) {
data = [commandesValue];
}
// Pagination
if (dataMap.containsKey('pagination')) {
final pagination = dataMap['pagination'] as Map<String, dynamic>?;
if (pagination != null) {
currentPage = pagination['currentPage'] ?? page;
totalPages = pagination['totalPages'] ?? 1;
totalItems = pagination['totalItems'] ?? data.length;
print('=== PAGINATION ===');
print('Page: $currentPage/$totalPages, Total: $totalItems');
}
} else {
// Si pas de pagination dans la réponse, calculer approximativement
totalItems = data.length;
currentPage = page;
totalPages = (totalItems / itemsPerPage).ceil();
}
} else {
print('=== PAS DE COMMANDES DANS DATA ===');
totalItems = 0;
currentPage = 1;
totalPages = 1;
}
} else if (responseBody.containsKey('commandes')) {
// Fallback: commandes directement dans responseBody
final commandesValue = responseBody['commandes'];
print('=== COMMANDES DIRECTES ===');
if (commandesValue is List<dynamic>) {
data = commandesValue;
} else if (commandesValue != null) {
data = [commandesValue];
}
totalItems = data.length;
currentPage = page;
totalPages = (totalItems / itemsPerPage).ceil();
} else {
print('=== STRUCTURE INCONNUE ===');
print('Clés disponibles: ${responseBody.keys.toList()}');
totalItems = 0;
currentPage = 1;
totalPages = 1;
}
} else if (responseBody is List<dynamic>) {
print('=== RESPONSE EST UNE LISTE ===');
data = responseBody;
totalItems = data.length;
currentPage = page;
totalPages = (totalItems / itemsPerPage).ceil();
} else {
throw Exception('Format de réponse inattendu: ${responseBody.runtimeType}');
}
print('=== DONNÉES EXTRAITES ===');
print('Nombre d\'éléments: ${data.length}');
print('Data: $data');
// Conversion sécurisée avec prints détaillés
List<CommandeData> parsedCommandes = [];
for (int i = 0; i < data.length; i++) {
try {
final item = data[i];
print('=== ITEM $i ===');
print('Type: ${item.runtimeType}');
print('Contenu complet: $item');
if (item is Map<String, dynamic>) {
print('--- ANALYSE DES CHAMPS ---');
item.forEach((key, value) {
print('$key: $value (${value.runtimeType})');
});
final commandeData = CommandeData.fromJson(item);
print('--- COMMANDE PARSÉE ---');
print('ID: ${commandeData.id}');
print('Numéro: ${commandeData.numeroCommande}');
print('Table name: ${commandeData.tablename}');
print('Serveur: ${commandeData.serveur}');
print('Date commande: ${commandeData.dateCommande}');
print('Date paiement: ${commandeData.datePaiement}');
print('Total TTC: ${commandeData.totalTtc}');
print('Mode paiement: ${commandeData.modePaiement}');
print('Nombre d\'items: ${commandeData.items?.length ?? 0}');
if (commandeData.items != null) {
print('--- ITEMS DE LA COMMANDE ---');
for (int j = 0; j < commandeData.items!.length; j++) {
final commandeItem = commandeData.items![j];
print('Item $j:');
print(' - Menu nom: ${commandeItem.menuNom}');
print(' - Quantité: ${commandeItem.quantite}');
print(' - Prix unitaire: ${commandeItem.prixUnitaire}');
print(' - Total: ${commandeItem.totalItem}');
print(' - Commentaires: ${commandeItem.commentaires}');
}
}
parsedCommandes.add(commandeData);
} else {
print('ERROR: Item $i n\'est pas un Map: ${item.runtimeType}');
}
} catch (e, stackTrace) {
print('ERROR: Erreur lors du parsing de l\'item $i: $e');
print('Stack trace: $stackTrace');
// Continue avec les autres items
}
}
print('=== RÉSULTAT FINAL ===');
print('Nombre de commandes parsées: ${parsedCommandes.length}');
setState(() {
commandes = parsedCommandes;
isLoading = false;
});
// Initialiser les animations après avoir mis à jour l'état
_initializeAnimations();
_startAnimations();
} else {
print('ERROR: HTTP ${response.statusCode}: ${response.reasonPhrase}');
setState(() {
error = 'Erreur lors du chargement des commandes';
error = 'Erreur HTTP ${response.statusCode}: ${response.reasonPhrase}';
isLoading = false;
});
}
} catch (e) {
} catch (e, stackTrace) {
print('=== ERREUR GÉNÉRALE ===');
print('Erreur: $e');
print('Stack trace: $stackTrace');
setState(() {
error = 'Erreur de connexion: $e';
isLoading = false;
@ -87,7 +226,33 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
}
}
// 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(
@ -98,11 +263,13 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
}
void _startAnimations() async {
if (!mounted) return;
_animationController.forward();
for (int i = 0; i < _cardAnimationControllers.length; i++) {
await Future.delayed(Duration(milliseconds: 150));
if (mounted) {
if (mounted && i < _cardAnimationControllers.length) {
_cardAnimationControllers[i].forward();
}
}
@ -128,13 +295,14 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
elevation: 0,
),
body: RefreshIndicator(
onRefresh: _loadCommandes,
onRefresh: () => _loadCommandes(page: currentPage),
child: Column(
children: [
_buildHeader(),
Expanded(
child: _buildContent(),
),
if (totalPages > 1) _buildPagination(),
],
),
),
@ -188,7 +356,9 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
'$totalItems commande${totalItems > 1 ? 's' : ''} • Page $currentPage/$totalPages',
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,
@ -206,10 +376,158 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
);
}
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 Center(
child: CircularProgressIndicator(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4CAF50)),
),
SizedBox(height: 16),
Text(
'Chargement des commandes...',
style: TextStyle(color: Colors.grey.shade600),
),
],
),
);
}
@ -220,11 +538,22 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(error!, style: TextStyle(color: Colors.grey)),
Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Text(
error!,
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadCommandes,
onPressed: () => _loadCommandes(page: currentPage),
child: Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
),
],
),
@ -236,12 +565,25 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.payment, size: 64, color: Colors.grey),
Icon(Icons.restaurant_menu, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Aucune commande payée',
currentPage > 1
? 'Aucune commande sur cette page'
: 'Aucune commande payée',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
if (currentPage > 1) ...[
SizedBox(height: 16),
ElevatedButton(
onPressed: () => _goToPage(1),
child: Text('Retour à la première page'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
),
],
],
),
);
@ -345,7 +687,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
commande.tablename,
commande.tablename ?? 'Table inconnue',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@ -354,7 +696,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
),
SizedBox(height: 2),
Text(
commande.numeroCommande,
commande.numeroCommande ?? 'N/A',
style: TextStyle(
fontSize: 10,
color: Colors.grey,
@ -367,18 +709,31 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
Icon(Icons.calendar_today, size: 12, color: Colors.grey),
SizedBox(width: 3),
Text(
_formatDateTime(commande.dateCommande),
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,
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),
),
],
),
],
),
),
@ -386,7 +741,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF45a049)],
colors: [Color(0xFF4CAF50), Color(0xFF388E3C)],
),
borderRadius: BorderRadius.circular(10),
),
@ -420,7 +775,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
return Container(
padding: EdgeInsets.all(10),
child: Column(
children: commande.items.map((item) => _buildOrderItem(item)).toList(),
children: (commande.items ?? []).map((item) => _buildOrderItem(item)).toList(),
),
);
}
@ -477,7 +832,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
),
),
Text(
'${item.quantite}x × ${_formatPrice(item.prixUnitaire)}',
'${item.quantite} × ${_formatPrice(item.prixUnitaire)}',
style: TextStyle(
fontSize: 10,
color: Colors.grey,
@ -519,7 +874,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
height: 28,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF45a049)],
colors: [Color(0xFF4CAF50), Color(0xFF388E3C)],
),
borderRadius: BorderRadius.circular(6),
),
@ -542,9 +897,9 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
color: Colors.grey.shade600,
),
),
if (commande.totalTva > 0)
if ((commande.totalTva ?? 0) > 0)
Text(
'TVA: ${_formatPrice(commande.totalTva)}',
'TVA: ${_formatPrice(commande.totalTva ?? 0)}',
style: TextStyle(
fontSize: 9,
color: Colors.grey.shade500,
@ -569,7 +924,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
size: 14,
),
Text(
_formatPrice(commande.totalTtc),
_formatPrice(commande.totalTtc ?? 0),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -589,7 +944,6 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
}
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';
}
@ -644,87 +998,178 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
}
}
// Modèles de données adaptés à votre API
// 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 int? id;
final int? clientId;
final int? tableId;
final int? reservationId;
final String? numeroCommande;
final String? statut;
final double? totalHt;
final double? totalTva;
final double? totalTtc;
final String? modePaiement;
final String? commentaires;
final String serveur;
final DateTime dateCommande;
final DateTime? dateService;
final DateTime createdAt;
final DateTime updatedAt;
final List<CommandeItem> items;
final String tablename;
final String? serveur;
final DateTime? dateCommande;
final DateTime? datePaiement;
final DateTime? createdAt;
final DateTime? updatedAt;
final List<CommandeItem>? 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.id,
this.clientId,
this.tableId,
this.reservationId,
this.numeroCommande,
this.statut,
this.totalHt,
this.totalTva,
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,
this.serveur,
this.dateCommande,
this.datePaiement,
this.createdAt,
this.updatedAt,
this.items,
this.tablename,
});
factory CommandeData.fromJson(Map<String, dynamic> 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',
);
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,
);
print('=== COMMANDE PARSÉE AVEC SUCCÈS ===');
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 {
final result = DateTime.parse(value);
print('String to datetime: "$value" -> $result');
return result;
} catch (e) {
print('Erreur parsing date: $value - $e');
return null;
}
}
print('Impossible de parser en datetime: $value');
return null;
}
static List<CommandeItem>? _parseItems(dynamic value) {
print('=== PARSING ITEMS ===');
print('Items bruts: $value (${value.runtimeType})');
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++) {
print('--- ITEM $i ---');
final item = value[i];
print('Item brut: $item (${item.runtimeType})');
if (item is Map<String, dynamic>) {
final commandeItem = CommandeItem.fromJson(item);
result.add(commandeItem);
print('Item parsé: ${commandeItem.menuNom}');
} else {
print('Item $i n\'est pas un Map');
}
}
print('Total items parsés: ${result.length}');
return result;
} catch (e) {
print('Erreur parsing items: $e');
return null;
}
}
String getTableShortName() {
if (tablename.toLowerCase().contains('caisse')) return 'C';
if (tablename.toLowerCase().contains('terrasse')) return 'T';
final name = tablename ?? 'Table';
if (name.toLowerCase().contains('caisse')) return 'C';
if (name.toLowerCase().contains('terrasse')) return 'T';
// Extraire le numéro de la table
RegExp regExp = RegExp(r'\d+');
Match? match = regExp.firstMatch(tablename);
Match? match = regExp.firstMatch(name);
if (match != null) {
return 'T${match.group(0)}';
}
return tablename.substring(0, 1).toUpperCase();
return name.isNotEmpty ? name.substring(0, 1).toUpperCase() : 'T';
}
}
@ -737,8 +1182,8 @@ class CommandeItem {
final double totalItem;
final String? commentaires;
final String statut;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? createdAt;
final DateTime? updatedAt;
final String menuNom;
final String menuDescription;
final double menuPrixActuel;
@ -753,8 +1198,8 @@ class CommandeItem {
required this.totalItem,
this.commentaires,
required this.statut,
required this.createdAt,
required this.updatedAt,
this.createdAt,
this.updatedAt,
required this.menuNom,
required this.menuDescription,
required this.menuPrixActuel,
@ -762,31 +1207,82 @@ class CommandeItem {
});
factory CommandeItem.fromJson(Map<String, dynamic> 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'] ?? '',
);
try {
print('=== PARSING COMMANDE ITEM ===');
print('JSON item: $json');
// Debug chaque champ
final id = json['id'] ?? 0;
print('ID: ${json['id']} -> $id');
final commandeId = json['commande_id'] ?? 0;
print('Commande ID: ${json['commande_id']} -> $commandeId');
final menuId = json['menu_id'] ?? 0;
print('Menu ID: ${json['menu_id']} -> $menuId');
final quantite = json['quantite'] ?? json['quantity'] ?? 0;
print('Quantité: ${json['quantite']} / ${json['quantity']} -> $quantite');
final prixUnitaire = CommandeData._parseDouble(json['prix_unitaire']) ??
CommandeData._parseDouble(json['unit_price']) ?? 0.0;
print('Prix unitaire: ${json['prix_unitaire']} / ${json['unit_price']} -> $prixUnitaire');
final totalItem = CommandeData._parseDouble(json['total_item']) ??
CommandeData._parseDouble(json['total']) ?? 0.0;
print('Total item: ${json['total_item']} / ${json['total']} -> $totalItem');
final commentaires = json['commentaires']?.toString() ?? json['comments']?.toString();
print('Commentaires: ${json['commentaires']} / ${json['comments']} -> $commentaires');
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() ?? '';
print('Menu description: ${json['menu_description']} / ${json['description']} -> $menuDescription');
final menuPrixActuel = CommandeData._parseDouble(json['menu_prix_actuel']) ??
CommandeData._parseDouble(json['current_price']) ?? 0.0;
print('Menu prix actuel: ${json['menu_prix_actuel']} / ${json['current_price']} -> $menuPrixActuel');
final tablename = json['tablename']?.toString() ??
json['table_name']?.toString() ?? '';
print('Table name: ${json['tablename']} / ${json['table_name']} -> $tablename');
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;
}
}
}
// 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,

586
lib/services/pdf_service.dart

@ -11,14 +11,15 @@ import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import '../models/command_detail.dart';
import 'package:intl/intl.dart';
class PlatformPrintService {
// Format spécifique 58mm pour petites imprimantes
// Format spécifique 58mm pour petites imprimantes - CENTRÉ POUR L'IMPRESSION
static const PdfPageFormat ticket58mmFormat = PdfPageFormat(
58 * PdfPageFormat.mm, // Largeur exacte 58mm
48 * PdfPageFormat.mm, // Largeur exacte 58mm
double.infinity, // Hauteur automatique
marginLeft: 1 * PdfPageFormat.mm,
marginRight: 1 * PdfPageFormat.mm,
marginLeft: 4 * PdfPageFormat.mm, // Marges équilibrées pour centrer
marginRight: 4 * PdfPageFormat.mm, // Marges équilibrées pour centrer
marginTop: 2 * PdfPageFormat.mm,
marginBottom: 2 * PdfPageFormat.mm,
);
@ -40,305 +41,314 @@ class PlatformPrintService {
}
}
// Générer PDF optimisé pour 58mm
static Future<Uint8List> _generate58mmTicketPdf({
required CommandeDetail commande,
required String paymentMethod,
}) async {
final pdf = pw.Document();
// Configuration pour 58mm (très petit)
const double titleSize = 9;
const double headerSize = 8;
const double bodySize = 7;
const double smallSize = 6;
const double lineHeight = 1.2;
final restaurantInfo = {
'nom': 'RESTAURANT ITRIMOBE',
'adresse': 'Moramanga, Antananarivo',
'ville': 'Madagascar',
'contact': '261348415301',
'email': 'contact@careeragency.mg',
'nif': '4002141594',
'stat': '10715 33 2025 0 00414',
};
final factureNumber =
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}';
final dateTime = DateTime.now();
pdf.addPage(
pw.Page(
pageFormat: ticket58mmFormat,
margin: const pw.EdgeInsets.all(2), // 🔧 Marges minimales
build: (pw.Context context) {
return pw.Container(
width: double.infinity, // 🔧 Forcer la largeur complète
child: pw.Column(
crossAxisAlignment:
pw.CrossAxisAlignment.start, // 🔧 Alignement à gauche
children: [
// En-tête Restaurant (centré et compact)
pw.Container(
width: double.infinity,
child: pw.Text(
restaurantInfo['nom']!,
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 1),
// Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN
static Future<Uint8List> _generate58mmTicketPdf({
required CommandeDetail commande,
required String paymentMethod,
}) async {
final pdf = pw.Document();
const double titleSize = 8;
const double headerSize = 8;
const double bodySize = 7;
const double smallSize = 6;
const double lineHeight = 1.2;
final restaurantInfo = {
'nom': 'RESTAURANT ITRIMOBE',
'adresse': 'Moramanga, Madagascar',
'contact': '+261 34 12 34 56',
'nif': '4002141594',
'stat': '10715 33 2025 0 00414',
};
final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
final dateTime = DateTime.now();
String paymentMethodText;
switch (paymentMethod) {
case 'mvola':
paymentMethodText = 'MVola';
break;
case 'carte':
paymentMethodText = 'CB';
break;
case 'especes':
paymentMethodText = 'Espèces';
break;
default:
paymentMethodText = 'CB';
}
pw.Container(
width: double.infinity,
child: pw.Text(
restaurantInfo['adresse']!,
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
pdf.addPage(
pw.Page(
pageFormat: ticket58mmFormat,
margin: const pw.EdgeInsets.all(2),
build: (pw.Context context) {
return pw.Container(
width: double.infinity,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// TITRE CENTRÉ
pw.Container(
width: double.infinity,
child: pw.Text(
restaurantInfo['nom']!,
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
textAlign: pw.TextAlign.center,
),
pw.Container(
width: double.infinity,
child: pw.Text(
restaurantInfo['ville']!,
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 2),
// ADRESSE GAUCHE DÉCALÉE VERS LA GAUCHE (marginRight)
pw.Container(
width: double.infinity,
margin: const pw.EdgeInsets.only(right: 6),
child: pw.Text(
'Adresse: ${restaurantInfo['adresse']!}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.left,
),
pw.Container(
width: double.infinity,
child: pw.Text(
'Tel: ${restaurantInfo['contact']!}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
// CONTACT GAUCHE DÉCALÉE
pw.Container(
width: double.infinity,
margin: const pw.EdgeInsets.only(right: 8),
child: pw.Text(
'Contact: ${restaurantInfo['contact']!}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.left,
),
pw.SizedBox(height: 3),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
// NIF GAUCHE DÉCALÉE
pw.Container(
width: double.infinity,
margin: const pw.EdgeInsets.only(right: 8),
child: pw.Text(
'NIF: ${restaurantInfo['nif']!}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.left,
),
pw.SizedBox(height: 2),
// Informations ticket
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Ticket: $factureNumber',
style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
),
],
),
// STAT GAUCHE DÉCALÉE
pw.Container(
width: double.infinity,
margin: const pw.EdgeInsets.only(right: 8),
child: pw.Text(
'STAT: ${restaurantInfo['stat']!}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.left,
),
),
pw.SizedBox(height: 3),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// FACTURE CENTRÉE
pw.Container(
width: double.infinity,
child: pw.Text(
'Facture n° $factureNumber',
style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 1),
pw.SizedBox(height: 1),
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
_formatDate(dateTime),
style: pw.TextStyle(fontSize: smallSize),
// DATE CENTRÉE
pw.Container(
width: double.infinity,
child: pw.Text(
'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
// TABLE CENTRÉE
pw.Container(
width: double.infinity,
child: pw.Text(
'Via: ${commande.tablename ?? "N/A"}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
// PAIEMENT CENTRÉ
pw.Container(
width: double.infinity,
child: pw.Text(
'Paiement: $paymentMethodText',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 3),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// EN-TÊTE DES ARTICLES
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Qte Designation',
style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
pw.Text(
_formatTime(dateTime),
style: pw.TextStyle(fontSize: smallSize),
),
pw.Text(
'Prix',
style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
],
),
),
pw.SizedBox(height: 2),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
],
),
pw.SizedBox(height: 2),
// Articles (format très compact)
...commande.items
.map(
(item) => pw.Container(
width: double.infinity, // 🔧 Largeur complète
margin: const pw.EdgeInsets.only(bottom: 1),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Nom du plat
pw.Container(
width: double.infinity,
child: pw.Text(
'${item.menuNom}',
style: pw.TextStyle(fontSize: bodySize),
maxLines: 2,
),
),
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// ARTICLES
...commande.items
.map(
(item) => pw.Container(
width: double.infinity,
margin: const pw.EdgeInsets.only(bottom: 1),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Text(
'${item.quantite} ${item.menuNom}',
style: pw.TextStyle(fontSize: smallSize),
maxLines: 2,
),
// Quantité, prix unitaire et total sur une ligne
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment:
pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}MGA',
style: pw.TextStyle(fontSize: smallSize),
),
pw.Text(
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}MGA',
style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
),
],
),
),
],
),
),
pw.Text(
'${NumberFormat("#,##0.00", "fr_FR").format(item.prixUnitaire * item.quantite)}AR',
style: pw.TextStyle(fontSize: smallSize),
),
],
),
)
.toList(),
pw.SizedBox(height: 2),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// Total
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'TOTAL',
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
),
)
.toList(),
pw.SizedBox(height: 2),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// TOTAL
pw.Container(
width: double.infinity,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Total:',
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
pw.Text(
'${commande.totalTtc.toStringAsFixed(2)}MGA',
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
),
pw.Text(
'${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR',
style: pw.TextStyle(
fontSize: titleSize,
fontWeight: pw.FontWeight.bold,
),
],
),
),
pw.SizedBox(height: 3),
// Mode de paiement
pw.Container(
width: double.infinity,
child: pw.Text(
'Paiement: ${paymentMethod.toLowerCase()}',
style: pw.TextStyle(fontSize: bodySize),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 3),
// Ligne de séparation
pw.Container(
width: double.infinity,
height: 0.5,
color: PdfColors.black,
),
pw.SizedBox(height: 2),
// Message de remerciement
pw.Container(
width: double.infinity,
child: pw.Text(
'Merci de votre visite !',
style: pw.TextStyle(
fontSize: bodySize,
fontStyle: pw.FontStyle.italic,
),
textAlign: pw.TextAlign.center,
),
),
pw.Container(
width: double.infinity,
child: pw.Text(
'A bientôt !',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
],
),
pw.SizedBox(height: 3),
// Code de suivi (optionnel)
pw.Container(
width: double.infinity,
child: pw.Text(
'Code: ${factureNumber}',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
pw.SizedBox(height: 4),
// MESSAGE FINAL CENTRÉ
pw.Container(
width: double.infinity,
child: pw.Text(
'Merci et a bientot !',
style: pw.TextStyle(
fontSize: bodySize,
fontStyle: pw.FontStyle.italic,
),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 4),
pw.SizedBox(height: 4),
// Ligne de découpe
pw.Container(
width: double.infinity,
child: pw.Text(
'- - - - - - - - - - - - - - - -',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
// Ligne de découpe
pw.Container(
width: double.infinity,
child: pw.Text(
'- - - - - - - - - - - - - - - -',
style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
),
),
pw.SizedBox(height: 2),
],
),
);
},
),
);
pw.SizedBox(height: 2),
],
),
);
},
),
);
return pdf.save();
}
return pdf.save();
}
// Imprimer ticket 58mm
static Future<bool> printTicket({
@ -399,20 +409,24 @@ class PlatformPrintService {
}
final fileName =
'Ticket_58mm_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf';
'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf';
final file = File('${directory.path}/$fileName');
await file.writeAsBytes(pdfData);
await Share.shareXFiles(
[XFile(file.path)],
subject: 'Ticket ${commande.numeroCommande}',
text: 'Ticket de caisse 58mm',
);
// VRAIE SAUVEGARDE au lieu de partage automatique
if (Platform.isAndroid) {
// Sur Android, on peut proposer les deux options
await Share.shareXFiles(
[XFile(file.path)],
subject: 'Facture ${commande.numeroCommande}',
text: 'Facture de restaurant',
);
}
return true;
} catch (e) {
print('Erreur sauvegarde 58mm: $e');
print('Erreur sauvegarde: $e');
return false;
}
}
@ -435,7 +449,7 @@ class PlatformPrintService {
return await printTicket(commande: commande, paymentMethod: paymentMethod);
}
// Utilitaires de formatageπ
// Utilitaires de formatage
static String _formatDate(DateTime dateTime) {
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}';
}
@ -456,4 +470,4 @@ class PlatformPrintService {
return 'Non spécifié';
}
}
}
}

26
lib/services/restaurant_api_service.dart

@ -17,7 +17,33 @@ class RestaurantApiService {
'Accept': 'application/json',
};
static Future<bool> updateCommandeStatus({
required String commandeId,
required String newStatus,
}) async {
try {
final response = await http.put(
Uri.parse('$baseUrl/api/commandes/$commandeId/status'),
headers: _headers,
body: json.encode({'statut': newStatus}),
);
if (response.statusCode == 200 || response.statusCode == 204) {
return true;
} else {
print('Erreur updateCommandeStatus: ${response.statusCode} ${response.body}');
return false;
}
} catch (e) {
print('Exception updateCommandeStatus: $e');
return false;
}
}
// Récupérer les commandes
static Future<List<TableOrder>> getCommandes() async {
try {
final response = await http

82
lib/widgets/bottom_navigation.dart

@ -238,48 +238,48 @@ class AppBottomNavigation extends StatelessWidget {
),
),
// const SizedBox(width: 20),
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,
// ),
// ),
// ],
// ),
// ),
// ),
GestureDetector(
onTap: () => onItemTapped(6),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color:
selectedIndex == 6
? Colors.green.shade700
: Colors.transparent,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.payment,
color:
selectedIndex == 6
? Colors.white
: Colors.grey.shade600,
size: 16,
),
const SizedBox(width: 6),
Text(
'Historique',
style: TextStyle(
color:
selectedIndex == 6
? Colors.white
: Colors.grey.shade600,
fontWeight:
selectedIndex == 6
? FontWeight.w500
: FontWeight.normal,
),
),
],
),
),
),
const Spacer(),

1
pubspec.yaml

@ -26,6 +26,7 @@ dependencies:
path_provider: ^2.1.1
share_plus: ^7.2.1
permission_handler: ^11.1.0
intl: ^0.18.1
# Dépendances de développement/test
dev_dependencies:

BIN
windows/runner/resources/app_icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save