diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index b21d693..a8064a3 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -143,6 +143,10 @@ class SessionCubit extends Cubit { } } + void updateCurrentCompany(CompanyModel newCompany) { + emit(state.copyWith(company: newCompany)); + } + // --- FUNZIONE EXTRA: CAMBIO NEGOZIO DALLA DASHBOARD --- Future changeStore(StoreModel newStore) async { if (newStore.id != null) { diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index f57a553..552ad08 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -9,6 +9,7 @@ import 'package:flux/core/widgets/set_password_screen.dart'; import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; import 'package:flux/core/widgets/shared_forms/upload_success_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; +import 'package:flux/features/company/ui/company_settings_screen.dart'; import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; @@ -25,9 +26,10 @@ import 'package:flux/features/master_data/store/ui/stores_screen.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/operation_form_screen.dart'; -import 'package:flux/features/operations/ui/operations_screen.dart'; +import 'package:flux/features/operations/ui/operation_list_screen.dart'; import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; @@ -128,6 +130,10 @@ class AppRouter { return const ProductsScreen(); }, ), + GoRoute( + path: 'company_settings', + builder: (context, state) => const CompanySettingsScreen(), + ), GoRoute( path: 'staff', // Diventa /master-data/staff builder: (context, state) => const StaffScreen(), @@ -160,7 +166,7 @@ class AppRouter { ), GoRoute( path: '/operations', - builder: (context, state) => const OperationsScreen(), + builder: (context, state) => const OperationListScreen(), ), GoRoute( path: '/customers', @@ -235,7 +241,6 @@ class AppRouter { GoRoute( path: '/operations/form/:id', - name: 'operation-form', builder: (context, state) { final String pathId = state.pathParameters['id'] ?? 'new'; final OperationModel? operationFromExtra = @@ -254,14 +259,19 @@ class AppRouter { context.read().loadBrands(); context.read().loadStaffForStore(currentStoreId); - return BlocProvider( - create: (context) => AttachmentsBloc( - parentId: realOperationId, - parentType: AttachmentParentType.operation, - ), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => AttachmentsBloc( + parentId: realOperationId, + parentType: AttachmentParentType.operation, + ), + ), + BlocProvider(create: (context) => OperationFormCubit()), + ], child: OperationFormScreen( - operationId: operationId ?? existingOperation?.id, - existingOperation: existingOperation, + operationId: realOperationId, + existingOperation: operationFromExtra, ), ); }, diff --git a/lib/features/company/bloc/company_bloc.dart b/lib/features/company/bloc/company_bloc.dart deleted file mode 100644 index 0637471..0000000 --- a/lib/features/company/bloc/company_bloc.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/features/company/data/company_repository.dart'; -import 'package:flux/features/company/models/company_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'company_events.dart'; -part 'company_state.dart'; - -class CompanyBloc extends Bloc { - final CompanyRepository _repository = GetIt.I(); - CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) { - on((event, emit) async { - emit(const CompanyState(status: CompanyStatus.loading)); - try { - final createdCompany = await _repository.createCompany(event.company); - emit( - state.copyWith( - status: CompanyStatus.success, - company: createdCompany, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: CompanyStatus.failure, - errorMessage: e.toString(), - ), - ); - } - }); - } -} diff --git a/lib/features/company/bloc/company_events.dart b/lib/features/company/bloc/company_events.dart deleted file mode 100644 index 041b0fd..0000000 --- a/lib/features/company/bloc/company_events.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'company_bloc.dart'; - -// lib/blocs/company/company_event.dart - -abstract class CompanyEvent extends Equatable { - const CompanyEvent(); - - @override - List get props => []; -} - -class CreateCompanyRequested extends CompanyEvent { - final CompanyModel company; - - const CreateCompanyRequested({required this.company}); - - @override - List get props => [company]; -} diff --git a/lib/features/company/bloc/company_settings_cubit.dart b/lib/features/company/bloc/company_settings_cubit.dart new file mode 100644 index 0000000..1cbf4ab --- /dev/null +++ b/lib/features/company/bloc/company_settings_cubit.dart @@ -0,0 +1,117 @@ +import 'dart:io'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; // Per kIsWeb +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/company/data/company_repository.dart'; +import 'package:flux/features/company/models/company_model.dart'; +import 'package:get_it/get_it.dart'; +import 'package:image_picker/image_picker.dart'; + +part 'company_settings_state.dart'; + +class CompanySettingsCubit extends Cubit { + final CompanyRepository _repository = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); + + CompanySettingsCubit() : super(const CompanySettingsState()); + + void initSettings() { + final currentCompany = _sessionCubit.state.company; + if (currentCompany != null) { + emit( + state.copyWith( + company: currentCompany, + status: CompanySettingsStatus.ready, + ), + ); + } + } + + void updateFields({ + String? name, + String? vatId, + String? address, + String? city, + String? zipCode, + String? phone, + String? email, + }) { + if (state.company == null) return; + + final updated = state.company!.copyWith( + name: name ?? state.company!.name, + vatId: vatId ?? state.company!.vatId, + address: address ?? state.company!.address, + city: city ?? state.company!.city, + zipCode: zipCode ?? state.company!.zipCode, + phone: phone ?? state.company!.phone, + email: email ?? state.company!.email, + ); + emit(state.copyWith(company: updated)); + } + + Future saveSettings() async { + if (state.company == null) return; + emit( + state.copyWith(status: CompanySettingsStatus.saving, errorMessage: null), + ); + + try { + // 1. Salva i dati su Supabase + final updatedCompany = await _repository.updateCompany(state.company!); + + // 2. Aggiorna la sessione globale per riflettere i cambiamenti in tutta l'app + _sessionCubit.updateCurrentCompany(updatedCompany); + + emit( + state.copyWith( + status: CompanySettingsStatus.success, + company: updatedCompany, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: CompanySettingsStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + // Metodo per gestire l'upload del logo + Future uploadLogo(Uint8List bytes, String fileName) async { + if (state.company == null) return; + emit(state.copyWith(status: CompanySettingsStatus.uploadingLogo)); + + try { + // Usa il tuo repository per caricare il file nel bucket 'company_logos' + // Il file può essere Uint8List (se sei su Web) o File (se sei su Mobile/Desktop) + final publicUrl = await _repository.uploadCompanyLogo( + companyId: state.company!.id!, + fileBytes: bytes, + fileName: fileName, + ); + + final updatedCompany = state.company!.copyWith(logoUrl: publicUrl); + + emit( + state.copyWith( + company: updatedCompany, + status: CompanySettingsStatus.ready, + ), + ); + + // Chiamiamo il salvataggio per rendere definitivo l'URL nel record della compagnia + await saveSettings(); + } catch (e) { + emit( + state.copyWith( + status: CompanySettingsStatus.failure, + errorMessage: "Errore caricamento logo: $e", + ), + ); + } + } +} diff --git a/lib/features/company/bloc/company_settings_state.dart b/lib/features/company/bloc/company_settings_state.dart new file mode 100644 index 0000000..c6eee74 --- /dev/null +++ b/lib/features/company/bloc/company_settings_state.dart @@ -0,0 +1,34 @@ +part of 'company_settings_cubit.dart'; + +class CompanySettingsState { + final CompanySettingsStatus status; + final CompanyModel? company; + final String? errorMessage; + + const CompanySettingsState({ + this.status = CompanySettingsStatus.initial, + this.company, + this.errorMessage, + }); + + CompanySettingsState copyWith({ + CompanySettingsStatus? status, + CompanyModel? company, + String? errorMessage, + }) { + return CompanySettingsState( + status: status ?? this.status, + company: company ?? this.company, + errorMessage: errorMessage, + ); + } +} + +enum CompanySettingsStatus { + initial, + ready, + saving, + uploadingLogo, + success, + failure, +} diff --git a/lib/features/company/bloc/company_state.dart b/lib/features/company/bloc/company_state.dart deleted file mode 100644 index 1460579..0000000 --- a/lib/features/company/bloc/company_state.dart +++ /dev/null @@ -1,26 +0,0 @@ -part of 'company_bloc.dart'; - -enum CompanyStatus { initial, loading, success, failure } - -class CompanyState extends Equatable { - final CompanyStatus status; - final String? errorMessage; - final CompanyModel? company; - - const CompanyState({required this.status, this.errorMessage, this.company}); - - CompanyState copyWith({ - CompanyStatus? status, - String? errorMessage, - CompanyModel? company, - }) { - return CompanyState( - status: status ?? this.status, - errorMessage: errorMessage ?? this.errorMessage, - company: company ?? this.company, - ); - } - - @override - List get props => [status, errorMessage, company]; -} diff --git a/lib/features/company/data/company_repository.dart b/lib/features/company/data/company_repository.dart index 5a6bb17..11e4891 100644 --- a/lib/features/company/data/company_repository.dart +++ b/lib/features/company/data/company_repository.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/company_model.dart'; @@ -21,6 +23,62 @@ class CompanyRepository { } } + Future updateCompany(CompanyModel company) async { + try { + final response = await _supabase + .from('company') + .update(company.toMap()) + .eq('id', company.id!) + .select() + .single(); + + return CompanyModel.fromMap(response); + } on PostgrestException catch (e) { + throw e.message; + } catch (e) { + throw e.toString(); + } + } + + Future uploadCompanyLogo({ + required String companyId, + required Uint8List fileBytes, + required String fileName, + }) async { + try { + // 1. Prepariamo il path. + // Organizziamo per companyId e aggiungiamo un timestamp per evitare cache del browser + // quando l'utente cambia logo più volte. + final extension = fileName.split('.').last; + final timestamp = DateTime.now().millisecondsSinceEpoch; + final filePath = '$companyId/logo_$timestamp.$extension'; + + // 2. Caricamento fisico dei bytes + // Usiamo uploadBinary che è perfetto per Uint8List + await _supabase.storage + .from('company_logos') + .uploadBinary( + filePath, + fileBytes, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: + true, // Se esiste già un file con lo stesso nome, lo sovrascrive + ), + ); + + // 3. Otteniamo l'URL pubblico. + // Nota: il bucket 'company_logos' deve essere impostato come PUBLIC su Supabase + final String publicUrl = _supabase.storage + .from('company_logos') + .getPublicUrl(filePath); + + return publicUrl; + } catch (e) { + throw Exception("Errore durante l'upload del logo: $e"); + } + } + Future getCompany() async { try { final userId = _supabase.auth.currentUser?.id; diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 3e270ab..29cea37 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -53,7 +53,9 @@ class CompanyModel extends Equatable { final String vatId; final String fiscalCode; final String sdi; - final String companyLogo; + final String? phone; + final String? email; + final String? logoUrl; // Stato Pagamenti (Ibride: manuale + Stripe) final bool isPaid; @@ -78,7 +80,9 @@ class CompanyModel extends Equatable { required this.vatId, required this.fiscalCode, required this.sdi, - this.companyLogo = '', + this.phone, + this.email, + this.logoUrl, this.isPaid = false, this.paymentExpiration, this.subscriptionTier = SubscriptionTier.free, @@ -100,7 +104,9 @@ class CompanyModel extends Equatable { String? vatId, String? fiscalCode, String? sdi, - String? companyLogo, + String? logoUrl, + String? phone, + String? email, bool? isPaid, DateTime? paymentExpiration, SubscriptionTier? subscriptionTier, @@ -121,7 +127,9 @@ class CompanyModel extends Equatable { vatId: vatId ?? this.vatId, fiscalCode: fiscalCode ?? this.fiscalCode, sdi: sdi ?? this.sdi, - companyLogo: companyLogo ?? this.companyLogo, + logoUrl: logoUrl ?? this.logoUrl, + phone: phone ?? this.phone, + email: email ?? this.email, isPaid: isPaid ?? this.isPaid, paymentExpiration: paymentExpiration ?? this.paymentExpiration, subscriptionTier: subscriptionTier ?? this.subscriptionTier, @@ -163,7 +171,9 @@ class CompanyModel extends Equatable { vatId: map['vat_id'] ?? '', fiscalCode: map['fiscal_code'] ?? '', sdi: map['sdi'] ?? '', - companyLogo: map['company_logo'] ?? '', + logoUrl: map['company_logo'], + phone: map['phone'] ?? '', + email: map['email'] ?? '', isPaid: map['is_paid'] ?? false, paymentExpiration: map['payment_expiration'] != null ? DateTime.tryParse(map['payment_expiration']) @@ -193,7 +203,9 @@ class CompanyModel extends Equatable { 'vat_id': vatId, 'fiscal_code': fiscalCode, 'sdi': sdi, - 'company_logo': companyLogo, + 'company_logo': logoUrl, + 'phone': phone, + 'email': 'email', 'is_paid': isPaid, if (paymentExpiration != null) 'payment_expiration': paymentExpiration!.toIso8601String(), @@ -221,7 +233,9 @@ class CompanyModel extends Equatable { vatId, fiscalCode, sdi, - companyLogo, + logoUrl, + phone, + email, isPaid, paymentExpiration, subscriptionTier, diff --git a/lib/features/company/ui/company_settings_screen.dart b/lib/features/company/ui/company_settings_screen.dart new file mode 100644 index 0000000..178ca5c --- /dev/null +++ b/lib/features/company/ui/company_settings_screen.dart @@ -0,0 +1,310 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/company/bloc/company_settings_cubit.dart'; +import 'package:image_picker/image_picker.dart'; + +class CompanySettingsScreen extends StatefulWidget { + const CompanySettingsScreen({super.key}); + + @override + State createState() => _CompanySettingsScreenState(); +} + +class _CompanySettingsScreenState extends State { + final _formKey = GlobalKey(); + + final _nameCtrl = TextEditingController(); + final _vatCtrl = TextEditingController(); + final _addressCtrl = TextEditingController(); + final _cityCtrl = TextEditingController(); + final _zipCtrl = TextEditingController(); + final _phoneCtrl = TextEditingController(); + final _emailCtrl = TextEditingController(); + + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + context.read().initSettings(); + } + + @override + void dispose() { + _nameCtrl.dispose(); + _vatCtrl.dispose(); + _addressCtrl.dispose(); + _cityCtrl.dispose(); + _zipCtrl.dispose(); + _phoneCtrl.dispose(); + _emailCtrl.dispose(); + super.dispose(); + } + + void _syncControllers(company) { + if (_nameCtrl.text.isEmpty) _nameCtrl.text = company.name ?? ''; + if (_vatCtrl.text.isEmpty) _vatCtrl.text = company.vatNumber ?? ''; + if (_addressCtrl.text.isEmpty) _addressCtrl.text = company.address ?? ''; + if (_cityCtrl.text.isEmpty) _cityCtrl.text = company.city ?? ''; + if (_zipCtrl.text.isEmpty) _zipCtrl.text = company.zipCode ?? ''; + if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? ''; + if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? ''; + _isInitialized = true; + } + + void _flushToCubit() { + context.read().updateFields( + name: _nameCtrl.text, + vatId: _vatCtrl.text, + address: _addressCtrl.text, + city: _cityCtrl.text, + zipCode: _zipCtrl.text, + phone: _phoneCtrl.text, + email: _emailCtrl.text, + ); + } + + Future _pickAndUploadLogo() async { + final picker = ImagePicker(); + final companySettingsCubit = context.read(); + + final pickedFile = await picker.pickImage(source: ImageSource.gallery); + + if (pickedFile != null && mounted) { + // Passiamo i bytes per compatibilità totale con Flutter Web + final bytes = await pickedFile.readAsBytes(); + companySettingsCubit.uploadLogo(bytes, pickedFile.name); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar(title: const Text('Impostazioni Azienda')), + body: BlocConsumer( + listener: (context, state) { + if (state.status == CompanySettingsStatus.ready && !_isInitialized) { + _syncControllers(state.company!); + } + if (state.status == CompanySettingsStatus.success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Impostazioni salvate con successo!'), + backgroundColor: Colors.green, + ), + ); + } + if (state.status == CompanySettingsStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Errore'), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + if (state.company == null) { + return const Center(child: CircularProgressIndicator()); + } + + final company = state.company!; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.all(24.0), + children: [ + // --- SEZIONE LOGO --- + Center( + child: Stack( + alignment: Alignment.bottomRight, + children: [ + Container( + height: 120, + width: 120, + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + shape: BoxShape.circle, + border: Border.all(color: theme.dividerColor), + image: company.logoUrl != null + ? DecorationImage( + image: NetworkImage(company.logoUrl!), + fit: BoxFit.contain, + ) + : null, + ), + child: company.logoUrl == null + ? const Icon( + Icons.business, + size: 50, + color: Colors.grey, + ) + : null, + ), + if (state.status == + CompanySettingsStatus.uploadingLogo) + const Positioned.fill( + child: Center(child: CircularProgressIndicator()), + ), + FloatingActionButton.small( + onPressed: + state.status == + CompanySettingsStatus.uploadingLogo + ? null + : _pickAndUploadLogo, + child: const Icon(Icons.camera_alt), + ), + ], + ), + ), + const SizedBox(height: 32), + + // --- SEZIONE DATI PRINCIPALI --- + Text( + 'Dati Legali', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Divider(), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + controller: _nameCtrl, + decoration: const InputDecoration( + labelText: 'Ragione Sociale', + prefixIcon: Icon(Icons.badge), + ), + validator: (val) => val == null || val.isEmpty + ? 'Campo obbligatorio' + : null, + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 1, + child: TextFormField( + controller: _vatCtrl, + decoration: const InputDecoration( + labelText: 'Partita IVA / C.F.', + prefixIcon: Icon(Icons.receipt_long), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // --- SEZIONE INDIRIZZO E CONTATTI --- + Text( + 'Sede e Contatti', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Divider(), + const SizedBox(height: 16), + TextFormField( + controller: _addressCtrl, + decoration: const InputDecoration( + labelText: 'Indirizzo (Via e numero civico)', + prefixIcon: Icon(Icons.location_on), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + flex: 2, + child: TextFormField( + controller: _cityCtrl, + decoration: const InputDecoration( + labelText: 'Città', + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 1, + child: TextFormField( + controller: _zipCtrl, + decoration: const InputDecoration(labelText: 'CAP'), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _phoneCtrl, + decoration: const InputDecoration( + labelText: 'Telefono', + prefixIcon: Icon(Icons.phone), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + controller: _emailCtrl, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email), + ), + ), + ), + ], + ), + const SizedBox(height: 48), + + // --- PULSANTE SALVATAGGIO --- + SizedBox( + height: 50, + child: ElevatedButton.icon( + onPressed: state.status == CompanySettingsStatus.saving + ? null + : () { + if (_formKey.currentState!.validate()) { + _flushToCubit(); + context + .read() + .saveSettings(); + } + }, + icon: state.status == CompanySettingsStatus.saving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.save), + label: const Text( + 'Salva Impostazioni', + style: TextStyle(fontSize: 16), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart deleted file mode 100644 index 07ead76..0000000 --- a/lib/features/company/ui/create_company_screen.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/company/bloc/company_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/core/widgets/flux_text_field.dart'; -import 'package:flux/features/company/models/company_model.dart'; - -class CreateCompanyScreen extends StatefulWidget { - const CreateCompanyScreen({super.key}); - - @override - State createState() => _CreateCompanyScreenState(); -} - -// lib/ui/setup/create_company_screen.dart - -class _CreateCompanyScreenState extends State { - final _formKey = GlobalKey(); - - // Controller per i campi obbligatori - final _ragioneSocialeController = TextEditingController(); - final _indirizzoController = TextEditingController(); - final _capController = TextEditingController(); - final _cittaController = TextEditingController(); - final _provinciaController = TextEditingController(); - final _pIvaController = TextEditingController(); - final _cfController = TextEditingController(); - final _univocoController = TextEditingController(); - - @override - void dispose() { - // Ricordati sempre di chiuderli! - _ragioneSocialeController.dispose(); - _indirizzoController.dispose(); - _capController.dispose(); - _cittaController.dispose(); - _provinciaController.dispose(); - _pIvaController.dispose(); - _cfController.dispose(); - _univocoController.dispose(); - super.dispose(); - } - - void _onSave() { - if (_formKey.currentState!.validate()) { - // Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc - final userId = context.read().state.user!.id; - - final company = CompanyModel( - userId: userId, - name: _ragioneSocialeController.text.trim(), - address: _indirizzoController.text.trim(), - zipCode: _capController.text.trim(), - city: _cittaController.text.trim(), - province: _provinciaController.text.trim(), - vatId: _pIvaController.text.trim(), - fiscalCode: _cfController.text.trim(), - sdi: _univocoController.text.trim().toUpperCase(), - // Gli altri campi hanno i default nel modello - ); - - // Spariamo l'evento al Bloc - context.read().add(CreateCompanyRequested(company: company)); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.createCompanyScreenCompanyConfiguration), - actions: [ - IconButton( - icon: const Icon(Icons.logout_rounded), - onPressed: () { - // Qui chiami il tuo Bloc dell'autenticazione per fare logout - // Esempio se hai un AuthBloc o SessionBloc: - //context.read().add(LogoutRequested()); - - // Se vuoi solo tornare brutalmente alla login per testare il logo: - // Navigator.of(context).pushReplacementNamed('/login'); - }, - ), - ], - ), - body: BlocConsumer( - listener: (context, state) { - if (state.status == CompanyStatus.success && state.company != null) { - // 1. Aggiorniamo la singleton con i dati reali (ID incluso) - //GetIt.I.get().setCurrentCompany(state.company); - - // 2. Notifichiamo il SessionBloc per cambiare pagina - //context.read().add(AppStarted()); - } - - if (state.status == CompanyStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - state.errorMessage ?? context.l10n.commonSavingError, - ), - backgroundColor: Colors.redAccent, - ), - ); - } - }, - builder: (context, state) { - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 32), - - // --- SEZIONE 1: IDENTITÀ FISCALE --- - _SectionTitle( - title: context.l10n.createCompanyScreenFiscalData, - ), - const SizedBox(height: 16), - FluxTextField( - label: context.l10n.createCompanyScreenCompanyName, - icon: Icons.business, - controller: _ragioneSocialeController, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: FluxTextField( - label: context.l10n.createCompanyScreenVatId, - icon: Icons.numbers, - controller: _pIvaController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: context.l10n.createCompanyScreenFiscalCode, - icon: Icons.badge_outlined, - controller: _cfController, - ), - ), - ], - ), - const SizedBox(height: 16), - FluxTextField( - label: context.l10n.createCompanyScreenSdiPec, - icon: Icons.send_and_archive_outlined, - controller: _univocoController, - ), - - const SizedBox(height: 32), - - // --- SEZIONE 2: SEDE LEGALE --- - _SectionTitle( - title: - context.l10n.createCompanyScreenCompanyLegalAddress, - ), - const SizedBox(height: 16), - FluxTextField( - label: context.l10n.commonAddress, - icon: Icons.home_work_outlined, - controller: _indirizzoController, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - flex: 2, - child: FluxTextField( - label: context.l10n.commonCity, - icon: Icons.location_city, - controller: _cittaController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: context.l10n.commonZipCode, - icon: Icons.map_outlined, - controller: _capController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: context.l10n.commonProvince, - icon: Icons.explore_outlined, - controller: _provinciaController, - ), - ), - ], - ), - - const SizedBox(height: 32), - - // --- SEZIONE 3: LOGO AZIENDALE --- - _SectionTitle(title: 'BRANDING'), - const SizedBox(height: 16), - _buildLogoPicker(context), - - const SizedBox(height: 48), - - // --- BOTTONE INVIO --- - _buildSubmitButton(context, state), - ], - ), - ), - ), - ); - }, - ), - ); - } - - // Placeholder per il futuro caricamento logo - Widget _buildLogoPicker(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: context.accent.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(16), - // Bordo continuo ma sottile e semitrasparente per un look pulito - border: Border.all( - color: context.accent.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Column( - children: [ - Icon(Icons.cloud_upload_outlined, color: context.accent, size: 32), - const SizedBox(height: 12), - Text( - context.l10n.createCompanyScreenUploadLogo, - style: TextStyle( - color: context.primaryText, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - context.l10n.createCompanyScreenWillBeUsedForReceipts, - textAlign: TextAlign.center, - style: TextStyle(color: context.secondaryText, fontSize: 12), - ), - ], - ), - ); - } - - Widget _buildSubmitButton(BuildContext context, CompanyState state) { - return SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: state.status == CompanyStatus.loading - ? null - : () => _onSave(), - child: state.status == CompanyStatus.loading - ? const CircularProgressIndicator() - : Text(context.l10n.createCompanyScreenSaveCompany), - ), - ); - } - - Widget _buildHeader(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: context.accent.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - Icons.domain_add_rounded, - color: context.accent, - size: 32, - ), - ), - const SizedBox(height: 24), - Text( - context.l10n.createCompanyScreenSetupYourCompany, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: context.primaryText, - ), - ), - const SizedBox(height: 12), - Text( - context.l10n.createCompanyScreenFluxNeedsYourFiscalData, - style: TextStyle( - color: context.secondaryText, - fontSize: 15, - height: 1.5, - ), - ), - ], - ); - } -} - -// Widget di supporto per i titoli delle sezioni -class _SectionTitle extends StatelessWidget { - final String title; - const _SectionTitle({required this.title}); - - @override - Widget build(BuildContext context) { - return Text( - title, - style: TextStyle( - color: context.accent, - fontWeight: FontWeight.w800, - letterSpacing: 1.2, - fontSize: 13, - ), - ); - } -} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 98a4106..53152f4 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -186,7 +186,7 @@ class HomeScreen extends StatelessWidget { color: Colors.blue, onTap: () { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio - context.push('/operation-form'); + context.push('/operations/form/new'); }, ), const SizedBox(width: 12), diff --git a/lib/features/operations/blocs/operation_form_cubit.dart b/lib/features/operations/blocs/operation_form_cubit.dart index e69de29..50a7318 100644 --- a/lib/features/operations/blocs/operation_form_cubit.dart +++ b/lib/features/operations/blocs/operation_form_cubit.dart @@ -0,0 +1,270 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; +import 'package:uuid/uuid.dart'; + +part 'operation_form_state.dart'; + +class OperationFormCubit extends Cubit { + final OperationsRepository _repository = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); + final Uuid _uuid = const Uuid(); + + OperationFormCubit() + : super( + OperationFormState( + // Inizializziamo con un modello vuoto di sicurezza + operation: OperationModel( + storeId: '', + companyId: '', + reference: '', + status: OperationStatus.draft, + createdAt: DateTime.now(), + ), + ), + ); + + Future initForm({ + OperationModel? existingOperation, + String? operationId, + }) async { + emit(state.copyWith(status: OperationFormStatus.loading)); + + try { + if (existingOperation != null) { + emit( + state.copyWith( + operation: existingOperation, + status: OperationFormStatus.ready, + ), + ); + } else if (operationId != null) { + // Avendo separato i cubit, se ci passano solo l'ID lo scarichiamo dal DB + final operation = await _repository.fetchOperationById(operationId); + emit( + state.copyWith( + operation: operation, + status: OperationFormStatus.ready, + ), + ); + } else { + // NUOVA PRATICA: Creiamo un nuovo Batch UUID + emit( + state.copyWith( + operation: OperationModel( + storeId: _sessionCubit.state.currentStore?.id ?? '', + reference: '', + createdAt: DateTime.now(), + companyId: _sessionCubit.state.company!.id!, + status: OperationStatus.draft, + batchUuid: _uuid.v4(), + ), + status: OperationFormStatus.ready, + ), + ); + } + } catch (e) { + emit( + state.copyWith( + status: OperationFormStatus.failure, + errorMessage: "Errore inizializzazione form: $e", + ), + ); + } + } + + // --- LOGICA BATCH --- + + void _prepareNextOperationInBatch() { + final current = state.operation; + + emit( + state.copyWith( + status: OperationFormStatus.ready, // Torna ready per il nuovo form + operation: OperationModel( + companyId: current.companyId, + storeId: current.storeId, + storeDisplayName: current.storeDisplayName, + batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO + customerId: current.customerId, // MANTIENE IL CLIENTE + customerDisplayName: current.customerDisplayName, + status: OperationStatus.draft, + createdAt: DateTime.now(), + ), + ), + ); + } + + // --- SALVATAGGIO --- + + Future saveOperation({ + required OperationStatus targetStatus, + required bool keepAdding, + }) async { + emit( + state.copyWith(status: OperationFormStatus.saving, errorMessage: null), + ); + + try { + final operationToSave = state.operation.copyWith(status: targetStatus); + final savedOperation = await _repository.saveFullOperation( + operation: operationToSave, + ); + + if (keepAdding) { + // Salviamo nella "memoria" del batch le pratiche create finora + final updatedBatchList = List.from( + state.savedBatchOperations, + )..add(savedOperation); + + emit( + state.copyWith( + status: OperationFormStatus.successAndAddAnother, + savedBatchOperations: updatedBatchList, + ), + ); + + // Pulisce i campi per la prossima operazione + _prepareNextOperationInBatch(); + } else { + emit( + state.copyWith( + status: OperationFormStatus.success, + operation: savedOperation, // Aggiorniamo con l'ID restituito dal DB + ), + ); + } + } catch (e) { + emit( + state.copyWith( + status: OperationFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + Future saveOperationDraft() async { + try { + final operationToSave = state.operation; + if (operationToSave.customerId == null || + operationToSave.customerId!.isEmpty) { + throw Exception('Seleziona un cliente prima di poter usare il QR'); + } + final savedOperation = await _repository.saveFullOperation( + operation: operationToSave, + ); + emit( + state.copyWith( + operation: savedOperation, + status: OperationFormStatus.ready, + ), + ); + return savedOperation.id; + } catch (e) { + emit( + state.copyWith( + status: OperationFormStatus.failure, + errorMessage: e.toString(), + ), + ); + return null; + } + } + + // --- GESTIONE DEI CAMPI IN TEMPO REALE --- + + void updateFields({ + String? customerId, + String? customerDisplayName, + String? reference, + String? note, + String? type, + String? providerId, + String? providerDisplayName, + String? subtype, + String? description, + DateTime? expirationDate, + int? quantity, + String? modelId, + String? modelDisplayName, + String? staffId, + String? staffDisplayName, + OperationStatus? status, + + bool clearProvider = false, + bool clearType = false, + bool clearSubtype = false, + bool clearDescription = false, + bool clearExpiration = false, + bool clearQuantity = false, + bool clearModel = false, + }) { + final current = state.operation; + + int? newQuantity; + if (clearQuantity) newQuantity = 1; + if (quantity != null && quantity <= 0) newQuantity = 0; + if (quantity != null && quantity > 0) newQuantity = quantity; + + final updated = current.copyWith( + customerId: + customerId ?? + current.customerId, // Se non passo customerId, tengo il vecchio + customerDisplayName: customerDisplayName ?? current.customerDisplayName, + reference: reference ?? current.reference, + note: note ?? current.note, + providerId: clearProvider ? null : (providerId ?? current.providerId), + providerDisplayName: clearProvider + ? null + : (providerDisplayName ?? current.providerDisplayName), + quantity: newQuantity ?? current.quantity, + type: clearType ? null : (type ?? current.type), + description: clearDescription + ? null + : (description ?? current.description), + subtype: clearSubtype ? null : (subtype ?? current.subtype), + expirationDate: clearExpiration + ? null + : (expirationDate ?? current.expirationDate), + modelId: clearModel ? null : (modelId ?? current.modelId), + modelDisplayName: clearModel + ? null + : (modelDisplayName ?? current.modelDisplayName), + staffId: staffId ?? current.staffId, + staffDisplayName: staffDisplayName ?? current.staffDisplayName, + status: status ?? current.status, + ); + + emit(state.copyWith(operation: updated)); + } + + // --- UTILS --- + + void setTypeWithSmartDefault(String type) { + DateTime? defaultDate; + final now = DateTime.now(); + + if (type == 'Energy') { + defaultDate = DateTime(now.year, now.month + 24, now.day); + } + if (type == 'Fin') { + defaultDate = DateTime(now.year, now.month + 30, now.day); + } + if (type == 'Entertainment') { + defaultDate = DateTime(now.year, now.month + 12, now.day); + } + + updateFields( + type: type, + expirationDate: defaultDate, + clearProvider: true, + clearSubtype: true, + clearModel: true, + clearQuantity: true, + ); + } +} diff --git a/lib/features/operations/blocs/operation_form_state.dart b/lib/features/operations/blocs/operation_form_state.dart index f15709e..5dd0419 100644 --- a/lib/features/operations/blocs/operation_form_state.dart +++ b/lib/features/operations/blocs/operation_form_state.dart @@ -1,39 +1,48 @@ -import 'package:equatable/equatable.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; +part of 'operation_form_cubit.dart'; enum OperationFormStatus { initial, - ready, loading, + ready, saving, success, - successAndAddAnother, + successAndAddAnother, // Nuovo stato in stile Ticket! failure, } class OperationFormState extends Equatable { - final OperationModel operation; final OperationFormStatus status; + final OperationModel operation; final String? errorMessage; + // Teniamo traccia delle operazioni salvate in questa sessione (per UI riepilogo) + final List savedBatchOperations; const OperationFormState({ - required this.operation, this.status = OperationFormStatus.initial, + required this.operation, this.errorMessage, + this.savedBatchOperations = const [], }); - @override - List get props => [operation, status, errorMessage]; - OperationFormState copyWith({ - OperationModel? operation, OperationFormStatus? status, + OperationModel? operation, String? errorMessage, + List? savedBatchOperations, }) { return OperationFormState( - operation: operation ?? this.operation, status: status ?? this.status, + operation: operation ?? this.operation, errorMessage: errorMessage, + savedBatchOperations: savedBatchOperations ?? this.savedBatchOperations, ); } + + @override + List get props => [ + status, + operation, + errorMessage, + savedBatchOperations, + ]; } diff --git a/lib/features/operations/blocs/operation_list_cubit.dart b/lib/features/operations/blocs/operation_list_cubit.dart new file mode 100644 index 0000000..2541178 --- /dev/null +++ b/lib/features/operations/blocs/operation_list_cubit.dart @@ -0,0 +1,81 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'operation_list_state.dart'; + +class OperationListCubit extends Cubit { + final OperationsRepository _repository = GetIt.I(); + final SessionCubit _sessionCubit = GetIt.I(); + + OperationListCubit() : super(const OperationListState()); + + Future loadOperations({bool refresh = false}) async { + if (state.status == OperationListStatus.loading) return; + if (!refresh && state.hasReachedMax) return; + + emit( + state.copyWith( + status: OperationListStatus.loading, + errorMessage: null, + operations: refresh ? [] : state.operations, + hasReachedMax: refresh ? false : state.hasReachedMax, + ), + ); + + try { + final currentOffset = refresh ? 0 : state.operations.length; + final companyId = _sessionCubit.state.company?.id; + + if (companyId == null) { + throw Exception("Company ID non trovato nella sessione"); + } + + final newOperations = await _repository.fetchOperations( + companyId: companyId, + offset: currentOffset, + limit: 50, + searchTerm: state.query, + dateRange: state.dateRange, + ); + + final bool reachedMax = newOperations.length < 50; + + emit( + state.copyWith( + status: OperationListStatus.success, + operations: refresh + ? newOperations + : [...state.operations, ...newOperations], + hasReachedMax: reachedMax, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: OperationListStatus.failure, + errorMessage: "Errore nel caricamento operazioni: $e", + ), + ); + } + } + + void updateFilters({String? query, DateTimeRange? range}) { + emit( + state.copyWith( + query: query ?? state.query, + dateRange: range ?? state.dateRange, + ), + ); + loadOperations(refresh: true); + } + + void clearFilters() { + emit(const OperationListState()); // Resetta tutto allo stato iniziale + loadOperations(refresh: true); + } +} diff --git a/lib/features/operations/blocs/operation_list_state.dart b/lib/features/operations/blocs/operation_list_state.dart new file mode 100644 index 0000000..34e77fd --- /dev/null +++ b/lib/features/operations/blocs/operation_list_state.dart @@ -0,0 +1,49 @@ +part of 'operation_list_cubit.dart'; + +enum OperationListStatus { initial, loading, success, failure } + +class OperationListState extends Equatable { + final OperationListStatus status; + final List operations; + final bool hasReachedMax; + final String? errorMessage; + final String query; + final DateTimeRange? dateRange; + + const OperationListState({ + this.status = OperationListStatus.initial, + this.operations = const [], + this.hasReachedMax = false, + this.errorMessage, + this.query = '', + this.dateRange, + }); + + OperationListState copyWith({ + OperationListStatus? status, + List? operations, + bool? hasReachedMax, + String? errorMessage, + String? query, + DateTimeRange? dateRange, + }) { + return OperationListState( + status: status ?? this.status, + operations: operations ?? this.operations, + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + errorMessage: errorMessage, + query: query ?? this.query, + dateRange: dateRange ?? this.dateRange, + ); + } + + @override + List get props => [ + status, + operations, + hasReachedMax, + errorMessage, + query, + dateRange, + ]; +} diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart deleted file mode 100644 index da3df30..0000000 --- a/lib/features/operations/blocs/operations_cubit.dart +++ /dev/null @@ -1,304 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/features/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:get_it/get_it.dart'; -import 'package:collection/collection.dart'; -import 'package:uuid/uuid.dart'; -part 'operations_state.dart'; - -class OperationsCubit extends Cubit { - final OperationsRepository _repository = GetIt.I(); - final SessionCubit _sessionCubit = GetIt.I(); - final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch - - OperationsCubit() - : super(const OperationsState(status: OperationsStatus.initial)); - - // --- CARICAMENTO E PAGINAZIONE --- - - Future loadOperations({bool refresh = false}) async { - if (state.status == OperationsStatus.loading) return; - if (!refresh && state.hasReachedMax) return; - - emit( - state.copyWith( - status: OperationsStatus.loading, - errorMessage: null, - allOperations: refresh ? [] : state.allOperations, - hasReachedMax: refresh ? false : state.hasReachedMax, - ), - ); - - try { - final currentOffset = refresh ? 0 : state.allOperations.length; - final companyId = _sessionCubit.state.company?.id; - - if (companyId == null) { - throw Exception("Company ID non trovato nella sessione"); - } - - final newOperations = await _repository.fetchOperations( - companyId: companyId, - offset: currentOffset, - limit: 50, - searchTerm: state.query, - dateRange: state.dateRange, - ); - - final bool reachedMax = newOperations.length < 50; - - emit( - state.copyWith( - status: OperationsStatus.ready, - allOperations: refresh - ? newOperations - : [...state.allOperations, ...newOperations], - hasReachedMax: reachedMax, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: "Errore nel caricamento operazioni: $e", - ), - ); - } - } - - // --- GESTIONE FILTRI --- - - void updateFilters({String? query, DateTimeRange? range}) { - emit( - state.copyWith( - query: query ?? state.query, - dateRange: range ?? state.dateRange, - ), - ); - loadOperations(refresh: true); - } - - void clearFilters() { - emit(state.copyWith(query: '', dateRange: null)); - loadOperations(refresh: true); - } - - void initOperationForm({ - OperationModel? existingOperation, - String? operationId, - String? staffId, - String? staffDisplayName, - }) async { - if (existingOperation != null) { - emit( - state.copyWith( - currentOperation: existingOperation, - status: OperationsStatus.ready, - ), - ); - } else if (operationId != null) { - OperationModel? operationModel = state.allOperations.firstWhereOrNull( - (s) => s.id == operationId, - ); - operationModel ??= await _repository.fetchOperationById(operationId); - emit( - state.copyWith( - currentOperation: operationModel, - status: OperationsStatus.ready, - ), - ); - } else { - // NUOVA PRATICA: Creiamo un nuovo Batch UUID - emit( - state.copyWith( - currentOperation: OperationModel( - storeId: _sessionCubit.state.currentStore?.id ?? '', - reference: '', - createdAt: DateTime.now(), - companyId: _sessionCubit.state.company!.id!, - status: OperationStatus.draft, - batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO - ), - status: OperationsStatus.ready, - ), - ); - } - } - - /// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica. - /// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto. - void prepareNextOperationInBatch() { - if (state.currentOperation == null) return; - - final current = state.currentOperation!; - - emit( - state.copyWith( - status: OperationsStatus.ready, - currentOperation: OperationModel( - companyId: current.companyId, - storeId: current.storeId, - storeDisplayName: current.storeDisplayName, - batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO - customerId: current.customerId, // <-- MANTIENE IL CLIENTE - customerDisplayName: current.customerDisplayName, - status: OperationStatus.draft, - createdAt: DateTime.now(), - ), - ), - ); - } - - // --- PERSISTENZA --- - - Future saveCurrentOperation({ - required OperationStatus targetStatus, - bool shouldPop = true, - }) async { - if (state.currentOperation == null) return; - - emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); - try { - final operationToSave = state.currentOperation!.copyWith( - status: targetStatus, - ); - - final updatedOperation = await _repository.saveFullOperation( - operation: operationToSave, - ); - - emit( - state.copyWith( - // Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente - status: shouldPop - ? OperationsStatus.saved - : OperationsStatus.savedNoPop, - currentOperation: shouldPop ? null : updatedOperation, - ), - ); - - // Ricarica in background per la dashboard - loadOperations(refresh: true); - } catch (e) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: e.toString(), - ), - ); - } - } - - // --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) --- - - /// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica" - List getOperationsInCurrentBatch() { - if (state.currentOperation == null) return []; - final currentBatch = state.currentOperation!.batchUuid; - - // Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci) - return state.allOperations - .where( - (op) => - op.batchUuid == currentBatch && - op.id != state.currentOperation!.id, - ) - .toList(); - } - - // --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE --- - void updateOperationFields({ - String? customerId, - String? customerDisplayName, - String? type, - String? providerId, - String? providerDisplayName, - String? subtype, - String? description, - DateTime? expirationDate, - int? quantity, - String? modelId, - String? modelDisplayName, - String? staffId, - String? staffDisplayName, - // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo - bool clearProvider = false, - bool clearType = false, - bool clearSubtype = false, - bool clearDescription = false, - bool clearExpiration = false, - bool clearQuantity = false, - bool clearModel = false, - }) { - if (state.currentOperation == null) return; - - final current = state.currentOperation!; - - // Creiamo il modello aggiornato - // ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith! - int? newQuantity; - if (clearQuantity) { - newQuantity = 1; - } - if (quantity != null && quantity <= 0) { - newQuantity = 0; - } - if (quantity != null && quantity > 0) { - newQuantity = quantity; - } - final updated = current.copyWith( - customerId: customerId, - customerDisplayName: customerDisplayName, - - // Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta) - providerId: clearProvider ? null : (providerId ?? current.providerId), - providerDisplayName: clearProvider - ? null - : (providerDisplayName ?? current.providerDisplayName), - quantity: newQuantity, - type: clearType ? null : (type ?? current.type), - description: clearDescription - ? null - : (description ?? current.description), - subtype: clearSubtype ? null : (subtype ?? current.subtype), - expirationDate: clearExpiration - ? null - : (expirationDate ?? current.expirationDate), - modelId: clearModel ? null : (modelId ?? current.modelId), - modelDisplayName: clearModel - ? null - : (modelDisplayName ?? current.modelDisplayName), - staffId: staffId ?? current.staffId, - staffDisplayName: staffDisplayName ?? current.staffDisplayName, - ); - - emit(state.copyWith(currentOperation: updated)); - } - - // Metodo di utilità per calcolare la data X mesi da oggi - DateTime _calculateMonths(int months) { - final now = DateTime.now(); - return DateTime(now.year, now.month + months, now.day); - } - - // Quando l'utente seleziona un tipo, impostiamo il default - void setTypeWithSmartDefault(String type) { - DateTime? defaultDate; - - if (type == 'Energy') defaultDate = _calculateMonths(24); - if (type == 'Fin') defaultDate = _calculateMonths(30); - if (type == 'Entertainment') defaultDate = _calculateMonths(12); - - updateOperationFields( - type: type, - expirationDate: defaultDate, - clearProvider: true, - clearSubtype: true, - clearModel: true, - clearQuantity: true, - ); - } -} diff --git a/lib/features/operations/blocs/operations_state.dart b/lib/features/operations/blocs/operations_state.dart deleted file mode 100644 index 97276ad..0000000 --- a/lib/features/operations/blocs/operations_state.dart +++ /dev/null @@ -1,68 +0,0 @@ -part of 'operations_cubit.dart'; - -enum OperationsStatus { - initial, - loading, - ready, - saving, - saved, - savedNoPop, - success, - failure, -} - -class OperationsState extends Equatable { - final OperationsStatus status; - final List allOperations; - final OperationModel? currentOperation; // La bozza che stiamo editando - final String? errorMessage; - final String query; - final DateTimeRange? dateRange; - final bool hasReachedMax; - final bool isSavingDraft; - - const OperationsState({ - required this.status, - this.allOperations = const [], - this.currentOperation, - this.errorMessage, - this.query = '', - this.dateRange, - this.hasReachedMax = false, - this.isSavingDraft = false, - }); - - OperationsState copyWith({ - OperationsStatus? status, - List? allOperations, - OperationModel? currentOperation, - String? errorMessage, - String? query, - DateTimeRange? dateRange, - bool? hasReachedMax, - bool? isSavingDraft, - }) { - return OperationsState( - status: status ?? this.status, - allOperations: allOperations ?? this.allOperations, - currentOperation: currentOperation ?? this.currentOperation, - errorMessage: errorMessage, - query: query ?? this.query, - dateRange: dateRange ?? this.dateRange, - hasReachedMax: hasReachedMax ?? this.hasReachedMax, - isSavingDraft: isSavingDraft ?? this.isSavingDraft, - ); - } - - @override - List get props => [ - status, - allOperations, - currentOperation, - errorMessage, - query, - dateRange, - hasReachedMax, - isSavingDraft, - ]; -} diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 57efa90..e465f3e 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -3,13 +3,11 @@ import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; enum OperationStatus { - ok('ok'), - waitingforaction('waiting_for_action'), - waitingforsupport('waiting_for_support'), - waitingfordeployment('waiting_for_deployment'), - ko('ko'), - draft('draft'), - canceled('canceled'); + success('success', 'OK'), + waitingForAction('waiting_for_action', 'In attesa di azione'), + waitingForSupport('waiting_for_support', 'In attesa di supporto'), + failure('failure', 'KO'), + draft('draft', 'Bozza'); static OperationStatus fromString(String value) { final normalizedValue = value.replaceAll('_', '').toLowerCase(); @@ -19,8 +17,9 @@ enum OperationStatus { } final String supabaseName; + final String displayName; - const OperationStatus(this.supabaseName); + const OperationStatus(this.supabaseName, this.displayName); } class OperationModel extends Equatable { @@ -163,8 +162,8 @@ class OperationModel extends Equatable { attachments, ]; - factory OperationModel.empty({required String companyId}) { - return OperationModel(id: null, createdAt: null, companyId: companyId); + factory OperationModel.empty() { + return OperationModel(id: null, createdAt: null, companyId: ''); } factory OperationModel.fromMap(Map map) { diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index a92a719..fb694ce 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,14 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/features/operations/ui/widgets/details_section.dart'; import 'package:flux/core/widgets/shared_forms/attachments_section.dart'; import 'package:flux/core/widgets/shared_forms/staff_section.dart'; -import 'package:get_it/get_it.dart'; class OperationFormScreen extends StatefulWidget { final String? operationId; @@ -49,26 +48,10 @@ class _OperationFormScreenState extends State { @override void initState() { super.initState(); - final cubit = context.read(); - final currentLoggedStaff = GetIt.I - .get() - .state - .currentStaffMember!; - - // 1. Diciamo al Cubit di prepararsi - cubit.initOperationForm( + context.read().initForm( existingOperation: widget.existingOperation, operationId: widget.operationId, - staffId: currentLoggedStaff.id, - staffDisplayName: currentLoggedStaff.name, ); - - // 2. IL TRUCCO MAGICO: - // Se abbiamo passato existingOperation, il Cubit si è appena aggiornato. - // Lo stato è già pronto, quindi sincronizziamo i controller SUBITO! - if (cubit.state.currentOperation != null) { - _syncTextControllers(cubit.state.currentOperation!); - } } @override @@ -76,50 +59,83 @@ class _OperationFormScreenState extends State { _referenceController.dispose(); _noteController.dispose(); _freeTextSubtypeController.dispose(); + _freeTextDescriptionController.dispose(); super.dispose(); } void _syncTextControllers(OperationModel model) { - if (_referenceController.text.isEmpty && model.reference.isNotEmpty) { + if (_referenceController.text.isEmpty) { _referenceController.text = model.reference; } - if (_noteController.text.isEmpty && model.note.isNotEmpty) { + if (_noteController.text.isEmpty) { _noteController.text = model.note; } - if (_freeTextSubtypeController.text.isEmpty && - model.subtype != null && - model.subtype!.isNotEmpty) { - _freeTextSubtypeController.text = model.subtype!; + if (_freeTextSubtypeController.text.isEmpty) { + _freeTextSubtypeController.text = model.subtype ?? ''; } - if (_freeTextDescriptionController.text.isEmpty && - model.description != null && - model.description!.isNotEmpty) { - _freeTextDescriptionController.text = model.description!; + if (_freeTextDescriptionController.text.isEmpty) { + _freeTextDescriptionController.text = model.description ?? ''; } + + // Se è una nuova pratica (draft), impostiamo di default il target su OK per comodità UI + if (model.id == null && model.status == OperationStatus.draft) { + // Usiamo addPostFrameCallback per non interferire con il build attuale + WidgetsBinding.instance.addPostFrameCallback((_) { + // Supponendo tu aggiunga la possibilità di aggiornare lo status nel metodo updateFields del Cubit + // context.read().updateFields(status: OperationStatus.ok); + }); + } + _isInitialized = true; } - void _saveOperation({required bool keepAdding}) { + void _flushControllersToCubit() { + context.read().updateFields( + reference: _referenceController.text, + note: _noteController.text, + subtype: _freeTextSubtypeController.text, + description: _freeTextDescriptionController.text, + ); + } + + void _saveOperation({ + required OperationStatus targetStatus, + required bool keepAdding, + }) { if (_formKey.currentState!.validate()) { - final cubit = context.read(); - final currentOperation = cubit.state.currentOperation!; - - final operationToSave = currentOperation.copyWith( - reference: _referenceController.text, - note: _noteController.text, - subtype: ['Entertainment', 'Custom'].contains(currentOperation.type) - ? _freeTextSubtypeController.text - : currentOperation.subtype, - description: ['Energy', 'Custom'].contains(currentOperation.type) - ? _freeTextDescriptionController.text - : currentOperation.description, + _flushControllersToCubit(); + context.read().saveOperation( + targetStatus: targetStatus, + keepAdding: keepAdding, ); + } + } - cubit.initOperationForm(existingOperation: operationToSave); - cubit.saveCurrentOperation( - targetStatus: OperationStatus.ok, - shouldPop: !keepAdding, - ); + Future _generateIdForQr() async { + if (!_formKey.currentState!.validate()) return null; + _flushControllersToCubit(); + final attachmentsBloc = context.read(); + // Presumo tu abbia creato il metodo saveOperationDraft() nel Cubit! + final newId = await context.read().saveOperationDraft(); + if (newId != null && context.mounted) { + attachmentsBloc.add(ParentEntitySavedEvent(newId)); + } + return newId; + } + + // Helper per assegnare un colore agli stati + Color _getStatusColor(OperationStatus status) { + switch (status) { + case OperationStatus.success: + return Colors.green; + case OperationStatus.waitingForAction: + return Colors.orange; + case OperationStatus.waitingForSupport: + return Colors.blue; + case OperationStatus.failure: + return Colors.red; + case OperationStatus.draft: + return Colors.grey; } } @@ -127,33 +143,27 @@ class _OperationFormScreenState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); - return BlocConsumer( - listenWhen: (previous, current) => - previous.status != current.status || - previous.currentOperation?.id != current.currentOperation?.id, + return BlocConsumer( + listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - if (state.status == OperationsStatus.ready && - state.currentOperation != null && - !_isInitialized) { - _syncTextControllers(state.currentOperation!); + if (state.status == OperationFormStatus.ready && !_isInitialized) { + _syncTextControllers(state.operation); } - - if (state.status == OperationsStatus.saved) { + if (state.status == OperationFormStatus.success) { Navigator.of(context).pop(); - } else if (state.status == OperationsStatus.savedNoPop) { - context.read().prepareNextOperationInBatch(); + } else if (state.status == OperationFormStatus.successAndAddAnother) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Servizio aggiunto! Inserisci il prossimo.'), + content: Text('Operazione salvata! Inserisci la prossima'), ), ); _freeTextSubtypeController.clear(); _freeTextDescriptionController.clear(); - } else if (state.status == OperationsStatus.failure) { + } else if (state.status == OperationFormStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(state.errorMessage ?? 'Errore'), - backgroundColor: theme.colorScheme.error, + content: Text(state.errorMessage ?? 'Errore di salvataggio'), + backgroundColor: Colors.red, ), ); } @@ -161,19 +171,45 @@ class _OperationFormScreenState extends State { builder: (context, state) { if (!_isInitialized && (widget.operationId != null || widget.existingOperation != null) && - state.status == OperationsStatus.loading) { + state.status == OperationFormStatus.loading) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } + // Determiniamo lo stato da mostrare nel form. + // Se è una bozza appena creata, mostriamo visivamente "OK" come default per il salvataggio. + final displayStatus = + state.operation.status == OperationStatus.draft && + state.operation.id == null + ? OperationStatus.success + : state.operation.status; + return Scaffold( appBar: AppBar( title: Text( - state.currentOperation?.id == null - ? 'Nuova Pratica' - : 'Modifica Pratica', + state.operation.id == null ? 'Nuova Pratica' : 'Modifica Pratica', ), + // Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK + actions: + displayStatus != OperationStatus.success && + displayStatus != OperationStatus.draft + ? [ + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Chip( + label: Text( + displayStatus.displayName, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + backgroundColor: _getStatusColor(displayStatus), + ), + ), + ] + : null, ), body: Form( key: _formKey, @@ -182,26 +218,22 @@ class _OperationFormScreenState extends State { final isUltraWide = constraints.maxWidth > 1400; final isDesktop = constraints.maxWidth > 900; if (isUltraWide) { - // --- LAYOUT 3 COLONNE (Schermi giganti) --- return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 1. FORM PRINCIPALE (40%) Expanded( flex: 4, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - // Attenzione: devi togliere la sezione file dal _buildMainFormContent! child: _buildMainFormContent( theme, state, + displayStatus, showFiles: false, ), ), ), VerticalDivider(width: 1, color: theme.dividerColor), - - // 2. NOTE (30%) Expanded( flex: 3, child: Padding( @@ -210,15 +242,15 @@ class _OperationFormScreenState extends State { ), ), VerticalDivider(width: 1, color: theme.dividerColor), - - // 3. FILE (30%) Expanded( flex: 3, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: SharedAttachmentsSection( - parentType: AttachmentParentType.operation, - parentId: state.currentOperation?.id, + child: SharedFilesSection( + titleNameForUpload: + state.operation.customerDisplayName ?? + 'Nuova operazione', + onGenerateIdForQr: _generateIdForQr, ), ), ), @@ -232,7 +264,11 @@ class _OperationFormScreenState extends State { flex: 7, child: SingleChildScrollView( padding: const EdgeInsets.all(16.0), - child: _buildMainFormContent(theme, state), + child: _buildMainFormContent( + theme, + state, + displayStatus, + ), ), ), VerticalDivider(width: 1, color: theme.dividerColor), @@ -251,7 +287,7 @@ class _OperationFormScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildMainFormContent(theme, state), + _buildMainFormContent(theme, state, displayStatus), const Divider(height: 32), _buildNotesSection(isDesktop: false), const SizedBox(height: 80), @@ -270,9 +306,13 @@ class _OperationFormScreenState extends State { Expanded( flex: 1, child: OutlinedButton( - onPressed: state.status == OperationsStatus.saving + onPressed: state.status == OperationFormStatus.saving ? null - : () => _saveOperation(keepAdding: true), + : () => _saveOperation( + keepAdding: true, + targetStatus: + displayStatus, // <-- Usiamo lo stato selezionato nel form! + ), child: const Text( 'Salva e Aggiungi Altro', textAlign: TextAlign.center, @@ -283,10 +323,27 @@ class _OperationFormScreenState extends State { Expanded( flex: 1, child: ElevatedButton( - onPressed: state.status == OperationsStatus.saving + style: ElevatedButton.styleFrom( + // Se c'è un KO o un blocco, cambiamo il colore del bottone principale per attirare l'attenzione + backgroundColor: + displayStatus != OperationStatus.success && + displayStatus != OperationStatus.draft + ? _getStatusColor(displayStatus) + : null, + foregroundColor: + displayStatus != OperationStatus.success && + displayStatus != OperationStatus.draft + ? Colors.white + : null, + ), + onPressed: state.status == OperationFormStatus.saving ? null - : () => _saveOperation(keepAdding: false), - child: state.status == OperationsStatus.saving + : () => _saveOperation( + keepAdding: false, + targetStatus: + displayStatus, // <-- Usiamo lo stato selezionato nel form! + ), + child: state.status == OperationFormStatus.saving ? const SizedBox( width: 20, height: 20, @@ -309,32 +366,102 @@ class _OperationFormScreenState extends State { Widget _buildMainFormContent( ThemeData theme, - OperationsState state, { + OperationFormState state, + OperationStatus displayStatus, { bool showFiles = true, }) { - final currentOp = state.currentOperation; - final currentType = currentOp?.type ?? 'AL'; + final currentOp = state.operation; + final currentType = currentOp.type; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ StaffSection( - staffId: currentOp?.staffId, - staffName: currentOp?.staffDisplayName, + staffId: currentOp.staffId, + staffName: currentOp.staffDisplayName, onStaffSelected: (staff) => { - context.read().updateOperationFields( + context.read().updateFields( staffId: staff.id, staffDisplayName: staff.name, ), }, ), const Divider(height: 50), + + // --- SEZIONE STATO OPERAZIONE --- + _buildSectionTitle('Esito / Stato Operazione'), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor(displayStatus).withValues(alpha: 0.1), + border: Border.all( + color: _getStatusColor(displayStatus).withValues(alpha: 0.3), + ), + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: displayStatus, + icon: Icon( + Icons.arrow_drop_down, + color: _getStatusColor(displayStatus), + ), + items: OperationStatus.values + .where( + (s) => s != OperationStatus.draft, + ) // Nascondiamo 'Bozza' dal menu + .map( + (status) => DropdownMenuItem( + value: status, + child: Row( + children: [ + Icon( + status == OperationStatus.success + ? Icons.check_circle + : Icons.error_outline, + color: _getStatusColor(status), + size: 20, + ), + const SizedBox(width: 12), + Text( + status.displayName, + style: TextStyle( + fontWeight: FontWeight.w600, + color: _getStatusColor(status), + ), + ), + ], + ), + ), + ) + .toList(), + onChanged: (newStatus) { + if (newStatus != null) { + // Assicurati che il metodo updateFields nel tuo Cubit accetti anche 'status' + context.read().updateFields( + status: newStatus, + ); + } + }, + ), + ), + ), + const SizedBox(height: 8), + Text( + displayStatus == OperationStatus.success + ? 'Lascia OK se la pratica è stata caricata con successo.' + : 'Attenzione: la pratica verrà salvata come ${displayStatus.displayName}.', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + const Divider(height: 32), + _buildSectionTitle('Cliente & Riferimento'), SharedCustomerSection( - customerId: currentOp?.customerId, - customerName: currentOp?.customerDisplayName, + customerId: currentOp.customerId, + customerName: currentOp.customerDisplayName, onCustomerSelected: (customer) { - context.read().updateOperationFields( + context.read().updateFields( customerId: customer.id, customerDisplayName: customer.name, ); @@ -360,7 +487,9 @@ class _OperationFormScreenState extends State { selected: currentType == type, onSelected: (selected) { if (selected) { - context.read().setTypeWithSmartDefault(type); + context.read().setTypeWithSmartDefault( + type, + ); } }, ); @@ -384,23 +513,23 @@ class _OperationFormScreenState extends State { IconButton( icon: const Icon(Icons.remove), onPressed: () { - final q = currentOp?.quantity ?? 1; + final q = currentOp.quantity; if (q > 1) { - context.read().updateOperationFields( + context.read().updateFields( quantity: q - 1, ); } }, ), Text( - '${currentOp?.quantity ?? 1}', + '${currentOp.quantity}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), IconButton( icon: const Icon(Icons.add), onPressed: () { - final q = currentOp?.quantity ?? 1; - context.read().updateOperationFields( + final q = currentOp.quantity; + context.read().updateFields( quantity: q + 1, ); }, @@ -410,9 +539,14 @@ class _OperationFormScreenState extends State { const Divider(height: 32), if (showFiles) ...[ - SharedAttachmentsSection( + /* SharedAttachmentsSection( parentType: AttachmentParentType.operation, - parentId: currentOp?.id, + parentId: currentOp.id, + ), */ + SharedFilesSection( + titleNameForUpload: + state.operation.customerDisplayName ?? 'Nuova pratica', + onGenerateIdForQr: _generateIdForQr, ), ], ], @@ -444,7 +578,7 @@ class _OperationFormScreenState extends State { backgroundColor: Colors.blue.withValues(alpha: 0.05), onPressed: () { final now = DateTime.now(); - context.read().updateOperationFields( + context.read().updateFields( expirationDate: DateTime( now.year, now.month + months, diff --git a/lib/features/operations/ui/operations_screen.dart b/lib/features/operations/ui/operation_list_screen.dart similarity index 80% rename from lib/features/operations/ui/operations_screen.dart rename to lib/features/operations/ui/operation_list_screen.dart index d2d5eed..4ed3c27 100644 --- a/lib/features/operations/ui/operations_screen.dart +++ b/lib/features/operations/ui/operation_list_screen.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit -class OperationsScreen extends StatefulWidget { - const OperationsScreen({super.key}); +class OperationListScreen extends StatefulWidget { + const OperationListScreen({super.key}); @override - State createState() => _OperationsScreenState(); + State createState() => _OperationListScreenState(); } -class _OperationsScreenState extends State { +class _OperationListScreenState extends State { final ScrollController _scrollController = ScrollController(); @override @@ -20,13 +20,11 @@ class _OperationsScreenState extends State { super.initState(); // Agganciamo il listener per la paginazione (Scroll Infinito) _scrollController.addListener(_onScroll); - // Carichiamo i servizi iniziali - context.read().loadOperations(); } void _onScroll() { if (_isBottom) { - context.read().loadOperations(); + context.read().loadOperations(); } } @@ -59,16 +57,16 @@ class _OperationsScreenState extends State { ), ], ), - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { // 1. Stato di caricamento iniziale - if (state.status == OperationsStatus.loading && - state.allOperations.isEmpty) { + if (state.status == OperationListStatus.loading && + state.operations.isEmpty) { return const Center(child: CircularProgressIndicator()); } // 2. Lista vuota - if (state.allOperations.isEmpty) { + if (state.operations.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -77,7 +75,7 @@ class _OperationsScreenState extends State { const SizedBox(height: 10), ElevatedButton( onPressed: () => context - .read() + .read() .loadOperations(refresh: true), child: const Text("Riprova"), ), @@ -88,16 +86,17 @@ class _OperationsScreenState extends State { // 3. La Lista (con Pull-to-refresh) return RefreshIndicator( - onRefresh: () => - context.read().loadOperations(refresh: true), + onRefresh: () => context.read().loadOperations( + refresh: true, + ), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB itemCount: state.hasReachedMax - ? state.allOperations.length - : state.allOperations.length + 1, + ? state.operations.length + : state.operations.length + 1, itemBuilder: (context, index) { - if (index >= state.allOperations.length) { + if (index >= state.operations.length) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), @@ -106,7 +105,7 @@ class _OperationsScreenState extends State { ); } - final operation = state.allOperations[index]; + final operation = state.operations[index]; return _buildOperationCard(context, operation); }, ), @@ -173,17 +172,16 @@ class _OperationsScreenState extends State { Widget _buildOperationStatus(OperationStatus status) { Color color; switch (status) { - case OperationStatus.canceled || OperationStatus.ko: + case OperationStatus.failure: color = Colors.grey.shade800; break; - case OperationStatus.waitingforaction || OperationStatus.draft: + case OperationStatus.waitingForAction || OperationStatus.draft: color = Colors.orange; break; - case OperationStatus.ok: + case OperationStatus.success: color = Colors.green; break; - case OperationStatus.waitingfordeployment || - OperationStatus.waitingforsupport: + case OperationStatus.waitingForSupport: color = Colors.blue; break; } diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index 73deeff..e26334d 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/shared_forms/model_section.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; class DetailsSection extends StatelessWidget { @@ -117,12 +117,10 @@ class DetailsSection extends StatelessWidget { ), ), onTap: () { - context - .read() - .updateOperationFields( - providerId: provider.id, - providerDisplayName: provider.name, - ); + context.read().updateFields( + providerId: provider.id, + providerDisplayName: provider.name, + ); Navigator.pop(modalContext); }, ); @@ -190,9 +188,7 @@ class DetailsSection extends StatelessWidget { ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) { if (val != null) { - context.read().updateOperationFields( - subtype: val, - ); + context.read().updateFields(subtype: val); } }, ), @@ -215,7 +211,7 @@ class DetailsSection extends StatelessWidget { modelId: currentOp?.modelId, modelName: currentOp?.modelDisplayName, onModelSelected: (id, name) { - context.read().updateOperationFields( + context.read().updateFields( modelId: id, modelDisplayName: name, ); @@ -271,7 +267,7 @@ class DetailsSection extends StatelessWidget { lastDate: DateTime.now().add(const Duration(days: 3650)), ); if (date != null && context.mounted) { - context.read().updateOperationFields( + context.read().updateFields( expirationDate: date, ); } diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 37f10f2..75f34e3 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -107,13 +107,15 @@ class _TicketFormScreenState extends State { // 2. Sincronizziamo i testi scritti a mano nel Cubit _flushControllersToCubit(); + final attachmentsBloc = context.read(); + // 3. Facciamo il salvataggio silenzioso final newId = await context.read().saveTicketDraft(); if (newId != null && context.mounted) { // 4. IL TOCCO DI CLASSE: Diciamo all'AttachmentsBloc che ora la pratica ha un ID! // Questo farà partire l'upload automatico di eventuali file "in bozza" - context.read().add(ParentEntitySavedEvent(newId)); + attachmentsBloc.add(ParentEntitySavedEvent(newId)); } return newId; diff --git a/lib/main.dart b/lib/main.dart index 22fea11..b567e8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; +import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; @@ -28,7 +29,6 @@ import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/settings/settings.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; @@ -55,7 +55,7 @@ void main() async { BlocProvider(create: (_) => CustomersCubit()), BlocProvider(create: (_) => ProductsCubit()), BlocProvider(create: (_) => StaffCubit()), - BlocProvider(create: (_) => OperationsCubit()), + BlocProvider(create: (_) => OperationListCubit()), BlocProvider(create: (_) => ProvidersCubit()), ], child: const FluxApp(),