diff --git a/lib/layouts/main_layout.dart b/lib/layouts/main_layout.dart index 646ad26..ad8b25f 100644 --- a/lib/layouts/main_layout.dart +++ b/lib/layouts/main_layout.dart @@ -38,6 +38,8 @@ class _MainLayoutState extends State { return 5; case '/historique': return 6; + case '/information': + return 7; default: return 0; } @@ -71,6 +73,9 @@ class _MainLayoutState extends State { case 6: route = '/historique'; break; + case 7: + route = '/information'; + break; default: route = '/tables'; } diff --git a/lib/main.dart b/lib/main.dart index 26c60de..a1d2b91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'pages/commandes_screen.dart'; import 'pages/login_screen.dart'; import 'pages/menus_screen.dart'; import 'pages/historique_commande.dart'; +import 'pages/information.dart'; import 'pages/encaissement_screen.dart'; // NOUVEAU void main() { @@ -58,6 +59,11 @@ class MyApp extends StatelessWidget { currentRoute: '/historique', child: OrderHistoryPage(), ), + '/information': + (context) => MainLayout( + currentRoute: '/information', + child: PrintTemplateManagementScreen(), + ), }, ); } diff --git a/lib/pages/caisse_screen.dart b/lib/pages/caisse_screen.dart index 5ae672a..49a4cd5 100644 --- a/lib/pages/caisse_screen.dart +++ b/lib/pages/caisse_screen.dart @@ -6,6 +6,8 @@ import '../models/payment_method.dart'; import '../services/restaurant_api_service.dart'; import 'package:intl/intl.dart'; +import 'information.dart'; + class CaisseScreen extends StatefulWidget { final String commandeId; final int tableNumber; @@ -22,6 +24,7 @@ class CaisseScreen extends StatefulWidget { class _CaisseScreenState extends State { CommandeDetail? commande; + PrintTemplate? template; PaymentMethod? selectedPaymentMethod; bool isLoading = true; bool isProcessingPayment = false; @@ -70,8 +73,10 @@ class _CaisseScreenState extends State { final result = await RestaurantApiService.getCommandeDetails( widget.commandeId, ); + final loadedTemplate = await RestaurantApiService.getPrintTemplate(); setState(() { commande = result; + template = loadedTemplate; isLoading = false; }); } catch (e) { @@ -111,7 +116,8 @@ class _CaisseScreenState extends State { MaterialPageRoute( builder: (context) => FactureScreen( commande: commande!, - paymentMethod: selectedPaymentMethod!.id, + template: template!, + paymentMethod: selectedPaymentMethod!.id, ), ), ); diff --git a/lib/pages/cart_page.dart b/lib/pages/cart_page.dart index 45a6ec3..32c4897 100644 --- a/lib/pages/cart_page.dart +++ b/lib/pages/cart_page.dart @@ -13,6 +13,8 @@ import 'package:itrimobe/services/pdf_service.dart'; import '../layouts/main_layout.dart'; import 'package:intl/intl.dart'; +import 'information.dart'; + class CartPage extends StatefulWidget { final int tableId; final int personne; @@ -462,12 +464,14 @@ class _CartPageState extends State { // Convertir le Map en objet CommandeDetail var commandeDetail = CommandeDetail.fromJson(commandeData['data']); + var template = PrintTemplate.fromJson(commandeData['data']); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => FactureScreen( commande: commandeDetail, + template: template, paymentMethod: selectedPaymentMethod!.id, tablename: widget.tablename ?? diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart index 6e978ae..9e53b88 100644 --- a/lib/pages/commandes_screen.dart +++ b/lib/pages/commandes_screen.dart @@ -249,43 +249,50 @@ class _OrdersManagementScreenState extends State { print('Error creating order: $e'); } } +Future deleteOrder(Order order) async { + try { + final response = await http.delete( + Uri.parse('$baseUrl/commandes/${order.id}'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ); - Future deleteOrder(Order order) async { - try { - final response = await http.delete( - Uri.parse('$baseUrl/commandes/${order.id}'), - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - ); + if (response.statusCode == 200) { + final responseData = json.decode(response.body); - if (response.statusCode == 200) { - final responseData = json.decode(response.body); + if (responseData['success'] == true) { + setState(() { + orders.remove(order); + }); - if (responseData['success'] == true) { - setState(() { - orders.remove(order); - }); + // ✅ Mettre la table associée en "available" + await updateTableStatus(order.tableId, "available"); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Commande supprimée avec succès'), - backgroundColor: Colors.green, - ), - ); - } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Commande supprimée avec succès'), + backgroundColor: Colors.green, + ), + ); + } else { + throw Exception('Suppression échouée du côté serveur.'); } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Erreur lors de la suppression: $e'), - backgroundColor: Colors.red, - ), - ); - print('Error deleting order: $e'); + } else { + throw Exception('Échec suppression: ${response.statusCode}'); } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la suppression: $e'), + backgroundColor: Colors.red, + ), + ); + print('Error deleting order: $e'); } +} + List get activeOrders { return orders diff --git a/lib/pages/facture_screen.dart b/lib/pages/facture_screen.dart index 99c25da..fcc428e 100644 --- a/lib/pages/facture_screen.dart +++ b/lib/pages/facture_screen.dart @@ -6,8 +6,10 @@ import 'package:flutter/services.dart'; import '../models/command_detail.dart'; import '../services/pdf_service.dart'; import 'package:intl/intl.dart'; +import 'information.dart'; class FactureScreen extends StatefulWidget { + final PrintTemplate template; final CommandeDetail commande; final String paymentMethod; final String? tablename; @@ -17,6 +19,7 @@ class FactureScreen extends StatefulWidget { required this.commande, required this.paymentMethod, this.tablename, + required this.template, }); @override @@ -38,9 +41,14 @@ class _FactureScreenState extends State { } String get factureNumber { - return 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; + return 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(4)}'; } + String formatTemplateContent(String content) { + return content.replaceAll('\\r\\n', '\n').replaceAll('\\n', '\n'); +} + + @override Widget build(BuildContext context) { return Scaffold( @@ -116,37 +124,29 @@ class _FactureScreenState extends State { ); } - Widget _buildHeader() { - return const Column( - children: [ - Text( - 'RESTAURANT ITRIMOBE', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - SizedBox(height: 12), - Text( - 'Adresse: Moramanga, Madagascar', - style: TextStyle(fontSize: 12, color: Colors.black87), - ), - Text( - 'Contact: +261 34 12 34 56', - style: TextStyle(fontSize: 12, color: Colors.black87), + Widget _buildHeader() { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.template.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, ), - Text( - 'NIF: 4002141594', - style: TextStyle(fontSize: 12, color: Colors.black87), - ), - Text( - 'STAT: 10715 33 2025 0 00414', - style: TextStyle(fontSize: 12, color: Colors.black87), - ), - ], - ); - } + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + formatTemplateContent(widget.template.content), + style: const TextStyle(fontSize: 12, color: Colors.black87), + textAlign: TextAlign.center, + ), + ], + ); +} + Widget _buildFactureInfo() { final now = DateTime.now(); @@ -333,11 +333,13 @@ class _FactureScreenState extends State { if (action == 'print') { success = await PlatformPrintService.printFacture( commande: widget.commande, + template: widget.template, paymentMethod: widget.paymentMethod, ); } else if (action == 'save') { success = await PlatformPrintService.saveFacturePdf( commande: widget.commande, + template: widget.template, paymentMethod: widget.paymentMethod, ); } @@ -387,6 +389,7 @@ class _FactureScreenState extends State { if (shouldSave == true) { final success = await PlatformPrintService.saveFacturePdf( commande: widget.commande, + template: widget.template, paymentMethod: widget.paymentMethod, ); diff --git a/lib/pages/historique_commande.dart b/lib/pages/historique_commande.dart index 93ada34..f16ada7 100644 --- a/lib/pages/historique_commande.dart +++ b/lib/pages/historique_commande.dart @@ -54,34 +54,19 @@ class _OrderHistoryPageState extends State 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) { final dynamic responseBody = json.decode(response.body); - print('=== PARSED RESPONSE ==='); - print('Type: ${responseBody.runtimeType}'); - print('Content: $responseBody'); List data = []; // Gestion améliorée de la réponse if (responseBody is Map) { - 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) { final dataMap = responseBody['data'] as Map; - 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) { data = commandesValue; @@ -96,8 +81,6 @@ class _OrderHistoryPageState extends State 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 @@ -106,7 +89,6 @@ class _OrderHistoryPageState extends State totalPages = (totalItems / itemsPerPage).ceil(); } } else { - print('=== PAS DE COMMANDES DANS DATA ==='); totalItems = 0; currentPage = 1; totalPages = 1; @@ -114,7 +96,6 @@ class _OrderHistoryPageState extends State } else if (responseBody.containsKey('commandes')) { // Fallback: commandes directement dans responseBody final commandesValue = responseBody['commandes']; - print('=== COMMANDES DIRECTES ==='); if (commandesValue is List) { data = commandesValue; @@ -125,14 +106,11 @@ class _OrderHistoryPageState extends State 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) { - print('=== RESPONSE EST UNE LISTE ==='); data = responseBody; totalItems = data.length; currentPage = page; @@ -141,47 +119,22 @@ class _OrderHistoryPageState extends State 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 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) { - 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}'); } } @@ -196,9 +149,6 @@ class _OrderHistoryPageState extends State } } - print('=== RÉSULTAT FINAL ==='); - print('Nombre de commandes parsées: ${parsedCommandes.length}'); - setState(() { commandes = parsedCommandes; isLoading = false; @@ -1081,8 +1031,6 @@ class CommandeData { items: items, tablename: tablename, ); - - print('=== COMMANDE PARSÉE AVEC SUCCÈS ==='); return result; } catch (e, stackTrace) { print('=== ERREUR PARSING COMMANDE ==='); @@ -1109,7 +1057,6 @@ class CommandeData { 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'); @@ -1121,8 +1068,6 @@ class CommandeData { } static List? _parseItems(dynamic value) { - print('=== PARSING ITEMS ==='); - print('Items bruts: $value (${value.runtimeType})'); if (value == null) { print('Items null'); @@ -1137,20 +1082,14 @@ class CommandeData { try { List 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) { 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'); @@ -1208,32 +1147,23 @@ class CommandeItem { factory CommandeItem.fromJson(Map json) { 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() ?? @@ -1242,15 +1172,12 @@ class CommandeItem { 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, diff --git a/lib/pages/information.dart b/lib/pages/information.dart new file mode 100644 index 0000000..97909bc --- /dev/null +++ b/lib/pages/information.dart @@ -0,0 +1,467 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Gestion des Templates d\'Impression', + theme: ThemeData( + primarySwatch: Colors.green, + fontFamily: 'Roboto', + ), + home: PrintTemplateManagementScreen(), + ); + } +} + +class PrintTemplate { + final int id; + final String title; + final String content; + final String createdAt; + final String updatedAt; + + PrintTemplate({ + required this.id, + required this.title, + required this.content, + required this.createdAt, + required this.updatedAt, + }); + + factory PrintTemplate.fromJson(Map json) { + return PrintTemplate( + id: json['id'], + title: json['title'], + content: json['content'], + createdAt: json['created_at'], + updatedAt: json['updated_at'], + ); + } +} + +class ApiResponse { + final bool success; + final List data; + final Map pagination; + + ApiResponse({ + required this.success, + required this.data, + required this.pagination, + }); + + factory ApiResponse.fromJson(Map json) { + var dataList = json['data'] as List; + List templates = dataList.map((item) => PrintTemplate.fromJson(item)).toList(); + + return ApiResponse( + success: json['success'], + data: templates, + pagination: json['pagination'], + ); + } +} + +class PrintTemplateManagementScreen extends StatefulWidget { + @override + _PrintTemplateManagementScreenState createState() => _PrintTemplateManagementScreenState(); +} + +class _PrintTemplateManagementScreenState extends State { + List templates = []; + bool isLoading = true; + String? errorMessage; + + // Remplacez par votre URL d'API réelle + final String apiBaseUrl = 'https://restaurant.careeracademy.mg'; + + @override + void initState() { + super.initState(); + fetchTemplates(); + } + + Future fetchTemplates() async { + setState(() { + isLoading = true; + errorMessage = null; + }); + + try { + final response = await http.get( + Uri.parse('$apiBaseUrl/api/print-templates'), + headers: { + 'Content-Type': 'application/json', + }, + ); + if (response.statusCode == 200) { + final apiResponse = ApiResponse.fromJson(json.decode(response.body)); + + setState(() { + templates = apiResponse.data; + isLoading = false; + }); + } else { + setState(() { + errorMessage = 'Erreur HTTP: ${response.statusCode}'; + isLoading = false; + }); + } + } catch (e) { + print('Error fetching templates: $e'); + setState(() { + errorMessage = 'Erreur de connexion: $e'; + isLoading = false; + }); + } + } + + Future updateTemplate(int id, String title, String content) async { + try { + final response = await http.put( + Uri.parse('$apiBaseUrl/api/print-templates/$id'), + headers: { + 'Content-Type': 'application/json', + }, + body: json.encode({ + 'title': title, + 'content': content, + }), + ); + + if (response.statusCode == 200) { + // Recharger les templates après modification + await fetchTemplates(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Template modifié avec succès')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors de la modification: ${response.statusCode}')), + ); + } + } catch (e) { + print('Error updating template: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur de connexion: $e')), + ); + } + } + + // Fonction pour formater le contenu en remplaçant \r\n par des sauts de ligne + String _formatContent(String content) { + return content.replaceAll('\\r\\n', '\n').replaceAll('\\n', '\n'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[100], + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gestion des Templates d\'Impression', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 5), + Text( + 'Gérez les templates d\'impression de votre système', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + ], + ), + IconButton( + onPressed: fetchTemplates, + icon: Icon(Icons.refresh), + tooltip: 'Actualiser', + ), + ], + ), + SizedBox(height: 30), + + // Content + Expanded( + child: isLoading + ? _buildLoadingWidget() + : errorMessage != null + ? _buildErrorWidget() + : _buildTemplateTable(), + ), + ], + ), + ), + ); + } + + Widget _buildLoadingWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des templates...'), + ], + ), + ); + } + + Widget _buildErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, size: 64, color: Colors.red), + SizedBox(height: 16), + Text( + 'Erreur de chargement', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text(errorMessage ?? 'Erreur inconnue'), + SizedBox(height: 16), + ElevatedButton( + onPressed: fetchTemplates, + child: Text('Réessayer'), + ), + ], + ), + ); + } + + Widget _buildTemplateTable() { + if (templates.isEmpty) { + return Center( + child: Text( + 'Aucun template trouvé', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ); + } + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Table Header + Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + children: [ + Expanded(flex: 3, child: _buildHeaderCell('Titre')), + Expanded(flex: 7, child: _buildHeaderCell('Contenu')), + Expanded(flex: 1, child: _buildHeaderCell('Actions')), + ], + ), + ), + + // Table Body + Expanded( + child: ListView.builder( + itemCount: templates.length, + itemBuilder: (context, index) { + return _buildTemplateRow(templates[index]); + }, + ), + ), + ], + ), + ); + } + + Widget _buildHeaderCell(String text) { + return Text( + text, + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey[700], + fontSize: 14, + ), + ); + } + + Widget _buildTemplateRow(PrintTemplate template) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 20), // Augmenté le padding vertical + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey[200]!, width: 1), + ), + ), + child: IntrinsicHeight( // Pour que toutes les cellules aient la même hauteur + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Text( + template.title, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + ), + Expanded( + flex: 7, + child: Container( + constraints: BoxConstraints(minHeight: 80), // Hauteur minimum pour le contenu + child: Text( + _formatContent(template.content), + style: TextStyle( + color: Colors.grey[700], + fontSize: 14, + height: 1.4, // Espacement entre les lignes + ), + ), + ), + ), + Expanded( + flex: 1, + child: Align( + alignment: Alignment.topCenter, + child: IconButton( + onPressed: () { + _showEditTemplateDialog(context, template); + }, + icon: Icon(Icons.edit, color: Colors.blue, size: 20), + tooltip: 'Modifier', + ), + ), + ), + ], + ), + ), + ); + } + + void _showEditTemplateDialog(BuildContext context, PrintTemplate template) { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(text: template.title); + final _contentController = TextEditingController(text: _formatContent(template.content)); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Modifier Template'), + content: Container( + width: double.maxFinite, + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTextField(_titleController, 'Titre', true), + SizedBox(height: 16), + TextFormField( + controller: _contentController, + maxLines: 10, // Augmenté le nombre de lignes + decoration: InputDecoration( + labelText: 'Contenu', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), + alignLabelWithHint: true, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Ce champ est requis'; + } + return null; + }, + ), + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + // Reconvertir les sauts de ligne pour l'API + String contentForApi = _contentController.text.replaceAll('\n', '\\r\\n'); + updateTemplate( + template.id, + _titleController.text, + contentForApi, + ); + Navigator.of(context).pop(); + } + }, + child: Text('Modifier'), + ), + ], + ); + }, + ); + } + + Widget _buildTextField(TextEditingController controller, String label, bool required) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: label, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + validator: required ? (value) { + if (value == null || value.isEmpty) { + return 'Ce champ est requis'; + } + return null; + } : null, + ), + ); + } +} \ No newline at end of file diff --git a/lib/services/pdf_service.dart b/lib/services/pdf_service.dart index 8686ee9..69410f2 100644 --- a/lib/services/pdf_service.dart +++ b/lib/services/pdf_service.dart @@ -13,6 +13,8 @@ import 'package:permission_handler/permission_handler.dart'; import '../models/command_detail.dart'; import 'package:intl/intl.dart'; +import '../pages/information.dart'; + class PlatformPrintService { // Format spécifique 58mm pour petites imprimantes - CENTRÉ POUR L'IMPRESSION static const PdfPageFormat ticket58mmFormat = PdfPageFormat( @@ -44,6 +46,7 @@ class PlatformPrintService { // Générer PDF optimisé pour 58mm - VERSION IDENTIQUE À L'ÉCRAN static Future _generate58mmTicketPdf({ required CommandeDetail commande, + required PrintTemplate template, required String paymentMethod, }) async { final pdf = pw.Document(); @@ -51,16 +54,12 @@ static Future _generate58mmTicketPdf({ const double titleSize = 8; const double headerSize = 8; const double bodySize = 7; - const double smallSize = 6; + const double smallSize = 5; const double lineHeight = 1.2; - final restaurantInfo = { - 'nom': 'RESTAURANT ITRIMOBE', - 'adresse': 'Moramanga, Madagascar', - 'contact': '+261 34 12 34 56', - 'nif': '4002141594', - 'stat': '10715 33 2025 0 00414', - }; +final restauranTitle = template.title ?? 'Nom du restaurant'; +final restaurantContent = template.content ?? 'Adresse inconnue'; + final factureNumber = 'F${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; final dateTime = DateTime.now(); @@ -79,6 +78,9 @@ static Future _generate58mmTicketPdf({ default: paymentMethodText = 'CB'; } + String formatTemplateContent(String content) { + return content.replaceAll('\\r\\n', '\n').replaceAll('\\n', '\n'); +} pdf.addPage( pw.Page( @@ -95,7 +97,7 @@ static Future _generate58mmTicketPdf({ pw.Container( width: double.infinity, child: pw.Text( - restaurantInfo['nom']!, + restauranTitle, style: pw.TextStyle( fontSize: titleSize, fontWeight: pw.FontWeight.bold, @@ -106,59 +108,15 @@ static Future _generate58mmTicketPdf({ 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), + margin: const pw.EdgeInsets.only(right: 2), child: pw.Text( - 'NIF: ${restaurantInfo['nif']!}', + formatTemplateContent(restaurantContent), 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 @@ -353,6 +311,7 @@ static Future _generate58mmTicketPdf({ // Imprimer ticket 58mm static Future printTicket({ required CommandeDetail commande, + required PrintTemplate template, required String paymentMethod, }) async { try { @@ -363,6 +322,7 @@ static Future _generate58mmTicketPdf({ final pdfData = await _generate58mmTicketPdf( commande: commande, + template: template, paymentMethod: paymentMethod, ); @@ -385,6 +345,7 @@ static Future _generate58mmTicketPdf({ // Sauvegarder ticket 58mm static Future saveTicketPdf({ required CommandeDetail commande, + required PrintTemplate template, required String paymentMethod, }) async { try { @@ -393,6 +354,7 @@ static Future _generate58mmTicketPdf({ final pdfData = await _generate58mmTicketPdf( commande: commande, + template: template, paymentMethod: paymentMethod, ); @@ -434,19 +396,22 @@ static Future _generate58mmTicketPdf({ // Méthodes pour compatibilité static Future saveFacturePdf({ required CommandeDetail commande, + required PrintTemplate template, required String paymentMethod, }) async { return await saveTicketPdf( commande: commande, + template: template, paymentMethod: paymentMethod, ); } static Future printFacture({ required CommandeDetail commande, + required PrintTemplate template, required String paymentMethod, }) async { - return await printTicket(commande: commande, paymentMethod: paymentMethod); + return await printTicket(commande: commande,template: template, paymentMethod: paymentMethod); } // Utilitaires de formatage diff --git a/lib/services/restaurant_api_service.dart b/lib/services/restaurant_api_service.dart index d84077d..091304b 100644 --- a/lib/services/restaurant_api_service.dart +++ b/lib/services/restaurant_api_service.dart @@ -9,6 +9,8 @@ import 'package:itrimobe/models/command_detail.dart'; import 'package:itrimobe/models/payment_method.dart'; import 'package:itrimobe/models/tables_order.dart'; +import '../pages/information.dart'; + class RestaurantApiService { static const String baseUrl = 'https://restaurant.careeracademy.mg'; @@ -40,6 +42,24 @@ class RestaurantApiService { } } +static Future getPrintTemplate() async { + final url = Uri.parse('$baseUrl/api/print-templates'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final jsonResponse = json.decode(response.body); + final List templates = jsonResponse['data']; + + if (templates.isEmpty) { + throw Exception("Aucun template trouvé"); + } + + return PrintTemplate.fromJson(templates.first); + } else { + throw Exception("Erreur lors de la récupération du template (${response.statusCode})"); + } + } + // Récupérer les commandes @@ -58,7 +78,7 @@ class RestaurantApiService { if (response.statusCode == 200) { final dynamic responseBody = json.decode(response.body); - print('Réponse getCommandes: ${responseBody['data']['commandes']}'); + // print('Réponse getCommandes: ${responseBody['data']['commandes']}'); // Validation de la structure de réponse if (responseBody == null) { throw const FormatException('Réponse vide du serveur'); diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart index 2972f20..a87a715 100644 --- a/lib/widgets/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation.dart @@ -281,6 +281,51 @@ class AppBottomNavigation extends StatelessWidget { ), ), + + const SizedBox(width: 20), + + GestureDetector( + onTap: () => onItemTapped(7), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: + selectedIndex == 7 + ? Colors.green.shade700 + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.payment, + color: + selectedIndex == 7 + ? Colors.white + : Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Information', + style: TextStyle( + color: + selectedIndex == 7 + ? Colors.white + : Colors.grey.shade600, + fontWeight: + selectedIndex == 7 + ? FontWeight.w500 + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + + const Spacer(), // User Profile Section diff --git a/pubspec.lock b/pubspec.lock index 38b74ee..bc291d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -168,6 +168,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" leak_tracker: dependency: transitive description: