Browse Source

push final

master
andrymodeste 4 months ago
parent
commit
cccbe0d684
  1. 88
      lib/models/menus.dart
  2. 337
      lib/pages/PlatEdit_screen.dart
  3. 16
      lib/pages/menu.dart
  4. 774
      lib/pages/menus_screen.dart

88
lib/models/menus.dart

@ -0,0 +1,88 @@
class MenuCategory {
final int id;
final String nom;
final String? description;
final int? ordre;
final bool actif;
MenuCategory({
required this.id,
required this.nom,
this.description,
this.ordre,
required this.actif,
});
factory MenuCategory.fromJson(Map<String, dynamic> json) {
return MenuCategory(
id: json['id'],
nom: json['nom'],
description: json['description'],
ordre: json['ordre'],
actif: json['actif'] ?? true,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MenuCategory &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
class MenuPlat {
final int id;
final String nom;
final String? commentaire;
final double prix;
final String? ingredients;
final String? imageUrl;
final bool disponible;
final MenuCategory? category; // Single category pour la compatibilité API
final List<MenuCategory>? categories; // Multiple categories si besoin
MenuPlat({
required this.id,
required this.nom,
this.commentaire,
required this.prix,
this.ingredients,
this.imageUrl,
required this.disponible,
this.category,
this.categories,
});
factory MenuPlat.fromJson(Map<String, dynamic> json) {
double parsePrix(dynamic p) {
if (p is int) return p.toDouble();
if (p is double) return p;
if (p is String) return double.tryParse(p) ?? 0;
return 0;
}
return MenuPlat(
id: json['id'],
nom: json['nom'],
commentaire: json['commentaire'],
prix: parsePrix(json['prix']),
ingredients: json['ingredients'],
imageUrl: json['image_url'],
disponible: json['disponible'] ?? true,
// Support pour single category (API actuelle)
category: json['category'] != null
? MenuCategory.fromJson(json['category'])
: null,
// Support pour multiple categories si l'API évolue
categories: json['categories'] != null
? (json['categories'] as List)
.map((c) => MenuCategory.fromJson(c))
.toList()
: null,
);
}
}

337
lib/pages/PlatEdit_screen.dart

@ -1,62 +1,9 @@
import 'dart:convert';
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
// Use your actual models and http logic import '../models/menus.dart';
class MenuPlat {
final int id;
final String nom;
final String? commentaire;
final double prix;
final String? imageUrl;
final bool disponible;
final List<MenuCategory> categories; // Default to true
MenuPlat({
required this.id,
required this.nom,
this.commentaire,
required this.prix,
this.imageUrl,
required this.disponible,
required this.categories,
});
factory MenuPlat.fromJson(Map<String, dynamic> json) {
return MenuPlat(
id: json['id'],
nom: json['nom'],
commentaire: json['commentaire'],
prix: (json['prix'] as num).toDouble(),
imageUrl: json['image_url'],
disponible: json['disponible'] ?? true,
categories:
(json['categories'] as List)
.map((c) => MenuCategory.fromJson(c))
.toList(),
);
}
}
class MenuCategory {
final int id;
final String nom;
MenuCategory({required this.id, required this.nom});
factory MenuCategory.fromJson(Map<String, dynamic> json) {
return MenuCategory(id: json['id'], nom: json['nom']);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MenuCategory &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
class PlatEditPage extends StatefulWidget { class PlatEditPage extends StatefulWidget {
final List<MenuCategory> categories; final List<MenuCategory> categories;
@ -76,9 +23,9 @@ class PlatEditPage extends StatefulWidget {
class _PlatEditPageState extends State<PlatEditPage> { class _PlatEditPageState extends State<PlatEditPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
late TextEditingController nomCtrl, descCtrl, prixCtrl, imgCtrl; late TextEditingController nomCtrl, descCtrl, prixCtrl, ingredientsCtrl;
late bool disponible; late bool disponible;
late List<MenuCategory> selectedCategories; MenuCategory? selectedCategory; // Une seule catégorie pour correspondre à l'API
@override @override
void initState() { void initState() {
@ -88,9 +35,14 @@ class _PlatEditPageState extends State<PlatEditPage> {
prixCtrl = TextEditingController( prixCtrl = TextEditingController(
text: widget.plat != null ? widget.plat!.prix.toStringAsFixed(2) : "", text: widget.plat != null ? widget.plat!.prix.toStringAsFixed(2) : "",
); );
imgCtrl = TextEditingController(text: widget.plat?.imageUrl ?? ""); ingredientsCtrl = TextEditingController(text: widget.plat?.ingredients ?? "");
disponible = widget.plat?.disponible ?? true; disponible = widget.plat?.disponible ?? true;
selectedCategories = widget.plat?.categories.toList() ?? [];
// Utiliser la catégorie unique ou la première des multiples
selectedCategory = widget.plat?.category ??
(widget.plat?.categories?.isNotEmpty == true
? widget.plat!.categories!.first
: null);
} }
@override @override
@ -98,37 +50,43 @@ class _PlatEditPageState extends State<PlatEditPage> {
nomCtrl.dispose(); nomCtrl.dispose();
descCtrl.dispose(); descCtrl.dispose();
prixCtrl.dispose(); prixCtrl.dispose();
imgCtrl.dispose(); ingredientsCtrl.dispose();
super.dispose(); super.dispose();
} }
void submit() async { Future<void> submit() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
if (selectedCategories.isEmpty) { if (selectedCategory == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sélectionnez au moins une catégorie.')), const SnackBar(content: Text('Sélectionnez une catégorie.')),
); );
return; return;
} }
// Build request data // Build request data selon votre API
final body = { final body = {
"nom": nomCtrl.text, "nom": nomCtrl.text,
"commentaire": descCtrl.text, "commentaire": descCtrl.text,
"ingredients": ingredientsCtrl.text,
"prix": double.tryParse(prixCtrl.text) ?? 0, "prix": double.tryParse(prixCtrl.text) ?? 0,
"categories": selectedCategories.map((c) => c.id).toList(), "categorie_id": selectedCategory!.id, // L'API attend categorie_id
"disponible": disponible, "disponible": disponible,
"categorie_id":
selectedCategories.isNotEmpty
? selectedCategories.first.id
: null, // Use first category if available
}; };
try { try {
final res = await http.post( final isEdit = widget.plat != null;
Uri.parse( final url = isEdit
'https://restaurant.careeracademy.mg/api/menus', ? 'https://restaurant.careeracademy.mg/api/menus/${widget.plat!.id}'
), // your API URL here : 'https://restaurant.careeracademy.mg/api/menus';
final res = isEdit
? await http.put(
Uri.parse(url),
headers: {"Content-Type": "application/json"},
body: json.encode(body),
)
: await http.post(
Uri.parse(url),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: json.encode(body), body: json.encode(body),
); );
@ -136,18 +94,28 @@ class _PlatEditPageState extends State<PlatEditPage> {
if (res.statusCode == 200 || res.statusCode == 201) { if (res.statusCode == 200 || res.statusCode == 201) {
widget.onSaved?.call(); widget.onSaved?.call();
Navigator.pop(context); Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEdit ? 'Plat modifié avec succès' : 'Plat créé avec succès'),
backgroundColor: Colors.green,
),
);
} else { } else {
// show error print('Error: ${res.body}\nBody sent: $body');
print('Error creating plat: ${res.body}\n here is body: $body');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la création du plat')), SnackBar(
content: Text('Erreur lors de ${isEdit ? "la modification" : "la création"} du plat'),
backgroundColor: Colors.red,
),
); );
} }
} catch (e) { } catch (e) {
// show error ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of( SnackBar(
context, content: Text('Erreur réseau: $e'),
).showSnackBar(SnackBar(content: Text('Erreur réseau: $e'))); backgroundColor: Colors.red,
),
);
} }
} }
@ -155,6 +123,7 @@ class _PlatEditPageState extends State<PlatEditPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEdit = widget.plat != null; final isEdit = widget.plat != null;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xfffcfbf9),
appBar: AppBar( appBar: AppBar(
title: Text(isEdit ? 'Éditer le plat' : 'Nouveau plat'), title: Text(isEdit ? 'Éditer le plat' : 'Nouveau plat'),
leading: IconButton( leading: IconButton(
@ -163,8 +132,8 @@ class _PlatEditPageState extends State<PlatEditPage> {
), ),
centerTitle: true, centerTitle: true,
elevation: 0, elevation: 0,
backgroundColor: Colors.white, backgroundColor: Colors.transparent,
foregroundColor: Colors.black, foregroundColor: Colors.black87,
), ),
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
@ -179,51 +148,84 @@ class _PlatEditPageState extends State<PlatEditPage> {
padding: const EdgeInsets.all(28), padding: const EdgeInsets.all(28),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'Informations du plat', isEdit ? 'Modifier le plat' : 'Nouveau plat',
style: TextStyle( style: const TextStyle(
fontSize: 21, fontSize: 21,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 14), const SizedBox(height: 20),
// Nom du plat
TextFormField( TextFormField(
controller: nomCtrl, controller: nomCtrl,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Nom du plat *", labelText: "Nom du plat *",
hintText: "Ex: Steak frites", hintText: "Ex: Steak frites",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
), ),
validator: validator: (v) => (v == null || v.isEmpty) ? "Obligatoire" : null,
(v) =>
(v == null || v.isEmpty) ? "Obligatoire" : null,
), ),
const SizedBox(height: 13), const SizedBox(height: 16),
// Description
TextFormField( TextFormField(
controller: descCtrl, controller: descCtrl,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Description", labelText: "Description",
hintText: "Description détaillée du plat...", hintText: "Description détaillée du plat...",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
), ),
maxLines: 3, maxLines: 3,
), ),
const SizedBox(height: 13), const SizedBox(height: 16),
// Ingrédients
TextFormField(
controller: ingredientsCtrl,
decoration: const InputDecoration(
labelText: "Ingrédients",
hintText: "Liste des ingrédients...",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
maxLines: 2,
),
const SizedBox(height: 16),
Row( Row(
children: [ children: [
// Prix
Expanded( Expanded(
child: TextFormField( child: TextFormField(
controller: prixCtrl, controller: prixCtrl,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Prix (MGA) *", labelText: "Prix (MGA) *",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
validator: filled: true,
(v) => fillColor: Color(0xFFF7F7F7),
(v == null || ),
v.isEmpty || validator: (v) =>
double.tryParse(v) == null) (v == null || v.isEmpty || double.tryParse(v) == null)
? "Obligatoire" ? "Obligatoire"
: null, : null,
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
@ -232,93 +234,36 @@ class _PlatEditPageState extends State<PlatEditPage> {
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
// Multi-category picker
Expanded(
child: InkWell(
onTap: () async {
final result = await showDialog<MenuCategory?>(
context: context,
builder: (context) {
MenuCategory? temp =
selectedCategories.isNotEmpty
? selectedCategories.first
: null;
return StatefulBuilder( // Catégorie
builder: (context, setStateDialog) { Expanded(
return AlertDialog( child: DropdownButtonFormField<MenuCategory>(
title: const Text("Catégories"), value: selectedCategory,
content: SizedBox(
width: 330,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children:
widget.categories.map((cat) {
return RadioListTile<
MenuCategory
>(
value: cat,
groupValue: temp,
title: Text(cat.nom),
onChanged: (value) {
setStateDialog(() {
temp = value;
});
},
);
}).toList(),
),
),
),
actions: [
TextButton(
onPressed: () {
if (temp != null) {
Navigator.pop(context, temp);
} else {
Navigator.pop(context, null);
}
},
child: const Text('OK'),
),
],
);
},
);
},
);
if (result != null) {
setState(() => selectedCategories = [result]);
}
},
child: InputDecorator(
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: "Catégorie *", labelText: "Catégorie *",
border: OutlineInputBorder(gapPadding: 2), border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
child: filled: true,
selectedCategories.isEmpty fillColor: Color(0xFFF7F7F7),
? const Text(
"Aucune sélection",
style: TextStyle(color: Colors.grey),
)
: Text(selectedCategories.first.nom),
), ),
validator: (v) => v == null ? "Obligatoire" : null,
items: widget.categories.map((cat) {
return DropdownMenuItem(
value: cat,
child: Text(cat.nom),
);
}).toList(),
onChanged: (value) {
setState(() => selectedCategory = value);
},
), ),
), ),
], ],
), ),
// const SizedBox(height: 13), const SizedBox(height: 20),
// TextFormField(
// controller: imgCtrl, // Switch disponibilité
// decoration: const InputDecoration(
// labelText: "URL de l'image",
// hintText: "/api/placeholder/300/200",
// ),
// ),
const SizedBox(height: 13),
Row( Row(
children: [ children: [
Switch( Switch(
@ -326,36 +271,51 @@ class _PlatEditPageState extends State<PlatEditPage> {
activeColor: Colors.green, activeColor: Colors.green,
onChanged: (v) => setState(() => disponible = v), onChanged: (v) => setState(() => disponible = v),
), ),
const SizedBox(width: 7), const SizedBox(width: 8),
const Text('Plat disponible'), Text(
'Plat disponible',
style: TextStyle(
fontSize: 16,
color: disponible ? Colors.black87 : Colors.grey,
),
),
], ],
), ),
const SizedBox(height: 22), const SizedBox(height: 30),
// Boutons
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
OutlinedButton( OutlinedButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text( child: const Text(
"Annuler", "Annuler",
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black54),
), ),
), ),
const SizedBox(width: 18), const SizedBox(width: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, backgroundColor: Colors.green[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
), ),
onPressed: submit, shape: RoundedRectangleBorder(
icon: const Icon( borderRadius: BorderRadius.circular(6),
Icons.save,
size: 18,
color: Colors.white,
), ),
label: Text(
isEdit ? "Enregistrer" : "Ajouter",
style: const TextStyle(color: Colors.white),
), ),
onPressed: submit,
icon: const Icon(Icons.save, size: 18),
label: Text(isEdit ? "Enregistrer" : "Créer le plat"),
), ),
], ],
), ),
@ -366,6 +326,7 @@ class _PlatEditPageState extends State<PlatEditPage> {
), ),
), ),
), ),
),
); );
} }
} }

