feat-insert-service (#5)

Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/5
Co-authored-by: mark-cachy <marco@catelli.it>
Co-committed-by: mark-cachy <marco@catelli.it>
This commit is contained in:
2026-04-20 16:52:20 +02:00
committed by brontomark
parent 667bbf6404
commit c3d4f3fac7
63 changed files with 4715 additions and 1371 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/home/ui/dashboard_action_card.dart';
import 'package:flux/features/services/utils/service_actions.dart';
import 'package:go_router/go_router.dart';
class DashboardAdaptiveGrid extends StatelessWidget {
@@ -36,7 +37,7 @@ class DashboardAdaptiveGrid extends StatelessWidget {
label: 'Nuova Op',
icon: Icons.add_task,
color: context.accent,
onTap: () {},
onTap: () => startNewService(context),
),
DashboardActionCard(
label: 'Clienti',

View File

@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/auth/bloc/auth_bloc.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 'dashboard_content.dart'; // Importiamo il contenuto della dashboard
import 'package:get_it/get_it.dart';
import 'dashboard_content.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@@ -21,8 +23,6 @@ class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
// Caricamento "silenzioso" all'avvio dell'app
// Usiamo WidgetsBinding per assicurarci che il contesto sia pronto
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<ServicesCubit>().loadServices();
});
@@ -34,15 +34,31 @@ class _HomeScreenState extends State<HomeScreen> {
builder: (context, state) {
return LayoutBuilder(
builder: (context, constraints) {
// Se lo schermo è più largo di 900px usiamo il layout Desktop
final bool isLargeScreen = constraints.maxWidth > 900;
final bool veryLargeScreen = constraints.maxWidth > 1200;
final bool isMenuExtended = veryLargeScreen ? true : _extendRailway;
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)
_buildNavigationRail(constraints.maxWidth > 1200),
if (isLargeScreen) _buildDesktopSidebar(isMenuExtended),
// --- CONTENUTO DINAMICO ---
Expanded(
@@ -61,7 +77,209 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
// --- BOTTOM NAVIGATION BAR (Mobile) ---
// ===========================================================================
// 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
child: Column(
children: [
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<String>(
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,
),
),
const SizedBox(width: 12),
Text(
GetIt.I.get<SessionBloc>().state.company?.ragioneSociale ??
"Utente",
style: TextStyle(
fontWeight: FontWeight.bold,
color: context.accent,
),
),
],
),
)
: 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"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
onPressed: () {
Navigator.pop(dialogContext); // Chiude la Dialog
context.read<AuthBloc>().add(
LogoutRequested(),
); // Esegue il logout
},
child: const Text("Esci"),
),
],
),
);
}
// ... mantieni gli altri tuoi metodi intatti (_buildRailHeader, _buildPageContent, _buildBottomNavigationBar)
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,
),
)
: Icon(Icons.bolt, color: context.accent, size: 32),
),
);
}
Widget _buildBottomNavigationBar(int selectedIndex) {
return BottomNavigationBar(
currentIndex: selectedIndex,
@@ -85,80 +303,6 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
// --- NAVIGATION RAIL (Desktop) ---
Widget _buildNavigationRail(bool veryLargeScreen) {
return MouseRegion(
onEnter: (_) => setState(() => _extendRailway = true),
onExit: (_) => setState(() => _extendRailway = false),
child: NavigationRail(
// Manteniamo 'extended' dinamico in base alla larghezza per un look Pro
extended: veryLargeScreen ? true : _extendRailway,
selectedIndex: _selectedIndex,
onDestinationSelected: (index) =>
setState(() => _selectedIndex = index),
backgroundColor: context.background,
indicatorColor: context.accent.withValues(alpha: 0.2),
// Header con il logo FLUX o l'icona bolt
leading: _buildRailHeader(veryLargeScreen),
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',
), // Questo caricherà il MasterDataHubContent
),
],
),
);
}
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,
),
)
: Icon(Icons.bolt, color: context.accent, size: 32),
),
);
}
// Switch tra le sottopagine
Widget _buildPageContent(int index, bool isLargeScreen) {
return IndexedStack(
index: index,
@@ -167,12 +311,8 @@ class _HomeScreenState extends State<HomeScreen> {
isLargeScreen: isLargeScreen,
onTabRequested: (idx) => setState(() => _selectedIndex = 2),
),
ServicesScreen(),
// L'unico punto di ingresso per tutte le anagrafiche
const ServicesScreen(),
MasterDataHubContent(
// Qui gestiamo la navigazione "interna" all'hub
onOpenPage: (widget) {
Navigator.push(
context,