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

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")),
],
);
}