3 changed files with 1290 additions and 0 deletions
|
After Width: | Height: | Size: 239 KiB |
@ -0,0 +1,421 @@ |
|||
import 'package:flutter/material.dart'; |
|||
|
|||
class CartPage extends StatefulWidget { |
|||
final int tableId; |
|||
final int personne; |
|||
final List<dynamic> cartItems; |
|||
|
|||
const CartPage({ |
|||
Key? key, |
|||
required this.tableId, |
|||
required this.personne, |
|||
required this.cartItems, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
State<CartPage> createState() => _CartPageState(); |
|||
} |
|||
|
|||
class _CartPageState extends State<CartPage> { |
|||
List<CartItemModel> _cartItems = []; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_processCartItems(); |
|||
} |
|||
|
|||
void _processCartItems() { |
|||
// Grouper les articles identiques |
|||
Map<String, CartItemModel> 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, |
|||
}); |
|||
} |
|||
@ -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<CategoriesPage> createState() => _CategoriesPageState(); |
|||
} |
|||
|
|||
class _CategoriesPageState extends State<CategoriesPage> { |
|||
List<Category> categories = []; |
|||
bool isLoading = true; |
|||
String? error; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_loadCategories(); |
|||
} |
|||
|
|||
Future<void> _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<String, dynamic>)) |
|||
.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<void> _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<void> _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<void> _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<String, dynamic> 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<String, dynamic> toJson() { |
|||
final map = <String, dynamic>{ |
|||
'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, |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue