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 { // On essaie de décoder le corps pour extraire un message clair String message; try { final Map body = jsonDecode(response.body); message = body['message'] ?? body['error'] ?? response.body; } catch (_) { // Le corps n'est pas en JSON ou est vide message = response.body.isNotEmpty ? response.body : 'Statut ${response.statusCode} sans contenu'; } throw Exception('($message)'); } } 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, ); } }