2026-04-29 11:40:17 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2026-05-24 09:49:07 +02:00
|
|
|
import 'package:flux/core/routes/routes.dart';
|
2026-05-04 15:36:42 +02:00
|
|
|
import 'package:flux/core/utils/extensions.dart';
|
2026-04-29 11:40:17 +02:00
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
|
|
2026-05-24 09:49:07 +02:00
|
|
|
// ==========================================
|
|
|
|
|
// 1. IL GUSCIO (QUELLO CHE PASSI AL ROUTER)
|
|
|
|
|
// ==========================================
|
2026-04-29 11:40:17 +02:00
|
|
|
class AppShell extends StatelessWidget {
|
|
|
|
|
final Widget child;
|
|
|
|
|
|
|
|
|
|
const AppShell({super.key, required this.child});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-05-24 09:49:07 +02:00
|
|
|
// Breakpoint a 900px: sotto è Mobile/Tablet (Drawer), sopra è Desktop (Sidebar)
|
|
|
|
|
final isDesktop = MediaQuery.sizeOf(context).width >= 900;
|
|
|
|
|
final currentPath = GoRouterState.of(context).uri.path;
|
2026-04-29 11:40:17 +02:00
|
|
|
|
|
|
|
|
return Scaffold(
|
2026-05-24 09:49:07 +02:00
|
|
|
// 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),
|
|
|
|
|
),
|
2026-04-29 11:40:17 +02:00
|
|
|
body: isDesktop
|
|
|
|
|
? Row(
|
|
|
|
|
children: [
|
2026-05-24 09:49:07 +02:00
|
|
|
// Su desktop inietta il menu a sinistra!
|
|
|
|
|
AppMenu(currentPath: currentPath, isDrawer: false),
|
2026-04-29 11:40:17 +02:00
|
|
|
const VerticalDivider(thickness: 1, width: 1),
|
|
|
|
|
Expanded(child: child),
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-05-24 09:49:07 +02:00
|
|
|
: 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<AppMenu> createState() => _AppMenuState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _AppMenuState extends State<AppMenu> {
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-04-29 11:40:17 +02:00
|
|
|
),
|
2026-05-24 09:49:07 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==========================================
|
|
|
|
|
// 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<String>(
|
|
|
|
|
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,
|
|
|
|
|
),
|
2026-04-29 11:40:17 +02:00
|
|
|
),
|
2026-05-24 09:49:07 +02:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.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,
|
2026-04-29 11:40:17 +02:00
|
|
|
),
|
2026-05-24 09:49:07 +02:00
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.clip,
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
if (widget.isDrawer) Navigator.pop(context);
|
|
|
|
|
context.goNamed(item.routeName); // <--- goNamed!
|
|
|
|
|
},
|
2026-04-29 11:40:17 +02:00
|
|
|
),
|
2026-05-24 09:49:07 +02:00
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
2026-04-29 11:40:17 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-24 09:49:07 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|