Browse Source

commit

master
andrymodeste 4 months ago
parent
commit
31c3d72a71
  1. 29
      lib/pages/caisse_screen.dart
  2. 21
      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. 9
      lib/pages/facture_screen.dart
  7. 740
      lib/pages/historique_commande.dart
  8. 260
      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

29
lib/pages/caisse_screen.dart

@ -4,6 +4,7 @@ import 'package:itrimobe/pages/facture_screen.dart';
import '../models/command_detail.dart'; import '../models/command_detail.dart';
import '../models/payment_method.dart'; import '../models/payment_method.dart';
import '../services/restaurant_api_service.dart'; import '../services/restaurant_api_service.dart';
import 'package:intl/intl.dart';
class CaisseScreen extends StatefulWidget { class CaisseScreen extends StatefulWidget {
final String commandeId; final String commandeId;
@ -95,11 +96,20 @@ class _CaisseScreenState extends State<CaisseScreen> {
); );
if (success) { if (success) {
// Navigation vers la facture au lieu du dialog de succès final updateSuccess = await RestaurantApiService.updateCommandeStatus(
commandeId: widget.commandeId,
newStatus: 'payee',
);
if (!updateSuccess) {
_showErrorDialog("Paiement effectué, mais échec lors de la mise à jour du statut.");
return;
}
// 🔄 Redirige vers la facture
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: builder: (context) => FactureScreen(
(context) => FactureScreen(
commande: commande!, commande: commande!,
paymentMethod: selectedPaymentMethod!.id, paymentMethod: selectedPaymentMethod!.id,
), ),
@ -115,7 +125,8 @@ class _CaisseScreenState extends State<CaisseScreen> {
setState(() => isProcessingPayment = false); setState(() => isProcessingPayment = false);
} }
} }
} }
void _showErrorDialog(String message) { void _showErrorDialog(String message) {
showDialog( showDialog(
@ -148,7 +159,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
], ],
), ),
content: Text( 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: [ actions: [
TextButton( TextButton(
@ -233,7 +244,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
), ),
), ),
Text( Text(
'${commande!.totalTtc.toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(commande!.totalTtc)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -302,7 +313,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'${item.totalItem.toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(item.totalItem)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -405,7 +416,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'${amount.toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(amount)} MGA',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
@ -467,7 +478,7 @@ class _CaisseScreenState extends State<CaisseScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
selectedPaymentMethod != null 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', : 'Sélectionnez une méthode de paiement',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,

21
lib/pages/cart_page.dart

@ -11,6 +11,7 @@ import 'package:itrimobe/pages/tables.dart';
import 'package:itrimobe/services/pdf_service.dart'; import 'package:itrimobe/services/pdf_service.dart';
import '../layouts/main_layout.dart'; import '../layouts/main_layout.dart';
import 'package:intl/intl.dart';
class CartPage extends StatefulWidget { class CartPage extends StatefulWidget {
final int tableId; final int tableId;
@ -122,7 +123,7 @@ class _CartPageState extends State<CartPage> {
Text('• Table: ${widget.tableId}'), Text('• Table: ${widget.tableId}'),
Text('• Personnes: ${widget.personne}'), Text('• Personnes: ${widget.personne}'),
Text('• Articles: ${_getTotalArticles()}'), Text('• Articles: ${_getTotalArticles()}'),
Text('• Total: ${_calculateTotal().toStringAsFixed(2)} MGA'), Text('• Total: ${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA'),
], ],
), ),
actions: [ actions: [
@ -340,7 +341,7 @@ class _CartPageState extends State<CartPage> {
Text('Articles: ${_cartItems.length}'), Text('Articles: ${_cartItems.length}'),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Total: ${total.toStringAsFixed(0)} MGA', 'Total: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -612,7 +613,7 @@ class _CartPageState extends State<CartPage> {
Text('Table ${widget.tablename} libérée'), Text('Table ${widget.tablename} libérée'),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Montant: ${total.toStringAsFixed(0)} MGA', 'Montant: ${NumberFormat("#,##0.00", "fr_FR").format(total)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@ -887,7 +888,7 @@ class _CartPageState extends State<CartPage> {
], ],
), ),
Text( Text(
'${item.prix.toStringAsFixed(2)} MGA l\'unité', '${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[600], color: Colors.grey[600],
@ -949,7 +950,7 @@ class _CartPageState extends State<CartPage> {
), ),
// Prix total de l'article // Prix total de l'article
Text( Text(
'${(item.prix * item.quantity).toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -1038,7 +1039,7 @@ class _CartPageState extends State<CartPage> {
), ),
), ),
Text( Text(
'${_calculateTotal().toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(_calculateTotal())} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -1047,6 +1048,8 @@ class _CartPageState extends State<CartPage> {
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Bouton Valider la commande (toujours visible)
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@ -1101,9 +1104,11 @@ 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 // Bouton Payer directement (uniquement sur desktop - largeur >= 768px)
if (MediaQuery.of(context).size.width >= 768)
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(

3
lib/pages/commande_item_screen.dart

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

13
lib/pages/commande_item_validation.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'package:intl/intl.dart';
class ValidateAddItemsPage extends StatefulWidget { class ValidateAddItemsPage extends StatefulWidget {
final int commandeId; final int commandeId;
@ -151,7 +152,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
Text('• Commande: ${widget.numeroCommande}'), Text('• Commande: ${widget.numeroCommande}'),
Text('• Nouveaux articles: ${_getTotalNewArticles()}'), Text('• Nouveaux articles: ${_getTotalNewArticles()}'),
Text( 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( Text(
'${item.prix.toStringAsFixed(2)} MGA l\'unité', '${NumberFormat("#,##0.00", "fr_FR").format(item.prix)} MGA l\'unité',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[600], color: Colors.grey[600],
@ -535,7 +536,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
), ),
// Prix total de l'article // Prix total de l'article
Text( Text(
'${(item.prix * item.quantity).toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(item.prix * item.quantity)} MGA',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -584,7 +585,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
Text( Text(
'${_calculateExistingItemsTotal().toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(_calculateExistingItemsTotal())} MGA',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
], ],
@ -611,7 +612,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
style: TextStyle(fontSize: 16, color: Colors.green[700]), style: TextStyle(fontSize: 16, color: Colors.green[700]),
), ),
Text( Text(
'${_calculateNewItemsTotal().toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(_calculateNewItemsTotal())} MGA',
style: TextStyle(fontSize: 16, color: Colors.green[700]), style: TextStyle(fontSize: 16, color: Colors.green[700]),
), ),
], ],
@ -630,7 +631,7 @@ class _ValidateAddItemsPageState extends State<ValidateAddItemsPage> {
), ),
), ),
Text( Text(
'${_calculateGrandTotal().toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(_calculateGrandTotal())} MGA',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, 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:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'package:intl/intl.dart';
import 'commande_item_screen.dart'; import 'commande_item_screen.dart';
class OrdersManagementScreen extends StatefulWidget { class OrdersManagementScreen extends StatefulWidget {
@ -291,7 +291,7 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
return orders return orders
.where( .where(
(order) => (order) =>
order.statut == "en_attente" || order.statut == "en_preparation", order.statut == "en_attente" || order.statut == "en_preparation" || order.statut == "prete",
) )
.toList(); .toList();
} }
@ -311,7 +311,7 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( 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; return Colors.blue;
case 'payee': case 'payee':
return Colors.grey; return Colors.grey;
case 'prete':
return Colors.grey;
default: default:
return Colors.grey; return Colors.grey;
} }
@ -560,6 +562,8 @@ class OrderCard extends StatelessWidget {
return 'Servie'; return 'Servie';
case 'payee': case 'payee':
return 'Payée'; return 'Payée';
case 'prete':
return 'prête';
default: default:
return status; return status;
} }
@ -655,7 +659,7 @@ class OrderCard extends StatelessWidget {
), ),
), ),
Text( Text(
'${(item.pu ?? 0) * item.quantite} MGA', '${NumberFormat("#,##0.00", "fr_FR").format((item.pu ?? 0) * item.quantite)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.black87, color: Colors.black87,
@ -688,7 +692,7 @@ class OrderCard extends StatelessWidget {
), ),
), ),
Text( Text(
'${order.totalHt.toStringAsFixed(2)} MGA', '${NumberFormat("#,##0.00", "fr_FR").format(order.totalHt)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -701,11 +705,11 @@ class OrderCard extends StatelessWidget {
// Action buttons // Action buttons
if (order.statut == 'en_attente' || if (order.statut == 'en_attente' ||
order.statut == 'en_preparation') order.statut == 'en_preparation' || order.statut == 'prete')
Row( Row(
children: [ children: [
if (order.statut == 'en_attente' || if (order.statut == 'en_attente' ||
order.statut == 'en_preparation') order.statut == 'en_preparation' || order.statut == 'prete')
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed:
@ -787,6 +791,25 @@ class OrderCard extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), 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( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.red.shade200), border: Border.all(color: Colors.red.shade200),

9
lib/pages/facture_screen.dart

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

740
lib/pages/historique_commande.dart

@ -15,13 +15,13 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
bool isLoading = true; bool isLoading = true;
String? error; String? error;
// Informations de pagination // Informations d'affichage et pagination
int totalItems = 0;
int currentPage = 1; int currentPage = 1;
int totalPages = 1; int totalPages = 1;
int totalItems = 0; final int itemsPerPage = 10; // Nombre d'éléments par page
int itemsPerPage = 10;
final String baseUrl = 'https://restaurant.careeracademy.mg'; // Remplacez par votre URL final String baseUrl = 'https://restaurant.careeracademy.mg';
final Map<String, String> _headers = { final Map<String, String> _headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
@ -38,48 +38,187 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
_loadCommandes(); _loadCommandes();
} }
Future<void> _loadCommandes() async { Future<void> _loadCommandes({int page = 1}) async {
try { try {
setState(() { setState(() {
isLoading = true; isLoading = true;
error = null; error = null;
}); });
final response = await http.get( // Ajouter les paramètres de pagination à l'URL
Uri.parse('$baseUrl/api/commandes?statut=payee'), final uri = Uri.parse('$baseUrl/api/commandes').replace(queryParameters: {
headers: _headers, 'statut': 'payee',
); 'page': page.toString(),
'limit': itemsPerPage.toString(),
});
final dynamic responseBody = json.decode(response.body); final response = await http.get(uri, headers: _headers);
print('Réponse getCommandes: ${responseBody}');
print('=== DÉBUT DEBUG RESPONSE ===');
print('Status Code: ${response.statusCode}');
print('Response Body: ${response.body}');
if (response.statusCode == 200) { if (response.statusCode == 200) {
// Adapter la structure de réponse {data: [...], pagination: {...}} final dynamic responseBody = json.decode(response.body);
final Map<String, dynamic> responseData = json.decode(response.body); print('=== PARSED RESPONSE ===');
final List<dynamic> data = responseData['data'] ?? []; print('Type: ${responseBody.runtimeType}');
final Map<String, dynamic> pagination = responseData['pagination'] ?? {}; print('Content: $responseBody');
setState(() { List<dynamic> data = [];
commandes = data.map((json) => CommandeData.fromJson(json)).toList();
// 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];
}
// Mettre à jour les informations de pagination // Pagination
currentPage = pagination['currentPage'] ?? 1; if (dataMap.containsKey('pagination')) {
final pagination = dataMap['pagination'] as Map<String, dynamic>?;
if (pagination != null) {
currentPage = pagination['currentPage'] ?? page;
totalPages = pagination['totalPages'] ?? 1; totalPages = pagination['totalPages'] ?? 1;
totalItems = pagination['totalItems'] ?? 0; totalItems = pagination['totalItems'] ?? data.length;
itemsPerPage = pagination['itemsPerPage'] ?? 10; 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; isLoading = false;
}); });
// Initialiser les animations après avoir mis à jour l'état
_initializeAnimations(); _initializeAnimations();
_startAnimations(); _startAnimations();
} else { } else {
print('ERROR: HTTP ${response.statusCode}: ${response.reasonPhrase}');
setState(() { setState(() {
error = 'Erreur lors du chargement des commandes'; error = 'Erreur HTTP ${response.statusCode}: ${response.reasonPhrase}';
isLoading = false; isLoading = false;
}); });
} }
} catch (e) { } catch (e, stackTrace) {
print('=== ERREUR GÉNÉRALE ===');
print('Erreur: $e');
print('Stack trace: $stackTrace');
setState(() { setState(() {
error = 'Erreur de connexion: $e'; error = 'Erreur de connexion: $e';
isLoading = false; 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() { void _initializeAnimations() {
// Disposer les anciens contrôleurs
for (var controller in _cardAnimationControllers) {
controller.dispose();
}
_cardAnimationControllers = List.generate( _cardAnimationControllers = List.generate(
commandes.length, commandes.length,
(index) => AnimationController( (index) => AnimationController(
@ -98,11 +263,13 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
} }
void _startAnimations() async { void _startAnimations() async {
if (!mounted) return;
_animationController.forward(); _animationController.forward();
for (int i = 0; i < _cardAnimationControllers.length; i++) { for (int i = 0; i < _cardAnimationControllers.length; i++) {
await Future.delayed(Duration(milliseconds: 150)); await Future.delayed(Duration(milliseconds: 150));
if (mounted) { if (mounted && i < _cardAnimationControllers.length) {
_cardAnimationControllers[i].forward(); _cardAnimationControllers[i].forward();
} }
} }
@ -128,13 +295,14 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
elevation: 0, elevation: 0,
), ),
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: _loadCommandes, onRefresh: () => _loadCommandes(page: currentPage),
child: Column( child: Column(
children: [ children: [
_buildHeader(), _buildHeader(),
Expanded( Expanded(
child: _buildContent(), child: _buildContent(),
), ),
if (totalPages > 1) _buildPagination(),
], ],
), ),
), ),
@ -188,7 +356,9 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
Padding( Padding(
padding: EdgeInsets.only(top: 4), padding: EdgeInsets.only(top: 4),
child: Text( 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( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey.shade500, 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() { Widget _buildContent() {
if (isLoading) { if (isLoading) {
return Center( 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: [ children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey), Icon(Icons.error_outline, size: 64, color: Colors.grey),
SizedBox(height: 16), 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), SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: _loadCommandes, onPressed: () => _loadCommandes(page: currentPage),
child: Text('Réessayer'), child: Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF4CAF50),
foregroundColor: Colors.white,
),
), ),
], ],
), ),
@ -236,12 +565,25 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.payment, size: 64, color: Colors.grey), Icon(Icons.restaurant_menu, size: 64, color: Colors.grey),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text(
'Aucune commande payée', currentPage > 1
? 'Aucune commande sur cette page'
: 'Aucune commande payée',
style: TextStyle(color: Colors.grey, fontSize: 16), 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
commande.tablename, commande.tablename ?? 'Table inconnue',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -354,7 +696,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
), ),
SizedBox(height: 2), SizedBox(height: 2),
Text( Text(
commande.numeroCommande, commande.numeroCommande ?? 'N/A',
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey, color: Colors.grey,
@ -367,18 +709,31 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
Icon(Icons.calendar_today, size: 12, color: Colors.grey), Icon(Icons.calendar_today, size: 12, color: Colors.grey),
SizedBox(width: 3), SizedBox(width: 3),
Text( Text(
_formatDateTime(commande.dateCommande), commande.dateCommande != null
? _formatDateTime(commande.dateCommande!)
: 'Date inconnue',
style: TextStyle(color: Colors.grey, fontSize: 10), style: TextStyle(color: Colors.grey, fontSize: 10),
), ),
SizedBox(width: 8), SizedBox(width: 8),
Icon(Icons.person, size: 12, color: Colors.grey), Icon(Icons.person, size: 12, color: Colors.grey),
SizedBox(width: 3), SizedBox(width: 3),
Text( Text(
commande.serveur, commande.serveur ?? 'Serveur inconnu',
style: TextStyle(color: Colors.grey, fontSize: 10), 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), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF45a049)], colors: [Color(0xFF4CAF50), Color(0xFF388E3C)],
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
@ -420,7 +775,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
return Container( return Container(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
child: Column( 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( Text(
'${item.quantite}x × ${_formatPrice(item.prixUnitaire)}', '${item.quantite} × ${_formatPrice(item.prixUnitaire)}',
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey, color: Colors.grey,
@ -519,7 +874,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
height: 28, height: 28,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF45a049)], colors: [Color(0xFF4CAF50), Color(0xFF388E3C)],
), ),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
@ -542,9 +897,9 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
color: Colors.grey.shade600, color: Colors.grey.shade600,
), ),
), ),
if (commande.totalTva > 0) if ((commande.totalTva ?? 0) > 0)
Text( Text(
'TVA: ${_formatPrice(commande.totalTva)}', 'TVA: ${_formatPrice(commande.totalTva ?? 0)}',
style: TextStyle( style: TextStyle(
fontSize: 9, fontSize: 9,
color: Colors.grey.shade500, color: Colors.grey.shade500,
@ -569,7 +924,7 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
size: 14, size: 14,
), ),
Text( Text(
_formatPrice(commande.totalTtc), _formatPrice(commande.totalTtc ?? 0),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -589,7 +944,6 @@ class _OrderHistoryPageState extends State<OrderHistoryPage>
} }
String _formatPrice(double priceInCents) { 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'; 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 { class CommandeData {
final int id; final int? id;
final int clientId; final int? clientId;
final int tableId; final int? tableId;
final int reservationId; final int? reservationId;
final String numeroCommande; final String? numeroCommande;
final String statut; final String? statut;
final double totalHt; final double? totalHt;
final double totalTva; final double? totalTva;
final double totalTtc; final double? totalTtc;
final String? modePaiement; final String? modePaiement;
final String? commentaires; final String? commentaires;
final String serveur; final String? serveur;
final DateTime dateCommande; final DateTime? dateCommande;
final DateTime? dateService; final DateTime? datePaiement;
final DateTime createdAt; final DateTime? createdAt;
final DateTime updatedAt; final DateTime? updatedAt;
final List<CommandeItem> items; final List<CommandeItem>? items;
final String tablename; final String? tablename;
CommandeData({ CommandeData({
required this.id, this.id,
required this.clientId, this.clientId,
required this.tableId, this.tableId,
required this.reservationId, this.reservationId,
required this.numeroCommande, this.numeroCommande,
required this.statut, this.statut,
required this.totalHt, this.totalHt,
required this.totalTva, this.totalTva,
required this.totalTtc, this.totalTtc,
this.modePaiement, this.modePaiement,
this.commentaires, this.commentaires,
required this.serveur, this.serveur,
required this.dateCommande, this.dateCommande,
this.dateService, this.datePaiement,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
required this.items, this.items,
required this.tablename, this.tablename,
}); });
factory CommandeData.fromJson(Map<String, dynamic> json) { factory CommandeData.fromJson(Map<String, dynamic> json) {
return CommandeData( try {
id: json['id'],
// 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'], clientId: json['client_id'],
tableId: json['table_id'], tableId: json['table_id'],
reservationId: json['reservation_id'], reservationId: json['reservation_id'],
numeroCommande: json['numero_commande'], numeroCommande: numeroCommande,
statut: json['statut'], statut: json['statut']?.toString(),
totalHt: (json['total_ht'] ?? 0).toDouble(), totalHt: _parseDouble(json['total_ht']),
totalTva: (json['total_tva'] ?? 0).toDouble(), totalTva: _parseDouble(json['total_tva']),
totalTtc: (json['total_ttc'] ?? 0).toDouble(), totalTtc: totalTtc,
modePaiement: json['mode_paiement'], modePaiement: modePaiement,
commentaires: json['commentaires'], commentaires: json['commentaires']?.toString(),
serveur: json['serveur'], serveur: serveur,
dateCommande: DateTime.parse(json['date_commande']), dateCommande: dateCommande,
dateService: json['date_service'] != null datePaiement: datePaiement,
? DateTime.parse(json['date_service']) createdAt: _parseDateTime(json['created_at']),
: null, updatedAt: _parseDateTime(json['updated_at']),
createdAt: DateTime.parse(json['created_at']), items: items,
updatedAt: DateTime.parse(json['updated_at']), tablename: tablename,
items: (json['items'] as List)
.map((item) => CommandeItem.fromJson(item))
.toList(),
tablename: json['tablename'] ?? 'Table inconnue',
); );
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() { String getTableShortName() {
if (tablename.toLowerCase().contains('caisse')) return 'C'; final name = tablename ?? 'Table';
if (tablename.toLowerCase().contains('terrasse')) return 'T'; 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+'); RegExp regExp = RegExp(r'\d+');
Match? match = regExp.firstMatch(tablename); Match? match = regExp.firstMatch(name);
if (match != null) { if (match != null) {
return 'T${match.group(0)}'; 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 double totalItem;
final String? commentaires; final String? commentaires;
final String statut; final String statut;
final DateTime createdAt; final DateTime? createdAt;
final DateTime updatedAt; final DateTime? updatedAt;
final String menuNom; final String menuNom;
final String menuDescription; final String menuDescription;
final double menuPrixActuel; final double menuPrixActuel;
@ -753,8 +1198,8 @@ class CommandeItem {
required this.totalItem, required this.totalItem,
this.commentaires, this.commentaires,
required this.statut, required this.statut,
required this.createdAt, this.createdAt,
required this.updatedAt, this.updatedAt,
required this.menuNom, required this.menuNom,
required this.menuDescription, required this.menuDescription,
required this.menuPrixActuel, required this.menuPrixActuel,
@ -762,31 +1207,82 @@ class CommandeItem {
}); });
factory CommandeItem.fromJson(Map<String, dynamic> json) { factory CommandeItem.fromJson(Map<String, dynamic> json) {
return CommandeItem( try {
id: json['id'], print('=== PARSING COMMANDE ITEM ===');
commandeId: json['commande_id'], print('JSON item: $json');
menuId: json['menu_id'],
quantite: json['quantite'], // Debug chaque champ
prixUnitaire: (json['prix_unitaire'] ?? 0).toDouble(), final id = json['id'] ?? 0;
totalItem: (json['total_item'] ?? 0).toDouble(), print('ID: ${json['id']} -> $id');
commentaires: json['commentaires'],
statut: json['statut'], final commandeId = json['commande_id'] ?? 0;
createdAt: DateTime.parse(json['created_at']), print('Commande ID: ${json['commande_id']} -> $commandeId');
updatedAt: DateTime.parse(json['updated_at']),
menuNom: json['menu_nom'], final menuId = json['menu_id'] ?? 0;
menuDescription: json['menu_description'], print('Menu ID: ${json['menu_id']} -> $menuId');
menuPrixActuel: (json['menu_prix_actuel'] ?? 0).toDouble(),
tablename: json['tablename'] ?? '', 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 { class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Historique des Commandes',
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity, visualDensity: VisualDensity.adaptivePlatformDensity,

260
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:share_plus/share_plus.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../models/command_detail.dart'; import '../models/command_detail.dart';
import 'package:intl/intl.dart';
class PlatformPrintService { 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( static const PdfPageFormat ticket58mmFormat = PdfPageFormat(
58 * PdfPageFormat.mm, // Largeur exacte 58mm 48 * PdfPageFormat.mm, // Largeur exacte 58mm
double.infinity, // Hauteur automatique double.infinity, // Hauteur automatique
marginLeft: 1 * PdfPageFormat.mm, marginLeft: 4 * PdfPageFormat.mm, // Marges équilibrées pour centrer
marginRight: 1 * PdfPageFormat.mm, marginRight: 4 * PdfPageFormat.mm, // Marges équilibrées pour centrer
marginTop: 2 * PdfPageFormat.mm, marginTop: 2 * PdfPageFormat.mm,
marginBottom: 2 * PdfPageFormat.mm, marginBottom: 2 * PdfPageFormat.mm,
); );
@ -40,15 +41,14 @@ class PlatformPrintService {
} }
} }
// Générer PDF optimisé pour 58mm // Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN
static Future<Uint8List> _generate58mmTicketPdf({ static Future<Uint8List> _generate58mmTicketPdf({
required CommandeDetail commande, required CommandeDetail commande,
required String paymentMethod, required String paymentMethod,
}) async { }) async {
final pdf = pw.Document(); final pdf = pw.Document();
// Configuration pour 58mm (très petit) const double titleSize = 8;
const double titleSize = 9;
const double headerSize = 8; const double headerSize = 8;
const double bodySize = 7; const double bodySize = 7;
const double smallSize = 6; const double smallSize = 6;
@ -56,30 +56,42 @@ class PlatformPrintService {
final restaurantInfo = { final restaurantInfo = {
'nom': 'RESTAURANT ITRIMOBE', 'nom': 'RESTAURANT ITRIMOBE',
'adresse': 'Moramanga, Antananarivo', 'adresse': 'Moramanga, Madagascar',
'ville': 'Madagascar', 'contact': '+261 34 12 34 56',
'contact': '261348415301',
'email': 'contact@careeragency.mg',
'nif': '4002141594', 'nif': '4002141594',
'stat': '10715 33 2025 0 00414', 'stat': '10715 33 2025 0 00414',
}; };
final factureNumber = final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
'T${DateTime.now().millisecondsSinceEpoch.toString().substring(8)}';
final dateTime = DateTime.now(); 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';
}
pdf.addPage( pdf.addPage(
pw.Page( pw.Page(
pageFormat: ticket58mmFormat, pageFormat: ticket58mmFormat,
margin: const pw.EdgeInsets.all(2), // 🔧 Marges minimales margin: const pw.EdgeInsets.all(2),
build: (pw.Context context) { build: (pw.Context context) {
return pw.Container( return pw.Container(
width: double.infinity, // 🔧 Forcer la largeur complète width: double.infinity,
child: pw.Column( child: pw.Column(
crossAxisAlignment: crossAxisAlignment: pw.CrossAxisAlignment.start,
pw.CrossAxisAlignment.start, // 🔧 Alignement à gauche
children: [ children: [
// En-tête Restaurant (centré et compact) // TITRE CENTRÉ
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Text( child: pw.Text(
@ -92,32 +104,49 @@ class PlatformPrintService {
), ),
), ),
pw.SizedBox(height: 1), pw.SizedBox(height: 2),
// ADRESSE GAUCHE DÉCALÉE VERS LA GAUCHE (marginRight)
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
margin: const pw.EdgeInsets.only(right: 6),
child: pw.Text( child: pw.Text(
restaurantInfo['adresse']!, 'Adresse: ${restaurantInfo['adresse']!}',
style: pw.TextStyle(fontSize: smallSize), style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center, textAlign: pw.TextAlign.left,
), ),
), ),
// CONTACT GAUCHE DÉCALÉE
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
margin: const pw.EdgeInsets.only(right: 8),
child: pw.Text( child: pw.Text(
restaurantInfo['ville']!, 'Contact: ${restaurantInfo['contact']!}',
style: pw.TextStyle(fontSize: smallSize), style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center, textAlign: pw.TextAlign.left,
), ),
), ),
// NIF GAUCHE DÉCALÉE
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
margin: const pw.EdgeInsets.only(right: 8),
child: pw.Text( child: pw.Text(
'Tel: ${restaurantInfo['contact']!}', 'NIF: ${restaurantInfo['nif']!}',
style: pw.TextStyle(fontSize: smallSize), style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center, 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,
), ),
), ),
@ -132,43 +161,52 @@ class PlatformPrintService {
pw.SizedBox(height: 2), pw.SizedBox(height: 2),
// Informations ticket // FACTURE CENTRÉE
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Row( child: pw.Text(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, 'Facture n° $factureNumber',
children: [
pw.Text(
'Ticket: $factureNumber',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: bodySize, fontSize: bodySize,
fontWeight: pw.FontWeight.bold, fontWeight: pw.FontWeight.bold,
), ),
), textAlign: pw.TextAlign.center,
],
), ),
), ),
pw.SizedBox(height: 1), pw.SizedBox(height: 1),
// DATE CENTRÉE
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Row( child: pw.Text(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, 'Date: ${_formatDate(dateTime)} ${_formatTime(dateTime)}',
children: [
pw.Text(
_formatDate(dateTime),
style: pw.TextStyle(fontSize: smallSize), style: pw.TextStyle(fontSize: smallSize),
textAlign: pw.TextAlign.center,
), ),
pw.Text( ),
_formatTime(dateTime),
// TABLE CENTRÉE
pw.Container(
width: double.infinity,
child: pw.Text(
'Via: ${commande.tablename ?? "N/A"}',
style: pw.TextStyle(fontSize: smallSize), 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: 2), pw.SizedBox(height: 3),
// Ligne de séparation // Ligne de séparation
pw.Container( pw.Container(
@ -179,38 +217,21 @@ class PlatformPrintService {
pw.SizedBox(height: 2), pw.SizedBox(height: 2),
// Articles (format très compact) // EN-TÊTE DES ARTICLES
...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( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Row( child: pw.Row(
mainAxisAlignment: mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
pw.MainAxisAlignment.spaceBetween,
children: [ children: [
pw.Text( pw.Text(
'${item.quantite}x ${item.prixUnitaire.toStringAsFixed(2)}MGA', 'Qte Designation',
style: pw.TextStyle(fontSize: smallSize), style: pw.TextStyle(
fontSize: bodySize,
fontWeight: pw.FontWeight.bold,
),
), ),
pw.Text( pw.Text(
'${(item.prixUnitaire * item.quantite).toStringAsFixed(2)}MGA', 'Prix',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: bodySize, fontSize: bodySize,
fontWeight: pw.FontWeight.bold, fontWeight: pw.FontWeight.bold,
@ -219,6 +240,36 @@ class PlatformPrintService {
], ],
), ),
), ),
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),
),
], ],
), ),
), ),
@ -236,21 +287,21 @@ class PlatformPrintService {
pw.SizedBox(height: 2), pw.SizedBox(height: 2),
// Total // TOTAL
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Row( child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [ children: [
pw.Text( pw.Text(
'TOTAL', 'Total:',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: titleSize, fontSize: titleSize,
fontWeight: pw.FontWeight.bold, fontWeight: pw.FontWeight.bold,
), ),
), ),
pw.Text( pw.Text(
'${commande.totalTtc.toStringAsFixed(2)}MGA', '${NumberFormat("#,##0.00", "fr_FR").format(commande.totalTtc)}AR',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: titleSize, fontSize: titleSize,
fontWeight: pw.FontWeight.bold, fontWeight: pw.FontWeight.bold,
@ -260,34 +311,13 @@ class PlatformPrintService {
), ),
), ),
pw.SizedBox(height: 3), pw.SizedBox(height: 4),
// 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 // MESSAGE FINAL CENTRÉ
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
child: pw.Text( child: pw.Text(
'Merci de votre visite !', 'Merci et a bientot !',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: bodySize, fontSize: bodySize,
fontStyle: pw.FontStyle.italic, fontStyle: pw.FontStyle.italic,
@ -296,27 +326,6 @@ class PlatformPrintService {
), ),
), ),
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), pw.SizedBox(height: 4),
// Ligne de découpe // Ligne de découpe
@ -338,7 +347,8 @@ class PlatformPrintService {
); );
return pdf.save(); return pdf.save();
} }
// Imprimer ticket 58mm // Imprimer ticket 58mm
static Future<bool> printTicket({ static Future<bool> printTicket({
@ -399,20 +409,24 @@ class PlatformPrintService {
} }
final fileName = final fileName =
'Ticket_58mm_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf'; 'Facture_${commande.numeroCommande}_${DateTime.now().millisecondsSinceEpoch}.pdf';
final file = File('${directory.path}/$fileName'); final file = File('${directory.path}/$fileName');
await file.writeAsBytes(pdfData); await file.writeAsBytes(pdfData);
// VRAIE SAUVEGARDE au lieu de partage automatique
if (Platform.isAndroid) {
// Sur Android, on peut proposer les deux options
await Share.shareXFiles( await Share.shareXFiles(
[XFile(file.path)], [XFile(file.path)],
subject: 'Ticket ${commande.numeroCommande}', subject: 'Facture ${commande.numeroCommande}',
text: 'Ticket de caisse 58mm', text: 'Facture de restaurant',
); );
}
return true; return true;
} catch (e) { } catch (e) {
print('Erreur sauvegarde 58mm: $e'); print('Erreur sauvegarde: $e');
return false; return false;
} }
} }
@ -435,7 +449,7 @@ class PlatformPrintService {
return await printTicket(commande: commande, paymentMethod: paymentMethod); return await printTicket(commande: commande, paymentMethod: paymentMethod);
} }
// Utilitaires de formatageπ // Utilitaires de formatage
static String _formatDate(DateTime dateTime) { static String _formatDate(DateTime dateTime) {
return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}'; return '${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}';
} }

26
lib/services/restaurant_api_service.dart

@ -17,7 +17,33 @@ class RestaurantApiService {
'Accept': 'application/json', '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 // Récupérer les commandes
static Future<List<TableOrder>> getCommandes() async { static Future<List<TableOrder>> getCommandes() async {
try { try {
final response = await http 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( GestureDetector(
// onTap: () => onItemTapped(6), onTap: () => onItemTapped(6),
// child: Container( child: Container(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
// decoration: BoxDecoration( decoration: BoxDecoration(
// color: color:
// selectedIndex == 5 selectedIndex == 6
// ? Colors.green.shade700 ? Colors.green.shade700
// : Colors.transparent, : Colors.transparent,
// borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
// ), ),
// child: Row( child: Row(
// mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
// children: [ children: [
// Icon( Icon(
// Icons.payment, Icons.payment,
// color: color:
// selectedIndex == 5 selectedIndex == 6
// ? Colors.white ? Colors.white
// : Colors.grey.shade600, : Colors.grey.shade600,
// size: 16, size: 16,
// ), ),
// const SizedBox(width: 6), const SizedBox(width: 6),
// Text( Text(
// 'Historique', 'Historique',
// style: TextStyle( style: TextStyle(
// color: color:
// selectedIndex == 5 selectedIndex == 6
// ? Colors.white ? Colors.white
// : Colors.grey.shade600, : Colors.grey.shade600,
// fontWeight: fontWeight:
// selectedIndex == 5 selectedIndex == 6
// ? FontWeight.w500 ? FontWeight.w500
// : FontWeight.normal, : FontWeight.normal,
// ), ),
// ), ),
// ], ],
// ), ),
// ), ),
// ), ),
const Spacer(), const Spacer(),

1
pubspec.yaml

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