sistemato deeplinking alla serviceformscreen, aggiunto logout e sistemate altre cose
This commit is contained in:
@@ -9,9 +9,8 @@ import 'package:flux/features/home/ui/home_screen.dart';
|
||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
import 'package:flux/features/services/data/services_repository.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'dart:async';
|
||||
|
||||
@@ -83,14 +82,15 @@ class AppRouter {
|
||||
path: '/service-form',
|
||||
name: 'service-form',
|
||||
builder: (context, state) {
|
||||
// Recuperiamo il serviceId dai parametri della query (es: /service-form?serviceId=123)
|
||||
// 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'];
|
||||
if (serviceId != null) {
|
||||
context.read<ServicesCubit>().initServiceForm(
|
||||
serviceId: serviceId,
|
||||
);
|
||||
}
|
||||
return ServiceFormScreen();
|
||||
|
||||
return ServiceFormScreen(
|
||||
serviceId: serviceId ?? existingService?.id,
|
||||
existingService: existingService,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -41,7 +41,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
Future<void> createCustomer(CustomerModel customer) async {
|
||||
emit(state.copyWith(status: CustomerStatus.loading));
|
||||
try {
|
||||
final newCustomer = await _repository.createCustomer(customer);
|
||||
final newCustomer = await _repository.saveCustomer(customer);
|
||||
|
||||
// Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima
|
||||
final updatedList = List<CustomerModel>.from(state.customers)
|
||||
@@ -128,6 +128,29 @@ class CustomerCubit extends Cubit<CustomerState> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<CustomerModel?> quickCreateCustomer({
|
||||
required String name,
|
||||
String? phone,
|
||||
String? email,
|
||||
}) async {
|
||||
final newCustomer = CustomerModel(
|
||||
nome: name,
|
||||
telefono: phone ?? '',
|
||||
email: email ?? '',
|
||||
companyId: _sessionBloc.state.company!.id,
|
||||
note: '',
|
||||
);
|
||||
|
||||
try {
|
||||
final saved = await _repository.saveCustomer(newCustomer);
|
||||
// Lo aggiungiamo in cima ai suggerimenti
|
||||
emit(state.copyWith(customers: [saved, ...state.customers]));
|
||||
return saved;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Pulizia della memoria quando il Cubit viene distrutto
|
||||
@override
|
||||
Future<void> close() {
|
||||
|
||||
@@ -10,16 +10,16 @@ class CustomerRepository {
|
||||
final SupabaseClient _client = GetIt.I<SupabaseClient>();
|
||||
|
||||
// Crea un nuovo cliente
|
||||
Future<CustomerModel> createCustomer(CustomerModel customer) async {
|
||||
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
|
||||
try {
|
||||
final response = await _client
|
||||
.from('customer')
|
||||
.insert(customer.toJson())
|
||||
.upsert(customer.toJson())
|
||||
.select()
|
||||
.single();
|
||||
return CustomerModel.fromJson(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante la creazione del cliente: $e';
|
||||
throw 'Errore durante il salvataggio del cliente: $e';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
||||
import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:flux/features/customers/ui/quick_customer_dialog.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
|
||||
class CustomerSearchSheet extends StatefulWidget {
|
||||
@@ -86,18 +88,29 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
||||
// --- TASTO NUOVO CLIENTE ---
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Naviga alla pagina "Crea Cliente".
|
||||
},
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text("Crea Nuovo Cliente"),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
final servicesCubit = context.read<ServicesCubit>();
|
||||
// Apriamo la dialog passando la query attuale
|
||||
final CustomerModel? nuovoCliente = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => QuickCustomerDialog(
|
||||
initialQuery: _searchController.text,
|
||||
),
|
||||
);
|
||||
|
||||
if (nuovoCliente != null) {
|
||||
servicesCubit.updateField(
|
||||
customerId: nuovoCliente.id,
|
||||
customerDisplayName: nuovoCliente.nome,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
117
lib/features/customers/ui/quick_customer_dialog.dart
Normal file
117
lib/features/customers/ui/quick_customer_dialog.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/customers/blocs/customer_cubit.dart';
|
||||
|
||||
class QuickCustomerDialog extends StatefulWidget {
|
||||
final String initialQuery;
|
||||
|
||||
const QuickCustomerDialog({super.key, required this.initialQuery});
|
||||
|
||||
@override
|
||||
State<QuickCustomerDialog> createState() => _QuickCustomerDialogState();
|
||||
}
|
||||
|
||||
class _QuickCustomerDialogState extends State<QuickCustomerDialog> {
|
||||
late final TextEditingController _nameCtrl;
|
||||
final _phoneCtrl = TextEditingController();
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _noteCtrl = TextEditingController();
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Prendiamo tutta la stringa nuda e cruda!
|
||||
_nameCtrl = TextEditingController(text: widget.initialQuery.trim());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameCtrl.dispose();
|
||||
_phoneCtrl.dispose();
|
||||
_emailCtrl.dispose();
|
||||
_noteCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final NavigatorState navigator = Navigator.of(context);
|
||||
if (_nameCtrl.text.isEmpty) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
|
||||
final newCustomer = await context.read<CustomerCubit>().quickCreateCustomer(
|
||||
name: _nameCtrl.text.trim(),
|
||||
phone: _phoneCtrl.text.trim(),
|
||||
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit:
|
||||
// email: _emailCtrl.text.trim(),
|
||||
// note: _noteCtrl.text.trim(),
|
||||
);
|
||||
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
if (context.mounted) {
|
||||
navigator.pop(newCustomer); // Restituiamo il cliente creato
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Nuovo Cliente Rapido"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _nameCtrl,
|
||||
autofocus: true, // Focus immediato!
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Nome / Ragione Sociale *",
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _phoneCtrl,
|
||||
decoration: const InputDecoration(labelText: "Telefono"),
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _emailCtrl,
|
||||
decoration: const InputDecoration(labelText: "Email"),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _noteCtrl,
|
||||
decoration: const InputDecoration(labelText: "Note rapide"),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Annulla"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _save,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text("Salva e Usa"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
@@ -123,4 +124,34 @@ class ProductCubit extends Cubit<ProductState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ModelModel?> quickCreateProduct({
|
||||
required String brandName,
|
||||
required String modelName,
|
||||
}) async {
|
||||
try {
|
||||
await loadBrands();
|
||||
BrandModel? brand = state.brands.firstWhereOrNull(
|
||||
(b) => b.name.toLowerCase() == brandName.toLowerCase(),
|
||||
);
|
||||
// 1. Cerchiamo o creiamo il Brand
|
||||
// (Usa una funzione upsert o una ricerca rapida nel repository)
|
||||
brand ??= await _repository.upsertBrand(
|
||||
BrandModel(name: brandName, companyId: _sessionBloc.state.company!.id),
|
||||
);
|
||||
|
||||
// 2. Creiamo il Modello legato al Brand
|
||||
final newModel = await _repository.upsertModel(
|
||||
ModelModel(brandId: brand.id!, name: modelName),
|
||||
);
|
||||
|
||||
// 3. Aggiorniamo lo stato locale così la lista modelli lo vede subito
|
||||
emit(state.copyWith(models: [newModel, ...state.models]));
|
||||
|
||||
return newModel;
|
||||
} catch (e) {
|
||||
emit(state.copyWith(errorMessage: "Errore creazione rapida: $e"));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ class ModelModel extends Equatable {
|
||||
const ModelModel({
|
||||
this.id,
|
||||
required this.name,
|
||||
required this.nameWithBrand,
|
||||
this.nameWithBrand = '',
|
||||
required this.brandId,
|
||||
this.isActive = true,
|
||||
this.createdAt,
|
||||
|
||||
111
lib/features/master_data/products/ui/quick_product_dialog.dart
Normal file
111
lib/features/master_data/products/ui/quick_product_dialog.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||
import 'package:flux/features/master_data/products/models/brand_model.dart';
|
||||
|
||||
class QuickProductDialog extends StatefulWidget {
|
||||
final List<BrandModel> existingBrands;
|
||||
|
||||
const QuickProductDialog({super.key, required this.existingBrands});
|
||||
|
||||
@override
|
||||
State<QuickProductDialog> createState() => _QuickProductDialogState();
|
||||
}
|
||||
|
||||
class _QuickProductDialogState extends State<QuickProductDialog> {
|
||||
final _modelCtrl = TextEditingController();
|
||||
String _selectedBrandName = "";
|
||||
bool _isLoading = false;
|
||||
|
||||
Future<void> _save() async {
|
||||
final NavigatorState navigator = Navigator.of(context);
|
||||
if (_selectedBrandName.isEmpty || _modelCtrl.text.isEmpty) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final newModel = await context.read<ProductCubit>().quickCreateProduct(
|
||||
brandName: _selectedBrandName.trim(),
|
||||
modelName: _modelCtrl.text.trim(),
|
||||
);
|
||||
|
||||
setState(() => _isLoading = false);
|
||||
|
||||
if (context.mounted) {
|
||||
navigator.pop(newModel); // Restituiamo il modello creato
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("Nuovo Dispositivo"),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// AUTOCOMPLETE PER IL BRAND LOCALE
|
||||
Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return const Iterable<String>.empty();
|
||||
}
|
||||
final query = textEditingValue.text.toLowerCase();
|
||||
// Filtriamo i brand che contengono la stringa cercata
|
||||
return widget.existingBrands
|
||||
.map((b) => b.name)
|
||||
.where((name) => name.toLowerCase().contains(query));
|
||||
},
|
||||
onSelected: (String selection) {
|
||||
_selectedBrandName = selection;
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(
|
||||
context,
|
||||
textEditingController,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
return TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Marca (es: Apple, Samsung)",
|
||||
hintText: "Inizia a scrivere...",
|
||||
),
|
||||
onChanged: (val) => _selectedBrandName = val,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _modelCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Modello (es: iPhone 15 Pro)",
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => _save(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Annulla"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _save,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text("Crea"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class ProviderModel extends Equatable {
|
||||
final bool energia;
|
||||
final bool assicurazioni;
|
||||
final bool intrattenimento;
|
||||
final bool finanziamenti;
|
||||
final bool altro;
|
||||
final bool isActive;
|
||||
final String companyId;
|
||||
@@ -22,6 +23,7 @@ class ProviderModel extends Equatable {
|
||||
required this.energia,
|
||||
required this.assicurazioni,
|
||||
required this.intrattenimento,
|
||||
required this.finanziamenti,
|
||||
required this.altro,
|
||||
required this.isActive,
|
||||
required this.companyId,
|
||||
@@ -48,6 +50,7 @@ class ProviderModel extends Equatable {
|
||||
energia: map['energia'] ?? false,
|
||||
assicurazioni: map['assicurazioni'] ?? false,
|
||||
intrattenimento: map['intrattenimento'] ?? false,
|
||||
finanziamenti: map['finanziamenti'] ?? false,
|
||||
altro: map['altro'] ?? false,
|
||||
isActive: map['is_active'] ?? true,
|
||||
companyId: map['company_id'],
|
||||
@@ -63,6 +66,7 @@ class ProviderModel extends Equatable {
|
||||
'energia': energia,
|
||||
'assicurazioni': assicurazioni,
|
||||
'intrattenimento': intrattenimento,
|
||||
'finanziamenti': finanziamenti,
|
||||
'altro': altro,
|
||||
'is_active': isActive,
|
||||
'company_id': companyId,
|
||||
@@ -84,6 +88,7 @@ class ProviderModel extends Equatable {
|
||||
energia,
|
||||
assicurazioni,
|
||||
intrattenimento,
|
||||
finanziamenti,
|
||||
altro,
|
||||
isActive,
|
||||
companyId,
|
||||
@@ -98,6 +103,7 @@ class ProviderModel extends Equatable {
|
||||
bool? energia,
|
||||
bool? assicurazioni,
|
||||
bool? intrattenimento,
|
||||
bool? finanziamenti,
|
||||
bool? altro,
|
||||
bool? isActive,
|
||||
String? companyId,
|
||||
@@ -111,6 +117,7 @@ class ProviderModel extends Equatable {
|
||||
energia: energia ?? this.energia,
|
||||
assicurazioni: assicurazioni ?? this.assicurazioni,
|
||||
intrattenimento: intrattenimento ?? this.intrattenimento,
|
||||
finanziamenti: finanziamenti ?? this.finanziamenti,
|
||||
altro: altro ?? this.altro,
|
||||
isActive: isActive ?? this.isActive,
|
||||
companyId: companyId ?? this.companyId,
|
||||
|
||||
@@ -20,6 +20,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
||||
late bool _energia;
|
||||
late bool _assicurazioni;
|
||||
late bool _intrattenimento;
|
||||
late bool _finanziamenti;
|
||||
late bool _altro;
|
||||
late bool _isActive;
|
||||
final List<String> _tempSelectedStoreIds =
|
||||
@@ -38,6 +39,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
||||
_energia = p?.energia ?? false;
|
||||
_assicurazioni = p?.assicurazioni ?? false;
|
||||
_intrattenimento = p?.intrattenimento ?? false;
|
||||
_finanziamenti = p?.finanziamenti ?? false;
|
||||
_altro = p?.altro ?? false;
|
||||
_isActive = p?.isActive ?? true;
|
||||
}
|
||||
@@ -61,6 +63,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
||||
energia: _energia,
|
||||
assicurazioni: _assicurazioni,
|
||||
intrattenimento: _intrattenimento,
|
||||
finanziamenti: _finanziamenti,
|
||||
altro: _altro,
|
||||
isActive: _isActive,
|
||||
companyId:
|
||||
@@ -130,6 +133,11 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
||||
_intrattenimento,
|
||||
(v) => setState(() => _intrattenimento = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Finanziamenti",
|
||||
_finanziamenti,
|
||||
(v) => setState(() => _finanziamenti = v),
|
||||
),
|
||||
_buildSwitch(
|
||||
"Altro/Accessori",
|
||||
_altro,
|
||||
|
||||
@@ -199,22 +199,24 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
|
||||
// --- PERSISTENZA ---
|
||||
|
||||
Future<void> saveCurrentService() async {
|
||||
Future<void> saveCurrentService({required bool isBozza}) async {
|
||||
if (state.currentService == null) return;
|
||||
|
||||
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
||||
try {
|
||||
// Usiamo il repository corazzato che abbiamo scritto prima
|
||||
await _repository.saveFullService(state.currentService!);
|
||||
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente
|
||||
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
|
||||
|
||||
await loadServices(refresh: true);
|
||||
// Reset della bozza e ricaricamento lista
|
||||
// 2. Salvataggio corazzato
|
||||
await _repository.saveFullService(serviceToSave);
|
||||
|
||||
// 3. Reset e ricaricamento
|
||||
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
||||
await loadServices(refresh: true);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.failure,
|
||||
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/service_model.dart';
|
||||
|
||||
@@ -187,4 +188,28 @@ class ServicesRepository {
|
||||
]; // Fallback se non c'è ancora storia
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> uploadAttachment({
|
||||
required String serviceId,
|
||||
required String fileName,
|
||||
required Uint8List fileBytes,
|
||||
}) async {
|
||||
try {
|
||||
// 1. Upload fisico nel bucket 'service_documents'
|
||||
final path = '$serviceId/$fileName';
|
||||
await _supabase.storage
|
||||
.from('service_documents')
|
||||
.uploadBinary(path, fileBytes);
|
||||
|
||||
// 2. Registriamo l'esistenza del file nel database
|
||||
await _supabase.from('service_attachment').insert({
|
||||
'service_id': serviceId,
|
||||
'file_path': path,
|
||||
'file_name': fileName,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
} catch (e) {
|
||||
throw "Errore upload: $e";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ class _EntertainmentList extends StatelessWidget {
|
||||
telefoniaFissa: false,
|
||||
telefoniaMobile: false,
|
||||
assicurazioni: false,
|
||||
finanziamenti: false,
|
||||
altro: false,
|
||||
intrattenimento: false,
|
||||
),
|
||||
|
||||
@@ -171,6 +171,7 @@ class _FinanceList extends StatelessWidget {
|
||||
assicurazioni: false,
|
||||
altro: false,
|
||||
intrattenimento: false,
|
||||
finanziamenti: false,
|
||||
),
|
||||
)
|
||||
.nome;
|
||||
@@ -292,12 +293,13 @@ class _FinanceFormState extends State<_FinanceForm> {
|
||||
// 1. SCELTA ISTITUTO (Solo attivi)
|
||||
BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||
builder: (context, state) {
|
||||
final finProviders = state
|
||||
.activeProviders; // Già filtrati dal caricamento della dialog
|
||||
final finProviders = state.activeProviders
|
||||
.where((p) => p.finanziamenti)
|
||||
.toList(); // Già filtrati dal caricamento della dialog
|
||||
return DropdownButtonFormField<String>(
|
||||
initialValue: _selectedProviderId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Istituto di Credito",
|
||||
labelText: "Gestore",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: finProviders
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/customer_section.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
|
||||
|
||||
class ServiceFormScreen extends StatelessWidget {
|
||||
const ServiceFormScreen({super.key});
|
||||
class ServiceFormScreen extends StatefulWidget {
|
||||
final String? serviceId;
|
||||
final ServiceModel? existingService; // <-- AGGIUNTO
|
||||
|
||||
const ServiceFormScreen({
|
||||
super.key,
|
||||
this.serviceId,
|
||||
this.existingService, // <-- AGGIUNTO
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServiceFormScreen> createState() => _ServiceFormScreenState();
|
||||
}
|
||||
|
||||
class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Diamo in pasto al Cubit tutto quello che abbiamo!
|
||||
context.read<ServicesCubit>().initServiceForm(
|
||||
existingService: widget.existingService,
|
||||
serviceId: widget.serviceId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _performSave(BuildContext context, {required bool isBozza}) {
|
||||
FocusScope.of(context).unfocus();
|
||||
context.read<ServicesCubit>().saveCurrentService(isBozza: isBozza);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ServicesCubit, ServicesState>(
|
||||
return BlocConsumer<ServicesCubit, ServicesState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ServicesStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -19,91 +49,123 @@ class ServiceFormScreen extends StatelessWidget {
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context); // Torna alla lista di pratiche
|
||||
Navigator.pop(context);
|
||||
} else if (state.status == ServicesStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Si è verificato un errore ${state.errorMessage ?? ''}",
|
||||
),
|
||||
content: Text("Errore: ${state.errorMessage ?? ''}"),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Nuova Pratica"),
|
||||
actions: [
|
||||
_SaveButton(), // Tasto salva intelligente
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||
builder: (context, state) {
|
||||
final service = state.currentService;
|
||||
|
||||
// Se la bozza non è ancora inizializzata, mostriamo un loader
|
||||
if (service == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// SEZIONE 1: CLIENTE
|
||||
CustomerSection(service: service),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// SEZIONE 2: INFO GENERALI
|
||||
GeneralInfoSection(service: service),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// SEZIONE 3: I MODULI
|
||||
ServicesGrid(service: service),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// TODO SEZIONE 4: ALLEGATI (Da fare)
|
||||
// const _AttachmentsSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- COMPONENTI DELLA PAGINA ---
|
||||
|
||||
class _SaveButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == ServicesStatus.saving) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
tooltip: "Salva Pratica",
|
||||
onPressed: () {
|
||||
context.read<ServicesCubit>().saveCurrentService();
|
||||
},
|
||||
final service = state.currentService;
|
||||
final isSaving = state.status == ServicesStatus.saving;
|
||||
final isEditMode = widget.serviceId != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isEditMode ? "Modifica Pratica" : "Nuova Pratica"),
|
||||
actions: [
|
||||
if (isSaving)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 20.0),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (service != null) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_note),
|
||||
tooltip: "Salva come Bozza",
|
||||
onPressed: () => _performSave(context, isBozza: true),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.check_circle_outline,
|
||||
color: Colors.green,
|
||||
),
|
||||
tooltip: "Conferma Pratica",
|
||||
onPressed: () => _performSave(context, isBozza: false),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
body: (service == null)
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomerSection(service: service),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
GeneralInfoSection(service: service),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
ServicesGrid(service: service),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// TODO: _AttachmentsSection(),
|
||||
_buildBottomActionButtons(context, isSaving: isSaving),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomActionButtons(
|
||||
BuildContext context, {
|
||||
required bool isSaving,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
icon: const Icon(Icons.edit_note),
|
||||
label: const Text("Salva in Bozza"),
|
||||
onPressed: isSaving
|
||||
? null
|
||||
: () => _performSave(context, isBozza: true),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
label: const Text(
|
||||
"CONFERMA PRATICA",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1),
|
||||
),
|
||||
onPressed: isSaving
|
||||
? null
|
||||
: () => _performSave(context, isBozza: false),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,9 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => context.pushNamed(
|
||||
'service-form',
|
||||
queryParameters: {'serviceId': service.id},
|
||||
extra: service, // <-- LA MAGIA È QUI: Passa l'oggetto intero!
|
||||
// Teniamo anche il parametro URL per coerenza di routing
|
||||
queryParameters: service.id != null ? {'serviceId': service.id!} : {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user