diff --git a/assets/logo_transparent.png b/assets/logo_transparent.png new file mode 100644 index 0000000..4561850 Binary files /dev/null and b/assets/logo_transparent.png differ diff --git a/lib/pages/cart_page.dart b/lib/pages/cart_page.dart new file mode 100644 index 0000000..6b224b3 --- /dev/null +++ b/lib/pages/cart_page.dart @@ -0,0 +1,421 @@ +import 'package:flutter/material.dart'; + +class CartPage extends StatefulWidget { + final int tableId; + final int personne; + final List cartItems; + + const CartPage({ + Key? key, + required this.tableId, + required this.personne, + required this.cartItems, + }) : super(key: key); + + @override + State createState() => _CartPageState(); +} + +class _CartPageState extends State { + List _cartItems = []; + + @override + void initState() { + super.initState(); + _processCartItems(); + } + + void _processCartItems() { + // Grouper les articles identiques + Map groupedItems = {}; + + for (var item in widget.cartItems) { + String key = "${item['id']}_${item['notes'] ?? ''}"; + + if (groupedItems.containsKey(key)) { + groupedItems[key]!.quantity++; + } else { + groupedItems[key] = CartItemModel( + id: item['id'], + nom: item['nom'] ?? 'Article', + prix: _parsePrice(item['prix']), + quantity: 1, + notes: item['notes'] ?? '', + ); + } + } + + setState(() { + _cartItems = groupedItems.values.toList(); + }); + } + + double _parsePrice(dynamic prix) { + if (prix == null) return 0.0; + if (prix is num) return prix.toDouble(); + if (prix is String) return double.tryParse(prix) ?? 0.0; + return 0.0; + } + + void _updateQuantity(int index, int newQuantity) { + setState(() { + if (newQuantity > 0) { + _cartItems[index].quantity = newQuantity; + } else { + _cartItems.removeAt(index); + } + }); + } + + void _removeItem(int index) { + setState(() { + _cartItems.removeAt(index); + }); + } + + double _calculateTotal() { + return _cartItems.fold(0.0, (sum, item) => sum + (item.prix * item.quantity)); + } + + int _getTotalArticles() { + return _cartItems.fold(0, (sum, item) => sum + item.quantity); + } + + void _validateOrder() { + // TODO: Implémenter la validation de commande + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Commande validée'), + content: Text('Votre commande a été envoyée en cuisine !'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Fermer le dialog + Navigator.of(context).pop(); // Retourner au menu + Navigator.of(context).pop(); // Retourner aux tables + }, + child: Text('OK'), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.arrow_back, color: Colors.black), + ), + title: Text( + 'Panier', + style: TextStyle( + color: Colors.black, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Retour au menu', + style: TextStyle( + color: Colors.black, + fontSize: 16, + ), + ), + ), + SizedBox(width: 16), + ], + ), + body: _cartItems.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.shopping_cart_outlined, + size: 80, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Votre panier est vide', + style: TextStyle( + fontSize: 18, + color: Colors.grey[600], + ), + ), + ], + ), + ) + : Column( + children: [ + // Header avec infos table + Container( + width: double.infinity, + padding: EdgeInsets.all(20), + color: Colors.white, + child: Text( + 'Table ${widget.tableId} • ${widget.personne} personne${widget.personne > 1 ? 's' : ''}', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + ), + + // Liste des articles + Expanded( + child: ListView.separated( + padding: EdgeInsets.all(16), + itemCount: _cartItems.length, + separatorBuilder: (context, index) => SizedBox(height: 12), + itemBuilder: (context, index) { + final item = _cartItems[index]; + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item.nom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + onPressed: () => _removeItem(index), + icon: Icon( + Icons.delete_outline, + color: Colors.red, + ), + constraints: BoxConstraints(), + padding: EdgeInsets.zero, + ), + ], + ), + Text( + '${item.prix.toStringAsFixed(2)} € l\'unité', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + if (item.notes.isNotEmpty) ...[ + SizedBox(height: 8), + Text( + 'Notes: ${item.notes}', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ], + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Contrôles de quantité + Row( + children: [ + IconButton( + onPressed: () => _updateQuantity(index, item.quantity - 1), + icon: Icon(Icons.remove), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), + ), + ), + SizedBox(width: 16), + Text( + item.quantity.toString(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 16), + IconButton( + onPressed: () => _updateQuantity(index, item.quantity + 1), + icon: Icon(Icons.add), + style: IconButton.styleFrom( + backgroundColor: Colors.grey[200], + minimumSize: Size(40, 40), + ), + ), + ], + ), + // Prix total de l'article + Text( + '${(item.prix * item.quantity).toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green[700], + ), + ), + ], + ), + ], + ), + ); + }, + ), + ), + + // Récapitulatif + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide(color: Colors.grey[200]!), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Récapitulatif', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Articles:', style: TextStyle(fontSize: 16)), + Text( + _getTotalArticles().toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Table:', style: TextStyle(fontSize: 16)), + Text( + widget.tableId.toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Personnes:', style: TextStyle(fontSize: 16)), + Text( + widget.personne.toString(), + style: TextStyle(fontSize: 16), + ), + ], + ), + SizedBox(height: 16), + Divider(), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${_calculateTotal().toStringAsFixed(2)} €', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _cartItems.isNotEmpty ? _validateOrder : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[700], + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + disabledBackgroundColor: Colors.grey[300], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.check, size: 20), + SizedBox(width: 8), + Text( + 'Valider la commande', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class CartItemModel { + final int id; + final String nom; + final double prix; + int quantity; + final String notes; + + CartItemModel({ + required this.id, + required this.nom, + required this.prix, + required this.quantity, + required this.notes, + }); +} \ No newline at end of file diff --git a/lib/pages/categorie.dart b/lib/pages/categorie.dart new file mode 100644 index 0000000..b0cee0e --- /dev/null +++ b/lib/pages/categorie.dart @@ -0,0 +1,869 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; + +class CategoriesPage extends StatefulWidget { + const CategoriesPage({super.key}); + + @override + State createState() => _CategoriesPageState(); +} + +class _CategoriesPageState extends State { + List categories = []; + bool isLoading = true; + String? error; + + @override + void initState() { + super.initState(); + _loadCategories(); + } + + Future _loadCategories() async { + try { + setState(() { + isLoading = true; + error = null; + }); + + final response = await http.get( + Uri.parse("https://restaurant.careeracademy.mg/api/menu-categories"), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + final jsonBody = json.decode(response.body); + final dynamic rawData = jsonBody['data']['categories']; // ✅ ici ! + + print('✅ Données récupérées :'); + print(rawData); + + if (rawData is List) { + setState(() { + categories = rawData + .map((json) => Category.fromJson(json as Map)) + .toList(); + categories.sort((a, b) => a.ordre.compareTo(b.ordre)); + isLoading = false; + }); + } else { + throw Exception("Format inattendu pour les catégories"); + } +} + else { + setState(() { + error = 'Erreur lors du chargement des catégories (${response.statusCode})'; + isLoading = false; + }); + if (kDebugMode) { + print('Erreur HTTP: ${response.statusCode}'); + print('Contenu de la réponse: ${response.body}'); + } + } + } catch (e) { + setState(() { + error = 'Erreur de connexion: $e'; + isLoading = false; + }); + if (kDebugMode) { + print('Erreur dans _loadCategories: $e'); + } + } + } + + Future _createCategory(Category category) async { + try { + final response = await http.post( + Uri.parse('https://restaurant.careeracademy.mg/api/menu-categories'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(category.toJson()), + ); + + if (response.statusCode == 201 || response.statusCode == 200) { + await _loadCategories(); // Recharger la liste + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Catégorie créée avec succès')), + ); + } + } else { + throw Exception('Erreur lors de la création (${response.statusCode})'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur: $e')), + ); + } + } + } + + Future _updateCategory(Category category) async { + try { + final response = await http.put( + Uri.parse('https://restaurant.careeracademy.mg/api/menu-categories/${category.id}'), + headers: {'Content-Type': 'application/json'}, + body: json.encode(category.toJson()), + ); + + if (response.statusCode == 200) { + await _loadCategories(); // Recharger la liste + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Catégorie mise à jour avec succès')), + ); + } + } else { + throw Exception('Erreur lors de la mise à jour (${response.statusCode})'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur: $e')), + ); + } + } + } + + Future _deleteCategory(int categoryId) async { + try { + final response = await http.delete( + Uri.parse('https://restaurant.careeracademy.mg/api/menu-categories/$categoryId'), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200 || response.statusCode == 204) { + await _loadCategories(); // Recharger la liste + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Catégorie supprimée avec succès')), + ); + } + } else { + throw Exception('Erreur lors de la suppression (${response.statusCode})'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur: $e')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Gestion des Catégories', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Text( + 'Gérez les catégories de votre menu', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + Row( + children: [ + IconButton( + onPressed: _loadCategories, + icon: const Icon(Icons.refresh), + tooltip: 'Actualiser', + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: _showAddCategoryDialog, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + icon: const Icon(Icons.add, size: 18), + label: const Text('Nouvelle catégorie'), + ), + ], + ), + ], + ), + + const SizedBox(height: 30), + + // Content + Expanded( + child: _buildContent(), + ), + ], + ), + ), + ), + ); + } + + Widget _buildContent() { + if (isLoading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('Chargement des catégories...'), + ], + ), + ); + } + + if (error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Colors.red.shade400, + ), + const SizedBox(height: 16), + Text( + error!, + style: TextStyle( + fontSize: 16, + color: Colors.red.shade600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadCategories, + child: const Text('Réessayer'), + ), + ], + ), + ); + } + + if (categories.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.category_outlined, + size: 64, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'Aucune catégorie trouvée', + style: TextStyle( + fontSize: 16, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _showAddCategoryDialog, + icon: const Icon(Icons.add), + label: const Text('Créer une catégorie'), + ), + ], + ), + ); + } + + return LayoutBuilder( + builder: (context, constraints) { + // Version responsive + if (constraints.maxWidth > 800) { + return _buildDesktopTable(); + } else { + return _buildMobileList(); + } + }, + ); + } + + Widget _buildDesktopTable() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // Table Header + Container( + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + 'Ordre', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Nom', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ), + Expanded( + flex: 3, + child: Text( + 'Description', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ), + SizedBox( + width: 80, + child: Text( + 'Statut', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ), + SizedBox( + width: 120, + child: Text( + 'Actions', + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + ), + ], + ), + ), + + // Table Body + Expanded( + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: categories.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: Colors.grey.shade200, + ), + itemBuilder: (context, index) { + final category = categories[index]; + return _buildCategoryRow(category, index); + }, + ), + ), + ], + ), + ); + } + + Widget _buildMobileList() { + return ListView.separated( + itemCount: categories.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final category = categories[index]; + return _buildMobileCategoryCard(category, index); + }, + ); + } + + Widget _buildMobileCategoryCard(Category category, int index) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + category.nom, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: category.actif ? Colors.green.shade100 : Colors.red.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + category.actif ? 'Actif' : 'Inactif', + style: TextStyle( + color: category.actif ? Colors.green.shade700 : Colors.red.shade700, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + category.description, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 14, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Ordre: ${category.ordre}', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => _moveCategory(index, -1), + icon: Icon( + Icons.keyboard_arrow_up, + color: index > 0 ? Colors.grey.shade600 : Colors.grey.shade300, + ), + ), + IconButton( + onPressed: () => _moveCategory(index, 1), + icon: Icon( + Icons.keyboard_arrow_down, + color: index < categories.length - 1 + ? Colors.grey.shade600 + : Colors.grey.shade300, + ), + ), + IconButton( + onPressed: () => _editCategory(category), + icon: Icon( + Icons.edit_outlined, + color: Colors.blue.shade600, + ), + ), + IconButton( + onPressed: () => _confirmDeleteCategory(category), + icon: Icon( + Icons.delete_outline, + color: Colors.red.shade400, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildCategoryRow(Category category, int index) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + // Ordre avec flèches + SizedBox( + width: 80, + child: Column( + children: [ + Text( + '${category.ordre}', + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => _moveCategory(index, -1), + child: Icon( + Icons.keyboard_arrow_up, + size: 16, + color: index > 0 ? Colors.grey.shade600 : Colors.grey.shade300, + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: () => _moveCategory(index, 1), + child: Icon( + Icons.keyboard_arrow_down, + size: 16, + color: index < categories.length - 1 + ? Colors.grey.shade600 + : Colors.grey.shade300, + ), + ), + ], + ), + ], + ), + ), + + // Nom + Expanded( + flex: 2, + child: Text( + category.nom, + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, + ), + overflow: TextOverflow.ellipsis, + ), + ), + + // Description + Expanded( + flex: 3, + child: Text( + category.description, + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + ), + + // Statut + SizedBox( + width: 80, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: category.actif ? Colors.green.shade100 : Colors.red.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + category.actif ? 'Actif' : 'Inactif', + style: TextStyle( + color: category.actif ? Colors.green.shade700 : Colors.red.shade700, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ), + + // Actions + SizedBox( + width: 120, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () => _editCategory(category), + icon: Icon( + Icons.edit_outlined, + size: 18, + color: Colors.blue.shade600, + ), + tooltip: 'Modifier', + ), + IconButton( + onPressed: () => _confirmDeleteCategory(category), + icon: Icon( + Icons.delete_outline, + size: 18, + color: Colors.red.shade400, + ), + tooltip: 'Supprimer', + ), + ], + ), + ), + ], + ), + ); + } + + void _moveCategory(int index, int direction) async { + if ((direction == -1 && index > 0) || + (direction == 1 && index < categories.length - 1)) { + + final category1 = categories[index]; + final category2 = categories[index + direction]; + + // Échanger les ordres + final tempOrdre = category1.ordre; + final updatedCategory1 = category1.copyWith(ordre: category2.ordre); + final updatedCategory2 = category2.copyWith(ordre: tempOrdre); + + // Mettre à jour sur le serveur + await _updateCategory(updatedCategory1); + await _updateCategory(updatedCategory2); + } + } + + void _editCategory(Category category) { + _showEditCategoryDialog(category); + } + + void _confirmDeleteCategory(Category category) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Supprimer la catégorie'), + content: Text('Êtes-vous sûr de vouloir supprimer "${category.nom}" ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + _deleteCategory(category.id!); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Supprimer'), + ), + ], + ), + ); + } + + void _showAddCategoryDialog() { + _showCategoryDialog(); + } + + void _showEditCategoryDialog(Category category) { + _showCategoryDialog(category: category); + } + + void _showCategoryDialog({Category? category}) { + final nomController = TextEditingController(text: category?.nom ?? ''); + final descriptionController = TextEditingController(text: category?.description ?? ''); + final ordreController = TextEditingController(text: (category?.ordre ?? (categories.length + 1)).toString()); + bool actif = category?.actif ?? true; + + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(category == null ? 'Nouvelle catégorie' : 'Modifier la catégorie'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nomController, + decoration: const InputDecoration( + labelText: 'Nom de la catégorie', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: descriptionController, + decoration: const InputDecoration( + labelText: 'Description', + border: OutlineInputBorder(), + ), + maxLines: 3, + ), + const SizedBox(height: 16), + TextField( + controller: ordreController, + decoration: const InputDecoration( + labelText: 'Ordre', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: actif, + onChanged: (value) { + setDialogState(() { + actif = value ?? true; + }); + }, + ), + const Text('Catégorie active'), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + if (nomController.text.isNotEmpty) { + final newCategory = Category( + id: category?.id, + nom: nomController.text.trim(), + description: descriptionController.text.trim(), + ordre: int.tryParse(ordreController.text) ?? 1, + actif: actif, + ); + + if (category == null) { + _createCategory(newCategory); + } else { + _updateCategory(newCategory); + } + + Navigator.pop(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + ), + child: Text(category == null ? 'Ajouter' : 'Modifier'), + ), + ], + ), + ), + ); + } +} + +class Category { + final int? id; + final String nom; + final String description; + final int ordre; + final bool actif; + + Category({ + this.id, + required this.nom, + required this.description, + required this.ordre, + required this.actif, + }); + + factory Category.fromJson(Map json) { + return Category( + id: json['id'] is int ? json['id'] : int.tryParse(json['id'].toString()) ?? 0, + nom: json['nom']?.toString() ?? '', + description: json['description']?.toString() ?? '', + ordre: json['ordre'] is int ? json['ordre'] : int.tryParse(json['ordre'].toString()) ?? 1, + actif: json['actif'] is bool ? json['actif'] : (json['actif'].toString().toLowerCase() == 'true'), + ); + } + + Map toJson() { + final map = { + 'nom': nom, + 'description': description, + 'ordre': ordre, + 'actif': actif, + }; + + // N'inclure l'ID que s'il existe (pour les mises à jour) + if (id != null) { + map['id'] = id; + } + + return map; + } + + Category copyWith({ + int? id, + String? nom, + String? description, + int? ordre, + bool? actif, + }) { + return Category( + id: id ?? this.id, + nom: nom ?? this.nom, + description: description ?? this.description, + ordre: ordre ?? this.ordre, + actif: actif ?? this.actif, + ); + } +} \ No newline at end of file