From 0dcd0ae0d22e1d47b93160608e6125d6ae7e40b7 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sat, 2 Aug 2025 01:44:31 +0300 Subject: [PATCH 1/4] design table --- lib/pages/tables.dart | 366 ++++++++++++++++++++++++++++++++---------- 1 file changed, 279 insertions(+), 87 deletions(-) diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index 6aa14a7..89133aa 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -61,7 +61,7 @@ class _TablesScreenState extends State { case 'available': return Colors.green; case 'occupied': - return Colors.red; + return Colors.orange; case 'reserved': return Colors.orange; default: @@ -82,6 +82,10 @@ class _TablesScreenState extends State { } } + bool isTableSelectable(String status) { + return status == 'available'; + } + @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; @@ -94,107 +98,295 @@ class _TablesScreenState extends State { } return Scaffold( - appBar: AppBar(title: const Text('Sélectionner une table')), - body: isLoading - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.all(12.0), - child: GridView.builder( - itemCount: tables.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - childAspectRatio: 2.3, // ⬅️ plus plat ici + backgroundColor: Colors.grey.shade50, + body: Column( + children: [ + // Header + Container( + width: double.infinity, + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), + child: Column( + children: [ + const Text( + 'Sélectionner une table', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), - itemBuilder: (context, index) { - final table = tables[index]; - final isAvailable = table.status == 'available'; - - return Card( - elevation: 1.5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), - child: Padding( - padding: const EdgeInsets.all(10), + const SizedBox(height: 8), + Text( + 'Choisissez une table pour commencer une nouvelle commande', + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + ], + ), + ), + + // Tables Grid + Expanded( + child: + isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(20.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Titre + badge - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - table.nom, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: getStatusColor(table.status), - borderRadius: BorderRadius.circular(50), - ), - child: Text( - getStatusLabel(table.status), - style: const TextStyle( + Expanded( + child: GridView.builder( + itemCount: tables.length, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + childAspectRatio: 1.3, + ), + itemBuilder: (context, index) { + final table = tables[index]; + final isSelectable = isTableSelectable( + table.status, + ); + + return Container( + decoration: BoxDecoration( color: Colors.white, - fontSize: 10, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.grey.shade200, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade100, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - ), - ), - ], + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + // Table name and status badge + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + table.nom, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.black87, + ), + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: getStatusColor( + table.status, + ), + borderRadius: + BorderRadius.circular(20), + ), + child: Text( + getStatusLabel(table.status), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Capacity + Row( + children: [ + Icon( + Icons.people_outline, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Text( + 'Capacité: ${table.capacity} personnes', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + ], + ), + + const Spacer(), + + // Button + SizedBox( + width: double.infinity, + height: 36, + child: ElevatedButton( + onPressed: + isSelectable ? () {} : null, + style: ElevatedButton.styleFrom( + backgroundColor: + isSelectable + ? Colors.green.shade700 + : Colors.grey.shade300, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(8), + ), + ), + child: Text( + isSelectable + ? "Sélectionner" + : "Indisponible", + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: + isSelectable + ? Colors.white + : Colors.grey.shade600, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), ), - const Spacer(), + + // Legend + const SizedBox(height: 20), Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.people_outline, - size: 14, color: Colors.grey), - const SizedBox(width: 4), - Text( - '${table.capacity} personnes', - style: const TextStyle( - fontSize: 11.5, - color: Colors.grey, - ), + _buildLegendItem('Disponible', Colors.green), + const SizedBox(width: 24), + _buildLegendItem( + 'Occupée', + Colors.green.shade800, ), + const SizedBox(width: 24), + _buildLegendItem('Réservée', Colors.orange), ], ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - height: 30, - child: ElevatedButton( - onPressed: isAvailable ? () {} : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.deepOrange, - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - child: const Text( - "Réserver", - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ), - ), ], ), ), - ); - }, - ), + ), + + // Bottom Navigation + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.green.shade700, + borderRadius: BorderRadius.circular(20), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.table_restaurant, + color: Colors.white, + size: 16, + ), + SizedBox(width: 6), + Text( + 'Tables', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(width: 20), + Row( + children: [ + Icon( + Icons.receipt_long_outlined, + color: Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Commandes', + style: TextStyle(color: Colors.grey.shade600), + ), + ], + ), + const Spacer(), + Row( + children: [ + Icon( + Icons.person_outline, + color: Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Chef Pierre', + style: TextStyle(color: Colors.grey.shade600), + ), + const SizedBox(width: 8), + Icon( + Icons.expand_more, + color: Colors.grey.shade600, + size: 16, + ), + ], + ), + ], ), + ), + ], + ), + ); + } + + Widget _buildLegendItem(String label, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + label, + style: TextStyle(fontSize: 13, color: Colors.grey.shade700), + ), + ], ); } } From b6c862038563be75177ff7e069cd56aa2daaec61 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sat, 2 Aug 2025 01:57:48 +0300 Subject: [PATCH 2/4] design des texte et responsive --- lib/pages/tables.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index 89133aa..96d25e6 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -201,7 +201,7 @@ class _TablesScreenState extends State { getStatusLabel(table.status), style: const TextStyle( color: Colors.white, - fontSize: 12, + fontSize: 9, fontWeight: FontWeight.w500, ), ), @@ -256,7 +256,7 @@ class _TablesScreenState extends State { ? "Sélectionner" : "Indisponible", style: TextStyle( - fontSize: 13, + fontSize: 10, fontWeight: FontWeight.w500, color: isSelectable From 495cd222c7d053372e5761076b99efd879f73f46 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sat, 2 Aug 2025 02:14:30 +0300 Subject: [PATCH 3/4] bottom navigation --- lib/layouts/main_layout.dart | 160 +++++ lib/main.dart | 28 +- lib/pages/categories_screen.dart | 1 + lib/pages/commandes_screen.dart | 1 + lib/pages/menus_screen.dart | 1 + lib/pages/tables.dart | 790 +++++++++++++++------- lib/widgets/bottom_navigation.dart | 130 ++++ lib/widgets/mobile_bottom_navigation.dart | 66 ++ 8 files changed, 913 insertions(+), 264 deletions(-) create mode 100644 lib/layouts/main_layout.dart create mode 100644 lib/pages/categories_screen.dart create mode 100644 lib/pages/commandes_screen.dart create mode 100644 lib/pages/menus_screen.dart create mode 100644 lib/widgets/bottom_navigation.dart create mode 100644 lib/widgets/mobile_bottom_navigation.dart diff --git a/lib/layouts/main_layout.dart b/lib/layouts/main_layout.dart new file mode 100644 index 0000000..6a8df76 --- /dev/null +++ b/lib/layouts/main_layout.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import '../widgets/bottom_navigation.dart'; + +class MainLayout extends StatefulWidget { + final Widget child; + final int currentIndex; + + const MainLayout({super.key, required this.child, this.currentIndex = 0}); + + @override + // ignore: library_private_types_in_public_api + _MainLayoutState createState() => _MainLayoutState(); +} + +class _MainLayoutState extends State { + int _selectedIndex = 0; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.currentIndex; + } + + @override + Widget build(BuildContext context) { + final isDesktop = MediaQuery.of(context).size.width >= 768; + + return Scaffold( + backgroundColor: Colors.grey.shade50, + body: Row( + children: [ + // Desktop Sidebar + if (isDesktop) _buildDesktopSidebar(), + + // Main Content + Expanded( + child: Column( + children: [ + // Page Content + Expanded(child: widget.child), + + // Bottom Navigation + AppBottomNavigation( + selectedIndex: _selectedIndex, + onItemTapped: _onItemTapped, + isDesktop: isDesktop, + ), + ], + ), + ), + ], + ), + ); + } + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + + // Global navigation logic + switch (index) { + case 0: + Navigator.pushReplacementNamed(context, '/tables'); + break; + case 1: + Navigator.pushReplacementNamed(context, '/commandes'); + break; + case 2: + Navigator.pushReplacementNamed(context, '/categories'); + break; + case 3: + Navigator.pushReplacementNamed(context, '/menus'); + break; + } + } + + Widget _buildDesktopSidebar() { + return Container( + width: 250, + color: Colors.white, + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + child: const Text( + 'Restaurant App', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + Expanded( + child: ListView( + children: [ + _buildSidebarItem( + icon: Icons.table_restaurant, + title: 'Tables', + route: '/tables', + index: 0, + ), + _buildSidebarItem( + icon: Icons.receipt_long_outlined, + title: 'Commandes', + route: '/commandes', + index: 1, + ), + _buildSidebarItem( + icon: Icons.category_outlined, + title: 'Catégories', + route: '/categories', + index: 2, + ), + _buildSidebarItem( + icon: Icons.restaurant_menu, + title: 'Menus', + route: '/menus', + index: 3, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSidebarItem({ + required IconData icon, + required String title, + required String route, + required int index, + }) { + final isSelected = _selectedIndex == index; + + return ListTile( + leading: Icon( + icon, + color: isSelected ? Colors.green.shade700 : Colors.grey.shade600, + ), + title: Text( + title, + style: TextStyle( + color: isSelected ? Colors.green.shade700 : Colors.grey.shade700, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + selected: isSelected, + selectedTileColor: Colors.green.shade50, + onTap: () { + setState(() { + _selectedIndex = index; + }); + Navigator.pushReplacementNamed(context, route); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index a347b78..507e235 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,37 @@ import 'package:flutter/material.dart'; -import '/pages/tables.dart'; // Assure-toi que ce fichier existe dans ton projet +import 'layouts/main_layout.dart'; +import 'pages/tables.dart'; +// import 'pages/commandes_screen.dart'; +// import 'pages/categories_screen.dart'; +// import 'pages/menus_screen.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({super.key}); + @override Widget build(BuildContext context) { return MaterialApp( - title: 'Sélection de Table', - debugShowCheckedModeBanner: false, + title: 'Restaurant App', theme: ThemeData( - primarySwatch: Colors.deepOrange, + primarySwatch: Colors.green, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: TablesScreen(), + initialRoute: '/tables', + routes: { + '/tables': + (context) => + const MainLayout(currentIndex: 0, child: TablesScreen()), + // '/commandes': + // (context) => MainLayout(currentIndex: 1, child: CommandesScreen()), + // '/categories': + // (context) => MainLayout(currentIndex: 2, child: CategoriesScreen()), + // '/menus': + // (context) => MainLayout(currentIndex: 3, child: MenusScreen()), + }, ); } } diff --git a/lib/pages/categories_screen.dart b/lib/pages/categories_screen.dart new file mode 100644 index 0000000..613744d --- /dev/null +++ b/lib/pages/categories_screen.dart @@ -0,0 +1 @@ +// TODO Implement this library. diff --git a/lib/pages/commandes_screen.dart b/lib/pages/commandes_screen.dart new file mode 100644 index 0000000..613744d --- /dev/null +++ b/lib/pages/commandes_screen.dart @@ -0,0 +1 @@ +// TODO Implement this library. diff --git a/lib/pages/menus_screen.dart b/lib/pages/menus_screen.dart new file mode 100644 index 0000000..613744d --- /dev/null +++ b/lib/pages/menus_screen.dart @@ -0,0 +1 @@ +// TODO Implement this library. diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index 96d25e6..ea8a4ab 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; @@ -26,7 +27,10 @@ class TableData { } class TablesScreen extends StatefulWidget { + const TablesScreen({super.key}); + @override + // ignore: library_private_types_in_public_api _TablesScreenState createState() => _TablesScreenState(); } @@ -41,18 +45,151 @@ class _TablesScreenState extends State { } Future fetchTables() async { - final url = Uri.parse("https://restaurant.careeracademy.mg/api/tables"); - final response = await http.get(url); - - if (response.statusCode == 200) { - final List data = json.decode(response.body)['data']; - setState(() { - tables = data.map((json) => TableData.fromJson(json)).toList(); - isLoading = false; - }); - } else { + try { + final url = Uri.parse("https://restaurant.careeracademy.mg/api/tables"); + final response = await http.get(url); + + if (response.statusCode == 200) { + final List data = json.decode(response.body)['data']; + setState(() { + tables = data.map((json) => TableData.fromJson(json)).toList(); + isLoading = false; + }); + } else { + setState(() => isLoading = false); + if (kDebugMode) { + print('Erreur API: ${response.statusCode}'); + } + } + } catch (e) { setState(() => isLoading = false); - print('Erreur API: ${response.statusCode}'); + if (kDebugMode) { + print('Erreur: $e'); + } + } + } + + Future _addTable() async { + // Add table logic + final result = await showDialog>( + context: context, + builder: (context) => const _AddEditTableDialog(), + ); + + if (result != null) { + // Call API to add table + _callAddTableAPI(result); + } + } + + Future _callAddTableAPI(Map tableData) async { + try { + final url = Uri.parse("https://restaurant.careeracademy.mg/api/tables"); + final response = await http.post( + url, + headers: {'Content-Type': 'application/json'}, + body: json.encode(tableData), + ); + + if (response.statusCode == 201) { + fetchTables(); // Refresh the list + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Table ajoutée avec succès')), + ); + } + } catch (e) { + ScaffoldMessenger.of( + // ignore: use_build_context_synchronously + context, + ).showSnackBar(SnackBar(content: Text('Erreur: $e'))); + } + } + + Future _editTable(TableData table) async { + final result = await showDialog>( + context: context, + builder: (context) => _AddEditTableDialog(table: table), + ); + + if (result != null) { + _callEditTableAPI(table.id, result); + } + } + + Future _callEditTableAPI(int id, Map tableData) async { + try { + final url = Uri.parse( + "https://restaurant.careeracademy.mg/api/tables/$id", + ); + final response = await http.put( + url, + headers: {'Content-Type': 'application/json'}, + body: json.encode(tableData), + ); + + if (response.statusCode == 200) { + fetchTables(); + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Table modifiée avec succès')), + ); + } + } catch (e) { + ScaffoldMessenger.of( + // ignore: use_build_context_synchronously + context, + ).showSnackBar(SnackBar(content: Text('Erreur: $e'))); + } + } + + Future _deleteTable(int id) async { + final confirm = await showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Confirmer la suppression'), + content: const Text( + 'Êtes-vous sûr de vouloir supprimer cette table?', + ), + 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'), + ), + ], + ), + ); + + if (confirm == true) { + _callDeleteTableAPI(id); + } + } + + Future _callDeleteTableAPI(int id) async { + try { + final url = Uri.parse( + "https://restaurant.careeracademy.mg/api/tables/$id", + ); + final response = await http.delete(url); + + if (response.statusCode == 200) { + fetchTables(); + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Table supprimée avec succès')), + ); + } + } catch (e) { + ScaffoldMessenger.of( + // ignore: use_build_context_synchronously + context, + ).showSnackBar(SnackBar(content: Text('Erreur: $e'))); } } @@ -89,6 +226,7 @@ class _TablesScreenState extends State { @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; + final isDesktop = screenWidth >= 768; int crossAxisCount = 2; if (screenWidth > 1200) { @@ -97,278 +235,284 @@ class _TablesScreenState extends State { crossAxisCount = 3; } - return Scaffold( - backgroundColor: Colors.grey.shade50, - body: Column( - children: [ - // Header - Container( - width: double.infinity, - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), - child: Column( - children: [ - const Text( - 'Sélectionner une table', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + // Header + Container( + color: Colors.white, + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Sélectionner une table', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + 'Choisissez une table pour commencer une nouvelle commande', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + ], ), - const SizedBox(height: 8), - Text( - 'Choisissez une table pour commencer une nouvelle commande', - style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + // Add button (desktop only) + if (isDesktop) + ElevatedButton.icon( + onPressed: _addTable, + icon: const Icon(Icons.add, size: 20), + label: const Text('Ajouter'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + ), ), - ], - ), + ], ), + ), - // Tables Grid - Expanded( - child: - isLoading - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Expanded( - child: GridView.builder( - itemCount: tables.length, - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - childAspectRatio: 1.3, - ), - itemBuilder: (context, index) { - final table = tables[index]; - final isSelectable = isTableSelectable( - table.status, - ); - - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey.shade200, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade100, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + // Content - Reduced height + Expanded( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + // Tables Grid - Reduced flex + Expanded( + flex: isDesktop ? 2 : 4, // Reduced from 3/4 to 2/4 + child: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: isDesktop ? 1.1 : 0.85, // Adjusted + ), + itemCount: tables.length, + itemBuilder: (context, index) { + final table = tables[index]; + final isSelectable = isTableSelectable(table.status); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + // ignore: deprecated_member_use + color: Colors.black.withOpacity(0.05), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( + children: [ + // Desktop CRUD menu + if (isDesktop) + Positioned( + top: 8, + right: 8, + child: PopupMenuButton( + icon: Icon( + Icons.more_vert, + size: 16, + color: Colors.grey.shade600, ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // Table name and status badge - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - table.nom, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: Colors.black87, - ), - ), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: getStatusColor( - table.status, - ), - borderRadius: - BorderRadius.circular(20), + onSelected: (value) { + switch (value) { + case 'edit': + _editTable(table); + break; + case 'delete': + _deleteTable(table.id); + break; + } + }, + itemBuilder: + (context) => [ + const PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Icon(Icons.edit, size: 16), + SizedBox(width: 8), + Text('Modifier'), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon( + Icons.delete, + size: 16, + color: Colors.red, ), - child: Text( - getStatusLabel(table.status), - style: const TextStyle( - color: Colors.white, - fontSize: 9, - fontWeight: FontWeight.w500, + SizedBox(width: 8), + Text( + 'Supprimer', + style: TextStyle( + color: Colors.red, ), ), - ), - ], + ], + ), ), + ], + ), + ), - const SizedBox(height: 12), - - // Capacity - Row( - children: [ - Icon( - Icons.people_outline, - size: 16, - color: Colors.grey.shade600, - ), - const SizedBox(width: 6), - Text( - 'Capacité: ${table.capacity} personnes', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - ), - ), - ], + // Table content + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + table.nom, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.black87, ), - - const Spacer(), - - // Button - SizedBox( - width: double.infinity, - height: 36, - child: ElevatedButton( - onPressed: - isSelectable ? () {} : null, - style: ElevatedButton.styleFrom( - backgroundColor: - isSelectable - ? Colors.green.shade700 - : Colors.grey.shade300, - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(8), - ), - ), - child: Text( - isSelectable - ? "Sélectionner" - : "Indisponible", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w500, - color: - isSelectable - ? Colors.white - : Colors.grey.shade600, - ), - ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: getStatusColor(table.status), + borderRadius: BorderRadius.circular( + 12, ), ), - ], + child: Text( + getStatusLabel(table.status), + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.people_outline, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 6), + Text( + 'Capacité: ${table.capacity} personnes', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + ], + ), + const Spacer(), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + isSelectable + ? () { + // Handle table selection + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Table ${table.nom} sélectionnée', + ), + ), + ); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: + isSelectable + ? Colors.green.shade700 + : Colors.grey.shade300, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 8, + ), + ), + ), + child: Text( + isSelectable + ? 'Sélectionner' + : 'Indisponible', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), ), ), - ); - }, - ), - ), - - // Legend - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildLegendItem('Disponible', Colors.green), - const SizedBox(width: 24), - _buildLegendItem( - 'Occupée', - Colors.green.shade800, + ], ), - const SizedBox(width: 24), - _buildLegendItem('Réservée', Colors.orange), - ], - ), - ], - ), - ), - ), + ), + ], + ), + ); + }, + ), + ), - // Bottom Navigation - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Row( - children: [ + // Legend - Compact Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.green.shade700, - borderRadius: BorderRadius.circular(20), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.table_restaurant, - color: Colors.white, - size: 16, - ), - SizedBox(width: 6), - Text( - 'Tables', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - ), - ), + _buildLegendItem('Disponible', Colors.green), + const SizedBox(width: 20), + _buildLegendItem('Occupée', Colors.orange), + const SizedBox(width: 20), + _buildLegendItem('Réservée', Colors.orange), ], ), ), - const SizedBox(width: 20), - Row( - children: [ - Icon( - Icons.receipt_long_outlined, - color: Colors.grey.shade600, - size: 16, - ), - const SizedBox(width: 6), - Text( - 'Commandes', - style: TextStyle(color: Colors.grey.shade600), - ), - ], - ), - const Spacer(), - Row( - children: [ - Icon( - Icons.person_outline, - color: Colors.grey.shade600, - size: 16, - ), - const SizedBox(width: 6), - Text( - 'Chef Pierre', - style: TextStyle(color: Colors.grey.shade600), - ), - const SizedBox(width: 8), - Icon( - Icons.expand_more, - color: Colors.grey.shade600, - size: 16, - ), - ], - ), ], ), ), - ], - ), + ), + ], ); } @@ -390,3 +534,133 @@ class _TablesScreenState extends State { ); } } + +// Add/Edit Table Dialog +class _AddEditTableDialog extends StatefulWidget { + final TableData? table; + + const _AddEditTableDialog({this.table}); + + @override + _AddEditTableDialogState createState() => _AddEditTableDialogState(); +} + +class _AddEditTableDialogState extends State<_AddEditTableDialog> { + final _formKey = GlobalKey(); + late TextEditingController _nomController; + late TextEditingController _capacityController; + String _selectedStatus = 'available'; + + final List> _statusOptions = [ + {'value': 'available', 'label': 'Disponible'}, + {'value': 'occupied', 'label': 'Occupée'}, + {'value': 'reserved', 'label': 'Réservée'}, + ]; + + @override + void initState() { + super.initState(); + _nomController = TextEditingController(text: widget.table?.nom ?? ''); + _capacityController = TextEditingController( + text: widget.table?.capacity.toString() ?? '', + ); + _selectedStatus = widget.table?.status ?? 'available'; + } + + @override + void dispose() { + _nomController.dispose(); + _capacityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isEditing = widget.table != null; + + return AlertDialog( + title: Text(isEditing ? 'Modifier la table' : 'Ajouter une table'), + content: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: _nomController, + decoration: const InputDecoration( + labelText: 'Nom de la table', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer un nom'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _capacityController, + decoration: const InputDecoration( + labelText: 'Capacité', + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer la capacité'; + } + if (int.tryParse(value) == null) { + return 'Veuillez entrer un nombre valide'; + } + return null; + }, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: _selectedStatus, + decoration: const InputDecoration( + labelText: 'Statut', + border: OutlineInputBorder(), + ), + items: + _statusOptions.map((option) { + return DropdownMenuItem( + value: option['value'], + child: Text(option['label']!), + ); + }).toList(), + onChanged: (value) { + setState(() { + _selectedStatus = value!; + }); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final result = { + 'nom': _nomController.text, + 'capacity': int.parse(_capacityController.text), + 'status': _selectedStatus, + }; + Navigator.pop(context, result); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade700, + ), + child: Text(isEditing ? 'Modifier' : 'Ajouter'), + ), + ], + ); + } +} diff --git a/lib/widgets/bottom_navigation.dart b/lib/widgets/bottom_navigation.dart new file mode 100644 index 0000000..d2b9dc3 --- /dev/null +++ b/lib/widgets/bottom_navigation.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +class AppBottomNavigation extends StatelessWidget { + final int selectedIndex; + final Function(int) onItemTapped; + final bool isDesktop; + + const AppBottomNavigation({ + super.key, + required this.selectedIndex, + required this.onItemTapped, + this.isDesktop = false, + }); + + @override + Widget build(BuildContext context) { + if (isDesktop) return const SizedBox.shrink(); + + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + // Tables Tab + GestureDetector( + onTap: () => onItemTapped(0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: + selectedIndex == 0 + ? Colors.green.shade700 + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.table_restaurant, + color: + selectedIndex == 0 + ? Colors.white + : Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Tables', + style: TextStyle( + color: + selectedIndex == 0 + ? Colors.white + : Colors.grey.shade600, + fontWeight: + selectedIndex == 0 + ? FontWeight.w500 + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + + const SizedBox(width: 20), + + // Commandes Tab + GestureDetector( + onTap: () => onItemTapped(1), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: + selectedIndex == 1 + ? Colors.green.shade700 + : Colors.transparent, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.receipt_long_outlined, + color: + selectedIndex == 1 + ? Colors.white + : Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Commandes', + style: TextStyle( + color: + selectedIndex == 1 + ? Colors.white + : Colors.grey.shade600, + fontWeight: + selectedIndex == 1 + ? FontWeight.w500 + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + + const Spacer(), + + // User Profile Section + _buildUserProfile(), + ], + ), + ); + } + + Widget _buildUserProfile() { + return Row( + children: [ + Icon(Icons.person_outline, color: Colors.grey.shade600, size: 16), + const SizedBox(width: 6), + Text('Chef Pierre', style: TextStyle(color: Colors.grey.shade600)), + const SizedBox(width: 8), + Icon(Icons.expand_more, color: Colors.grey.shade600, size: 16), + ], + ); + } +} diff --git a/lib/widgets/mobile_bottom_navigation.dart b/lib/widgets/mobile_bottom_navigation.dart new file mode 100644 index 0000000..9ddb11f --- /dev/null +++ b/lib/widgets/mobile_bottom_navigation.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +class MobileBottomNavigation extends StatelessWidget { + final int selectedIndex; + final Function(int) onItemTapped; + + const MobileBottomNavigation({ + super.key, + required this.selectedIndex, + required this.onItemTapped, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildBottomNavItem( + icon: Icons.table_restaurant, + label: 'Tables', + index: 0, + ), + _buildBottomNavItem( + icon: Icons.receipt_long_outlined, + label: 'Commandes', + index: 1, + ), + ], + ), + ); + } + + Widget _buildBottomNavItem({ + required IconData icon, + required String label, + required int index, + }) { + final isSelected = selectedIndex == index; + + return GestureDetector( + onTap: () => onItemTapped(index), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected ? Colors.green.shade700 : Colors.grey.shade600, + size: 24, + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + color: isSelected ? Colors.green.shade700 : Colors.grey.shade600, + fontSize: 12, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ); + } +} From 39d0b7103e54daeeb6c2baf53efa82c1f66850b7 Mon Sep 17 00:00:00 2001 From: Stephane Date: Sat, 2 Aug 2025 02:16:27 +0300 Subject: [PATCH 4/4] margin top --- lib/pages/tables.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/tables.dart b/lib/pages/tables.dart index ea8a4ab..d3a1cdb 100644 --- a/lib/pages/tables.dart +++ b/lib/pages/tables.dart @@ -251,6 +251,7 @@ class _TablesScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 30), const Text( 'Sélectionner une table', style: TextStyle(