push final
This commit is contained in:
parent
4c77552551
commit
cccbe0d684
88
lib/models/menus.dart
Normal file
88
lib/models/menus.dart
Normal file
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
} else {
|
|
||||||
// show error
|
|
||||||
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(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) {
|
} 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)),
|
||||||
),
|
),
|
||||||
validator:
|
filled: true,
|
||||||
(v) =>
|
fillColor: Color(0xFFF7F7F7),
|
||||||
(v == null || v.isEmpty) ? "Obligatoire" : null,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 13),
|
validator: (v) => (v == null || v.isEmpty) ? "Obligatoire" : null,
|
||||||
|
),
|
||||||
|
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
|
|
||||||
|
// Catégorie
|
||||||
Expanded(
|
Expanded(
|
||||||
child: InkWell(
|
child: DropdownButtonFormField<MenuCategory>(
|
||||||
onTap: () async {
|
value: selectedCategory,
|
||||||
final result = await showDialog<MenuCategory?>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
MenuCategory? temp =
|
|
||||||
selectedCategories.isNotEmpty
|
|
||||||
? selectedCategories.first
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return StatefulBuilder(
|
|
||||||
builder: (context, setStateDialog) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text("Catégories"),
|
|
||||||
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,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: submit,
|
onPressed: submit,
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.save, size: 18),
|
||||||
Icons.save,
|
label: Text(isEdit ? "Enregistrer" : "Créer le plat"),
|
||||||
size: 18,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
isEdit ? "Enregistrer" : "Ajouter",
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -366,6 +326,7 @@ class _PlatEditPageState extends State<PlatEditPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,13 +76,20 @@ 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'] ?? []);
|
||||||
|
|
||||||
setState(() {
|
// Filtrage côté client
|
||||||
_menus = menusList;
|
final List<dynamic> menusDisponibles = menusList.where((menu) {
|
||||||
});
|
final disponible = menu['disponible'];
|
||||||
|
return disponible == true || disponible == 1 || disponible == '1' || disponible == 'true';
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_menus = menusDisponibles;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print("Erreur API menus: ${response.statusCode}");
|
print("Erreur API menus: ${response.statusCode}");
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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…
Reference in New Issue
Block a user