16
lib/pages/menu.dart

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'dart:convert'; import 'dart:convert';
// Ajoutez cet import pour la page panier // Ajoutez cet import pour la page panier
@ -75,12 +76,19 @@ class _MenuPageState extends State<MenuPage> {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final jsonResponse = json.decode(response.body); final jsonResponse = json.decode(response.body);
print(jsonResponse);
final List<dynamic> menusList = final List<dynamic> menusList =
jsonResponse is List ? jsonResponse : (jsonResponse['data'] ?? []); jsonResponse is List ? jsonResponse : (jsonResponse['data'] ?? []);
// Filtrage côté client
final List<dynamic> menusDisponibles = menusList.where((menu) {
final disponible = menu['disponible'];
return disponible == true || disponible == 1 || disponible == '1' || disponible == 'true';
}).toList();
setState(() { setState(() {
_menus = menusList; _menus = menusDisponibles;
}); });
} else { } else {
if (kDebugMode) { if (kDebugMode) {
@ -225,7 +233,7 @@ class _MenuPageState extends State<MenuPage> {
), ),
subtitle: Text(item['commentaire'] ?? ''), subtitle: Text(item['commentaire'] ?? ''),
trailing: Text( trailing: Text(
"${formatPrix(item['prix'])} MGA", "${NumberFormat("#,##0.00", "fr_FR").format(double.tryParse(item['prix'].toString()) ?? 0.0)} MGA",
style: TextStyle( style: TextStyle(
color: Colors.green[700], color: Colors.green[700],
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -385,7 +393,7 @@ class _AddToCartModalState extends State<AddToCartModal> {
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
), ),
Text( Text(
"${formatPrix(widget.item['prix'])} MGA", "${NumberFormat("#,##0.00", "fr_FR").format(double.tryParse(widget.item['prix'].toString()) ?? 0.0)} MGA",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -483,7 +491,7 @@ class _AddToCartModalState extends State<AddToCartModal> {
), ),
), ),
Text( Text(
"${calculateTotal().toStringAsFixed(2)} MGA", "${NumberFormat("#,##0.00", "fr_FR").format(calculateTotal())} MGA",
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

774
lib/pages/menus_screen.dart

@ -2,8 +2,90 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import './PlatEdit_screen.dart'; import 'package:intl/intl.dart';
// ===== MODÈLES UNIFIÉS =====
class MenuCategory {
final int id;
final String nom;
final String? description;
final int? ordre;
final bool actif;
MenuCategory({
required this.id,
required this.nom,
this.description,
this.ordre,
required this.actif,
});
factory MenuCategory.fromJson(Map<String, dynamic> json) {
return MenuCategory(
id: json['id'],
nom: json['nom'],
description: json['description'],
ordre: json['ordre'],
actif: json['actif'] ?? true,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MenuCategory &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
class MenuPlat {
final int id;
final String nom;
final String? commentaire;
final double prix;
final String? ingredients;
final String? imageUrl;
final MenuCategory? category;
final bool disponible;
MenuPlat({
required this.id,
required this.nom,
this.commentaire,
required this.prix,
this.ingredients,
this.imageUrl,
this.category,
required this.disponible,
});
factory MenuPlat.fromJson(Map<String, dynamic> json) {
double parsePrix(dynamic p) {
if (p is int) return p.toDouble();
if (p is double) return p;
if (p is String) return double.tryParse(p) ?? 0;
return 0;
}
return MenuPlat(
id: json['id'],
nom: json['nom'],
commentaire: json['commentaire'],
prix: parsePrix(json['prix']),
ingredients: json['ingredients'],
imageUrl: json['image_url'],
category: json['category'] != null
? MenuCategory.fromJson(json['category'])
: null,
disponible: json['disponible'] ?? true,
);
}
}
// ===== ÉCRAN PRINCIPAL =====
class PlatsManagementScreen extends StatefulWidget { class PlatsManagementScreen extends StatefulWidget {
const PlatsManagementScreen({super.key}); const PlatsManagementScreen({super.key});
@ -34,6 +116,7 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
try { try {
final res = await http.get(Uri.parse('$_baseUrl/menu-categories')); final res = await http.get(Uri.parse('$_baseUrl/menu-categories'));
final data = json.decode(res.body); final data = json.decode(res.body);
print(data);
setState(() { setState(() {
categories = categories =
(data['data']['categories'] as List) (data['data']['categories'] as List)
@ -72,10 +155,65 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
} }
} }
// Nouvelle méthode pour changer la disponibilité
Future<void> _toggleDisponibilite(MenuPlat plat) async {
try {
final newDisponibilite = !plat.disponible;
final res = await http.put(
Uri.parse('$_baseUrl/menus/${plat.id}'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
"nom": plat.nom,
"commentaire": plat.commentaire,
"ingredients": plat.ingredients,
"prix": plat.prix,
"categorie_id": plat.category?.id,
"disponible": newDisponibilite,
}),
);
if (res.statusCode == 200) {
_fetchPlats(); // Recharger la liste
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
newDisponibilite
? '${plat.nom} est maintenant disponible'
: '${plat.nom} est maintenant indisponible'
),
backgroundColor: newDisponibilite ? Colors.green : Colors.orange,
),
);
} else {
if (kDebugMode) print("Erreur lors de la mise à jour: ${res.body}");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la mise à jour de la disponibilité'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
if (kDebugMode) print("Erreur: $e");
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur de connexion'),
backgroundColor: Colors.red,
),
);
}
}
Future<void> _deletePlat(int id) async { Future<void> _deletePlat(int id) async {
final res = await http.delete(Uri.parse('$_baseUrl/menus/$id')); final res = await http.delete(Uri.parse('$_baseUrl/menus/$id'));
if (res.statusCode == 200) { if (res.statusCode == 200) {
_fetchPlats(); _fetchPlats();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Plat supprimé avec succès'),
backgroundColor: Colors.green,
),
);
} else { } else {
if (kDebugMode) print("Error deleting plat: ${res.body}"); if (kDebugMode) print("Error deleting plat: ${res.body}");
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@ -88,8 +226,7 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
void _showEditPlatDialog(MenuPlat plat) { void _showEditPlatDialog(MenuPlat plat) {
showDialog( showDialog(
context: context, context: context,
builder: builder: (_) => EditPlatDialog(
(_) => EditPlatDialog(
plat: plat, plat: plat,
onPlatUpdated: _fetchPlats, onPlatUpdated: _fetchPlats,
categories: categories, categories: categories,
@ -113,8 +250,7 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
Padding( Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: onPressed: () => navigateToCreate(
() => navigateToCreate(
context, context,
categories, categories,
() => {_fetchPlats()}, () => {_fetchPlats()},
@ -212,6 +348,9 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
// Ajouter une opacité si le plat n'est pas disponible
child: Opacity(
opacity: p.disponible ? 1.0 : 0.6,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 16, vertical: 16,
@ -222,13 +361,51 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
children: [ children: [
Expanded( Expanded(
flex: 2, flex: 2,
child: Text( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
p.nom, p.nom,
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 18, fontSize: 18,
decoration: p.disponible
? TextDecoration.none
: TextDecoration.lineThrough,
),
),
const SizedBox(height: 4),
// Badge de statut
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: p.disponible
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: p.disponible
? Colors.green
: Colors.red,
width: 1,
), ),
), ),
child: Text(
p.disponible ? 'Disponible' : 'Indisponible',
style: TextStyle(
color: p.disponible
? Colors.green[700]
: Colors.red[700],
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
),
],
),
), ),
Expanded( Expanded(
flex: 6, flex: 6,
@ -271,19 +448,81 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
), ),
), ),
Expanded( Expanded(
flex: 2, flex: 3,
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
"${p.prix.toStringAsFixed(2)} MGA", "${NumberFormat("#,##0.00", "fr_FR").format(p.prix)} MGA",
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.green, color: Colors.green,
fontSize: 20, fontSize: 20,
), ),
), ),
const SizedBox(width: 10), const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton de disponibilité
SizedBox(
height: 38,
width: 38,
child: IconButton(
icon: Icon(
p.disponible
? Icons.visibility
: Icons.visibility_off,
color: p.disponible
? Colors.green
: Colors.red,
),
tooltip: p.disponible
? 'Rendre indisponible'
: 'Rendre disponible',
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(
p.disponible
? 'Rendre indisponible ?'
: 'Rendre disponible ?'
),
content: Text(
p.disponible
? '${p.nom} ne sera plus visible aux clients'
: '${p.nom} sera de nouveau visible aux clients'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: p.disponible
? Colors.orange
: Colors.green,
),
child: Text(
p.disponible
? 'Rendre indisponible'
: 'Rendre disponible',
style: const TextStyle(color: Colors.white),
),
),
],
),
);
if (confirm == true) {
_toggleDisponibilite(p);
}
},
),
),
SizedBox( SizedBox(
height: 38, height: 38,
width: 38, width: 38,
@ -306,8 +545,7 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
onPressed: () async { onPressed: () async {
final confirm = await showDialog( final confirm = await showDialog(
context: context, context: context,
builder: builder: (_) => AlertDialog(
(_) => AlertDialog(
title: const Text( title: const Text(
'Supprimer ce plat ?', 'Supprimer ce plat ?',
), ),
@ -316,23 +554,19 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: () => Navigator.pop(
() => Navigator.pop(
context, context,
false, false,
), ),
child: const Text("Annuler"), child: const Text("Annuler"),
), ),
ElevatedButton( ElevatedButton(
onPressed: onPressed: () => Navigator.pop(
() => Navigator.pop(
context, context,
true, true,
), ),
style: style: ElevatedButton.styleFrom(
ElevatedButton.styleFrom( backgroundColor: Colors.red,
backgroundColor:
Colors.red,
), ),
child: const Text( child: const Text(
"Supprimer", "Supprimer",
@ -350,10 +584,13 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
), ),
], ],
), ),
],
),
), ),
], ],
), ),
), ),
),
); );
}, },
), ),
@ -364,51 +601,12 @@ class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
} }
} }
class MenuPlat { // ===== COMPOSANTS =====
final int id;
final String nom;
final String? commentaire;
final double prix;
final String? ingredients;
final MenuCategory? category; // single category
MenuPlat({
required this.id,
required this.nom,
this.commentaire,
required this.prix,
this.ingredients,
this.category,
});
factory MenuPlat.fromJson(Map<String, dynamic> json) {
double parsePrix(dynamic p) {
if (p is int) return p.toDouble();
if (p is double) return p;
if (p is String) return double.tryParse(p) ?? 0;
return 0;
}
return MenuPlat(
id: json['id'],
nom: json['nom'],
commentaire: json['commentaire'],
prix: parsePrix(json['prix']),
ingredients: json['ingredients'],
category:
json['category'] != null
? MenuCategory.fromJson(json['category'])
: null,
);
}
get disponible => null;
}
class CategoryChip extends StatelessWidget { class CategoryChip extends StatelessWidget {
final String label; final String label;
final Color color; final Color color;
const CategoryChip({super.key, required this.label, required this.color}); const CategoryChip({super.key, required this.label, required this.color});
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) => Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -424,23 +622,12 @@ class CategoryChip extends StatelessWidget {
); );
} }
Future<void> navigateToCreate( // ===== DIALOGUE D'ÉDITION =====
BuildContext context,
List<MenuCategory> categories,
Function()? onSaved,
) async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlatEditPage(categories: categories, onSaved: onSaved),
),
);
}
class EditPlatDialog extends StatefulWidget { class EditPlatDialog extends StatefulWidget {
final MenuPlat plat; final MenuPlat plat;
final List<MenuCategory> categories; final List<MenuCategory> categories;
final VoidCallback onPlatUpdated; final VoidCallback onPlatUpdated;
const EditPlatDialog({ const EditPlatDialog({
super.key, super.key,
required this.plat, required this.plat,
@ -457,7 +644,7 @@ class _EditPlatDialogState extends State<EditPlatDialog> {
late String commentaire; late String commentaire;
late String ingredients; late String ingredients;
late double prix; late double prix;
// late bool disponible; late bool disponible;
MenuCategory? cat; MenuCategory? cat;
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
@ -468,12 +655,14 @@ class _EditPlatDialogState extends State<EditPlatDialog> {
commentaire = widget.plat.commentaire ?? ''; commentaire = widget.plat.commentaire ?? '';
ingredients = widget.plat.ingredients ?? ''; ingredients = widget.plat.ingredients ?? '';
prix = widget.plat.prix; prix = widget.plat.prix;
// cat = (widget.plat.categories) as MenuCategory?; disponible = widget.plat.disponible;
cat = widget.plat.category; cat = widget.plat.category;
} }
Future<void> submit() async { Future<void> submit() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
try {
final res = await http.put( final res = await http.put(
Uri.parse( Uri.parse(
'https://restaurant.careeracademy.mg/api/menus/${widget.plat.id}', 'https://restaurant.careeracademy.mg/api/menus/${widget.plat.id}',
@ -485,12 +674,38 @@ class _EditPlatDialogState extends State<EditPlatDialog> {
"ingredients": ingredients, "ingredients": ingredients,
"prix": prix, "prix": prix,
"categorie_id": cat?.id, "categorie_id": cat?.id,
"disponible": disponible,
}), }),
); );
if (res.statusCode == 200) { if (res.statusCode == 200) {
widget.onPlatUpdated(); widget.onPlatUpdated();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.pop(context); Navigator.pop(context);
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Plat modifié avec succès'),
backgroundColor: Colors.green,
),
);
} else {
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${res.body}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur réseau: $e'),
backgroundColor: Colors.red,
),
);
} }
} }
@ -509,37 +724,63 @@ class _EditPlatDialogState extends State<EditPlatDialog> {
decoration: const InputDecoration(labelText: "Nom"), decoration: const InputDecoration(labelText: "Nom"),
validator: (v) => (v?.isEmpty ?? true) ? "Obligatoire" : null, validator: (v) => (v?.isEmpty ?? true) ? "Obligatoire" : null,
), ),
const SizedBox(height: 8),
TextFormField( TextFormField(
initialValue: commentaire, initialValue: commentaire,
onChanged: (v) => commentaire = v, onChanged: (v) => commentaire = v,
decoration: const InputDecoration(labelText: "Commentaire"), decoration: const InputDecoration(labelText: "Commentaire"),
maxLines: 2,
), ),
const SizedBox(height: 8),
TextFormField( TextFormField(
initialValue: ingredients, initialValue: ingredients,
onChanged: (v) => ingredients = v, onChanged: (v) => ingredients = v,
decoration: const InputDecoration(labelText: "Ingrédients"), decoration: const InputDecoration(labelText: "Ingrédients"),
maxLines: 2,
), ),
const SizedBox(height: 8),
TextFormField( TextFormField(
initialValue: prix.toString(), initialValue: prix.toString(),
onChanged: (v) => prix = double.tryParse(v) ?? 0, onChanged: (v) => prix = double.tryParse(v) ?? 0,
decoration: const InputDecoration(labelText: "Prix"), decoration: const InputDecoration(labelText: "Prix (MGA)"),
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true,
), ),
validator: (v) => (v?.isEmpty ?? true || double.tryParse(v!) == null)
? "Prix obligatoire" : null,
), ),
DropdownButton<MenuCategory>( const SizedBox(height: 16),
hint: const Text("Catégorie"), DropdownButtonFormField<MenuCategory>(
value: cat, value: cat,
isExpanded: true, hint: const Text("Catégorie"),
items: decoration: const InputDecoration(
widget.categories labelText: "Catégorie",
.toSet() border: OutlineInputBorder(),
),
items: widget.categories
.map( .map(
(c) => DropdownMenuItem(value: c, child: Text(c.nom)), (c) => DropdownMenuItem(value: c, child: Text(c.nom)),
) )
.toList(), .toList(),
onChanged: (v) => setState(() => cat = v), onChanged: (v) => setState(() => cat = v),
), ),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Disponible",
style: TextStyle(fontSize: 16),
),
Switch(
value: disponible,
onChanged: (value) {
setState(() => disponible = value);
},
activeColor: Colors.green,
),
],
),
], ],
), ),
), ),
@ -549,7 +790,368 @@ class _EditPlatDialogState extends State<EditPlatDialog> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text("Annuler"), child: const Text("Annuler"),
), ),
ElevatedButton(onPressed: submit, child: const Text("Enregistrer")), ElevatedButton(
onPressed: submit,
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
child: const Text("Enregistrer", style: TextStyle(color: Colors.white)),
),
], ],
); );
} }
// ===== PAGE DE CRÉATION/ÉDITION =====
class PlatEditPage extends StatefulWidget {
final List<MenuCategory> categories;
final MenuPlat? plat;
final Function()? onSaved;
const PlatEditPage({
super.key,
required this.categories,
this.plat,
this.onSaved,
});
@override
State<PlatEditPage> createState() => _PlatEditPageState();
}
class _PlatEditPageState extends State<PlatEditPage> {
final _formKey = GlobalKey<FormState>();
late TextEditingController nomCtrl, descCtrl, prixCtrl, ingredientsCtrl;
late bool disponible;
MenuCategory? selectedCategory;
@override
void initState() {
super.initState();
nomCtrl = TextEditingController(text: widget.plat?.nom ?? "");
descCtrl = TextEditingController(text: widget.plat?.commentaire ?? "");
prixCtrl = TextEditingController(
text: widget.plat != null ? widget.plat!.prix.toStringAsFixed(2) : "",
);
ingredientsCtrl = TextEditingController(text: widget.plat?.ingredients ?? "");
disponible = widget.plat?.disponible ?? true;
selectedCategory = widget.plat?.category;
}
@override
void dispose() {
nomCtrl.dispose();
descCtrl.dispose();
prixCtrl.dispose();
ingredientsCtrl.dispose();
super.dispose();
}
Future<void> submit() async {
if (!_formKey.currentState!.validate()) return;
if (selectedCategory == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sélectionnez une catégorie.')),
);
return;
}
final body = {
"nom": nomCtrl.text,
"commentaire": descCtrl.text,
"ingredients": ingredientsCtrl.text,
"prix": double.tryParse(prixCtrl.text) ?? 0,
"categorie_id": selectedCategory!.id,
"disponible": disponible,
};
try {
final isEdit = widget.plat != null;
final url = isEdit
? 'https://restaurant.careeracademy.mg/api/menus/${widget.plat!.id}'
: 'https://restaurant.careeracademy.mg/api/menus';
final res = isEdit
? await http.put(
Uri.parse(url),
headers: {"Content-Type": "application/json"},
body: json.encode(body),
)
: await http.post(
Uri.parse(url),
headers: {"Content-Type": "application/json"},
body: json.encode(body),
);
if (res.statusCode == 200 || res.statusCode == 201) {
widget.onSaved?.call();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEdit ? 'Plat modifié avec succès' : 'Plat créé avec succès'),
backgroundColor: Colors.green,
),
);
} else {
print('Error: ${res.body}\nBody sent: $body');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de ${isEdit ? "la modification" : "la création"} du plat'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur réseau: $e'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
final isEdit = widget.plat != null;
return Scaffold(
backgroundColor: const Color(0xfffcfbf9),
appBar: AppBar(
title: Text(isEdit ? 'Éditer le plat' : 'Nouveau plat'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
centerTitle: true,
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black87,
),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 550),
child: Card(
elevation: 2,
margin: const EdgeInsets.all(32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(28),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isEdit ? 'Modifier le plat' : 'Nouveau plat',
style: const TextStyle(
fontSize: 21,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 20),
// Nom du plat
TextFormField(
controller: nomCtrl,
decoration: const InputDecoration(
labelText: "Nom du plat *",
hintText: "Ex: Steak frites",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
validator: (v) => (v == null || v.isEmpty) ? "Obligatoire" : null,
),
const SizedBox(height: 16),
// Description
TextFormField(
controller: descCtrl,
decoration: const InputDecoration(
labelText: "Description",
hintText: "Description détaillée du plat...",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
maxLines: 3,
),
const SizedBox(height: 16),
// Ingrédients
TextFormField(
controller: ingredientsCtrl,
decoration: const InputDecoration(
labelText: "Ingrédients",
hintText: "Liste des ingrédients...",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
maxLines: 2,
),
const SizedBox(height: 16),
Row(
children: [
// Prix
Expanded(
child: TextFormField(
controller: prixCtrl,
decoration: const InputDecoration(
labelText: "Prix (MGA) *",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
validator: (v) =>
(v == null || v.isEmpty || double.tryParse(v) == null)
? "Obligatoire"
: null,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
),
const SizedBox(width: 16),
// Catégorie
Expanded(
child: DropdownButtonFormField<MenuCategory>(
value: selectedCategory,
decoration: const InputDecoration(
labelText: "Catégorie *",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
filled: true,
fillColor: Color(0xFFF7F7F7),
),
validator: (v) => v == null ? "Obligatoire" : null,
items: widget.categories.map((cat) {
return DropdownMenuItem(
value: cat,
child: Text(cat.nom),
);
}).toList(),
onChanged: (value) {
setState(() => selectedCategory = value);
},
),
),
],
),
const SizedBox(height: 20),
// Switch disponibilité
Row(
children: [
Switch(
value: disponible,
activeColor: Colors.green,
onChanged: (v) => setState(() => disponible = v),
),
const SizedBox(width: 8),
Text(
'Plat disponible',
style: TextStyle(
fontSize: 16,
color: disponible ? Colors.black87 : Colors.grey,
),
),
],
),
const SizedBox(height: 30),
// Boutons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text(
"Annuler",
style: TextStyle(color: Colors.black54),
),
),
const SizedBox(width: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
onPressed: submit,
icon: const Icon(Icons.save, size: 18),
label: Text(isEdit ? "Enregistrer" : "Créer le plat"),
),
],
),
],
),
),
),
),
),
),
),
);
}
}
// ===== FONCTIONS DE NAVIGATION =====
Future<void> navigateToCreate(
BuildContext context,
List<MenuCategory> categories,
Function()? onSaved,
) async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlatEditPage(
categories: categories,
onSaved: onSaved
),
),
);
}
Future<void> navigateToEdit(
BuildContext context,
List<MenuCategory> categories,
MenuPlat plat,
Function()? onSaved,
) async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlatEditPage(
categories: categories,
plat: plat,
onSaved: onSaved,
),
),
);
}
Loading…
Cancel
Save