From 023665ae58007ed78a2a2c05ea21ce5692b16c9b Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 19 Apr 2026 10:57:55 +0200 Subject: [PATCH] sistemato deeplinking alla serviceformscreen, aggiunto logout e sistemate altre cose --- lib/core/routes/app_router.dart | 18 +- .../customers/blocs/customer_cubit.dart | 25 +- .../customers/data/customer_repository.dart | 6 +- .../customers/ui/customer_search_sheet.dart | 35 +- .../customers/ui/quick_customer_dialog.dart | 117 +++++++ lib/features/home/ui/home_screen.dart | 312 +++++++++++++----- .../products/blocs/product_cubit.dart | 31 ++ .../products/models/model_model.dart | 2 +- .../products/ui/quick_product_dialog.dart | 111 +++++++ .../providers/models/provider_model.dart | 7 + .../providers/ui/provider_form_sheet.dart | 8 + .../services/blocs/services_cubit.dart | 14 +- .../services/data/services_repository.dart | 25 ++ .../entertainment_service_card.dart | 1 + .../finance_service_dialog.dart | 8 +- .../service_form_screen.dart | 216 +++++++----- lib/features/services/ui/services_screen.dart | 4 +- 17 files changed, 742 insertions(+), 198 deletions(-) create mode 100644 lib/features/customers/ui/quick_customer_dialog.dart create mode 100644 lib/features/master_data/products/ui/quick_product_dialog.dart diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 5200cda..60f3481 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -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().initServiceForm( - serviceId: serviceId, - ); - } - return ServiceFormScreen(); + + return ServiceFormScreen( + serviceId: serviceId ?? existingService?.id, + existingService: existingService, + ); }, ), ], diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index e9902b5..7f39674 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -41,7 +41,7 @@ class CustomerCubit extends Cubit { Future 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.from(state.customers) @@ -128,6 +128,29 @@ class CustomerCubit extends Cubit { }); } + Future 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 close() { diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 7d8d9fe..7c8491f 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -10,16 +10,16 @@ class CustomerRepository { final SupabaseClient _client = GetIt.I(); // Crea un nuovo cliente - Future createCustomer(CustomerModel customer) async { + Future 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'; } } diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index bb5245c..4cc3ca9 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -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 { // --- 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(); + // 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), diff --git a/lib/features/customers/ui/quick_customer_dialog.dart b/lib/features/customers/ui/quick_customer_dialog.dart new file mode 100644 index 0000000..3082491 --- /dev/null +++ b/lib/features/customers/ui/quick_customer_dialog.dart @@ -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 createState() => _QuickCustomerDialogState(); +} + +class _QuickCustomerDialogState extends State { + 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 _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().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"), + ), + ], + ); + } +} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index d092197..fbde4fd 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -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 { @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().loadServices(); }); @@ -34,15 +34,31 @@ class _HomeScreenState extends State { 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 { ); } - // --- 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( + 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().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().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 { ); } - // --- 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 { 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, diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index 17e128a..b290a2a 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -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 { ); } } + + Future 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; + } + } } diff --git a/lib/features/master_data/products/models/model_model.dart b/lib/features/master_data/products/models/model_model.dart index 31aa504..4859356 100644 --- a/lib/features/master_data/products/models/model_model.dart +++ b/lib/features/master_data/products/models/model_model.dart @@ -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, diff --git a/lib/features/master_data/products/ui/quick_product_dialog.dart b/lib/features/master_data/products/ui/quick_product_dialog.dart new file mode 100644 index 0000000..aa92bae --- /dev/null +++ b/lib/features/master_data/products/ui/quick_product_dialog.dart @@ -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 existingBrands; + + const QuickProductDialog({super.key, required this.existingBrands}); + + @override + State createState() => _QuickProductDialogState(); +} + +class _QuickProductDialogState extends State { + final _modelCtrl = TextEditingController(); + String _selectedBrandName = ""; + bool _isLoading = false; + + Future _save() async { + final NavigatorState navigator = Navigator.of(context); + if (_selectedBrandName.isEmpty || _modelCtrl.text.isEmpty) return; + + setState(() => _isLoading = true); + + final newModel = await context.read().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( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text.isEmpty) { + return const Iterable.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"), + ), + ], + ); + } +} diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 0e2eddd..a4a9ede 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -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, diff --git a/lib/features/master_data/providers/ui/provider_form_sheet.dart b/lib/features/master_data/providers/ui/provider_form_sheet.dart index 5a3e4b7..2afc48d 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -20,6 +20,7 @@ class _ProviderFormSheetState extends State { late bool _energia; late bool _assicurazioni; late bool _intrattenimento; + late bool _finanziamenti; late bool _altro; late bool _isActive; final List _tempSelectedStoreIds = @@ -38,6 +39,7 @@ class _ProviderFormSheetState extends State { _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 { energia: _energia, assicurazioni: _assicurazioni, intrattenimento: _intrattenimento, + finanziamenti: _finanziamenti, altro: _altro, isActive: _isActive, companyId: @@ -130,6 +133,11 @@ class _ProviderFormSheetState extends State { _intrattenimento, (v) => setState(() => _intrattenimento = v), ), + _buildSwitch( + "Finanziamenti", + _finanziamenti, + (v) => setState(() => _finanziamenti = v), + ), _buildSwitch( "Altro/Accessori", _altro, diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index 2c75cb9..8c39014 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -199,22 +199,24 @@ class ServicesCubit extends Cubit { // --- PERSISTENZA --- - Future saveCurrentService() async { + Future 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(), ), ); diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index da967e0..6922f1b 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -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 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"; + } + } } diff --git a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart index 5d05381..9243a96 100644 --- a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/services/ui/service_form_screen/entertainment_service_card.dart @@ -142,6 +142,7 @@ class _EntertainmentList extends StatelessWidget { telefoniaFissa: false, telefoniaMobile: false, assicurazioni: false, + finanziamenti: false, altro: false, intrattenimento: false, ), diff --git a/lib/features/services/ui/service_form_screen/finance_service_dialog.dart b/lib/features/services/ui/service_form_screen/finance_service_dialog.dart index b85a52d..b03f4d7 100644 --- a/lib/features/services/ui/service_form_screen/finance_service_dialog.dart +++ b/lib/features/services/ui/service_form_screen/finance_service_dialog.dart @@ -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( 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( initialValue: _selectedProviderId, decoration: const InputDecoration( - labelText: "Istituto di Credito", + labelText: "Gestore", border: OutlineInputBorder(), ), items: finProviders diff --git a/lib/features/services/ui/service_form_screen/service_form_screen.dart b/lib/features/services/ui/service_form_screen/service_form_screen.dart index 217bcde..a19f016 100644 --- a/lib/features/services/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/services/ui/service_form_screen/service_form_screen.dart @@ -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 createState() => _ServiceFormScreenState(); +} + +class _ServiceFormScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // Diamo in pasto al Cubit tutto quello che abbiamo! + context.read().initServiceForm( + existingService: widget.existingService, + serviceId: widget.serviceId, + ); + }); + } + + void _performSave(BuildContext context, {required bool isBozza}) { + FocusScope.of(context).unfocus(); + context.read().saveCurrentService(isBozza: isBozza); + } @override Widget build(BuildContext context) { - return BlocListener( + return BlocConsumer( 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( - 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( 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().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), + ), + ), + ], + ); + } } diff --git a/lib/features/services/ui/services_screen.dart b/lib/features/services/ui/services_screen.dart index 4f85e26..dfb53f9 100644 --- a/lib/features/services/ui/services_screen.dart +++ b/lib/features/services/ui/services_screen.dart @@ -177,7 +177,9 @@ class _ServicesScreenState extends State { 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!} : {}, ), ), );