You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
695 lines
23 KiB
695 lines
23 KiB
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
|
|
// Import de la page de validation (à ajuster selon votre structure de dossiers)
|
|
import 'commande_item_validation.dart';
|
|
|
|
class AddItemsToOrderPage extends StatefulWidget {
|
|
final int commandeId;
|
|
final String numeroCommande;
|
|
|
|
const AddItemsToOrderPage({
|
|
Key? key,
|
|
required this.commandeId,
|
|
required this.numeroCommande
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<AddItemsToOrderPage> createState() => _AddItemsToOrderPageState();
|
|
}
|
|
|
|
class _AddItemsToOrderPageState extends State<AddItemsToOrderPage> {
|
|
int? _selectedCategory;
|
|
List<dynamic> _categories = [];
|
|
List<dynamic> _menus = [];
|
|
List<dynamic> _cart = [];
|
|
bool _isLoading = false;
|
|
Map<String, dynamic>? _commandeDetails;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
fetchCategories();
|
|
fetchCommandeDetails();
|
|
}
|
|
|
|
Future<void> fetchCategories() async {
|
|
try {
|
|
final url = Uri.parse(
|
|
"https://restaurant.careeracademy.mg/api/menu-categories",
|
|
);
|
|
final response = await http.get(url);
|
|
|
|
if (response.statusCode == 200) {
|
|
final jsonResponse = json.decode(response.body);
|
|
final categoriesList =
|
|
(jsonResponse['data']?['categories'] ?? []) as List<dynamic>;
|
|
|
|
setState(() {
|
|
_categories = categoriesList;
|
|
if (_categories.isNotEmpty) {
|
|
_selectedCategory = _categories[0]['id'];
|
|
fetchMenus(_selectedCategory!);
|
|
}
|
|
});
|
|
} else {
|
|
print("Erreur API catégories: ${response.statusCode}");
|
|
}
|
|
} catch (e) {
|
|
print("Exception fetchCategories: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> fetchMenus(int categoryId) async {
|
|
try {
|
|
final url = Uri.parse(
|
|
"https://restaurant.careeracademy.mg/api/menus/category/$categoryId?disponible=true",
|
|
);
|
|
final response = await http.get(url);
|
|
|
|
if (response.statusCode == 200) {
|
|
final jsonResponse = json.decode(response.body);
|
|
final List<dynamic> menusList =
|
|
jsonResponse is List ? jsonResponse : (jsonResponse['data'] ?? []);
|
|
|
|
setState(() {
|
|
_menus = menusList;
|
|
});
|
|
} else {
|
|
print("Erreur API menus: ${response.statusCode}");
|
|
}
|
|
} catch (e) {
|
|
print("Exception fetchMenus: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> fetchCommandeDetails() async {
|
|
try {
|
|
final url = Uri.parse("https://restaurant.careeracademy.mg/api/commandes/${widget.commandeId}");
|
|
final response = await http.get(url);
|
|
|
|
if (response.statusCode == 200) {
|
|
final jsonResponse = json.decode(response.body);
|
|
final data = jsonResponse['data'];
|
|
print(data);
|
|
|
|
setState(() {
|
|
_commandeDetails = data;
|
|
});
|
|
} else {
|
|
print("Erreur chargement commande: ${response.statusCode}");
|
|
}
|
|
} catch (e) {
|
|
print("Exception fetchCommandeDetails: $e");
|
|
}
|
|
}
|
|
|
|
void changeCategory(int id) {
|
|
setState(() {
|
|
_selectedCategory = id;
|
|
_menus = [];
|
|
});
|
|
fetchMenus(id);
|
|
}
|
|
|
|
void addToCart(dynamic item, int quantity, String notes) {
|
|
setState(() {
|
|
for (int i = 0; i < quantity; i++) {
|
|
Map<String, dynamic> cartItem = Map<String, dynamic>.from(item);
|
|
if (notes.isNotEmpty) {
|
|
cartItem['notes'] = notes;
|
|
}
|
|
_cart.add(cartItem);
|
|
}
|
|
});
|
|
}
|
|
|
|
void showAddToCartModal(dynamic item) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AddToCartModal(item: item, onAddToCart: addToCart);
|
|
},
|
|
);
|
|
}
|
|
|
|
// NOUVELLE FONCTION: Navigation vers la page de validation
|
|
Future<void> _navigateToValidationPage() async {
|
|
if (_cart.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Aucun article à ajouter'),
|
|
backgroundColor: Colors.orange,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Naviguer vers la page de validation
|
|
final result = await Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
builder: (context) => ValidateAddItemsPage(
|
|
commandeId: widget.commandeId,
|
|
numeroCommande: widget.numeroCommande,
|
|
newItems: _cart,
|
|
commandeDetails: _commandeDetails,
|
|
),
|
|
),
|
|
);
|
|
|
|
// Si l'ajout a été effectué avec succès, retourner à la page précédente
|
|
if (result == true) {
|
|
Navigator.of(context).pop(true);
|
|
}
|
|
}
|
|
|
|
/// Conversion sécurisée du prix en string avec 2 décimales
|
|
String formatPrix(dynamic prix) {
|
|
if (prix == null) return "";
|
|
double? val;
|
|
if (prix is num) {
|
|
val = prix.toDouble();
|
|
} else if (prix is String) {
|
|
val = double.tryParse(prix);
|
|
}
|
|
return val != null ? val.toStringAsFixed(2) : "";
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text("Ajouter des articles"),
|
|
backgroundColor: Colors.white,
|
|
elevation: 1,
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () {
|
|
if (_selectedCategory != null) fetchMenus(_selectedCategory!);
|
|
},
|
|
icon: const Icon(Icons.refresh),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header avec info commande
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16.0),
|
|
color: Colors.grey[50],
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Commande: ${widget.numeroCommande}",
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
const Text(
|
|
"Sélectionnez les articles à ajouter",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
if (_commandeDetails != null && _commandeDetails!['items'] != null)
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Articles déjà commandés:",
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
..._commandeDetails!['items'].map<Widget>((item) {
|
|
final nom = item['menu_nom'] ?? 'Inconnu';
|
|
final quantite = item['quantite'] ?? 1;
|
|
final description = item['menu_description'] ?? '';
|
|
// Correction: utiliser 'menu_prix_actuel' pour afficher le prix
|
|
final prix = item['menu_prix_actuel'] ?? item['prix_unitaire'] ?? 0;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
nom,
|
|
style: const TextStyle(fontSize: 14)
|
|
),
|
|
),
|
|
Text(
|
|
"x$quantite - ${formatPrix(prix)} MGA",
|
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)
|
|
),
|
|
],
|
|
),
|
|
if (description.isNotEmpty)
|
|
Text(
|
|
description,
|
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
|
),
|
|
const Divider(height: 8),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
],
|
|
)
|
|
else
|
|
const Text(
|
|
"Aucun article encore commandé.",
|
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Catégories
|
|
if (_categories.isNotEmpty)
|
|
Container(
|
|
color: Colors.white,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: _categories.map<Widget>((cat) {
|
|
return buildCategoryButton(cat['nom'], cat['id']);
|
|
}).toList(),
|
|
),
|
|
)
|
|
else
|
|
const Center(child: CircularProgressIndicator()),
|
|
|
|
// Liste des menus
|
|
Expanded(
|
|
child: _menus.isNotEmpty
|
|
? ListView.builder(
|
|
itemCount: _menus.length,
|
|
itemBuilder: (context, index) {
|
|
final item = _menus[index];
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
child: ListTile(
|
|
onTap: () => showAddToCartModal(item),
|
|
leading: Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.restaurant_menu,
|
|
color: Colors.green[700],
|
|
size: 30,
|
|
),
|
|
),
|
|
title: Text(
|
|
item['nom'] ?? 'Nom non disponible',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
subtitle: Text(item['commentaire'] ?? ''),
|
|
trailing: Text(
|
|
"${formatPrix(item['prix'])} MGA",
|
|
style: TextStyle(
|
|
color: Colors.green[700],
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: const Center(child: Text("Aucun menu disponible")),
|
|
),
|
|
|
|
// Bouton pour valider l'ajout - MODIFIÉ
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
color: Colors.white,
|
|
child: Column(
|
|
children: [
|
|
if (_cart.isNotEmpty)
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Text(
|
|
"${_cart.length} article(s) sélectionné(s)",
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: _cart.isEmpty ? null : _navigateToValidationPage, // CHANGÉ ICI
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green[700],
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
disabledBackgroundColor: Colors.grey[300], // Ajouté pour le style désactivé
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_cart.isEmpty ? Icons.shopping_cart_outlined : Icons.preview,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
_cart.isEmpty
|
|
? "Sélectionner des articles"
|
|
: "Valider l'ajout (${_cart.length})", // CHANGÉ LE TEXTE
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildCategoryButton(String label, int id) {
|
|
final selected = _selectedCategory == id;
|
|
return Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => changeCategory(id),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: selected ? Colors.green[700] : Colors.grey[100],
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: selected ? Colors.green[700]! : Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
|
|
color: selected ? Colors.white : Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Modal pour ajouter au panier (même que MenuPage)
|
|
class AddToCartModal extends StatefulWidget {
|
|
final dynamic item;
|
|
final Function(dynamic, int, String) onAddToCart;
|
|
|
|
const AddToCartModal({
|
|
Key? key,
|
|
required this.item,
|
|
required this.onAddToCart,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<AddToCartModal> createState() => _AddToCartModalState();
|
|
}
|
|
|
|
class _AddToCartModalState extends State<AddToCartModal> {
|
|
int _quantity = 1;
|
|
final TextEditingController _notesController = TextEditingController();
|
|
|
|
String formatPrix(dynamic prix) {
|
|
if (prix == null) return "0.00";
|
|
double? val;
|
|
if (prix is num) {
|
|
val = prix.toDouble();
|
|
} else if (prix is String) {
|
|
val = double.tryParse(prix);
|
|
}
|
|
return val != null ? val.toStringAsFixed(2) : "0.00";
|
|
}
|
|
|
|
double calculateTotal() {
|
|
double prix = 0.0;
|
|
if (widget.item['prix'] is num) {
|
|
prix = widget.item['prix'].toDouble();
|
|
} else if (widget.item['prix'] is String) {
|
|
prix = double.tryParse(widget.item['prix']) ?? 0.0;
|
|
}
|
|
return prix * _quantity;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
constraints: const BoxConstraints(maxWidth: 400),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.item['nom'] ?? 'Menu',
|
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: const Icon(Icons.close),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Image placeholder
|
|
Container(
|
|
width: double.infinity,
|
|
height: 150,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green[100],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
Icons.restaurant_menu,
|
|
size: 60,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Description
|
|
if (widget.item['commentaire'] != null &&
|
|
widget.item['commentaire'].toString().isNotEmpty)
|
|
Text(
|
|
widget.item['commentaire'],
|
|
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Prix unitaire
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Prix unitaire",
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
Text(
|
|
"${formatPrix(widget.item['prix'])} MGA",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Quantité
|
|
const Text(
|
|
"Quantité",
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
onPressed: _quantity > 1
|
|
? () {
|
|
setState(() {
|
|
_quantity--;
|
|
});
|
|
}
|
|
: null,
|
|
icon: const Icon(Icons.remove),
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.grey[200],
|
|
),
|
|
),
|
|
const SizedBox(width: 20),
|
|
Text(
|
|
_quantity.toString(),
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(width: 20),
|
|
IconButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_quantity++;
|
|
});
|
|
},
|
|
icon: const Icon(Icons.add),
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.grey[200],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Notes
|
|
const Text(
|
|
"Notes (optionnel)",
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
controller: _notesController,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: "Commentaires spéciaux (sans oignons, bien cuit...)",
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
contentPadding: const EdgeInsets.all(12),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// Total et bouton d'ajout
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[50],
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Total",
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
"${calculateTotal().toStringAsFixed(2)} MGA",
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
widget.onAddToCart(
|
|
widget.item,
|
|
_quantity,
|
|
_notesController.text,
|
|
);
|
|
Navigator.of(context).pop();
|
|
|
|
// Afficher un snackbar de confirmation
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
"${widget.item['nom']} ajouté au panier",
|
|
),
|
|
backgroundColor: Colors.green,
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green[700],
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text(
|
|
"Ajouter au panier",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|