CRUD menus

This commit is contained in:
Stephane 2025-08-03 10:26:52 +03:00
parent 79d208b69e
commit d4a5c60821
7 changed files with 945 additions and 501 deletions

View File

@ -6,11 +6,7 @@ class MainLayout extends StatefulWidget {
final Widget child;
final String? currentRoute;
const MainLayout({
super.key,
required this.child,
this.currentRoute,
});
const MainLayout({super.key, required this.child, this.currentRoute});
@override
_MainLayoutState createState() => _MainLayoutState();
@ -36,7 +32,7 @@ class _MainLayoutState extends State<MainLayout> {
return 2;
case '/menu':
return 3;
case '/cart':
case '/plats':
return 4;
default:
return 0;
@ -63,7 +59,7 @@ class _MainLayoutState extends State<MainLayout> {
route = '/menu';
break;
case 4:
route = '/cart';
route = '/plats';
break;
default:
route = '/tables';
@ -91,13 +87,14 @@ class _MainLayoutState extends State<MainLayout> {
],
),
// Show mobile navigation on smaller screens
bottomNavigationBar: isDesktop
? null
: MobileBottomNavigation(
currentRoute: widget.currentRoute ?? '/tables',
selectedIndex: _selectedIndex,
onItemTapped: _onItemTapped,
),
bottomNavigationBar:
isDesktop
? null
: MobileBottomNavigation(
currentRoute: widget.currentRoute ?? '/tables',
selectedIndex: _selectedIndex,
onItemTapped: _onItemTapped,
),
);
}
}
}

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:itrimobe/pages/menu.dart';
import 'layouts/main_layout.dart';
import 'pages/tables.dart';
import 'pages/categorie.dart';
import 'pages/commandes_screen.dart';
import 'pages/login_screen.dart';
import 'pages/menus_screen.dart';
void main() {
runApp(const MyApp());
@ -25,24 +25,29 @@ class MyApp extends StatelessWidget {
initialRoute: '/login',
routes: {
'/login': (context) => const LoginScreen(),
'/tables': (context) => const MainLayout(
'/tables':
(context) => const MainLayout(
currentRoute: '/tables',
child: TablesScreen(),
),
'/categories': (context) => const MainLayout(
'/categories':
(context) => const MainLayout(
currentRoute: '/categories',
child: CategoriesPage(),
),
'/commandes': (context) => const MainLayout(
'/commandes':
(context) => const MainLayout(
currentRoute: '/commandes',
child: OrdersManagementScreen(),
),
// MODIFICATION : Route simple pour le menu
// '/menu': (context) => const MainLayout(
// currentRoute: '/menu',
// child: MenuPage(), // Pas de paramètres requis maintenant
// ),
'/plats':
(context) => const MainLayout(
currentRoute: '/plats',
child:
PlatsManagementScreen(), // Pas de paramètres requis maintenant
),
},
);
}
}
}

View File

@ -0,0 +1,387 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// Use your actual models and http logic
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,
});
}
class MenuCategory {
final int id;
final String nom;
final String? description;
MenuCategory({required this.id, required this.nom, this.description});
factory MenuCategory.fromJson(Map<String, dynamic> json) {
return MenuCategory(
id: json['id'],
nom: json['nom'],
description: json['description'],
);
}
@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 {
final List<MenuCategory> categories;
final MenuPlat? plat; // if present = edit, else create
final Function()? onSaved; // callback when saved
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, imgCtrl;
late bool disponible;
late List<MenuCategory> selectedCategories;
@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) : "",
);
imgCtrl = TextEditingController(text: widget.plat?.imageUrl ?? "");
disponible = widget.plat?.disponible ?? true;
selectedCategories = widget.plat?.categories.toList() ?? [];
}
@override
void dispose() {
nomCtrl.dispose();
descCtrl.dispose();
prixCtrl.dispose();
imgCtrl.dispose();
super.dispose();
}
void submit() async {
if (!_formKey.currentState!.validate()) return;
if (selectedCategories.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sélectionnez au moins une catégorie.')),
);
return;
}
// Build request data
final body = {
"nom": nomCtrl.text,
"commentaire": descCtrl.text,
"prix": double.tryParse(prixCtrl.text) ?? 0,
"categories": selectedCategories.map((c) => c.id).toList(),
"disponible": disponible,
"categorie_id":
selectedCategories.isNotEmpty
? selectedCategories.first.id
: null, // Use first category if available
};
try {
final res = await http.post(
Uri.parse(
'https://restaurant.careeracademy.mg/api/menus',
), // your API URL here
headers: {"Content-Type": "application/json"},
body: json.encode(body),
);
if (res.statusCode == 200 || res.statusCode == 201) {
widget.onSaved?.call();
Navigator.pop(context);
} else {
// show error
print('Error creating plat: ${res.body}\n here is body: $body');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la création du plat')),
);
}
} catch (e) {
// show error
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erreur réseau: $e')));
}
}
@override
Widget build(BuildContext context) {
final isEdit = widget.plat != null;
return Scaffold(
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.white,
foregroundColor: Colors.black,
),
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: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informations du plat',
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 14),
TextFormField(
controller: nomCtrl,
decoration: const InputDecoration(
labelText: "Nom du plat *",
hintText: "Ex: Steak frites",
),
validator:
(v) =>
(v == null || v.isEmpty) ? "Obligatoire" : null,
),
const SizedBox(height: 13),
TextFormField(
controller: descCtrl,
decoration: const InputDecoration(
labelText: "Description",
hintText: "Description détaillée du plat...",
),
maxLines: 3,
),
const SizedBox(height: 13),
Row(
children: [
Expanded(
child: TextFormField(
controller: prixCtrl,
decoration: const InputDecoration(
labelText: "Prix (MGA) *",
),
validator:
(v) =>
(v == null ||
v.isEmpty ||
double.tryParse(v) == null)
? "Obligatoire"
: null,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
),
const SizedBox(width: 16),
// Multi-category picker
Expanded(
child: InkWell(
onTap: () async {
final result = await showDialog<
List<MenuCategory>
>(
context: context,
builder: (context) {
// temp inside a StatefulBuilder so state changes refresh UI
Set<int> temp =
selectedCategories
.map((e) => e.id)
.toSet();
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 CheckboxListTile(
value: temp.contains(
cat.id,
),
title: Text(cat.nom),
onChanged: (checked) {
setStateDialog(() {
if (checked == true) {
temp.add(cat.id);
} else {
temp.remove(cat.id);
}
});
},
);
}).toList(),
),
),
),
actions: [
TextButton(
onPressed: () {
final newList =
widget.categories
.where(
(cat) => temp.contains(
cat.id,
),
)
.toList();
Navigator.pop(context, newList);
},
child: const Text('OK'),
),
],
);
},
);
},
);
if (result != null)
setState(() => selectedCategories = result);
},
child: InputDecorator(
decoration: const InputDecoration(
labelText: "Catégories *",
border: OutlineInputBorder(gapPadding: 2),
),
child: Wrap(
spacing: 6,
runSpacing: -8,
children:
selectedCategories.isEmpty
? [
const Text(
"Aucune sélection",
style: TextStyle(
color: Colors.grey,
),
),
]
: selectedCategories
.map(
(c) => Chip(
label: Text(c.nom),
visualDensity:
VisualDensity.compact,
),
)
.toList(),
),
),
),
),
],
),
// const SizedBox(height: 13),
// TextFormField(
// controller: imgCtrl,
// decoration: const InputDecoration(
// labelText: "URL de l'image",
// hintText: "/api/placeholder/300/200",
// ),
// ),
const SizedBox(height: 13),
Row(
children: [
Switch(
value: disponible,
activeColor: Colors.green,
onChanged: (v) => setState(() => disponible = v),
),
const SizedBox(width: 7),
const Text('Plat disponible'),
],
),
const SizedBox(height: 22),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Annuler",
style: TextStyle(color: Colors.black),
),
),
const SizedBox(width: 18),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
onPressed: submit,
icon: const Icon(
Icons.save,
size: 18,
color: Colors.white,
),
label: Text(
isEdit ? "Enregistrer" : "Ajouter",
style: const TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
),
),
),
);
}
}

