diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart index dcf2bdd..8936a83 100644 --- a/lib/core/layout/app_shell.dart +++ b/lib/core/layout/app_shell.dart @@ -1,97 +1,375 @@ import 'package:flutter/material.dart'; +import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:go_router/go_router.dart'; +// ========================================== +// 1. IL GUSCIO (QUELLO CHE PASSI AL ROUTER) +// ========================================== class AppShell extends StatelessWidget { final Widget child; const AppShell({super.key, required this.child}); - // Calcoliamo l'indice attivo in base all'URL corrente! - int _calculateSelectedIndex(BuildContext context) { - final String location = GoRouterState.of(context).uri.path; - if (location.startsWith('/master-data')) return 1; - if (location.startsWith('/settings')) return 2; - return 0; // Default: Dashboard - } - - void _onItemTapped(int index, BuildContext context) { - switch (index) { - case 0: - context.go('/'); - break; - case 1: - context.go('/master-data'); - break; - case 2: - context.go('/settings'); - break; - } - } - @override Widget build(BuildContext context) { - final currentIndex = _calculateSelectedIndex(context); - // Breakpoint: se lo schermo è più largo di 600px, usiamo la Rail laterale - final isDesktop = MediaQuery.sizeOf(context).width >= 600; + // Breakpoint a 900px: sotto è Mobile/Tablet (Drawer), sopra è Desktop (Sidebar) + final isDesktop = MediaQuery.sizeOf(context).width >= 900; + final currentPath = GoRouterState.of(context).uri.path; return Scaffold( + // Su mobile usiamo un'AppBar minimale per avere il bottone "Hamburger" nativo + appBar: isDesktop + ? null + : AppBar( + title: const Text( + "FLUX", + style: TextStyle(fontWeight: FontWeight.bold), + ), + centerTitle: true, + elevation: 0, + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + ), + drawer: isDesktop + ? null + : Drawer( + // Su mobile inietta il menu qui! + child: AppMenu(currentPath: currentPath, isDrawer: true), + ), body: isDesktop ? Row( children: [ - NavigationRail( - selectedIndex: currentIndex, - onDestinationSelected: (index) => - _onItemTapped(index, context), - labelType: NavigationRailLabelType.all, - destinations: [ - NavigationRailDestination( - icon: Icon(Icons.dashboard_outlined), - selectedIcon: Icon(Icons.dashboard), - label: Text(context.l10n.commonDashboard), - ), - NavigationRailDestination( - icon: Icon(Icons.folder_special_outlined), - selectedIcon: Icon(Icons.folder_special), - label: Text(context.l10n.commonMasterData), - ), - NavigationRailDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings), - label: Text(context.l10n.commonSettings), - ), - ], - ), + // Su desktop inietta il menu a sinistra! + AppMenu(currentPath: currentPath, isDrawer: false), const VerticalDivider(thickness: 1, width: 1), - // Il contenuto della pagina Expanded(child: child), ], ) - : child, // Su mobile il contenuto prende tutto lo schermo... - // ... e mettiamo la barra in basso! - bottomNavigationBar: isDesktop - ? null - : NavigationBar( - selectedIndex: currentIndex, - onDestinationSelected: (index) => _onItemTapped(index, context), - destinations: [ - NavigationDestination( - icon: Icon(Icons.dashboard_outlined), - selectedIcon: Icon(Icons.dashboard), - label: context.l10n.commonDashboard, - ), - NavigationDestination( - icon: Icon(Icons.folder_special_outlined), - selectedIcon: Icon(Icons.folder_special), - label: context.l10n.commonMasterData, - ), - NavigationDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings), - label: context.l10n.commonSettings, - ), - ], - ), + : child, // Su mobile il child prende tutto lo schermo sotto l'AppBar ); } } + +class AppMenu extends StatefulWidget { + final String currentPath; // Lo usiamo ancora per capire cosa accendere + final bool isDrawer; + + const AppMenu({super.key, required this.currentPath, required this.isDrawer}); + + @override + State createState() => _AppMenuState(); +} + +class _AppMenuState extends State { + bool _isCollapsed = false; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final bool effectivelyCollapsed = _isCollapsed && !widget.isDrawer; + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + width: effectivelyCollapsed ? 72 : 260, + child: SafeArea( + child: Column( + children: [ + // --- HEADER --- + Container( + height: 80, + padding: const EdgeInsets.symmetric(horizontal: 20.0), + alignment: effectivelyCollapsed + ? Alignment.center + : Alignment.centerLeft, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.bolt, color: theme.colorScheme.primary, size: 32), + if (!effectivelyCollapsed) ...[ + const SizedBox(width: 12), + Text( + "FLUX", + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ], + ), + ), + + // --- VOCI DI MENU --- + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: SizedBox( + width: effectivelyCollapsed ? 72 : 260, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + children: [ + _buildRouteItem( + title: context.l10n.commonDashboard, + icon: Icons.dashboard_outlined, + routeName: Routes.home, // <--- Usiamo la tua costante! + pathToCheck: + '/', // Il path da controllare per colorarlo + isCollapsed: effectivelyCollapsed, + ), + const SizedBox(height: 8), + + // --- IL MENU GERARCHICO (ANAGRAFICHE) --- + _buildHierarchicalItem( + title: context.l10n.commonMasterData, + icon: Icons.folder_special_outlined, + basePathToCheck: + '/master-data', // Se il path inizia così, espandi + isCollapsed: effectivelyCollapsed, + subItems: [ + _SubMenuItem( + "Clienti", + Routes.customers, + '/master-data/customers', + ), + _SubMenuItem( + "Fornitori", + Routes.providers, + '/master-data/providers', + ), + _SubMenuItem( + "Prodotti", + Routes.products, + '/master-data/products', + ), + _SubMenuItem( + "Staff", + Routes.staff, + '/master-data/staff', + ), + ], + ), + + const SizedBox(height: 8), + _buildRouteItem( + title: context.l10n.commonSettings, + icon: Icons.settings_outlined, + routeName: Routes.settings, + pathToCheck: '/settings', + isCollapsed: effectivelyCollapsed, + ), + ], + ), + ), + ), + ), + + // --- PULSANTE TOGGLE (Solo Desktop) --- + if (!widget.isDrawer) + Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + tooltip: _isCollapsed ? 'Espandi Menu' : 'Riduci Menu', + icon: Icon( + _isCollapsed + ? Icons.keyboard_double_arrow_right + : Icons.keyboard_double_arrow_left, + color: theme.iconTheme.color?.withValues(alpha: 0.5), + ), + onPressed: () { + setState(() { + _isCollapsed = !_isCollapsed; + }); + }, + ), + ), + ], + ), + ), + ); + } + + // ========================================== + // WIDGET HELPER AGGIORNATI + // ========================================== + + Widget _buildRouteItem({ + required String title, + required IconData icon, + required String routeName, + required String pathToCheck, + required bool isCollapsed, + }) { + final isSelected = widget.currentPath == pathToCheck; + final theme = Theme.of(context); + + if (isCollapsed) { + return Tooltip( + message: title, + preferBelow: false, + child: InkWell( + onTap: () { + if (widget.isDrawer) Navigator.pop(context); + context.goNamed(routeName); // <--- goNamed! + }, + borderRadius: BorderRadius.circular(8), + child: Container( + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primaryContainer.withValues(alpha: 0.4) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: isSelected ? theme.colorScheme.primary : null, + ), + ), + ), + ); + } + + return ListTile( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + selectedTileColor: theme.colorScheme.primaryContainer.withValues( + alpha: 0.4, + ), + selected: isSelected, + leading: Icon(icon, color: isSelected ? theme.colorScheme.primary : null), + title: Text( + title, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + maxLines: 1, + overflow: TextOverflow.clip, + ), + onTap: () { + if (widget.isDrawer) Navigator.pop(context); + context.goNamed(routeName); // <--- goNamed! + }, + ); + } + + Widget _buildHierarchicalItem({ + required String title, + required IconData icon, + required String basePathToCheck, + required bool isCollapsed, + required List<_SubMenuItem> subItems, + }) { + final isSelected = widget.currentPath.startsWith(basePathToCheck); + final theme = Theme.of(context); + + if (isCollapsed) { + return PopupMenuButton( + tooltip: title, + offset: const Offset(60, 0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + onSelected: (routeName) { + // Il routeName arriva dal value del menu + if (widget.isDrawer) Navigator.pop(context); + context.goNamed(routeName); // <--- goNamed! + }, + itemBuilder: (context) => subItems + .map( + (item) => PopupMenuItem( + value: item + .routeName, // Passiamo il nome della rotta (Routes.customers) + child: Text( + item.title, + style: TextStyle( + fontWeight: widget.currentPath == item.pathToCheck + ? FontWeight.bold + : FontWeight.normal, + color: widget.currentPath == item.pathToCheck + ? theme.colorScheme.primary + : null, + ), + ), + ), + ) + .toList(), + child: Container( + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primaryContainer.withValues(alpha: 0.4) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: isSelected ? theme.colorScheme.primary : null, + ), + ), + ); + } + + return Theme( + data: theme.copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + initiallyExpanded: isSelected, + maintainState: true, + tilePadding: const EdgeInsets.symmetric(horizontal: 16), + leading: Icon( + icon, + color: isSelected ? theme.colorScheme.primary : null, + ), + title: Text( + title, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + maxLines: 1, + overflow: TextOverflow.clip, + ), + children: subItems.map((item) { + final isSubSelected = widget.currentPath == item.pathToCheck; + return Padding( + padding: const EdgeInsets.only(left: 32.0, bottom: 4.0), + child: ListTile( + dense: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + selectedTileColor: theme.colorScheme.primaryContainer.withValues( + alpha: 0.2, + ), + selected: isSubSelected, + title: Text( + item.title, + style: TextStyle( + fontWeight: isSubSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSubSelected + ? theme.colorScheme.primary + : theme.textTheme.bodyMedium?.color, + ), + maxLines: 1, + overflow: TextOverflow.clip, + ), + onTap: () { + if (widget.isDrawer) Navigator.pop(context); + context.goNamed(item.routeName); // <--- goNamed! + }, + ), + ); + }).toList(), + ), + ); + } +} + +// Struttura dati per le voci dei sottomenu aggiornata +class _SubMenuItem { + final String title; + final String routeName; // Es: Routes.customers + final String pathToCheck; // Es: '/master-data/customers' + _SubMenuItem(this.title, this.routeName, this.pathToCheck); +} diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 7103961..66eafdd 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -126,78 +126,89 @@ class AppRouter { ShellRoute( builder: (context, state, child) => AppShell(child: child), routes: [ + // ========================================== // 1. DASHBOARD + // ========================================== GoRoute( path: '/', name: Routes.home, builder: (context, state) => const HomeScreen(), ), + // ========================================== // 2. HUB ANAGRAFICHE E SOTTO-ROTTE + // ========================================== GoRoute( path: '/master-data', name: Routes.masterData, builder: (context, state) => const MasterDataHubScreen(), routes: [ GoRoute( - path: 'products', // Diventa /master-data/products + path: + 'customers', // Niente slash iniziale per le sottorotte! -> /master-data/customers + name: Routes.customers, + builder: (context, state) => const CustomersListScreen(), + ), + GoRoute( + path: 'providers', // -> /master-data/providers + name: Routes.providers, + builder: (context, state) => const ProviderListScreen(), + ), + GoRoute( + path: 'products', // -> /master-data/products name: Routes.products, builder: (context, state) { context.read().refreshCubit(); - return const ProductsScreen(); }, ), GoRoute( - path: 'company-settings', + path: 'staff', // -> /master-data/staff + name: Routes.staff, + builder: (context, state) => const StaffScreen(), + ), + GoRoute( + path: + 'stores', // Sistemata l'inversione path/name -> /master-data/stores + name: Routes.stores, + builder: (context, state) => const StoresScreen(), + ), + GoRoute( + path: 'company-settings', // -> /master-data/company-settings name: Routes.companySettings, builder: (context, state) => BlocProvider( create: (context) => CompanySettingsCubit(), child: const CompanySettingsScreen(), ), ), - GoRoute( - path: 'staff', - name: Routes.staff, // Diventa /master-data/staff - builder: (context, state) => const StaffScreen(), - ), - GoRoute( - path: Routes.stores, - name: 'stores', // Diventa /master-data/stores - builder: (context, state) => const StoresScreen(), - ), - GoRoute( - path: '/providers', - name: Routes.providers, - builder: (context, state) => const ProviderListScreen(), - ), ], ), + // ========================================== // 3. IMPOSTAZIONI + // ========================================== GoRoute( path: '/settings', name: Routes.settings, builder: (context, state) => const SettingsScreen(), routes: [ GoRoute( - path: 'themeSettings', + path: 'themeSettings', // -> /settings/themeSettings name: Routes.themeSettings, builder: (context, state) => const ThemeSettingsView(), ), ], ), + + // ========================================== + // 4. SCHERMATE PRINCIPALI EXTRA NELLA SHELL + // (Accessibili ad es. dalla dashboard, mantengono la sidebar) + // ========================================== GoRoute( path: '/operations', name: Routes.operations, builder: (context, state) => const OperationListScreen(), ), - GoRoute( - path: '/customers', - name: Routes.customers, - builder: (context, state) => - const CustomersListScreen(), // O come si chiama il tuo widget della lista! - ), GoRoute( path: '/tickets', name: Routes.tickets,