commit
This commit is contained in:
parent
d8910bb34a
commit
31c3d72a71
@ -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,
|
||||
),
|
||||
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);
|
||||
}
|
||||
),
|
||||
);
|
||||
} 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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
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 = 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;
|
||||
|
||||
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,
|
||||
|
||||
@ -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,306 +41,315 @@ class PlatformPrintService {
|
||||
}
|
||||
}
|
||||
|
||||
// Générer PDF optimisé pour 58mm
|
||||
static Future<Uint8List> _generate58mmTicketPdf({
|
||||
required CommandeDetail commande,
|
||||
required String paymentMethod,
|
||||
}) async {
|
||||
final pdf = pw.Document();
|
||||
// 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();
|
||||
|
||||
// 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;
|
||||
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, Antananarivo',
|
||||
'ville': 'Madagascar',
|
||||
'contact': '261348415301',
|
||||
'email': 'contact@careeragency.mg',
|
||||
'nif': '4002141594',
|
||||
'stat': '10715 33 2025 0 00414',
|
||||
};
|
||||
final restaurantInfo = {
|
||||
'nom': 'RESTAURANT ITRIMOBE',
|
||||
'adresse': 'Moramanga, Madagascar',
|
||||
'contact': '+261 34 12 34 56',
|
||||
'nif': '4002141594',
|
||||
'stat': '10715 33 2025 0 00414',
|
||||
};
|
||||
|
||||
final factureNumber =
|
||||
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}';
|
||||
final dateTime = DateTime.now();
|
||||
final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
||||
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),
|
||||
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
child: pw.Text(
|
||||
restaurantInfo['adresse']!,
|
||||
style: pw.TextStyle(fontSize: smallSize),
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
child: pw.Text(
|
||||
restaurantInfo['ville']!,
|
||||
style: pw.TextStyle(fontSize: smallSize),
|
||||
textAlign: pw.TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
child: pw.Text(
|
||||
'Tel: ${restaurantInfo['contact']!}',
|
||||
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),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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),
|
||||
),
|
||||
pw.Text(
|
||||
_formatTime(dateTime),
|
||||
style: pw.TextStyle(fontSize: smallSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.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.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),
|
||||
|
||||
// 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
String paymentMethodText;
|
||||
switch (paymentMethod) {
|
||||
case 'mvola':
|
||||
paymentMethodText = 'MVola';
|
||||
break;
|
||||
case 'carte':
|
||||
paymentMethodText = 'CB';
|
||||
break;
|
||||
case 'especes':
|
||||
paymentMethodText = 'Espèces';
|
||||
break;
|
||||
default:
|
||||
paymentMethodText = 'CB';
|
||||
}
|
||||
|
||||
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.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,
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
|
||||
// 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(
|
||||
'Prix',
|
||||
style: pw.TextStyle(
|
||||
fontSize: bodySize,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
'${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR',
|
||||
style: pw.TextStyle(
|
||||
fontSize: titleSize,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
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),
|
||||
|
||||
// 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
}
|
||||
|
||||
|
||||
// Imprimer ticket 58mm
|
||||
static Future<bool> printTicket({
|
||||
required CommandeDetail commande,
|
||||
@ -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é';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 4.1 KiB |
Loading…
Reference in New Issue
Block a user