From f7fd7c763df7b788754ecac041de44d195c1f419 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Wed, 29 Apr 2026 11:39:40 +0200 Subject: [PATCH] ok, design pulito e gorouter perfezionato --- lib/core/layout/app_shell.dart | 96 +++ lib/core/routes/app_router.dart | 118 ++-- lib/features/home/ui/home_screen.dart | 627 ++++++++++-------- .../home/ui/quick_actions_widget.dart | 45 ++ .../master_data/master_data_hub_content.dart | 295 ++++---- 5 files changed, 708 insertions(+), 473 deletions(-) create mode 100644 lib/core/layout/app_shell.dart create mode 100644 lib/features/home/ui/quick_actions_widget.dart diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart new file mode 100644 index 0000000..d5eb3af --- /dev/null +++ b/lib/core/layout/app_shell.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +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; + + return Scaffold( + body: isDesktop + ? Row( + children: [ + NavigationRail( + selectedIndex: currentIndex, + onDestinationSelected: (index) => + _onItemTapped(index, context), + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.dashboard_outlined), + selectedIcon: Icon(Icons.dashboard), + label: Text('Dashboard'), + ), + NavigationRailDestination( + icon: Icon(Icons.folder_special_outlined), + selectedIcon: Icon(Icons.folder_special), + label: Text('Anagrafiche'), + ), + NavigationRailDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: Text('Impostazioni'), + ), + ], + ), + 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: const [ + NavigationDestination( + icon: Icon(Icons.dashboard_outlined), + selectedIcon: Icon(Icons.dashboard), + label: 'Dashboard', + ), + NavigationDestination( + icon: Icon(Icons.folder_special_outlined), + selectedIcon: Icon(Icons.folder_special), + label: 'Anagrafiche', + ), + NavigationDestination( + icon: Icon(Icons.settings_outlined), + selectedIcon: Icon(Icons.settings), + label: 'Impostazioni', + ), + ], + ), + ); + } +} diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index ea2c5d3..89c892f 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,17 +1,18 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -// Importa il tuo SessionCubit e lo State import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/core_repository.dart'; +import 'package:flux/core/layout/app_shell.dart'; import 'package:flux/core/widgets/set_password_screen.dart'; -import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; +import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart'; +import 'package:flux/features/customers/ui/customers_content.dart'; import 'package:flux/features/home/ui/home_screen.dart'; +import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; @@ -22,57 +23,44 @@ import 'package:flux/features/services/ui/service_form_screen/service_mobile_upl import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; +// Nota: Dovrai creare questi placeholder o file per non avere errori di compilazione +// import 'package:flux/features/master_data/master_data_hub_screen.dart'; +// import 'package:flux/features/master_data/staff/ui/staff_screen.dart'; +// import 'package:flux/features/master_data/store/ui/stores_screen.dart'; + class AppRouter { static GoRouter createRouter(SessionCubit sessionCubit) { return GoRouter( initialLocation: '/', - // MAGIA 1: Il router "ascolta" ogni singolo respiro del SessionCubit refreshListenable: GoRouterRefreshStream(sessionCubit.stream), - - // MAGIA 2: Il Buttafuori Supremo redirect: (context, state) { final sessionState = sessionCubit.state; final isGoingToLogin = state.matchedLocation == '/login'; final isGoingToOnboarding = state.matchedLocation == '/onboarding'; final isGoingToSetPassword = state.matchedLocation == '/set-password'; - // Caso 1: L'app si sta ancora avviando. - // Restituiamo null per farlo rimanere sulla SplashScreen del main.dart - if (sessionState.status == SessionStatus.initial) { - return null; - } + if (sessionState.status == SessionStatus.initial) return null; - // Caso 2: Utente NON loggato. if (sessionState.status == SessionStatus.unauthenticated) { - // Se sta già andando al login, lascialo andare. Altrimenti, forzalo al login. if (isGoingToLogin || isGoingToSetPassword) return null; return '/login'; } - // Caso 3: Utente loggato MA manca un pezzo dell'azienda (Flusso Canalizzatore) if (sessionState.status == SessionStatus.onboardingRequired) { - // Se sta già andando all'onboarding, ok. Altrimenti forzalo lì. - // Non può "scappare" digitando l'URL della dashboard! return isGoingToOnboarding ? null : '/onboarding'; } - // Caso 4: Utente loggato e configurato (Tutto OK!) if (sessionState.status == SessionStatus.authenticated) { - // Attenzione: un utente appena invitato viene considerato "loggato" - // da Supabase appena clicca il link. Quindi se sta andando su /set-password, - // dobbiamo permetterglielo e non rimbalzarlo! - if (isGoingToLogin || isGoingToOnboarding) { - return '/'; - } - return null; // Lascia passare per /, /customer, e anche /set-password + if (isGoingToLogin || isGoingToOnboarding) return '/'; + return null; } return null; }, routes: [ + // --- ROTTE DI SERVIZIO (FUORI DALLA SHELL) --- GoRoute( path: '/login', - //builder: (context, state) => const LoginScreen(), builder: (context, state) => const AuthScreen(), ), GoRoute( @@ -88,18 +76,69 @@ class AppRouter { ), child: const OnboardingScreen(), ), - // Nota: All'interno di questa schermata useremo il PageView pilotato - // dall'OnboardingStep. Al router non interessa quale step è attivo, - // gli basta sapere che deve stare rinchiuso qui dentro! ), + + // --- CORE APP (DENTRO LA SHELL CON NAVIGATION BAR/RAIL) --- + ShellRoute( + builder: (context, state, child) => AppShell(child: child), + routes: [ + // 1. DASHBOARD + GoRoute(path: '/', builder: (context, state) => const HomeScreen()), + + // 2. HUB ANAGRAFICHE E SOTTO-ROTTE + GoRoute( + path: '/master-data', + builder: (context, state) => const MasterDataHubScreen(), + routes: [ + GoRoute( + path: 'products', // Diventa /master-data/products + builder: (context, state) => const ProductsScreen(), + ), + GoRoute( + path: 'staff', // Diventa /master-data/staff + builder: (context, state) => + const Scaffold(body: Center(child: Text("Lista Staff"))), + ), + GoRoute( + path: 'stores', // Diventa /master-data/stores + builder: (context, state) => + const Scaffold(body: Center(child: Text("Lista Negozi"))), + ), + GoRoute( + path: 'providers', // Diventa /master-data/providers + builder: (context, state) => const Scaffold( + body: Center(child: Text("Lista Fornitori")), + ), + ), + ], + ), + + // 3. IMPOSTAZIONI + GoRoute( + path: '/settings', + builder: (context, state) => Scaffold( + appBar: AppBar(title: const Text("Impostazioni")), + body: Center( + child: ElevatedButton.icon( + onPressed: () => context.read().signOut(), + icon: const Icon(Icons.logout), + label: const Text("Esci da FLUX"), + ), + ), + ), + ), + ], + ), + + // --- DETTAGLI E OPERATIVITÀ (FUORI DALLA SHELL - TUTTO SCHERMO) --- GoRoute( - path: '/', - builder: (context, state) => const HomeScreen(), // La tua home + path: '/customers', + builder: (context, state) => + const CustomersContent(), // O come si chiama il tuo widget della lista! ), GoRoute( path: '/customer/:id', builder: (context, state) { - // Recuperiamo l'oggetto customer passato tramite extra final customer = state.extra as CustomerModel; return BlocProvider( create: (context) => CustomerFilesBloc(customer.id!), @@ -111,10 +150,7 @@ class AppRouter { path: '/customer/:id/upload', builder: (context, state) { final customerId = state.pathParameters['id']!; - // Recuperiamo il nome dalle query se vogliamo mostrarlo nel titolo, - // oppure lo caricherà il bloc. final customerName = state.uri.queryParameters['name'] ?? 'Cliente'; - return BlocProvider( create: (context) => CustomerFilesBloc(customerId), child: CustomerMobileUploadScreen( @@ -124,20 +160,12 @@ class AppRouter { ); }, ), - GoRoute( - path: '/products', - name: 'products', - builder: (context, state) => const ProductsScreen(), - ), GoRoute( path: '/service-form', name: 'service-form', builder: (context, state) { - // Recuperiamo l'oggetto se passato tramite 'extra' final existingService = state.extra as ServiceModel?; - // Recuperiamo l'ID se presente nell'URL final serviceId = state.uri.queryParameters['serviceId']; - return BlocProvider( create: (context) => ServiceFilesBloc(serviceId: serviceId ?? existingService?.id), @@ -153,9 +181,7 @@ class AppRouter { builder: (context, state) { final serviceId = state.pathParameters['id']!; final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; - return BlocProvider( - // Inizializziamo il bloc col serviceId corretto! create: (context) => ServiceFilesBloc(serviceId: serviceId), child: ServiceMobileUploadScreen( serviceId: serviceId, @@ -169,8 +195,6 @@ class AppRouter { } } -/// Utility fondamentale per GoRouter: trasforma lo Stream del Cubit -/// in un Listenable che GoRouter può ascoltare per forzare i redirect. class GoRouterRefreshStream extends ChangeNotifier { GoRouterRefreshStream(Stream stream) { notifyListeners(); @@ -178,9 +202,7 @@ class GoRouterRefreshStream extends ChangeNotifier { (dynamic _) => notifyListeners(), ); } - late final StreamSubscription _subscription; - @override void dispose() { _subscription.cancel(); diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index bf11ead..9f9abac 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -2,323 +2,384 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/auth/bloc/auth_cubit.dart'; -import 'package:flux/features/master_data/master_data_hub_content.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/ui/services_screen.dart'; -import 'package:get_it/get_it.dart'; -import 'dashboard_content.dart'; +import 'package:flux/features/home/ui/quick_actions_widget.dart'; +import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; +import 'package:go_router/go_router.dart'; -class HomeScreen extends StatefulWidget { +class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); - @override - State createState() => _HomeScreenState(); -} - -class _HomeScreenState extends State { - int _selectedIndex = 0; - bool _extendRailway = false; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().loadServices(); - }); - } - @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return LayoutBuilder( - builder: (context, constraints) { - final bool isLargeScreen = constraints.maxWidth > 900; - final bool veryLargeScreen = constraints.maxWidth > 1200; - final bool isMenuExtended = veryLargeScreen ? true : _extendRailway; + final theme = Theme.of(context); - return Scaffold( - // --- APPBAR (Solo Mobile) --- - appBar: isLargeScreen - ? null - : AppBar( - title: const Text( - 'FLUX Gestionale', - style: TextStyle(fontWeight: FontWeight.bold), - ), - elevation: 0, - actions: [ - Padding( - padding: const EdgeInsets.only(right: 16.0), - child: _buildUserMenu(context, isExtended: false), - ), - ], - ), - body: Row( - children: [ - // --- SIDEBAR (Desktop) --- - if (isLargeScreen) _buildDesktopSidebar(isMenuExtended), - - // --- CONTENUTO DINAMICO --- - Expanded( - child: _buildPageContent(_selectedIndex, isLargeScreen), - ), - ], - ), - // --- BOTTOM BAR (Solo Mobile) --- - bottomNavigationBar: isLargeScreen - ? null - : _buildBottomNavigationBar(_selectedIndex), - ); - }, - ); - }, - ); - } - - // =========================================================================== - // COMPONENTI UI - // =========================================================================== - - // Costruisce l'intera colonna laterale (Rail + Menu Utente in fondo) - Widget _buildDesktopSidebar(bool isExtended) { - return MouseRegion( - // Spostiamo qui la logica dell'hover! - onEnter: (_) => setState(() => _extendRailway = true), - onExit: (_) => setState(() => _extendRailway = false), - child: Container( - color: context.background, // Mantiene lo stesso colore della Rail + return Scaffold( + backgroundColor: theme.colorScheme.surface, + body: SafeArea( child: Column( children: [ + // ========================================== + // 1. HEADER FISSO (Non scrolla mai) + // ========================================== + Container( + padding: const EdgeInsets.all(24.0), + // Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto + color: theme.colorScheme.surface, + child: _buildHeader(context, theme), + ), + + // ========================================== + // 2. CORPO DELLA DASHBOARD (Scrollabile) + // ========================================== Expanded( - child: _buildNavigationRail(isExtended), // Ora la Rail è "nuda" - ), - // --- AVATAR E MENU IN FONDO ALLA SIDEBAR --- - Padding( - padding: const EdgeInsets.only(bottom: 24.0, top: 8.0), - child: _buildUserMenu(context, isExtended: isExtended), - ), - ], - ), - ), - ); - } - - Widget _buildNavigationRail(bool isExtended) { - return NavigationRail( - extended: isExtended, - selectedIndex: _selectedIndex, - onDestinationSelected: (index) => setState(() => _selectedIndex = index), - backgroundColor: Colors - .transparent, // Impostato trasparente per prendere il colore del Container padre - indicatorColor: context.accent.withValues(alpha: 0.2), - leading: _buildRailHeader(isExtended), - selectedIconTheme: IconThemeData(color: context.accent, size: 28), - unselectedIconTheme: IconThemeData( - color: context.secondaryText, - size: 24, - ), - selectedLabelTextStyle: TextStyle( - color: context.accent, - fontWeight: FontWeight.bold, - ), - unselectedLabelTextStyle: TextStyle(color: context.secondaryText), - destinations: const [ - NavigationRailDestination( - icon: Icon(Icons.dashboard_outlined), - selectedIcon: Icon(Icons.dashboard), - label: Text('Dashboard'), - ), - NavigationRailDestination( - icon: Icon(Icons.receipt_long_outlined), - selectedIcon: Icon(Icons.receipt_long), - label: Text('Servizi'), - ), - NavigationRailDestination( - icon: Icon(Icons.folder_shared_outlined), - selectedIcon: Icon(Icons.folder_shared), - label: Text('Anagrafiche'), - ), - ], - ); - } - - // --- MENU UTENTE (Il "Pro" Avatar) --- - Widget _buildUserMenu(BuildContext context, {required bool isExtended}) { - // Il PopupMenuButton gestisce da solo l'apertura a tendina - return PopupMenuButton( - offset: const Offset( - 0, - -120, - ), // Apre il menu verso l'alto su desktop se necessario - tooltip: 'Profilo e Impostazioni', - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - onSelected: (value) { - if (value == 'logout') { - _showLogoutDialog(context); - } - }, - itemBuilder: (BuildContext context) => [ - const PopupMenuItem( - value: 'profile', - child: ListTile( - leading: Icon(Icons.person_outline), - title: Text('Il mio Profilo'), - contentPadding: EdgeInsets.zero, - ), - ), - const PopupMenuDivider(), - const PopupMenuItem( - value: 'logout', - child: ListTile( - leading: Icon(Icons.logout, color: Colors.red), - title: Text( - 'Esci', - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), - ), - contentPadding: EdgeInsets.zero, - ), - ), - ], - // L'aspetto del pulsante (Icona tonda o Icona + Nome se esteso) - child: isExtended - ? Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: context.accent.withValues(alpha: 0.1), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - CircleAvatar( - radius: 16, - backgroundColor: context.accent, - child: const Icon( - Icons.person, - color: Colors.white, - size: 20, + child: CustomScrollView( + slivers: [ + // --- QUICK ACTIONS: AZIONI RAPIDE --- + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: _buildQuickActions(context), ), ), - const SizedBox(width: 12), - Text( - GetIt.I.get().state.company?.ragioneSociale ?? - "Utente", - style: TextStyle( - fontWeight: FontWeight.bold, - color: context.accent, + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + + // --- I WIDGET DELLA DASHBOARD (Responsive Grid) --- + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 500, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.3, + ), + delegate: SliverChildListDelegate([ + _buildDashboardWidget( + title: 'Contratti in Scadenza', + icon: Icons.assignment_late_outlined, + color: Colors.orange, + context: context, + ), + _buildDashboardWidget( + title: 'Sticky Notes', + icon: Icons.sticky_note_2_outlined, + color: Colors.yellow.shade700, + context: context, + ), + _buildDashboardWidget( + title: 'I miei Task', + icon: Icons.check_box_outlined, + color: Colors.green, + context: context, + ), + _buildDashboardWidget( + title: 'Ultimi Servizi', + icon: Icons.design_services_outlined, + color: Colors.blue, + context: context, + ), + _buildDashboardWidget( + title: 'Ultime Assistenze', + icon: Icons.support_agent_outlined, + color: Colors.purple, + context: context, + ), + ]), ), ), + + // Spazio finale per non far attaccare l'ultima card al fondo + const SliverToBoxAdapter(child: SizedBox(height: 40)), ], ), - ) - : CircleAvatar( - radius: 18, - backgroundColor: context.accent, - child: const Icon(Icons.person, color: Colors.white), ), + ], + ), + ), ); } - // --- DIALOG DI CONFERMA LOGOUT --- - void _showLogoutDialog(BuildContext context) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Row( - children: [ - Icon(Icons.logout, color: Colors.red), - SizedBox(width: 8), - Text("Chiudi sessione"), - ], - ), - content: const Text("Sei sicuro di voler uscire dal gestionale?"), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: const Text("Annulla"), + // ========================================== + // WIDGET BUILDERS + // ========================================== + + Widget _buildHeader(BuildContext context, ThemeData theme) { + final user = context.watch().state.currentStaffMember; + final currentStore = context.watch().state.currentStore; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Bentornato, ${user!.name}! 👋", + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: -0.5, + color: context.primaryText, // Uso dell'estensione! + ), + ), + const SizedBox(height: 8), + + InkWell( + onTap: () => _showStoreSelector(context, theme), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: context.primary.withValues( + alpha: 0.08, + ), // Sfondo delicato + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: context.primary.withValues( + alpha: 0.2, + ), // Bordino netto + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.storefront, size: 16, color: context.primary), + const SizedBox(width: 8), + Text( + currentStore?.nome ?? "Nessun negozio", + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primary, + ), + ), + const SizedBox(width: 4), + Icon(Icons.arrow_drop_down, color: context.primary), + ], + ), + ), + ), + ], ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - onPressed: () { - Navigator.pop(dialogContext); // Chiude la Dialog - context.read().requestLogout(); // Esegue il logout + ), + CircleAvatar( + radius: 24, + // Usiamo il Turchese (accent) in trasparenza per l'avatar + backgroundColor: context.accent.withValues(alpha: 0.15), + child: Icon(Icons.person, color: context.accent, size: 26), + ), + ], + ); + } + + Widget _buildQuickActions(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + QuickActionButton( + icon: Icons.add, + label: "Servizio", + color: Colors.blue, + onTap: () { + // Entriamo nel form! Nessun parametro extra = Nuovo Servizio + context.push('/service-form'); + }, + ), + const SizedBox(width: 12), + QuickActionButton( + icon: Icons.handyman, + label: "Assistenza", + color: Colors.redAccent, + onTap: () { + // TODO: Quando avrai la rotta per la nuova assistenza + // context.push('/assistance-form'); + }, + ), + const SizedBox(width: 12), + QuickActionButton( + icon: Icons.note_add, + label: "Nota", + color: Colors.amber, + onTap: () { + // TODO: Quando faremo il modale/pagina delle note + }, + ), + const SizedBox(width: 12), + QuickActionButton( + icon: Icons.task_alt, + label: "Task", + color: Colors.teal, + onTap: () { + // TODO: Quando faremo i task }, - child: const Text("Esci"), ), ], ), ); } - // ... mantieni gli altri tuoi metodi intatti (_buildRailHeader, _buildPageContent, _buildBottomNavigationBar) + Widget _buildDashboardWidget({ + required BuildContext context, + required String title, + required IconData icon, + required Color color, + }) { + final theme = Theme.of(context); - Widget _buildRailHeader(bool veryLargeScreen) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 24), - child: GestureDetector( - onTap: veryLargeScreen - ? null - : () => setState(() => _extendRailway = !_extendRailway), - child: _extendRailway - ? Text( - 'FLUX', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - color: context.accent, + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: color, size: 20), ), - ) - : Icon(Icons.bolt, color: context.accent, size: 32), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + icon: Icon( + Icons.more_vert, + size: 20, + color: context.secondaryText, + ), + onPressed: () {}, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const Spacer(), + Center( + child: Text( + "(Coming Soon)", + style: TextStyle( + color: context.secondaryText.withValues(alpha: 0.7), + fontStyle: FontStyle.italic, + fontSize: 13, + ), + ), + ), + const Spacer(), + ], + ), ), ); } - Widget _buildBottomNavigationBar(int selectedIndex) { - return BottomNavigationBar( - currentIndex: selectedIndex, - onTap: (index) => setState(() => _selectedIndex = index), - selectedItemColor: context.accent, - unselectedItemColor: context.secondaryText, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.dashboard), - label: 'Dashboard', - ), - BottomNavigationBarItem( - icon: Icon(Icons.receipt_long), - label: 'Servizi', - ), - BottomNavigationBarItem( - icon: Icon(Icons.folder_shared), - label: 'Anagrafiche', - ), - ], - ); - } + void _showStoreSelector(BuildContext context, ThemeData theme) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (context) { + // Leggiamo la lista dei negozi dal Cubit dedicato (se ce l'hai lì) + // Oppure adatta il blocco se li salvi altrove! + final staffState = context.watch().state; + final currentStoreId = context + .read() + .state + .currentStore + ?.id; + final currentStaffId = context + .read() + .state + .currentStaffMember + ?.id; - Widget _buildPageContent(int index, bool isLargeScreen) { - return IndexedStack( - index: index, - children: [ - DashboardContent( - isLargeScreen: isLargeScreen, - onTabRequested: (idx) => setState(() => _selectedIndex = 2), - ), - const ServicesScreen(), - MasterDataHubContent( - onOpenPage: (widget) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => widget), - ); - }, - ), - ], + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Text( + "Seleziona Negozio", + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 16), + if (staffState.status == StaffStatus.loading) + const Center(child: CircularProgressIndicator()) + else if (staffState.storesByStaff[currentStaffId] == null) + const Padding( + padding: EdgeInsets.all(24.0), + child: Text("Nessun negozio disponibile."), + ) + else + ...staffState.storesByStaff[currentStaffId]!.map((store) { + final isSelected = store.id == currentStoreId; + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24.0, + ), + leading: Icon( + Icons.storefront, + color: isSelected + ? theme.colorScheme.primary + : theme.iconTheme.color, + ), + title: Text( + store.nome, + style: TextStyle( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.normal, + color: isSelected ? theme.colorScheme.primary : null, + ), + ), + trailing: isSelected + ? Icon( + Icons.check_circle, + color: theme.colorScheme.primary, + ) + : null, + onTap: () { + // Cambiamo il negozio nel SessionCubit! + context.read().changeStore(store); + Navigator.pop(context); + }, + ); + }), + ], + ), + ), + ); + }, ); } } diff --git a/lib/features/home/ui/quick_actions_widget.dart b/lib/features/home/ui/quick_actions_widget.dart new file mode 100644 index 0000000..e062fa0 --- /dev/null +++ b/lib/features/home/ui/quick_actions_widget.dart @@ -0,0 +1,45 @@ +// --- WIDGET MINORE DI SUPPORTO --- + +import 'package:flutter/material.dart'; + +class QuickActionButton extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + final VoidCallback onTap; + + const QuickActionButton({ + super.key, + required this.icon, + required this.label, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: color.withValues(alpha: 0.3)), + color: color.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 20), + const SizedBox(width: 8), + Text( + label, + style: TextStyle(fontWeight: FontWeight.bold, color: color), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/master_data/master_data_hub_content.dart b/lib/features/master_data/master_data_hub_content.dart index 2d2281f..b7d4843 100644 --- a/lib/features/master_data/master_data_hub_content.dart +++ b/lib/features/master_data/master_data_hub_content.dart @@ -1,157 +1,168 @@ import 'package:flutter/material.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/customers/ui/customers_content.dart'; -import 'package:flux/features/master_data/products/ui/products_screen.dart'; -import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.dart'; -import 'package:flux/features/master_data/staff/ui/staff_screen.dart'; -import 'package:flux/features/master_data/store/ui/stores_screen.dart'; +import 'package:go_router/go_router.dart'; +// Mantieni i tuoi import per il tema se usi le estensioni (es. context.accent) +// import 'package:flux/core/theme/theme.dart'; -class MasterDataHubContent extends StatelessWidget { - final Function(Widget) onOpenPage; - - const MasterDataHubContent({super.key, required this.onOpenPage}); +class MasterDataHubScreen extends StatelessWidget { + const MasterDataHubScreen({super.key}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(23.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Anagrafiche", - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: context.accent, - ), - ), - const SizedBox(height: 8), - Text( - "Gestisci i dati fondamentali del tuo business", - style: TextStyle(color: context.secondaryText), - ), - const SizedBox(height: 32), + final theme = Theme.of(context); - Expanded( - child: GridView( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: MediaQuery.of(context).size.width > 600 ? 3 : 2, - mainAxisSpacing: 14, - crossAxisSpacing: 14, - // LA MAGIA: Fissiamo l'altezza della card a 200 pixel - // indipendentemente da quanto sia stretta la colonna! - mainAxisExtent: 200, + return Scaffold( + backgroundColor: theme.colorScheme.surface, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Anagrafiche", + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: -0.5, + // Se preferisci la tua estensione, usa: color: context.accent, + color: theme.colorScheme.primary, + ), ), - children: [ - _buildHubCard( - context, - title: 'Prodotti', - subtitle: 'Anagrafica di Marche e Modelli', - icon: Icons.inventory_2_outlined, - color: Colors.blue, - onTap: () => onOpenPage(const ProductsScreen()), + const SizedBox(height: 8), + Text( + "Gestisci i dati fondamentali del tuo business", + style: theme.textTheme.bodyLarge?.copyWith( + // Se preferisci: color: context.secondaryText, + color: theme.colorScheme.onSurfaceVariant, ), - _buildHubCard( - context, - title: 'Clienti', - subtitle: 'Anagrafica dei clienti del tuo business', - icon: Icons.people_outlined, - color: Colors.orange, - onTap: () => onOpenPage(const CustomersContent()), - ), - _buildHubCard( - context, - title: 'Addetti', - subtitle: 'Anagrafica del personale e dei collaboratori', - icon: Icons.badge_outlined, - color: Colors.teal, - onTap: () => onOpenPage(const StaffScreen()), - ), - _buildHubCard( - context, - title: 'Negozi', - subtitle: 'Anagrafica punti vendita della tua azienda', - icon: Icons.storefront_outlined, - color: Colors.purple, - onTap: () => onOpenPage(const StoresScreen()), - ), - _buildHubCard( - context, - title: - 'Provider', // Accorciato per non andare a capo male su mobile - subtitle: 'Anagrafica mandati e servizi', - icon: Icons.handshake_rounded, - color: Colors.indigo, - onTap: () { - Navigator.push( + ), + const SizedBox(height: 32), + + Expanded( + child: GridView( + // MAGIA RESPONSIVA: invece di contare le colonne, diciamo "Ogni card + // occupa massimo 350px". Su PC ne metterà 4, su Tablet 2, su Telefono 1 o 2. + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 350, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + mainAxisExtent: 200, // Altezza fissa per impedire overflow + ), + children: [ + _buildHubCard( context, - MaterialPageRoute( - builder: (context) => const ProvidersMasterDataScreen(), - ), - ); - }, + title: 'Prodotti', + subtitle: 'Anagrafica di Marche e Modelli', + icon: Icons.inventory_2_outlined, + color: Colors.blue, + // Navighiamo dentro la Shell (la BottomBar rimane!) + onTap: () => context.go('/master-data/products'), + ), + _buildHubCard( + context, + title: 'Clienti', + subtitle: 'Anagrafica dei clienti del tuo business', + icon: Icons.people_outlined, + color: Colors.orange, + // Usiamo .push() perché avevamo detto che i clienti + // stanno FUORI dalla Shell (niente BottomBar) + onTap: () => context.push('/customers'), + ), + _buildHubCard( + context, + title: 'Addetti', + subtitle: 'Anagrafica del personale e collaboratori', + icon: Icons.badge_outlined, + color: Colors.teal, + onTap: () => context.go('/master-data/staff'), + ), + _buildHubCard( + context, + title: 'Negozi', + subtitle: 'Anagrafica punti vendita della tua azienda', + icon: Icons.storefront_outlined, + color: Colors.purple, + onTap: () => context.go('/master-data/stores'), + ), + _buildHubCard( + context, + title: 'Provider', + subtitle: 'Anagrafica mandati e servizi', + icon: Icons.handshake_rounded, + color: Colors.indigo, + onTap: () => context.go('/master-data/providers'), + ), + ], ), - ], - ), + ), + ], ), - ], + ), + ), + ); + } + + Widget _buildHubCard( + BuildContext context, { + required String title, + required String subtitle, + required IconData icon, + required Color color, + required VoidCallback onTap, + }) { + final theme = Theme.of(context); + + return Card( + clipBehavior: Clip.antiAlias, + elevation: 0, // Zero elevation, design più flat e moderno + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + // Stesso bordino elegante della Dashboard + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), // Niente withOpacity! ❤️ + shape: BoxShape.circle, + ), + child: Icon(icon, size: 36, color: color), + ), + const SizedBox(height: 12), + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Expanded( + child: Text( + subtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), ), ); } } - -Widget _buildHubCard( - BuildContext context, { - required String title, - required String subtitle, - required IconData icon, - required Color color, - required VoidCallback onTap, -}) { - return Card( - clipBehavior: Clip.antiAlias, - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: InkWell( - onTap: onTap, - child: Padding( - // Ridotto da 22 a 16 per dare più respiro orizzontale su mobile - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - child: Icon(icon, size: 40, color: color), - ), - const SizedBox(height: 12), // Leggermente ridotto - Text( - title, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), // Leggermente ridotto - Expanded( - // Impedisce matematicamente l'overflow verticale - child: Text( - subtitle, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 13, color: Colors.grey.shade500), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ); -} -- 2.43.0