You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
555 lines
19 KiB
555 lines
19 KiB
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import './PlatEdit_screen.dart';
|
|
|
|
class PlatsManagementScreen extends StatefulWidget {
|
|
const PlatsManagementScreen({super.key});
|
|
|
|
@override
|
|
State<PlatsManagementScreen> createState() => _PlatsManagementScreenState();
|
|
}
|
|
|
|
class _PlatsManagementScreenState extends State<PlatsManagementScreen> {
|
|
final _baseUrl = 'https://restaurant.careeracademy.mg/api';
|
|
List<MenuPlat> plats = [];
|
|
List<MenuCategory> categories = [];
|
|
String search = '';
|
|
int? selectedCategoryId;
|
|
String disponibilite = '';
|
|
bool isLoading = true;
|
|
|
|
final TextEditingController _searchController = TextEditingController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchCategories();
|
|
_fetchPlats();
|
|
}
|
|
|
|
// Fetch categories
|
|
Future<void> _fetchCategories() async {
|
|
try {
|
|
final res = await http.get(Uri.parse('$_baseUrl/menu-categories'));
|
|
final data = json.decode(res.body);
|
|
setState(() {
|
|
categories =
|
|
(data['data']['categories'] as List)
|
|
.map((item) => MenuCategory.fromJson(item))
|
|
.toList();
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
// Fetch plats
|
|
Future<void> _fetchPlats() async {
|
|
setState(() => isLoading = true);
|
|
try {
|
|
final uri = Uri.parse('$_baseUrl/menus').replace(
|
|
queryParameters: {
|
|
if (search.isNotEmpty) 'search': search,
|
|
if (selectedCategoryId != null)
|
|
'category_id': selectedCategoryId.toString(),
|
|
},
|
|
);
|
|
final res = await http.get(uri);
|
|
final data = json.decode(res.body);
|
|
setState(() {
|
|
plats =
|
|
(data['data']['menus'] as List)
|
|
.map((item) => MenuPlat.fromJson(item))
|
|
.toList();
|
|
isLoading = false;
|
|
});
|
|
if (kDebugMode) {
|
|
// print('fetched plat here: $plats items');
|
|
}
|
|
} catch (e) {
|
|
setState(() => isLoading = false);
|
|
if (kDebugMode) print("Error: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _deletePlat(int id) async {
|
|
final res = await http.delete(Uri.parse('$_baseUrl/menus/$id'));
|
|
if (res.statusCode == 200) {
|
|
_fetchPlats();
|
|
} else {
|
|
if (kDebugMode) print("Error deleting plat: ${res.body}");
|
|
// ignore: use_build_context_synchronously
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Des commandes sont liées à ce plat.')),
|
|
);
|
|
}
|
|
}
|
|
|
|
void _showEditPlatDialog(MenuPlat plat) {
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(_) => EditPlatDialog(
|
|
plat: plat,
|
|
onPlatUpdated: _fetchPlats,
|
|
categories: categories,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xfffcfbf9),
|
|
appBar: AppBar(
|
|
elevation: 0,
|
|
title: const Text(
|
|
'Gestion des plats',
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: Colors.black87,
|
|
actions: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 16.0),
|
|
child: ElevatedButton.icon(
|
|
onPressed:
|
|
() => navigateToCreate(
|
|
context,
|
|
categories,
|
|
() => {_fetchPlats()},
|
|
),
|
|
icon: const Icon(Icons.add, size: 18),
|
|
label: const Text('Nouveau plat'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green[700],
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 18,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 18),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 18),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.filter_alt_outlined, size: 22),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchController,
|
|
onChanged: (value) {
|
|
setState(() => search = value);
|
|
_fetchPlats();
|
|
},
|
|
decoration: const InputDecoration(
|
|
hintText: "Rechercher un plat...",
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: Color(0xFFF7F7F7),
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.symmetric(
|
|
vertical: 10,
|
|
horizontal: 14,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
// Catégories
|
|
DropdownButton<int?>(
|
|
value: selectedCategoryId,
|
|
hint: const Text("Toutes les catégories"),
|
|
items: [
|
|
const DropdownMenuItem(
|
|
value: null,
|
|
child: Text("Toutes les catégories"),
|
|
),
|
|
...categories.map(
|
|
(cat) => DropdownMenuItem(
|
|
value: cat.id,
|
|
child: Text(cat.nom),
|
|
),
|
|
),
|
|
],
|
|
onChanged: (v) {
|
|
setState(() => selectedCategoryId = v);
|
|
_fetchPlats();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (isLoading)
|
|
const Expanded(child: Center(child: CircularProgressIndicator()))
|
|
else
|
|
Expanded(
|
|
child: ListView.builder(
|
|
itemCount: plats.length,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
itemBuilder: (ctx, i) {
|
|
final p = plats[i];
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 20),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 16,
|
|
horizontal: 18,
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
p.nom,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 18,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 6,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
p.commentaire ?? "",
|
|
style: const TextStyle(fontSize: 15),
|
|
),
|
|
if (p.ingredients != null &&
|
|
p.ingredients!.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 3.0,
|
|
),
|
|
child: Text(
|
|
p.ingredients!,
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
if (p.category != null)
|
|
CategoryChip(
|
|
label: p.category!.nom,
|
|
color: Colors.black,
|
|
)
|
|
else
|
|
const CategoryChip(
|
|
label: "Catégorie",
|
|
color: Colors.black,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
"${p.prix.toStringAsFixed(2)} MGA",
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
SizedBox(
|
|
height: 38,
|
|
width: 38,
|
|
child: IconButton(
|
|
icon: const Icon(
|
|
Icons.edit,
|
|
color: Colors.black54,
|
|
),
|
|
onPressed: () => _showEditPlatDialog(p),
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 38,
|
|
width: 38,
|
|
child: IconButton(
|
|
icon: const Icon(
|
|
Icons.delete,
|
|
color: Colors.redAccent,
|
|
),
|
|
onPressed: () async {
|
|
final confirm = await showDialog(
|
|
context: context,
|
|
builder:
|
|
(_) => AlertDialog(
|
|
title: const Text(
|
|
'Supprimer ce plat ?',
|
|
),
|
|
content: Text(
|
|
'Supprimer ${p.nom} ',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed:
|
|
() => Navigator.pop(
|
|
context,
|
|
false,
|
|
),
|
|
child: const Text("Annuler"),
|
|
),
|
|
ElevatedButton(
|
|
onPressed:
|
|
() => Navigator.pop(
|
|
context,
|
|
true,
|
|
),
|
|
style:
|
|
ElevatedButton.styleFrom(
|
|
backgroundColor:
|
|
Colors.red,
|
|
),
|
|
child: const Text(
|
|
"Supprimer",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (confirm == true) _deletePlat(p.id);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MenuPlat {
|
|
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 {
|
|
final String label;
|
|
final Color color;
|
|
const CategoryChip({super.key, required this.label, required this.color});
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
// ignore: deprecated_member_use
|
|
color: color.withOpacity(0.16),
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(color: color, fontWeight: FontWeight.w600, fontSize: 13),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> navigateToCreate(
|
|
BuildContext context,
|
|
List<MenuCategory> categories,
|
|
Function()? onSaved,
|
|
) async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => PlatEditPage(categories: categories, onSaved: onSaved),
|
|
),
|
|
);
|
|
}
|
|
|
|
class EditPlatDialog extends StatefulWidget {
|
|
final MenuPlat plat;
|
|
final List<MenuCategory> categories;
|
|
final VoidCallback onPlatUpdated;
|
|
const EditPlatDialog({
|
|
super.key,
|
|
required this.plat,
|
|
required this.onPlatUpdated,
|
|
required this.categories,
|
|
});
|
|
|
|
@override
|
|
State<EditPlatDialog> createState() => _EditPlatDialogState();
|
|
}
|
|
|
|
class _EditPlatDialogState extends State<EditPlatDialog> {
|
|
late String nom;
|
|
late String commentaire;
|
|
late String ingredients;
|
|
late double prix;
|
|
// late bool disponible;
|
|
MenuCategory? cat;
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
nom = widget.plat.nom;
|
|
commentaire = widget.plat.commentaire ?? '';
|
|
ingredients = widget.plat.ingredients ?? '';
|
|
prix = widget.plat.prix;
|
|
// cat = (widget.plat.categories) as MenuCategory?;
|
|
cat = widget.plat.category;
|
|
}
|
|
|
|
Future<void> submit() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
final res = await http.put(
|
|
Uri.parse(
|
|
'https://restaurant.careeracademy.mg/api/menus/${widget.plat.id}',
|
|
),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: json.encode({
|
|
"nom": nom,
|
|
"commentaire": commentaire,
|
|
"ingredients": ingredients,
|
|
"prix": prix,
|
|
"categorie_id": cat?.id,
|
|
}),
|
|
);
|
|
if (res.statusCode == 200) {
|
|
widget.onPlatUpdated();
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.pop(context);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => AlertDialog(
|
|
title: const Text("Éditer le plat"),
|
|
content: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextFormField(
|
|
initialValue: nom,
|
|
onChanged: (v) => nom = v,
|
|
decoration: const InputDecoration(labelText: "Nom"),
|
|
validator: (v) => (v?.isEmpty ?? true) ? "Obligatoire" : null,
|
|
),
|
|
TextFormField(
|
|
initialValue: commentaire,
|
|
onChanged: (v) => commentaire = v,
|
|
decoration: const InputDecoration(labelText: "Commentaire"),
|
|
),
|
|
TextFormField(
|
|
initialValue: ingredients,
|
|
onChanged: (v) => ingredients = v,
|
|
decoration: const InputDecoration(labelText: "Ingrédients"),
|
|
),
|
|
TextFormField(
|
|
initialValue: prix.toString(),
|
|
onChanged: (v) => prix = double.tryParse(v) ?? 0,
|
|
decoration: const InputDecoration(labelText: "Prix"),
|
|
keyboardType: const TextInputType.numberWithOptions(
|
|
decimal: true,
|
|
),
|
|
),
|
|
DropdownButton<MenuCategory>(
|
|
hint: const Text("Catégorie"),
|
|
value: cat,
|
|
isExpanded: true,
|
|
items:
|
|
widget.categories
|
|
.toSet()
|
|
.map(
|
|
(c) => DropdownMenuItem(value: c, child: Text(c.nom)),
|
|
)
|
|
.toList(),
|
|
onChanged: (v) => setState(() => cat = v),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("Annuler"),
|
|
),
|
|
ElevatedButton(onPressed: submit, child: const Text("Enregistrer")),
|
|
],
|
|
);
|
|
}
|
|
|