ok, design pulito e gorouter perfezionato
This commit is contained in:
96
lib/core/layout/app_shell.dart
Normal file
96
lib/core/layout/app_shell.dart
Normal file
@@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SessionCubit>().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<dynamic> stream) {
|
||||
notifyListeners();
|
||||
@@ -178,9 +202,7 @@ class GoRouterRefreshStream extends ChangeNotifier {
|
||||
(dynamic _) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
|
||||
late final StreamSubscription<dynamic> _subscription;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
|
||||
Reference in New Issue
Block a user