View File

@ -138,8 +138,6 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
return null;
}
Future<void> updateOrderStatus(
Order order,
String newStatus, {
@ -287,9 +285,6 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
}
}
List<Order> get activeOrders {
return orders
.where(
@ -345,7 +340,6 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
style: TextStyle(color: Colors.white),
),
),
],
);
},
@ -354,22 +348,23 @@ class _OrdersManagementScreenState extends State<OrdersManagementScreen> {
);
}
Future<void> updateTableStatus(int tableId, String newStatus) async {
const String apiUrl = 'https://restaurant.careeracademy.mg/api/tables'; // adapte lURL si besoin
Future<void> updateTableStatus(int tableId, String newStatus) async {
const String apiUrl =
'https://restaurant.careeracademy.mg/api/tables'; // adapte lURL si besoin
final response = await http.put(
Uri.parse('$apiUrl/$tableId'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'status': newStatus}),
);
final response = await http.put(
Uri.parse('$apiUrl/$tableId'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'status': newStatus}),
);
if (response.statusCode != 200) {
print('Erreur lors de la mise à jour du statut de la table');
throw Exception('Erreur: ${response.body}');
} else {
print('✅ Table $tableId mise à jour en $newStatus');
if (response.statusCode != 200) {
print('Erreur lors de la mise à jour du statut de la table');
throw Exception('Erreur: ${response.body}');
} else {
print('✅ Table $tableId mise à jour en $newStatus');
}
}
}
@override
Widget build(BuildContext context) {

File diff suppressed because it is too large Load Diff

View File

@ -306,10 +306,7 @@ class _TablesScreenState extends State<TablesScreen> {
crossAxisCount: crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio:
isDesktop
? 1.7
: 2.1,
childAspectRatio: isDesktop ? 1.7 : 2.1,
),
itemCount: tables.length,
itemBuilder: (context, index) {

View File

@ -155,12 +155,12 @@ class AppBottomNavigation extends StatelessWidget {
const SizedBox(width: 20),
GestureDetector(
onTap: () => onItemTapped(3),
onTap: () => onItemTapped(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color:
selectedIndex == 3
selectedIndex == 4
? Colors.green.shade700
: Colors.transparent,
borderRadius: BorderRadius.circular(20),
@ -169,23 +169,23 @@ class AppBottomNavigation extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.restaurant_menu,
Icons.restaurant_menu,
color:
selectedIndex == 3
selectedIndex == 4
? Colors.white
: Colors.grey.shade600,
size: 16,
),
const SizedBox(width: 6),
Text(
'Menu',
'Plats',
style: TextStyle(
color:
selectedIndex == 3
selectedIndex == 4
? Colors.white
: Colors.grey.shade600,
fontWeight:
selectedIndex == 3
selectedIndex == 4
? FontWeight.w500
: FontWeight.normal,
),
@ -220,4 +220,4 @@ class AppBottomNavigation extends StatelessWidget {
],
);
}
}
}